test: speed up heavy suites with shared fixtures

This commit is contained in:
Peter Steinberger
2026-03-02 21:58:19 +00:00
parent 6358aae024
commit 3beb1b9da9
12 changed files with 227 additions and 151 deletions

View File

@@ -1,6 +1,6 @@
import os from "node:os";
import path from "node:path";
import { afterAll, describe, expect, it } from "vitest";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { runAcpRuntimeAdapterContract } from "../../../src/acp/runtime/adapter-contract.testkit.js";
import {
cleanupMockRuntimeFixtures,
@@ -10,7 +10,14 @@ import {
} from "./runtime-internals/test-fixtures.js";
import { AcpxRuntime, decodeAcpxRuntimeHandleState } from "./runtime.js";
let sharedFixture: Awaited<ReturnType<typeof createMockRuntimeFixture>> | null = null;
beforeAll(async () => {
sharedFixture = await createMockRuntimeFixture();
});
afterAll(async () => {
sharedFixture = null;
await cleanupMockRuntimeFixtures();
});
@@ -21,14 +28,10 @@ describe("AcpxRuntime", () => {
createRuntime: async () => fixture.runtime,
agentId: "codex",
successPrompt: "contract-pass",
errorPrompt: "trigger-error",
includeControlChecks: false,
assertSuccessEvents: (events) => {
expect(events.some((event) => event.type === "done")).toBe(true);
},
assertErrorOutcome: ({ events, thrown }) => {
expect(events.some((event) => event.type === "error") || Boolean(thrown)).toBe(true);
},
});
const logs = await readMockRuntimeLogEntries(fixture.logPath);
@@ -108,34 +111,12 @@ describe("AcpxRuntime", () => {
expect(promptArgs).toContain("--approve-all");
});
it("passes a queue-owner TTL by default to avoid long idle stalls", async () => {
const { runtime, logPath } = await createMockRuntimeFixture();
const handle = await runtime.ensureSession({
sessionKey: "agent:codex:acp:ttl-default",
agent: "codex",
mode: "persistent",
});
for await (const _event of runtime.runTurn({
handle,
text: "ttl-default",
mode: "prompt",
requestId: "req-ttl-default",
})) {
// drain
}
const logs = await readMockRuntimeLogEntries(logPath);
const prompt = logs.find((entry) => entry.kind === "prompt");
expect(prompt).toBeDefined();
const promptArgs = (prompt?.args as string[]) ?? [];
const ttlFlagIndex = promptArgs.indexOf("--ttl");
expect(ttlFlagIndex).toBeGreaterThanOrEqual(0);
expect(promptArgs[ttlFlagIndex + 1]).toBe("0.1");
});
it("preserves leading spaces across streamed text deltas", async () => {
const { runtime } = await createMockRuntimeFixture();
const runtime = sharedFixture?.runtime;
expect(runtime).toBeDefined();
if (!runtime) {
throw new Error("shared runtime fixture missing");
}
const handle = await runtime.ensureSession({
sessionKey: "agent:codex:acp:space",
agent: "codex",
@@ -156,10 +137,28 @@ describe("AcpxRuntime", () => {
expect(textDeltas).toEqual(["alpha", " beta", " gamma"]);
expect(textDeltas.join("")).toBe("alpha beta gamma");
// Keep the default queue-owner TTL assertion on a runTurn that already exists.
const activeLogPath = process.env.MOCK_ACPX_LOG;
expect(activeLogPath).toBeDefined();
const logs = await readMockRuntimeLogEntries(String(activeLogPath));
const prompt = logs.find(
(entry) =>
entry.kind === "prompt" && String(entry.sessionName ?? "") === "agent:codex:acp:space",
);
expect(prompt).toBeDefined();
const promptArgs = (prompt?.args as string[]) ?? [];
const ttlFlagIndex = promptArgs.indexOf("--ttl");
expect(ttlFlagIndex).toBeGreaterThanOrEqual(0);
expect(promptArgs[ttlFlagIndex + 1]).toBe("0.1");
});
it("emits done once when ACP stream repeats stop reason responses", async () => {
const { runtime } = await createMockRuntimeFixture();
const runtime = sharedFixture?.runtime;
expect(runtime).toBeDefined();
if (!runtime) {
throw new Error("shared runtime fixture missing");
}
const handle = await runtime.ensureSession({
sessionKey: "agent:codex:acp:double-done",
agent: "codex",
@@ -181,7 +180,11 @@ describe("AcpxRuntime", () => {
});
it("maps acpx error events into ACP runtime error events", async () => {
const { runtime } = await createMockRuntimeFixture();
const runtime = sharedFixture?.runtime;
expect(runtime).toBeDefined();
if (!runtime) {
throw new Error("shared runtime fixture missing");
}
const handle = await runtime.ensureSession({
sessionKey: "agent:codex:acp:456",
agent: "codex",

View File

@@ -1,7 +1,9 @@
import fs from "node:fs/promises";
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { withTempWorkspace } from "./skills-install.download-test-utils.js";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createFixtureSuite } from "../test-utils/fixture-suite.js";
import { createTempHomeEnv, type TempHomeEnv } from "../test-utils/temp-home.js";
import { setTempStateDir } from "./skills-install.download-test-utils.js";
import { installSkill } from "./skills-install.js";
import {
runCommandWithTimeoutMock,
@@ -36,6 +38,27 @@ metadata: {"openclaw":{"install":[{"id":"deps","kind":"node","package":"example-
return skillDir;
}
const workspaceSuite = createFixtureSuite("openclaw-skills-install-");
let tempHome: TempHomeEnv;
beforeAll(async () => {
tempHome = await createTempHomeEnv("openclaw-skills-install-home-");
await workspaceSuite.setup();
});
afterAll(async () => {
await workspaceSuite.cleanup();
await tempHome.restore();
});
async function withWorkspaceCase(
run: (params: { workspaceDir: string; stateDir: string }) => Promise<void>,
): Promise<void> {
const workspaceDir = await workspaceSuite.createCaseDir("case");
const stateDir = setTempStateDir(workspaceDir);
await run({ workspaceDir, stateDir });
}
describe("installSkill code safety scanning", () => {
beforeEach(() => {
runCommandWithTimeoutMock.mockClear();
@@ -50,7 +73,7 @@ describe("installSkill code safety scanning", () => {
});
it("adds detailed warnings for critical findings and continues install", async () => {
await withTempWorkspace(async ({ workspaceDir }) => {
await withWorkspaceCase(async ({ workspaceDir }) => {
const skillDir = await writeInstallableSkill(workspaceDir, "danger-skill");
scanDirectoryWithSummaryMock.mockResolvedValue({
scannedFiles: 1,
@@ -84,7 +107,7 @@ describe("installSkill code safety scanning", () => {
});
it("warns and continues when skill scan fails", async () => {
await withTempWorkspace(async ({ workspaceDir }) => {
await withWorkspaceCase(async ({ workspaceDir }) => {
await writeInstallableSkill(workspaceDir, "scanfail-skill");
scanDirectoryWithSummaryMock.mockRejectedValue(new Error("scanner exploded"));

View File

@@ -1,15 +1,19 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { withEnv } from "../test-utils/env.js";
import { createTrackedTempDirs } from "../test-utils/tracked-temp-dirs.js";
import { createFixtureSuite } from "../test-utils/fixture-suite.js";
import { writeSkill } from "./skills.e2e-test-helpers.js";
import { buildWorkspaceSkillSnapshot, buildWorkspaceSkillsPrompt } from "./skills.js";
const tempDirs = createTrackedTempDirs();
const fixtureSuite = createFixtureSuite("openclaw-skills-snapshot-suite-");
afterEach(async () => {
await tempDirs.cleanup();
beforeAll(async () => {
await fixtureSuite.setup();
});
afterAll(async () => {
await fixtureSuite.cleanup();
});
function withWorkspaceHome<T>(workspaceDir: string, cb: () => T): T {
@@ -18,7 +22,7 @@ function withWorkspaceHome<T>(workspaceDir: string, cb: () => T): T {
describe("buildWorkspaceSkillSnapshot", () => {
it("returns an empty snapshot when skills dirs are missing", async () => {
const workspaceDir = await tempDirs.make("openclaw-");
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
const snapshot = withWorkspaceHome(workspaceDir, () =>
buildWorkspaceSkillSnapshot(workspaceDir, {
@@ -32,7 +36,7 @@ describe("buildWorkspaceSkillSnapshot", () => {
});
it("omits disable-model-invocation skills from the prompt", async () => {
const workspaceDir = await tempDirs.make("openclaw-");
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
await writeSkill({
dir: path.join(workspaceDir, "skills", "visible-skill"),
name: "visible-skill",
@@ -61,7 +65,7 @@ describe("buildWorkspaceSkillSnapshot", () => {
});
it("keeps prompt output aligned with buildWorkspaceSkillsPrompt", async () => {
const workspaceDir = await tempDirs.make("openclaw-");
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
await writeSkill({
dir: path.join(workspaceDir, "skills", "visible"),
name: "visible",
@@ -106,7 +110,7 @@ describe("buildWorkspaceSkillSnapshot", () => {
});
it("truncates the skills prompt when it exceeds the configured char budget", async () => {
const workspaceDir = await tempDirs.make("openclaw-");
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
// Keep fixture size modest while still forcing truncation logic.
for (let i = 0; i < 8; i += 1) {
@@ -138,8 +142,8 @@ describe("buildWorkspaceSkillSnapshot", () => {
});
it("limits discovery for nested repo-style skills roots (dir/skills/*)", async () => {
const workspaceDir = await tempDirs.make("openclaw-");
const repoDir = await tempDirs.make("openclaw-skills-repo-");
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
const repoDir = await fixtureSuite.createCaseDir("skills-repo");
for (let i = 0; i < 8; i += 1) {
const name = `repo-skill-${String(i).padStart(2, "0")}`;
@@ -175,7 +179,7 @@ describe("buildWorkspaceSkillSnapshot", () => {
});
it("skips skills whose SKILL.md exceeds maxSkillFileBytes", async () => {
const workspaceDir = await tempDirs.make("openclaw-");
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
await writeSkill({
dir: path.join(workspaceDir, "skills", "small-skill"),
@@ -211,8 +215,8 @@ describe("buildWorkspaceSkillSnapshot", () => {
});
it("detects nested skills roots beyond the first 25 entries", async () => {
const workspaceDir = await tempDirs.make("openclaw-");
const repoDir = await tempDirs.make("openclaw-skills-repo-");
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
const repoDir = await fixtureSuite.createCaseDir("skills-repo");
// Create 30 nested dirs, but only the last one is an actual skill.
for (let i = 0; i < 30; i += 1) {
@@ -250,8 +254,8 @@ describe("buildWorkspaceSkillSnapshot", () => {
});
it("enforces maxSkillFileBytes for root-level SKILL.md", async () => {
const workspaceDir = await tempDirs.make("openclaw-");
const rootSkillDir = await tempDirs.make("openclaw-root-skill-");
const workspaceDir = await fixtureSuite.createCaseDir("workspace");
const rootSkillDir = await fixtureSuite.createCaseDir("root-skill");
await writeSkill({
dir: rootSkillDir,

View File

@@ -1,7 +1,7 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { createFixtureSuite } from "../test-utils/fixture-suite.js";
import { createTempHomeEnv, type TempHomeEnv } from "../test-utils/temp-home.js";
import { writeSkill } from "./skills.e2e-test-helpers.js";
import {
@@ -13,7 +13,7 @@ import {
loadWorkspaceSkillEntries,
} from "./skills.js";
const tempDirs: string[] = [];
const fixtureSuite = createFixtureSuite("openclaw-skills-suite-");
let tempHome: TempHomeEnv | null = null;
const resolveTestSkillDirs = (workspaceDir: string) => ({
@@ -21,11 +21,7 @@ const resolveTestSkillDirs = (workspaceDir: string) => ({
bundledSkillsDir: path.join(workspaceDir, ".bundled"),
});
const makeWorkspace = async () => {
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-"));
tempDirs.push(workspaceDir);
return workspaceDir;
};
const makeWorkspace = async () => await fixtureSuite.createCaseDir("workspace");
const withClearedEnv = <T>(
keys: string[],
@@ -52,6 +48,7 @@ const withClearedEnv = <T>(
};
beforeAll(async () => {
await fixtureSuite.setup();
tempHome = await createTempHomeEnv("openclaw-skills-home-");
await fs.mkdir(path.join(tempHome.home, ".openclaw", "agents", "main", "sessions"), {
recursive: true,
@@ -63,10 +60,7 @@ afterAll(async () => {
await tempHome.restore();
tempHome = null;
}
await Promise.all(
tempDirs.splice(0, tempDirs.length).map((dir) => fs.rm(dir, { recursive: true, force: true })),
);
await fixtureSuite.cleanup();
});
describe("buildWorkspaceSkillCommandSpecs", () => {

View File

@@ -5,7 +5,6 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi }
import { buildModelAliasIndex } from "../../agents/model-selection.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions.js";
import { saveSessionStore } from "../../config/sessions.js";
import { formatZonedTimestamp } from "../../infra/format-time/format-datetime.ts";
import { enqueueSystemEvent, resetSystemEventsForTest } from "../../infra/system-events.js";
import { applyResetModelOverride } from "./session-reset-model.js";
@@ -51,6 +50,14 @@ async function makeStorePath(prefix: string): Promise<string> {
const createStorePath = makeStorePath;
async function writeSessionStoreFast(
storePath: string,
store: Record<string, SessionEntry | Record<string, unknown>>,
): Promise<void> {
await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.writeFile(storePath, JSON.stringify(store), "utf-8");
}
describe("initSessionState thread forking", () => {
it("forks a new session from the parent session file", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
@@ -89,7 +96,7 @@ describe("initSessionState thread forking", () => {
const storePath = path.join(root, "sessions.json");
const parentSessionKey = "agent:main:slack:channel:c1";
await saveSessionStore(storePath, {
await writeSessionStoreFast(storePath, {
[parentSessionKey]: {
sessionId: parentSessionId,
sessionFile: parentSessionFile,
@@ -175,7 +182,7 @@ describe("initSessionState thread forking", () => {
const storePath = path.join(root, "sessions.json");
const parentSessionKey = "agent:main:slack:channel:c1";
const threadSessionKey = "agent:main:slack:channel:c1:thread:123";
await saveSessionStore(storePath, {
await writeSessionStoreFast(storePath, {
[parentSessionKey]: {
sessionId: parentSessionId,
sessionFile: parentSessionFile,
@@ -256,7 +263,7 @@ describe("initSessionState thread forking", () => {
const storePath = path.join(root, "sessions.json");
const parentSessionKey = "agent:main:slack:channel:c1";
// Set totalTokens well above PARENT_FORK_MAX_TOKENS (100_000)
await saveSessionStore(storePath, {
await writeSessionStoreFast(storePath, {
[parentSessionKey]: {
sessionId: parentSessionId,
sessionFile: parentSessionFile,
@@ -324,7 +331,7 @@ describe("initSessionState thread forking", () => {
const storePath = path.join(root, "sessions.json");
const parentSessionKey = "agent:main:slack:channel:c1";
await saveSessionStore(storePath, {
await writeSessionStoreFast(storePath, {
[parentSessionKey]: {
sessionId: parentSessionId,
sessionFile: parentSessionFile,
@@ -461,7 +468,7 @@ describe("initSessionState RawBody", () => {
vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
try {
await fs.mkdir(path.dirname(storePath), { recursive: true });
await saveSessionStore(storePath, {
await writeSessionStoreFast(storePath, {
[sessionKey]: {
sessionId,
sessionFile,
@@ -507,7 +514,7 @@ describe("initSessionState reset policy", () => {
const sessionKey = "agent:main:whatsapp:dm:s1";
const existingSessionId = "daily-session-id";
await saveSessionStore(storePath, {
await writeSessionStoreFast(storePath, {
[sessionKey]: {
sessionId: existingSessionId,
updatedAt: new Date(2026, 0, 18, 3, 0, 0).getTime(),
@@ -532,7 +539,7 @@ describe("initSessionState reset policy", () => {
const sessionKey = "agent:main:whatsapp:dm:s-edge";
const existingSessionId = "daily-edge-session";
await saveSessionStore(storePath, {
await writeSessionStoreFast(storePath, {
[sessionKey]: {
sessionId: existingSessionId,
updatedAt: new Date(2026, 0, 17, 3, 30, 0).getTime(),
@@ -557,7 +564,7 @@ describe("initSessionState reset policy", () => {
const sessionKey = "agent:main:whatsapp:dm:s2";
const existingSessionId = "idle-session-id";
await saveSessionStore(storePath, {
await writeSessionStoreFast(storePath, {
[sessionKey]: {
sessionId: existingSessionId,
updatedAt: new Date(2026, 0, 18, 4, 45, 0).getTime(),
@@ -587,7 +594,7 @@ describe("initSessionState reset policy", () => {
const sessionKey = "agent:main:slack:channel:c1:thread:123";
const existingSessionId = "thread-session-id";
await saveSessionStore(storePath, {
await writeSessionStoreFast(storePath, {
[sessionKey]: {
sessionId: existingSessionId,
updatedAt: new Date(2026, 0, 18, 3, 0, 0).getTime(),
@@ -618,7 +625,7 @@ describe("initSessionState reset policy", () => {
const sessionKey = "agent:main:discord:channel:c1";
const existingSessionId = "thread-nosuffix";
await saveSessionStore(storePath, {
await writeSessionStoreFast(storePath, {
[sessionKey]: {
sessionId: existingSessionId,
updatedAt: new Date(2026, 0, 18, 3, 0, 0).getTime(),
@@ -648,7 +655,7 @@ describe("initSessionState reset policy", () => {
const sessionKey = "agent:main:whatsapp:dm:s4";
const existingSessionId = "type-default-session";
await saveSessionStore(storePath, {
await writeSessionStoreFast(storePath, {
[sessionKey]: {
sessionId: existingSessionId,
updatedAt: new Date(2026, 0, 18, 3, 0, 0).getTime(),
@@ -678,7 +685,7 @@ describe("initSessionState reset policy", () => {
const sessionKey = "agent:main:whatsapp:dm:s3";
const existingSessionId = "legacy-session-id";
await saveSessionStore(storePath, {
await writeSessionStoreFast(storePath, {
[sessionKey]: {
sessionId: existingSessionId,
updatedAt: new Date(2026, 0, 18, 3, 30, 0).getTime(),
@@ -710,7 +717,7 @@ describe("initSessionState channel reset overrides", () => {
const sessionId = "session-override";
const updatedAt = Date.now() - (10080 - 1) * 60_000;
await saveSessionStore(storePath, {
await writeSessionStoreFast(storePath, {
[sessionKey]: {
sessionId,
updatedAt,
@@ -747,7 +754,7 @@ describe("initSessionState reset triggers in WhatsApp groups", () => {
sessionKey: string;
sessionId: string;
}): Promise<void> {
await saveSessionStore(params.storePath, {
await writeSessionStoreFast(params.storePath, {
[params.sessionKey]: {
sessionId: params.sessionId,
updatedAt: Date.now(),
@@ -840,7 +847,7 @@ describe("initSessionState reset triggers in Slack channels", () => {
sessionKey: string;
sessionId: string;
}): Promise<void> {
await saveSessionStore(params.storePath, {
await writeSessionStoreFast(params.storePath, {
[params.sessionKey]: {
sessionId: params.sessionId,
updatedAt: Date.now(),
@@ -989,7 +996,7 @@ describe("initSessionState preserves behavior overrides across /new and /reset",
sessionId: string;
overrides: Record<string, unknown>;
}): Promise<void> {
await saveSessionStore(params.storePath, {
await writeSessionStoreFast(params.storePath, {
[params.sessionKey]: {
sessionId: params.sessionId,
updatedAt: Date.now(),
@@ -1390,7 +1397,7 @@ describe("initSessionState stale threadId fallback", () => {
describe("initSessionState dmScope delivery migration", () => {
it("retires stale main-session delivery route when dmScope uses per-channel DM keys", async () => {
const storePath = await createStorePath("dm-scope-retire-main-route-");
await saveSessionStore(storePath, {
await writeSessionStoreFast(storePath, {
"agent:main:main": {
sessionId: "legacy-main",
updatedAt: Date.now(),
@@ -1436,7 +1443,7 @@ describe("initSessionState dmScope delivery migration", () => {
it("keeps legacy main-session delivery route when current DM target does not match", async () => {
const storePath = await createStorePath("dm-scope-keep-main-route-");
await saveSessionStore(storePath, {
await writeSessionStoreFast(storePath, {
"agent:main:main": {
sessionId: "legacy-main",
updatedAt: Date.now(),
@@ -1483,7 +1490,7 @@ describe("initSessionState internal channel routing preservation", () => {
it("keeps persisted external lastChannel when OriginatingChannel is internal webchat", async () => {
const storePath = await createStorePath("preserve-external-channel-");
const sessionKey = "agent:main:telegram:group:12345";
await saveSessionStore(storePath, {
await writeSessionStoreFast(storePath, {
[sessionKey]: {
sessionId: "sess-1",
updatedAt: Date.now(),
@@ -1517,7 +1524,7 @@ describe("initSessionState internal channel routing preservation", () => {
it("keeps persisted external route when OriginatingChannel is non-deliverable", async () => {
const storePath = await createStorePath("preserve-nondeliverable-route-");
const sessionKey = "agent:main:discord:channel:24680";
await saveSessionStore(storePath, {
await writeSessionStoreFast(storePath, {
[sessionKey]: {
sessionId: "sess-2",
updatedAt: Date.now(),

View File

@@ -126,20 +126,16 @@ async function waitForListMatch<T>(
timeoutMs = RELAY_LIST_MATCH_TIMEOUT_MS,
intervalMs = 20,
): Promise<T> {
let latest: T | undefined;
await expect
.poll(
async () => {
latest = await fetchList();
return predicate(latest);
},
{ timeout: timeoutMs, interval: intervalMs },
)
.toBe(true);
if (latest === undefined) {
throw new Error("expected list value");
const deadline = Date.now() + timeoutMs;
let latest: T | null = null;
while (Date.now() <= deadline) {
latest = await fetchList();
if (predicate(latest)) {
return latest;
}
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
return latest;
throw new Error("timeout waiting for list match");
}
describe("chrome extension relay server", () => {
@@ -453,14 +449,13 @@ describe("chrome extension relay server", () => {
}),
);
const list = await waitForListMatch(
await waitForListMatch(
async () =>
(await fetch(`${cdpUrl}/json/list`, {
headers: relayAuthHeaders(cdpUrl),
}).then((r) => r.json())) as Array<{ id?: string }>,
(entries) => entries.some((entry) => entry.id === "t-minimal"),
);
expect(list.some((entry) => entry.id === "t-minimal")).toBe(true);
});
it("waits briefly for extension reconnect before failing CDP commands", async () => {
@@ -666,7 +661,7 @@ describe("chrome extension relay server", () => {
}),
);
const list2 = await waitForListMatch(
await waitForListMatch(
async () =>
(await fetch(`${cdpUrl}/json/list`, {
headers: relayAuthHeaders(cdpUrl),
@@ -683,12 +678,6 @@ describe("chrome extension relay server", () => {
t.title === "DER STANDARD",
),
);
expect(
list2.some(
(t) =>
t.id === "t1" && t.url === "https://www.derstandard.at/" && t.title === "DER STANDARD",
),
).toBe(true);
const cdp = new WebSocket(`ws://127.0.0.1:${port}/cdp`, {
headers: relayAuthHeaders(`ws://127.0.0.1:${port}/cdp`),
@@ -699,7 +688,10 @@ describe("chrome extension relay server", () => {
cdp.send(JSON.stringify({ id: 1, method: "Target.getTargets" }));
const res1 = JSON.parse(await q.next()) as { id: number; result?: unknown };
expect(res1.id).toBe(1);
expect(JSON.stringify(res1.result ?? {})).toContain("t1");
const targetInfos = (
res1.result as { targetInfos?: Array<{ targetId?: string }> } | undefined
)?.targetInfos;
expect((targetInfos ?? []).some((target) => target.targetId === "t1")).toBe(true);
cdp.send(
JSON.stringify({
@@ -719,11 +711,13 @@ describe("chrome extension relay server", () => {
const res2 = received.find((m) => m.id === 2);
expect(res2?.id).toBe(2);
expect(JSON.stringify(res2?.result ?? {})).toContain("cb-tab-1");
expect((res2?.result as { sessionId?: string } | undefined)?.sessionId).toBe("cb-tab-1");
const evt = received.find((m) => m.method === "Target.attachedToTarget");
expect(evt?.method).toBe("Target.attachedToTarget");
expect(JSON.stringify(evt?.params ?? {})).toContain("t1");
expect(
(evt?.params as { targetInfo?: { targetId?: string } } | undefined)?.targetInfo?.targetId,
).toBe("t1");
cdp.close();
ext.close();
@@ -771,15 +765,13 @@ describe("chrome extension relay server", () => {
}),
);
const updatedList = await waitForListMatch(
await waitForListMatch(
async () =>
(await fetch(`${cdpUrl}/json/list`, {
headers: relayAuthHeaders(cdpUrl),
}).then((r) => r.json())) as Array<{ id?: string }>,
(list) => list.every((target) => target.id !== "t1"),
);
expect(updatedList.some((target) => target.id === "t1")).toBe(false);
ext.close();
});
@@ -860,14 +852,13 @@ describe("chrome extension relay server", () => {
expect(response?.id).toBe(77);
expect(response?.error?.message ?? "").toContain("No target with given id");
const updatedList = await waitForListMatch(
await waitForListMatch(
async () =>
(await fetch(`${cdpUrl}/json/list`, {
headers: relayAuthHeaders(cdpUrl),
}).then((r) => r.json())) as Array<{ id?: string }>,
(list) => list.every((target) => target.id !== "t1"),
);
expect(updatedList.some((target) => target.id === "t1")).toBe(false);
cdp.close();
ext.close();
@@ -903,7 +894,9 @@ describe("chrome extension relay server", () => {
const first = JSON.parse(await q.next()) as { method?: string; params?: unknown };
expect(first.method).toBe("Target.attachedToTarget");
expect(JSON.stringify(first.params ?? {})).toContain("t1");
expect(
(first.params as { targetInfo?: { targetId?: string } } | undefined)?.targetInfo?.targetId,
).toBe("t1");
ext.send(
JSON.stringify({
@@ -930,8 +923,11 @@ describe("chrome extension relay server", () => {
const detached = received.find((m) => m.method === "Target.detachedFromTarget");
const attached = received.find((m) => m.method === "Target.attachedToTarget");
expect(JSON.stringify(detached?.params ?? {})).toContain("t1");
expect(JSON.stringify(attached?.params ?? {})).toContain("t2");
expect((detached?.params as { targetId?: string } | undefined)?.targetId).toBe("t1");
expect(
(attached?.params as { targetInfo?: { targetId?: string } } | undefined)?.targetInfo
?.targetId,
).toBe("t2");
cdp.close();
ext.close();
@@ -1007,14 +1003,13 @@ describe("chrome extension relay server", () => {
}),
);
const list1 = await waitForListMatch(
await waitForListMatch(
async () =>
(await fetch(`${cdpUrl}/json/list`, {
headers: relayAuthHeaders(cdpUrl),
}).then((r) => r.json())) as Array<{ id?: string }>,
(list) => list.some((t) => t.id === "t10"),
);
expect(list1.some((t) => t.id === "t10")).toBe(true);
// Disconnect extension and wait for grace period cleanup.
const ext1Closed = waitForClose(ext1, 2_000);

View File

@@ -1,4 +1,3 @@
import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
@@ -18,6 +17,11 @@ import { buildOpenAiResponsesProviderConfig } from "./test-openai-responses-mode
let writeConfigFile: typeof import("../config/config.js").writeConfigFile;
let resolveConfigPath: typeof import("../config/config.js").resolveConfigPath;
const GATEWAY_E2E_TIMEOUT_MS = 30_000;
let gatewayTestSeq = 0;
function nextGatewayId(prefix: string): string {
return `${prefix}-${process.pid}-${process.env.VITEST_POOL_ID ?? "0"}-${gatewayTestSeq++}`;
}
describe("gateway e2e", () => {
beforeAll(async () => {
@@ -49,14 +53,14 @@ describe("gateway e2e", () => {
process.env.OPENCLAW_SKIP_CANVAS_HOST = "1";
process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER = "1";
const token = `test-${randomUUID()}`;
const token = nextGatewayId("test-token");
process.env.OPENCLAW_GATEWAY_TOKEN = token;
const workspaceDir = path.join(tempHome, "openclaw");
await fs.mkdir(workspaceDir, { recursive: true });
const nonceA = randomUUID();
const nonceB = randomUUID();
const nonceA = nextGatewayId("nonce-a");
const nonceB = nextGatewayId("nonce-b");
const toolProbePath = path.join(workspaceDir, `.openclaw-tool-probe.${nonceA}.txt`);
await fs.writeFile(toolProbePath, `nonceA=${nonceA}\nnonceB=${nonceB}\n`);
@@ -90,7 +94,7 @@ describe("gateway e2e", () => {
model: "openai/gpt-5.2",
});
const runId = randomUUID();
const runId = nextGatewayId("run");
const payload = await client.request<{
status?: unknown;
result?: unknown;
@@ -149,7 +153,7 @@ describe("gateway e2e", () => {
delete process.env.OPENCLAW_STATE_DIR;
delete process.env.OPENCLAW_CONFIG_PATH;
const wizardToken = `wiz-${randomUUID()}`;
const wizardToken = nextGatewayId("wiz-token");
const port = await getFreeGatewayPort();
const server = await startGatewayServer(port, {
bind: "loopback",

View File

@@ -26,6 +26,8 @@ import {
writeTrustedProxyControlUiConfig,
} from "./server.auth.shared.js";
let controlUiIdentityPathSeq = 0;
export function registerControlUiAndPairingSuite(): void {
const trustedProxyControlUiCases: Array<{
name: string;
@@ -195,7 +197,6 @@ export function registerControlUiAndPairingSuite(): void {
const challenge = await challengePromise;
const nonce = (challenge.payload as { nonce?: unknown } | undefined)?.nonce;
expect(typeof nonce).toBe("string");
const { randomUUID } = await import("node:crypto");
const os = await import("node:os");
const path = await import("node:path");
const scopes = [
@@ -210,7 +211,10 @@ export function registerControlUiAndPairingSuite(): void {
scopes,
clientId: GATEWAY_CLIENT_NAMES.CONTROL_UI,
clientMode: GATEWAY_CLIENT_MODES.WEBCHAT,
identityPath: path.join(os.tmpdir(), `openclaw-controlui-device-${randomUUID()}.json`),
identityPath: path.join(
os.tmpdir(),
`openclaw-controlui-device-${process.pid}-${process.env.VITEST_POOL_ID ?? "0"}-${controlUiIdentityPathSeq++}.json`,
),
nonce: String(nonce),
});
const res = await connectReq(ws, {

View File

@@ -1,4 +1,3 @@
import { randomUUID } from "node:crypto";
import os from "node:os";
import path from "node:path";
import { expect } from "vitest";
@@ -22,6 +21,22 @@ import {
withGatewayServer,
} from "./test-helpers.js";
let authIdentityPathSeq = 0;
function nextAuthIdentityPath(prefix: string): string {
const poolId = process.env.VITEST_POOL_ID ?? "0";
const fileName =
prefix +
"-" +
String(process.pid) +
"-" +
poolId +
"-" +
String(authIdentityPathSeq++) +
".json";
return path.join(os.tmpdir(), fileName);
}
async function waitForWsClose(ws: WebSocket, timeoutMs: number): Promise<boolean> {
if (ws.readyState === WebSocket.CLOSED) {
return true;
@@ -287,10 +302,7 @@ async function startRateLimitedTokenServerWithPairedDeviceToken() {
} as any;
const { server, ws, port, prevToken } = await startServerWithClient();
const deviceIdentityPath = path.join(
os.tmpdir(),
"openclaw-auth-rate-limit-" + randomUUID() + ".json",
);
const deviceIdentityPath = nextAuthIdentityPath("openclaw-auth-rate-limit");
try {
const initial = await connectReq(ws, { token: "secret", deviceIdentityPath });
if (!initial.ok) {
@@ -321,10 +333,7 @@ async function ensurePairedDeviceTokenForCurrentIdentity(ws: WebSocket): Promise
const { loadOrCreateDeviceIdentity } = await import("../infra/device-identity.js");
const { getPairedDevice } = await import("../infra/device-pairing.js");
const deviceIdentityPath = path.join(
os.tmpdir(),
"openclaw-auth-device-" + randomUUID() + ".json",
);
const deviceIdentityPath = nextAuthIdentityPath("openclaw-auth-device");
const res = await connectReq(ws, { token: "secret", deviceIdentityPath });
if (!res.ok) {

View File

@@ -61,6 +61,7 @@ const GATEWAY_TEST_ENV_KEYS = [
let gatewayEnvSnapshot: ReturnType<typeof captureEnv> | undefined;
let tempHome: string | undefined;
let tempConfigRoot: string | undefined;
let suiteConfigRootSeq = 0;
export async function writeSessionStore(params: {
entries: Record<string, Partial<SessionEntry>>;
@@ -121,7 +122,11 @@ async function resetGatewayTestState(options: { uniqueConfigRoot: boolean }) {
}
applyGatewaySkipEnv();
if (options.uniqueConfigRoot) {
tempConfigRoot = await fs.mkdtemp(path.join(tempHome, "openclaw-test-"));
const suiteRoot = path.join(tempHome, ".openclaw-test-suite");
await fs.mkdir(suiteRoot, { recursive: true });
tempConfigRoot = path.join(suiteRoot, `case-${suiteConfigRootSeq++}`);
await fs.rm(tempConfigRoot, { recursive: true, force: true });
await fs.mkdir(tempConfigRoot, { recursive: true });
} else {
tempConfigRoot = path.join(tempHome, ".openclaw-test");
await fs.rm(tempConfigRoot, { recursive: true, force: true });
@@ -182,6 +187,9 @@ async function cleanupGatewayTestHome(options: { restoreEnv: boolean }) {
tempHome = undefined;
}
tempConfigRoot = undefined;
if (options.restoreEnv) {
suiteConfigRootSeq = 0;
}
}
export function installGatewayTestHooks(options?: { scope?: "test" | "suite" }) {

View File

@@ -9,7 +9,7 @@ type AnyMock = MockFn<(...args: unknown[]) => unknown>;
type AnyAsyncMock = MockFn<(...args: unknown[]) => Promise<unknown>>;
const { sessionStorePath } = vi.hoisted(() => ({
sessionStorePath: `/tmp/openclaw-telegram-${Math.random().toString(16).slice(2)}.json`,
sessionStorePath: `/tmp/openclaw-telegram-${process.pid}-${process.env.VITEST_POOL_ID ?? "0"}.json`,
}));
const { loadWebMedia } = vi.hoisted((): { loadWebMedia: AnyMock } => ({
@@ -212,6 +212,17 @@ export const getOnHandler = (event: string) => {
return handler as (ctx: Record<string, unknown>) => Promise<void>;
};
const DEFAULT_TELEGRAM_TEST_CONFIG: OpenClawConfig = {
agents: {
defaults: {
envelopeTimezone: "utc",
},
},
channels: {
telegram: { dmPolicy: "open", allowFrom: ["*"] },
},
};
export function makeTelegramMessageCtx(params: {
chat: {
id: number;
@@ -265,16 +276,7 @@ export function makeForumGroupMessageCtx(params?: {
beforeEach(() => {
resetInboundDedupe();
loadConfig.mockReset();
loadConfig.mockReturnValue({
agents: {
defaults: {
envelopeTimezone: "utc",
},
},
channels: {
telegram: { dmPolicy: "open", allowFrom: ["*"] },
},
});
loadConfig.mockReturnValue(DEFAULT_TELEGRAM_TEST_CONFIG);
loadWebMedia.mockReset();
readChannelAllowFromStore.mockReset();
readChannelAllowFromStore.mockResolvedValue([]);

View File

@@ -13,6 +13,13 @@ type EnvSnapshot = {
stateDir: string | undefined;
};
type SharedHomeRootState = {
rootPromise: Promise<string>;
nextCaseId: number;
};
const SHARED_HOME_ROOTS = new Map<string, SharedHomeRootState>();
function snapshotEnv(): EnvSnapshot {
return {
home: process.env.HOME,
@@ -76,11 +83,27 @@ function setTempHome(base: string) {
process.env.HOMEPATH = match[2] || "\\";
}
async function allocateTempHomeBase(prefix: string): Promise<string> {
let state = SHARED_HOME_ROOTS.get(prefix);
if (!state) {
state = {
rootPromise: fs.mkdtemp(path.join(os.tmpdir(), prefix)),
nextCaseId: 0,
};
SHARED_HOME_ROOTS.set(prefix, state);
}
const root = await state.rootPromise;
const base = path.join(root, `case-${state.nextCaseId++}`);
await fs.mkdir(base, { recursive: true });
return base;
}
export async function withTempHome<T>(
fn: (home: string) => Promise<T>,
opts: { env?: Record<string, EnvValue>; prefix?: string } = {},
): Promise<T> {
const base = await fs.mkdtemp(path.join(os.tmpdir(), opts.prefix ?? "openclaw-test-home-"));
const prefix = opts.prefix ?? "openclaw-test-home-";
const base = await allocateTempHomeBase(prefix);
const snapshot = snapshotEnv();
const envKeys = Object.keys(opts.env ?? {});
for (const key of envKeys) {