mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
test: speed up heavy suites with shared fixtures
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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"));
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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" }) {
|
||||
|
||||
@@ -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([]);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user