diff --git a/src/agents/tools/web-fetch.cf-markdown.test.ts b/src/agents/tools/web-fetch.cf-markdown.test.ts index d73300681f..a9602291d2 100644 --- a/src/agents/tools/web-fetch.cf-markdown.test.ts +++ b/src/agents/tools/web-fetch.cf-markdown.test.ts @@ -1,9 +1,15 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import * as ssrf from "../../infra/net/ssrf.js"; import * as logger from "../../logger.js"; +import { createWebFetchTool } from "./web-tools.js"; const lookupMock = vi.fn(); const resolvePinnedHostname = ssrf.resolvePinnedHostname; +const baseToolConfig = { + config: { + tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } }, + }, +} as const; function makeHeaders(map: Record): { get: (key: string) => string | null } { return { @@ -51,12 +57,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => { // @ts-expect-error mock fetch global.fetch = fetchSpy; - const { createWebFetchTool } = await import("./web-tools.js"); - const tool = createWebFetchTool({ - config: { - tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } }, - }, - }); + const tool = createWebFetchTool(baseToolConfig); await tool?.execute?.("call", { url: "https://example.com/page" }); @@ -71,12 +72,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => { // @ts-expect-error mock fetch global.fetch = fetchSpy; - const { createWebFetchTool } = await import("./web-tools.js"); - const tool = createWebFetchTool({ - config: { - tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } }, - }, - }); + const tool = createWebFetchTool(baseToolConfig); const result = await tool?.execute?.("call", { url: "https://example.com/cf" }); expect(result?.details).toMatchObject({ @@ -96,12 +92,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => { // @ts-expect-error mock fetch global.fetch = fetchSpy; - const { createWebFetchTool } = await import("./web-tools.js"); - const tool = createWebFetchTool({ - config: { - tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } }, - }, - }); + const tool = createWebFetchTool(baseToolConfig); const result = await tool?.execute?.("call", { url: "https://example.com/html" }); expect(result?.details?.extractor).not.toBe("cf-markdown"); @@ -116,12 +107,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => { // @ts-expect-error mock fetch global.fetch = fetchSpy; - const { createWebFetchTool } = await import("./web-tools.js"); - const tool = createWebFetchTool({ - config: { - tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } }, - }, - }); + const tool = createWebFetchTool(baseToolConfig); await tool?.execute?.("call", { url: "https://example.com/tokens/private?token=secret" }); @@ -142,12 +128,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => { // @ts-expect-error mock fetch global.fetch = fetchSpy; - const { createWebFetchTool } = await import("./web-tools.js"); - const tool = createWebFetchTool({ - config: { - tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } }, - }, - }); + const tool = createWebFetchTool(baseToolConfig); const result = await tool?.execute?.("call", { url: "https://example.com/text-mode", @@ -169,12 +150,7 @@ describe("web_fetch Cloudflare Markdown for Agents", () => { // @ts-expect-error mock fetch global.fetch = fetchSpy; - const { createWebFetchTool } = await import("./web-tools.js"); - const tool = createWebFetchTool({ - config: { - tools: { web: { fetch: { cacheTtlMinutes: 0, firecrawl: { enabled: false } } } }, - }, - }); + const tool = createWebFetchTool(baseToolConfig); await tool?.execute?.("call", { url: "https://example.com/no-tokens" }); diff --git a/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts b/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts index 4a98144ed9..55216b79bb 100644 --- a/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts +++ b/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts @@ -28,10 +28,7 @@ const sessionMocks = vi.hoisted(() => ({ })); vi.mock("./pw-session.js", () => sessionMocks); - -async function importModule() { - return await import("./pw-tools-core.js"); -} +const mod = await import("./pw-tools-core.js"); describe("pw-tools-core", () => { beforeEach(() => { @@ -53,7 +50,6 @@ describe("pw-tools-core", () => { currentRefLocator = { scrollIntoViewIfNeeded }; currentPage = {}; - const mod = await importModule(); await mod.scrollIntoViewViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", targetId: "T1", @@ -70,7 +66,6 @@ describe("pw-tools-core", () => { currentRefLocator = { scrollIntoViewIfNeeded }; currentPage = {}; - const mod = await importModule(); await expect( mod.scrollIntoViewViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", @@ -86,7 +81,6 @@ describe("pw-tools-core", () => { currentRefLocator = { scrollIntoViewIfNeeded }; currentPage = {}; - const mod = await importModule(); await expect( mod.scrollIntoViewViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", @@ -102,7 +96,6 @@ describe("pw-tools-core", () => { currentRefLocator = { click }; currentPage = {}; - const mod = await importModule(); await expect( mod.clickViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", @@ -118,7 +111,6 @@ describe("pw-tools-core", () => { currentRefLocator = { click }; currentPage = {}; - const mod = await importModule(); await expect( mod.clickViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", @@ -136,7 +128,6 @@ describe("pw-tools-core", () => { currentRefLocator = { click }; currentPage = {}; - const mod = await importModule(); await expect( mod.clickViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", diff --git a/src/browser/pw-tools-core.last-file-chooser-arm-wins.test.ts b/src/browser/pw-tools-core.last-file-chooser-arm-wins.test.ts index a197691ca7..baaf3e1ba8 100644 --- a/src/browser/pw-tools-core.last-file-chooser-arm-wins.test.ts +++ b/src/browser/pw-tools-core.last-file-chooser-arm-wins.test.ts @@ -28,10 +28,7 @@ const sessionMocks = vi.hoisted(() => ({ })); vi.mock("./pw-session.js", () => sessionMocks); - -async function importModule() { - return await import("./pw-tools-core.js"); -} +const mod = await import("./pw-tools-core.js"); describe("pw-tools-core", () => { beforeEach(() => { @@ -75,7 +72,6 @@ describe("pw-tools-core", () => { keyboard: { press: vi.fn(async () => {}) }, }; - const mod = await importModule(); await mod.armFileUploadViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", paths: ["/tmp/1"], @@ -101,7 +97,6 @@ describe("pw-tools-core", () => { waitForEvent, }; - const mod = await importModule(); await mod.armDialogViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", accept: true, @@ -145,7 +140,6 @@ describe("pw-tools-core", () => { getByText: vi.fn(() => ({ first: () => ({ waitFor: vi.fn() }) })), }; - const mod = await importModule(); await mod.waitForViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", selector: "#main", diff --git a/src/browser/pw-tools-core.screenshots-element-selector.test.ts b/src/browser/pw-tools-core.screenshots-element-selector.test.ts index a297f7d512..96a4a06ea5 100644 --- a/src/browser/pw-tools-core.screenshots-element-selector.test.ts +++ b/src/browser/pw-tools-core.screenshots-element-selector.test.ts @@ -28,10 +28,7 @@ const sessionMocks = vi.hoisted(() => ({ })); vi.mock("./pw-session.js", () => sessionMocks); - -async function importModule() { - return await import("./pw-tools-core.js"); -} +const mod = await import("./pw-tools-core.js"); describe("pw-tools-core", () => { beforeEach(() => { @@ -57,7 +54,6 @@ describe("pw-tools-core", () => { screenshot: vi.fn(async () => Buffer.from("P")), }; - const mod = await importModule(); const res = await mod.takeScreenshotViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", targetId: "T1", @@ -78,7 +74,6 @@ describe("pw-tools-core", () => { screenshot: vi.fn(async () => Buffer.from("P")), }; - const mod = await importModule(); const res = await mod.takeScreenshotViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", targetId: "T1", @@ -99,8 +94,6 @@ describe("pw-tools-core", () => { screenshot: vi.fn(async () => Buffer.from("P")), }; - const mod = await importModule(); - await expect( mod.takeScreenshotViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", @@ -127,7 +120,6 @@ describe("pw-tools-core", () => { keyboard: { press: vi.fn(async () => {}) }, }; - const mod = await importModule(); await mod.armFileUploadViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", targetId: "T1", @@ -151,7 +143,6 @@ describe("pw-tools-core", () => { keyboard: { press }, }; - const mod = await importModule(); await mod.armFileUploadViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", paths: [], diff --git a/src/browser/pw-tools-core.waits-next-download-saves-it.test.ts b/src/browser/pw-tools-core.waits-next-download-saves-it.test.ts index 9ff8d1acab..59d233e000 100644 --- a/src/browser/pw-tools-core.waits-next-download-saves-it.test.ts +++ b/src/browser/pw-tools-core.waits-next-download-saves-it.test.ts @@ -33,10 +33,7 @@ const tmpDirMocks = vi.hoisted(() => ({ resolvePreferredOpenClawTmpDir: vi.fn(() => "/tmp/openclaw"), })); vi.mock("../infra/tmp-openclaw-dir.js", () => tmpDirMocks); - -async function importModule() { - return await import("./pw-tools-core.js"); -} +const mod = await import("./pw-tools-core.js"); describe("pw-tools-core", () => { beforeEach(() => { @@ -75,7 +72,6 @@ describe("pw-tools-core", () => { currentPage = { on, off }; - const mod = await importModule(); const targetPath = path.resolve("/tmp/file.bin"); const p = mod.waitForDownloadViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", @@ -113,7 +109,6 @@ describe("pw-tools-core", () => { currentPage = { on, off }; - const mod = await importModule(); const targetPath = path.resolve("/tmp/report.pdf"); const p = mod.downloadViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", @@ -152,7 +147,6 @@ describe("pw-tools-core", () => { tmpDirMocks.resolvePreferredOpenClawTmpDir.mockReturnValue("/tmp/openclaw-preferred"); currentPage = { on, off }; - const mod = await importModule(); const p = mod.waitForDownloadViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", targetId: "T1", @@ -194,7 +188,6 @@ describe("pw-tools-core", () => { text: async () => '{"ok":true,"value":123}', }; - const mod = await importModule(); const p = mod.responseBodyViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", targetId: "T1", @@ -218,7 +211,6 @@ describe("pw-tools-core", () => { currentRefLocator = { scrollIntoViewIfNeeded }; currentPage = {}; - const mod = await importModule(); await mod.scrollIntoViewViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", targetId: "T1", @@ -232,7 +224,6 @@ describe("pw-tools-core", () => { currentRefLocator = { scrollIntoViewIfNeeded: vi.fn(async () => {}) }; currentPage = {}; - const mod = await importModule(); await expect( mod.scrollIntoViewViaPlaywright({ cdpUrl: "http://127.0.0.1:18792", diff --git a/src/browser/server.agent-contract-snapshot-endpoints.test.ts b/src/browser/server.agent-contract-snapshot-endpoints.test.ts index ab8c70317d..8c4530a91a 100644 --- a/src/browser/server.agent-contract-snapshot-endpoints.test.ts +++ b/src/browser/server.agent-contract-snapshot-endpoints.test.ts @@ -154,6 +154,9 @@ vi.mock("./screenshot.js", () => ({ })), })); +const { startBrowserControlServerFromConfig, stopBrowserControlServer } = + await import("./server.js"); + async function getFreePort(): Promise { while (true) { const port = await new Promise((resolve, reject) => { @@ -271,12 +274,10 @@ describe("browser control server", () => { } else { process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort; } - const { stopBrowserControlServer } = await import("./server.js"); await stopBrowserControlServer(); }); const startServerAndBase = async () => { - const { startBrowserControlServerFromConfig } = await import("./server.js"); await startBrowserControlServerFromConfig(); const base = `http://127.0.0.1:${testPort}`; await realFetch(`${base}/start`, { method: "POST" }).then((r) => r.json()); diff --git a/src/browser/server.evaluate-disabled-does-not-block-storage.test.ts b/src/browser/server.evaluate-disabled-does-not-block-storage.test.ts index b24438f278..c7d3f6c952 100644 --- a/src/browser/server.evaluate-disabled-does-not-block-storage.test.ts +++ b/src/browser/server.evaluate-disabled-does-not-block-storage.test.ts @@ -63,6 +63,9 @@ vi.mock("./server-context.js", async (importOriginal) => { }; }); +const { startBrowserControlServerFromConfig, stopBrowserControlServer } = + await import("./server.js"); + async function getFreePort(): Promise { const probe = createServer(); await new Promise((resolve, reject) => { @@ -95,12 +98,10 @@ describe("browser control evaluate gating", () => { process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort; } - const { stopBrowserControlServer } = await import("./server.js"); await stopBrowserControlServer(); }); it("blocks act:evaluate but still allows cookies/storage reads", async () => { - const { startBrowserControlServerFromConfig } = await import("./server.js"); await startBrowserControlServerFromConfig(); const base = `http://127.0.0.1:${testPort}`; diff --git a/src/browser/server.post-tabs-open-profile-unknown-returns-404.test.ts b/src/browser/server.post-tabs-open-profile-unknown-returns-404.test.ts index e2c75a85f0..e4c828f6d3 100644 --- a/src/browser/server.post-tabs-open-profile-unknown-returns-404.test.ts +++ b/src/browser/server.post-tabs-open-profile-unknown-returns-404.test.ts @@ -153,6 +153,9 @@ vi.mock("./screenshot.js", () => ({ })), })); +const { startBrowserControlServerFromConfig, stopBrowserControlServer } = + await import("./server.js"); + async function getFreePort(): Promise { while (true) { const port = await new Promise((resolve, reject) => { @@ -270,12 +273,10 @@ describe("browser control server", () => { } else { process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort; } - const { stopBrowserControlServer } = await import("./server.js"); await stopBrowserControlServer(); }); it("POST /tabs/open?profile=unknown returns 404", async () => { - const { startBrowserControlServerFromConfig } = await import("./server.js"); await startBrowserControlServerFromConfig(); const base = `http://127.0.0.1:${testPort}`; @@ -307,9 +308,6 @@ describe("profile CRUD endpoints", () => { prevGatewayPort = process.env.OPENCLAW_GATEWAY_PORT; process.env.OPENCLAW_GATEWAY_PORT = String(testPort - 2); - prevGatewayPort = process.env.OPENCLAW_GATEWAY_PORT; - process.env.OPENCLAW_GATEWAY_PORT = String(testPort - 2); - vi.stubGlobal( "fetch", vi.fn(async (url: string) => { @@ -330,12 +328,10 @@ describe("profile CRUD endpoints", () => { } else { process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort; } - const { stopBrowserControlServer } = await import("./server.js"); await stopBrowserControlServer(); }); it("POST /profiles/create returns 400 for missing name", async () => { - const { startBrowserControlServerFromConfig } = await import("./server.js"); await startBrowserControlServerFromConfig(); const base = `http://127.0.0.1:${testPort}`; @@ -350,7 +346,6 @@ describe("profile CRUD endpoints", () => { }); it("POST /profiles/create returns 400 for invalid name format", async () => { - const { startBrowserControlServerFromConfig } = await import("./server.js"); await startBrowserControlServerFromConfig(); const base = `http://127.0.0.1:${testPort}`; @@ -365,7 +360,6 @@ describe("profile CRUD endpoints", () => { }); it("POST /profiles/create returns 409 for duplicate name", async () => { - const { startBrowserControlServerFromConfig } = await import("./server.js"); await startBrowserControlServerFromConfig(); const base = `http://127.0.0.1:${testPort}`; @@ -381,7 +375,6 @@ describe("profile CRUD endpoints", () => { }); it("POST /profiles/create accepts cdpUrl for remote profiles", async () => { - const { startBrowserControlServerFromConfig } = await import("./server.js"); await startBrowserControlServerFromConfig(); const base = `http://127.0.0.1:${testPort}`; @@ -402,7 +395,6 @@ describe("profile CRUD endpoints", () => { }); it("POST /profiles/create returns 400 for invalid cdpUrl", async () => { - const { startBrowserControlServerFromConfig } = await import("./server.js"); await startBrowserControlServerFromConfig(); const base = `http://127.0.0.1:${testPort}`; @@ -417,7 +409,6 @@ describe("profile CRUD endpoints", () => { }); it("DELETE /profiles/:name returns 404 for non-existent profile", async () => { - const { startBrowserControlServerFromConfig } = await import("./server.js"); await startBrowserControlServerFromConfig(); const base = `http://127.0.0.1:${testPort}`; @@ -430,7 +421,6 @@ describe("profile CRUD endpoints", () => { }); it("DELETE /profiles/:name returns 400 for default profile deletion", async () => { - const { startBrowserControlServerFromConfig } = await import("./server.js"); await startBrowserControlServerFromConfig(); const base = `http://127.0.0.1:${testPort}`; @@ -444,7 +434,6 @@ describe("profile CRUD endpoints", () => { }); it("DELETE /profiles/:name returns 400 for invalid name format", async () => { - const { startBrowserControlServerFromConfig } = await import("./server.js"); await startBrowserControlServerFromConfig(); const base = `http://127.0.0.1:${testPort}`; diff --git a/src/cli/exec-approvals-cli.test.ts b/src/cli/exec-approvals-cli.test.ts index 1d8a1d58dc..a875d58782 100644 --- a/src/cli/exec-approvals-cli.test.ts +++ b/src/cli/exec-approvals-cli.test.ts @@ -59,9 +59,11 @@ vi.mock("../infra/exec-approvals.js", async () => { }; }); +const { registerExecApprovalsCli } = await import("./exec-approvals-cli.js"); +const execApprovals = await import("../infra/exec-approvals.js"); + describe("exec approvals CLI", () => { - const createProgram = async () => { - const { registerExecApprovalsCli } = await import("./exec-approvals-cli.js"); + const createProgram = () => { const program = new Command(); program.exitOverride(); registerExecApprovalsCli(program); @@ -73,21 +75,21 @@ describe("exec approvals CLI", () => { runtimeErrors.length = 0; callGatewayFromCli.mockClear(); - const localProgram = await createProgram(); + const localProgram = createProgram(); await localProgram.parseAsync(["approvals", "get"], { from: "user" }); expect(callGatewayFromCli).not.toHaveBeenCalled(); expect(runtimeErrors).toHaveLength(0); callGatewayFromCli.mockClear(); - const gatewayProgram = await createProgram(); + const gatewayProgram = createProgram(); await gatewayProgram.parseAsync(["approvals", "get", "--gateway"], { from: "user" }); expect(callGatewayFromCli).toHaveBeenCalledWith("exec.approvals.get", expect.anything(), {}); expect(runtimeErrors).toHaveLength(0); callGatewayFromCli.mockClear(); - const nodeProgram = await createProgram(); + const nodeProgram = createProgram(); await nodeProgram.parseAsync(["approvals", "get", "--node", "macbook"], { from: "user" }); expect(callGatewayFromCli).toHaveBeenCalledWith("exec.approvals.node.get", expect.anything(), { @@ -101,11 +103,9 @@ describe("exec approvals CLI", () => { runtimeErrors.length = 0; callGatewayFromCli.mockClear(); - const execApprovals = await import("../infra/exec-approvals.js"); const saveExecApprovals = vi.mocked(execApprovals.saveExecApprovals); saveExecApprovals.mockClear(); - const { registerExecApprovalsCli } = await import("./exec-approvals-cli.js"); const program = new Command(); program.exitOverride(); registerExecApprovalsCli(program); diff --git a/src/cli/update-cli.test.ts b/src/cli/update-cli.test.ts index ca6a3cb165..aa77174127 100644 --- a/src/cli/update-cli.test.ts +++ b/src/cli/update-cli.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { UpdateRunResult } from "../infra/update-runner.js"; const confirm = vi.fn(); @@ -91,6 +91,23 @@ const { updateCommand, registerUpdateCli, updateStatusCommand, updateWizardComma await import("./update-cli.js"); describe("update-cli", () => { + let fixtureRoot = ""; + let fixtureCount = 0; + + const createCaseDir = async (prefix: string) => { + const dir = path.join(fixtureRoot, `${prefix}-${fixtureCount++}`); + await fs.mkdir(dir, { recursive: true }); + return dir; + }; + + beforeAll(async () => { + fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-tests-")); + }); + + afterAll(async () => { + await fs.rm(fixtureRoot, { recursive: true, force: true }); + }); + const baseSnapshot = { valid: true, config: {}, @@ -223,41 +240,37 @@ describe("update-cli", () => { }); it("defaults to stable channel for package installs when unset", async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-")); - try { - await fs.writeFile( - path.join(tempDir, "package.json"), - JSON.stringify({ name: "openclaw", version: "1.0.0" }), - "utf-8", - ); + const tempDir = await createCaseDir("openclaw-update"); + await fs.writeFile( + path.join(tempDir, "package.json"), + JSON.stringify({ name: "openclaw", version: "1.0.0" }), + "utf-8", + ); - vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir); - vi.mocked(checkUpdateStatus).mockResolvedValue({ - root: tempDir, - installKind: "package", - packageManager: "npm", - deps: { - manager: "npm", - status: "ok", - lockfilePath: null, - markerPath: null, - }, - }); - vi.mocked(runGatewayUpdate).mockResolvedValue({ + vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir); + vi.mocked(checkUpdateStatus).mockResolvedValue({ + root: tempDir, + installKind: "package", + packageManager: "npm", + deps: { + manager: "npm", status: "ok", - mode: "npm", - steps: [], - durationMs: 100, - }); + lockfilePath: null, + markerPath: null, + }, + }); + vi.mocked(runGatewayUpdate).mockResolvedValue({ + status: "ok", + mode: "npm", + steps: [], + durationMs: 100, + }); - await updateCommand({ yes: true }); + await updateCommand({ yes: true }); - const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0]; - expect(call?.channel).toBe("stable"); - expect(call?.tag).toBe("latest"); - } finally { - await fs.rm(tempDir, { recursive: true, force: true }); - } + const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0]; + expect(call?.channel).toBe("stable"); + expect(call?.tag).toBe("latest"); }); it("uses stored beta channel when configured", async () => { @@ -279,75 +292,67 @@ describe("update-cli", () => { }); it("falls back to latest when beta tag is older than release", async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-")); - try { - await fs.writeFile( - path.join(tempDir, "package.json"), - JSON.stringify({ name: "openclaw", version: "1.0.0" }), - "utf-8", - ); + const tempDir = await createCaseDir("openclaw-update"); + await fs.writeFile( + path.join(tempDir, "package.json"), + JSON.stringify({ name: "openclaw", version: "1.0.0" }), + "utf-8", + ); - vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir); - vi.mocked(readConfigFileSnapshot).mockResolvedValue({ - ...baseSnapshot, - config: { update: { channel: "beta" } }, - }); - vi.mocked(checkUpdateStatus).mockResolvedValue({ - root: tempDir, - installKind: "package", - packageManager: "npm", - deps: { - manager: "npm", - status: "ok", - lockfilePath: null, - markerPath: null, - }, - }); - vi.mocked(resolveNpmChannelTag).mockResolvedValue({ - tag: "latest", - version: "1.2.3-1", - }); - vi.mocked(runGatewayUpdate).mockResolvedValue({ + vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir); + vi.mocked(readConfigFileSnapshot).mockResolvedValue({ + ...baseSnapshot, + config: { update: { channel: "beta" } }, + }); + vi.mocked(checkUpdateStatus).mockResolvedValue({ + root: tempDir, + installKind: "package", + packageManager: "npm", + deps: { + manager: "npm", status: "ok", - mode: "npm", - steps: [], - durationMs: 100, - }); + lockfilePath: null, + markerPath: null, + }, + }); + vi.mocked(resolveNpmChannelTag).mockResolvedValue({ + tag: "latest", + version: "1.2.3-1", + }); + vi.mocked(runGatewayUpdate).mockResolvedValue({ + status: "ok", + mode: "npm", + steps: [], + durationMs: 100, + }); - await updateCommand({}); + await updateCommand({}); - const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0]; - expect(call?.channel).toBe("beta"); - expect(call?.tag).toBe("latest"); - } finally { - await fs.rm(tempDir, { recursive: true, force: true }); - } + const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0]; + expect(call?.channel).toBe("beta"); + expect(call?.tag).toBe("latest"); }); it("honors --tag override", async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-")); - try { - await fs.writeFile( - path.join(tempDir, "package.json"), - JSON.stringify({ name: "openclaw", version: "1.0.0" }), - "utf-8", - ); + const tempDir = await createCaseDir("openclaw-update"); + await fs.writeFile( + path.join(tempDir, "package.json"), + JSON.stringify({ name: "openclaw", version: "1.0.0" }), + "utf-8", + ); - vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir); - vi.mocked(runGatewayUpdate).mockResolvedValue({ - status: "ok", - mode: "npm", - steps: [], - durationMs: 100, - }); + vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir); + vi.mocked(runGatewayUpdate).mockResolvedValue({ + status: "ok", + mode: "npm", + steps: [], + durationMs: 100, + }); - await updateCommand({ tag: "next" }); + await updateCommand({ tag: "next" }); - const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0]; - expect(call?.tag).toBe("next"); - } finally { - await fs.rm(tempDir, { recursive: true, force: true }); - } + const call = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0]; + expect(call?.tag).toBe("next"); }); it("updateCommand outputs JSON when --json is set", async () => { @@ -471,95 +476,87 @@ describe("update-cli", () => { }); it("requires confirmation on downgrade when non-interactive", async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-")); - try { - setTty(false); - await fs.writeFile( - path.join(tempDir, "package.json"), - JSON.stringify({ name: "openclaw", version: "2.0.0" }), - "utf-8", - ); + const tempDir = await createCaseDir("openclaw-update"); + setTty(false); + await fs.writeFile( + path.join(tempDir, "package.json"), + JSON.stringify({ name: "openclaw", version: "2.0.0" }), + "utf-8", + ); - vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir); - vi.mocked(checkUpdateStatus).mockResolvedValue({ - root: tempDir, - installKind: "package", - packageManager: "npm", - deps: { - manager: "npm", - status: "ok", - lockfilePath: null, - markerPath: null, - }, - }); - vi.mocked(resolveNpmChannelTag).mockResolvedValue({ - tag: "latest", - version: "0.0.1", - }); - vi.mocked(runGatewayUpdate).mockResolvedValue({ + vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir); + vi.mocked(checkUpdateStatus).mockResolvedValue({ + root: tempDir, + installKind: "package", + packageManager: "npm", + deps: { + manager: "npm", status: "ok", - mode: "npm", - steps: [], - durationMs: 100, - }); - vi.mocked(defaultRuntime.error).mockClear(); - vi.mocked(defaultRuntime.exit).mockClear(); + lockfilePath: null, + markerPath: null, + }, + }); + vi.mocked(resolveNpmChannelTag).mockResolvedValue({ + tag: "latest", + version: "0.0.1", + }); + vi.mocked(runGatewayUpdate).mockResolvedValue({ + status: "ok", + mode: "npm", + steps: [], + durationMs: 100, + }); + vi.mocked(defaultRuntime.error).mockClear(); + vi.mocked(defaultRuntime.exit).mockClear(); - await updateCommand({}); + await updateCommand({}); - expect(defaultRuntime.error).toHaveBeenCalledWith( - expect.stringContaining("Downgrade confirmation required."), - ); - expect(defaultRuntime.exit).toHaveBeenCalledWith(1); - } finally { - await fs.rm(tempDir, { recursive: true, force: true }); - } + expect(defaultRuntime.error).toHaveBeenCalledWith( + expect.stringContaining("Downgrade confirmation required."), + ); + expect(defaultRuntime.exit).toHaveBeenCalledWith(1); }); it("allows downgrade with --yes in non-interactive mode", async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-")); - try { - setTty(false); - await fs.writeFile( - path.join(tempDir, "package.json"), - JSON.stringify({ name: "openclaw", version: "2.0.0" }), - "utf-8", - ); + const tempDir = await createCaseDir("openclaw-update"); + setTty(false); + await fs.writeFile( + path.join(tempDir, "package.json"), + JSON.stringify({ name: "openclaw", version: "2.0.0" }), + "utf-8", + ); - vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir); - vi.mocked(checkUpdateStatus).mockResolvedValue({ - root: tempDir, - installKind: "package", - packageManager: "npm", - deps: { - manager: "npm", - status: "ok", - lockfilePath: null, - markerPath: null, - }, - }); - vi.mocked(resolveNpmChannelTag).mockResolvedValue({ - tag: "latest", - version: "0.0.1", - }); - vi.mocked(runGatewayUpdate).mockResolvedValue({ + vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(tempDir); + vi.mocked(checkUpdateStatus).mockResolvedValue({ + root: tempDir, + installKind: "package", + packageManager: "npm", + deps: { + manager: "npm", status: "ok", - mode: "npm", - steps: [], - durationMs: 100, - }); - vi.mocked(defaultRuntime.error).mockClear(); - vi.mocked(defaultRuntime.exit).mockClear(); + lockfilePath: null, + markerPath: null, + }, + }); + vi.mocked(resolveNpmChannelTag).mockResolvedValue({ + tag: "latest", + version: "0.0.1", + }); + vi.mocked(runGatewayUpdate).mockResolvedValue({ + status: "ok", + mode: "npm", + steps: [], + durationMs: 100, + }); + vi.mocked(defaultRuntime.error).mockClear(); + vi.mocked(defaultRuntime.exit).mockClear(); - await updateCommand({ yes: true }); + await updateCommand({ yes: true }); - expect(defaultRuntime.error).not.toHaveBeenCalledWith( - expect.stringContaining("Downgrade confirmation required."), - ); - expect(runGatewayUpdate).toHaveBeenCalled(); - } finally { - await fs.rm(tempDir, { recursive: true, force: true }); - } + expect(defaultRuntime.error).not.toHaveBeenCalledWith( + expect.stringContaining("Downgrade confirmation required."), + ); + expect(runGatewayUpdate).toHaveBeenCalled(); }); it("updateWizardCommand requires a TTY", async () => { @@ -576,7 +573,7 @@ describe("update-cli", () => { }); it("updateWizardCommand offers dev checkout and forwards selections", async () => { - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-update-wizard-")); + const tempDir = await createCaseDir("openclaw-update-wizard"); const previousGitDir = process.env.OPENCLAW_GIT_DIR; try { setTty(true); @@ -608,7 +605,6 @@ describe("update-cli", () => { expect(call?.channel).toBe("dev"); } finally { process.env.OPENCLAW_GIT_DIR = previousGitDir; - await fs.rm(tempDir, { recursive: true, force: true }); } }); }); diff --git a/src/config/config.identity-defaults.test.ts b/src/config/config.identity-defaults.test.ts index fe5286fe6f..48a6710a44 100644 --- a/src/config/config.identity-defaults.test.ts +++ b/src/config/config.identity-defaults.test.ts @@ -1,19 +1,53 @@ import fs from "node:fs/promises"; +import os from "node:os"; import path from "node:path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js"; import { loadConfig } from "./config.js"; -import { withTempHome } from "./test-helpers.js"; + +type HomeEnvSnapshot = { + home: string | undefined; + userProfile: string | undefined; + homeDrive: string | undefined; + homePath: string | undefined; + stateDir: string | undefined; +}; + +function snapshotHomeEnv(): HomeEnvSnapshot { + return { + home: process.env.HOME, + userProfile: process.env.USERPROFILE, + homeDrive: process.env.HOMEDRIVE, + homePath: process.env.HOMEPATH, + stateDir: process.env.OPENCLAW_STATE_DIR, + }; +} + +function restoreHomeEnv(snapshot: HomeEnvSnapshot) { + const restoreKey = (key: string, value: string | undefined) => { + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + }; + restoreKey("HOME", snapshot.home); + restoreKey("USERPROFILE", snapshot.userProfile); + restoreKey("HOMEDRIVE", snapshot.homeDrive); + restoreKey("HOMEPATH", snapshot.homePath); + restoreKey("OPENCLAW_STATE_DIR", snapshot.stateDir); +} describe("config identity defaults", () => { - let previousHome: string | undefined; + let fixtureRoot = ""; + let fixtureCount = 0; - beforeEach(() => { - previousHome = process.env.HOME; + beforeAll(async () => { + fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-identity-")); }); - afterEach(() => { - process.env.HOME = previousHome; + afterAll(async () => { + await fs.rm(fixtureRoot, { recursive: true, force: true }); }); const writeAndLoadConfig = async (home: string, config: Record) => { @@ -27,6 +61,30 @@ describe("config identity defaults", () => { return loadConfig(); }; + const withTempHome = async (fn: (home: string) => Promise): Promise => { + const home = path.join(fixtureRoot, `home-${fixtureCount++}`); + await fs.mkdir(path.join(home, ".openclaw"), { recursive: true }); + + const snapshot = snapshotHomeEnv(); + process.env.HOME = home; + process.env.USERPROFILE = home; + process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw"); + + if (process.platform === "win32") { + const match = home.match(/^([A-Za-z]:)(.*)$/); + if (match) { + process.env.HOMEDRIVE = match[1]; + process.env.HOMEPATH = match[2] || "\\"; + } + } + + try { + return await fn(home); + } finally { + restoreHomeEnv(snapshot); + } + }; + it("does not derive mention defaults and only sets ackReactionScope when identity is present", async () => { await withTempHome(async (home) => { const cfg = await writeAndLoadConfig(home, { diff --git a/src/config/config.plugin-validation.test.ts b/src/config/config.plugin-validation.test.ts index 418af2fdba..c7389a59f2 100644 --- a/src/config/config.plugin-validation.test.ts +++ b/src/config/config.plugin-validation.test.ts @@ -1,8 +1,8 @@ import fs from "node:fs/promises"; +import os from "node:os"; import path from "node:path"; -import { describe, expect, it } from "vitest"; +import { afterAll, describe, expect, it } from "vitest"; import { validateConfigObjectWithPlugins } from "./config.js"; -import { withTempHome } from "./test-helpers.js"; async function writePluginFixture(params: { dir: string; @@ -31,145 +31,150 @@ async function writePluginFixture(params: { } describe("config plugin validation", () => { + const fixtureRoot = path.join(os.tmpdir(), "openclaw-config-plugin-validation"); + let caseIndex = 0; + + function createCaseHome() { + const home = path.join(fixtureRoot, `case-${caseIndex++}`); + return fs.mkdir(home, { recursive: true }).then(() => home); + } + const validateInHome = (home: string, raw: unknown) => { process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw"); return validateConfigObjectWithPlugins(raw); }; + afterAll(async () => { + await fs.rm(fixtureRoot, { recursive: true, force: true }); + }); + it("rejects missing plugin load paths", async () => { - await withTempHome(async (home) => { - const missingPath = path.join(home, "missing-plugin"); - const res = validateInHome(home, { - agents: { list: [{ id: "pi" }] }, - plugins: { enabled: false, load: { paths: [missingPath] } }, - }); - expect(res.ok).toBe(false); - if (!res.ok) { - const hasIssue = res.issues.some( - (issue) => - issue.path === "plugins.load.paths" && issue.message.includes("plugin path not found"), - ); - expect(hasIssue).toBe(true); - } + const home = await createCaseHome(); + const missingPath = path.join(home, "missing-plugin"); + const res = validateInHome(home, { + agents: { list: [{ id: "pi" }] }, + plugins: { enabled: false, load: { paths: [missingPath] } }, }); + expect(res.ok).toBe(false); + if (!res.ok) { + const hasIssue = res.issues.some( + (issue) => + issue.path === "plugins.load.paths" && issue.message.includes("plugin path not found"), + ); + expect(hasIssue).toBe(true); + } }); it("rejects missing plugin ids in entries", async () => { - await withTempHome(async (home) => { - const res = validateInHome(home, { - agents: { list: [{ id: "pi" }] }, - plugins: { enabled: false, entries: { "missing-plugin": { enabled: true } } }, - }); - expect(res.ok).toBe(false); - if (!res.ok) { - expect(res.issues).toContainEqual({ - path: "plugins.entries.missing-plugin", - message: "plugin not found: missing-plugin", - }); - } + const home = await createCaseHome(); + const res = validateInHome(home, { + agents: { list: [{ id: "pi" }] }, + plugins: { enabled: false, entries: { "missing-plugin": { enabled: true } } }, }); + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.issues).toContainEqual({ + path: "plugins.entries.missing-plugin", + message: "plugin not found: missing-plugin", + }); + } }); it("rejects missing plugin ids in allow/deny/slots", async () => { - await withTempHome(async (home) => { - const res = validateInHome(home, { - agents: { list: [{ id: "pi" }] }, - plugins: { - enabled: false, - allow: ["missing-allow"], - deny: ["missing-deny"], - slots: { memory: "missing-slot" }, - }, - }); - expect(res.ok).toBe(false); - if (!res.ok) { - expect(res.issues).toEqual( - expect.arrayContaining([ - { path: "plugins.allow", message: "plugin not found: missing-allow" }, - { path: "plugins.deny", message: "plugin not found: missing-deny" }, - { path: "plugins.slots.memory", message: "plugin not found: missing-slot" }, - ]), - ); - } + const home = await createCaseHome(); + const res = validateInHome(home, { + agents: { list: [{ id: "pi" }] }, + plugins: { + enabled: false, + allow: ["missing-allow"], + deny: ["missing-deny"], + slots: { memory: "missing-slot" }, + }, }); + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.issues).toEqual( + expect.arrayContaining([ + { path: "plugins.allow", message: "plugin not found: missing-allow" }, + { path: "plugins.deny", message: "plugin not found: missing-deny" }, + { path: "plugins.slots.memory", message: "plugin not found: missing-slot" }, + ]), + ); + } }); it("surfaces plugin config diagnostics", async () => { - await withTempHome(async (home) => { - const pluginDir = path.join(home, "bad-plugin"); - await writePluginFixture({ - dir: pluginDir, - id: "bad-plugin", - schema: { - type: "object", - additionalProperties: false, - properties: { - value: { type: "boolean" }, - }, - required: ["value"], + const home = await createCaseHome(); + const pluginDir = path.join(home, "bad-plugin"); + await writePluginFixture({ + dir: pluginDir, + id: "bad-plugin", + schema: { + type: "object", + additionalProperties: false, + properties: { + value: { type: "boolean" }, }, - }); - - const res = validateInHome(home, { - agents: { list: [{ id: "pi" }] }, - plugins: { - enabled: true, - load: { paths: [pluginDir] }, - entries: { "bad-plugin": { config: { value: "nope" } } }, - }, - }); - expect(res.ok).toBe(false); - if (!res.ok) { - const hasIssue = res.issues.some( - (issue) => - issue.path === "plugins.entries.bad-plugin.config" && - issue.message.includes("invalid config"), - ); - expect(hasIssue).toBe(true); - } + required: ["value"], + }, }); + + const res = validateInHome(home, { + agents: { list: [{ id: "pi" }] }, + plugins: { + enabled: true, + load: { paths: [pluginDir] }, + entries: { "bad-plugin": { config: { value: "nope" } } }, + }, + }); + expect(res.ok).toBe(false); + if (!res.ok) { + const hasIssue = res.issues.some( + (issue) => + issue.path === "plugins.entries.bad-plugin.config" && + issue.message.includes("invalid config"), + ); + expect(hasIssue).toBe(true); + } }); it("accepts known plugin ids", async () => { - await withTempHome(async (home) => { - const res = validateInHome(home, { - agents: { list: [{ id: "pi" }] }, - plugins: { enabled: false, entries: { discord: { enabled: true } } }, - }); - expect(res.ok).toBe(true); + const home = await createCaseHome(); + const res = validateInHome(home, { + agents: { list: [{ id: "pi" }] }, + plugins: { enabled: false, entries: { discord: { enabled: true } } }, }); + expect(res.ok).toBe(true); }); it("accepts plugin heartbeat targets", async () => { - await withTempHome(async (home) => { - const pluginDir = path.join(home, "bluebubbles-plugin"); - await writePluginFixture({ - dir: pluginDir, - id: "bluebubbles-plugin", - channels: ["bluebubbles"], - schema: { type: "object" }, - }); - - const res = validateInHome(home, { - agents: { defaults: { heartbeat: { target: "bluebubbles" } }, list: [{ id: "pi" }] }, - plugins: { enabled: false, load: { paths: [pluginDir] } }, - }); - expect(res.ok).toBe(true); + const home = await createCaseHome(); + const pluginDir = path.join(home, "bluebubbles-plugin"); + await writePluginFixture({ + dir: pluginDir, + id: "bluebubbles-plugin", + channels: ["bluebubbles"], + schema: { type: "object" }, }); + + const res = validateInHome(home, { + agents: { defaults: { heartbeat: { target: "bluebubbles" } }, list: [{ id: "pi" }] }, + plugins: { enabled: false, load: { paths: [pluginDir] } }, + }); + expect(res.ok).toBe(true); }); it("rejects unknown heartbeat targets", async () => { - await withTempHome(async (home) => { - const res = validateInHome(home, { - agents: { defaults: { heartbeat: { target: "not-a-channel" } }, list: [{ id: "pi" }] }, - }); - expect(res.ok).toBe(false); - if (!res.ok) { - expect(res.issues).toContainEqual({ - path: "agents.defaults.heartbeat.target", - message: "unknown heartbeat target: not-a-channel", - }); - } + const home = await createCaseHome(); + const res = validateInHome(home, { + agents: { defaults: { heartbeat: { target: "not-a-channel" } }, list: [{ id: "pi" }] }, }); + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.issues).toContainEqual({ + path: "agents.defaults.heartbeat.target", + message: "unknown heartbeat target: not-a-channel", + }); + } }); }); diff --git a/src/hooks/install.test.ts b/src/hooks/install.test.ts index 27a5616be2..0bbfc5bb6c 100644 --- a/src/hooks/install.test.ts +++ b/src/hooks/install.test.ts @@ -4,28 +4,29 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import * as tar from "tar"; -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterAll, describe, expect, it, vi } from "vitest"; -const tempDirs: string[] = []; +const fixtureRoot = path.join(os.tmpdir(), `openclaw-hook-install-${randomUUID()}`); +let tempDirIndex = 0; vi.mock("../process/exec.js", () => ({ runCommandWithTimeout: vi.fn(), })); function makeTempDir() { - const dir = path.join(os.tmpdir(), `openclaw-hook-install-${randomUUID()}`); + const dir = path.join(fixtureRoot, `case-${tempDirIndex++}`); fs.mkdirSync(dir, { recursive: true }); - tempDirs.push(dir); return dir; } -afterEach(() => { - for (const dir of tempDirs.splice(0)) { - try { - fs.rmSync(dir, { recursive: true, force: true }); - } catch { - // ignore cleanup failures - } +const { runCommandWithTimeout } = await import("../process/exec.js"); +const { installHooksFromArchive, installHooksFromPath } = await import("./install.js"); + +afterAll(() => { + try { + fs.rmSync(fixtureRoot, { recursive: true, force: true }); + } catch { + // ignore cleanup failures } }); @@ -61,7 +62,6 @@ describe("installHooksFromArchive", () => { fs.writeFileSync(archivePath, buffer); const hooksDir = path.join(stateDir, "hooks"); - const { installHooksFromArchive } = await import("./install.js"); const result = await installHooksFromArchive({ archivePath, hooksDir }); expect(result.ok).toBe(true); @@ -111,7 +111,6 @@ describe("installHooksFromArchive", () => { await tar.c({ cwd: workDir, file: archivePath }, ["package"]); const hooksDir = path.join(stateDir, "hooks"); - const { installHooksFromArchive } = await import("./install.js"); const result = await installHooksFromArchive({ archivePath, hooksDir }); expect(result.ok).toBe(true); @@ -160,7 +159,6 @@ describe("installHooksFromArchive", () => { await tar.c({ cwd: workDir, file: archivePath }, ["package"]); const hooksDir = path.join(stateDir, "hooks"); - const { installHooksFromArchive } = await import("./install.js"); const result = await installHooksFromArchive({ archivePath, hooksDir }); expect(result.ok).toBe(false); @@ -207,7 +205,6 @@ describe("installHooksFromArchive", () => { await tar.c({ cwd: workDir, file: archivePath }, ["package"]); const hooksDir = path.join(stateDir, "hooks"); - const { installHooksFromArchive } = await import("./install.js"); const result = await installHooksFromArchive({ archivePath, hooksDir }); expect(result.ok).toBe(false); @@ -253,11 +250,9 @@ describe("installHooksFromPath", () => { "utf-8", ); - const { runCommandWithTimeout } = await import("../process/exec.js"); const run = vi.mocked(runCommandWithTimeout); run.mockResolvedValue({ code: 0, stdout: "", stderr: "" }); - const { installHooksFromPath } = await import("./install.js"); const res = await installHooksFromPath({ path: pkgDir, hooksDir: path.join(stateDir, "hooks"), @@ -301,7 +296,6 @@ describe("installHooksFromPath", () => { fs.writeFileSync(path.join(hookDir, "handler.ts"), "export default async () => {};\n"); const hooksDir = path.join(stateDir, "hooks"); - const { installHooksFromPath } = await import("./install.js"); const result = await installHooksFromPath({ path: hookDir, hooksDir }); expect(result.ok).toBe(true); diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index cd27cc69ef..f32d04d0d8 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -2,19 +2,19 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterAll, afterEach, describe, expect, it } from "vitest"; import { loadOpenClawPlugins } from "./loader.js"; type TempPlugin = { dir: string; file: string; id: string }; -const tempDirs: string[] = []; +const fixtureRoot = path.join(os.tmpdir(), `openclaw-plugin-${randomUUID()}`); +let tempDirIndex = 0; const prevBundledDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; const EMPTY_PLUGIN_SCHEMA = { type: "object", additionalProperties: false, properties: {} }; function makeTempDir() { - const dir = path.join(os.tmpdir(), `openclaw-plugin-${randomUUID()}`); + const dir = path.join(fixtureRoot, `case-${tempDirIndex++}`); fs.mkdirSync(dir, { recursive: true }); - tempDirs.push(dir); return dir; } @@ -44,13 +44,6 @@ function writePlugin(params: { } afterEach(() => { - for (const dir of tempDirs.splice(0)) { - try { - fs.rmSync(dir, { recursive: true, force: true }); - } catch { - // ignore cleanup failures - } - } if (prevBundledDir === undefined) { delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; } else { @@ -58,6 +51,14 @@ afterEach(() => { } }); +afterAll(() => { + try { + fs.rmSync(fixtureRoot, { recursive: true, force: true }); + } catch { + // ignore cleanup failures + } +}); + describe("loadOpenClawPlugins", () => { it("disables bundled plugins by default", () => { const bundledDir = makeTempDir(); diff --git a/src/process/child-process-bridge.test.ts b/src/process/child-process-bridge.test.ts index 0a37ac7504..855b37ac2e 100644 --- a/src/process/child-process-bridge.test.ts +++ b/src/process/child-process-bridge.test.ts @@ -51,6 +51,17 @@ function canConnect(port: number): Promise { }); } +async function waitForPortClosed(port: number, timeoutMs = 1_000): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() <= deadline) { + if (!(await canConnect(port))) { + return; + } + await new Promise((resolve) => setTimeout(resolve, 10)); + } + throw new Error("timeout waiting for port to close"); +} + describe("attachChildProcessBridge", () => { const children: Array<{ kill: (signal?: NodeJS.Signals) => boolean }> = []; const detachments: Array<() => void> = []; @@ -111,7 +122,7 @@ describe("attachChildProcessBridge", () => { }); }); - await new Promise((r) => setTimeout(r, 250)); + await waitForPortClosed(port); expect(await canConnect(port)).toBe(false); }, 20_000); }); diff --git a/src/web/media.test.ts b/src/web/media.test.ts index d1f6d4e40c..0dee4ac0c1 100644 --- a/src/web/media.test.ts +++ b/src/web/media.test.ts @@ -2,19 +2,16 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import sharp from "sharp"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import * as ssrf from "../infra/net/ssrf.js"; import { optimizeImageToPng } from "../media/image-ops.js"; import { loadWebMedia, loadWebMediaRaw, optimizeImageToJpeg } from "./media.js"; -const tmpFiles: string[] = []; +let fixtureRoot = ""; +let fixtureFileCount = 0; async function writeTempFile(buffer: Buffer, ext: string): Promise { - const file = path.join( - os.tmpdir(), - `openclaw-media-${Date.now()}-${Math.random().toString(16).slice(2)}${ext}`, - ); - tmpFiles.push(file); + const file = path.join(fixtureRoot, `media-${fixtureFileCount++}${ext}`); await fs.writeFile(file, buffer); return file; } @@ -45,9 +42,15 @@ async function createLargeTestJpeg(): Promise<{ buffer: Buffer; file: string }> return { buffer, file }; } -afterEach(async () => { - await Promise.all(tmpFiles.map((file) => fs.rm(file, { force: true }))); - tmpFiles.length = 0; +beforeAll(async () => { + fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-test-")); +}); + +afterAll(async () => { + await fs.rm(fixtureRoot, { recursive: true, force: true }); +}); + +afterEach(() => { vi.restoreAllMocks(); });