From e0c3c80ebc8dd90957414f68b99d2f71b481fec2 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 17 May 2026 03:26:19 +0800 Subject: [PATCH] test: share Windows platform spy helpers --- src/plugins/lazy-service-module.test.ts | 15 +- src/plugins/public-surface-loader.test.ts | 8 +- src/plugins/setup-registry.test.ts | 11 +- .../stage-bundled-plugin-runtime.test.ts | 11 +- src/process/exec.windows.test.ts | 239 ++++++++---------- src/secrets/resolve.test.ts | 25 +- src/test-utils/vitest-spies.ts | 6 + 7 files changed, 133 insertions(+), 182 deletions(-) diff --git a/src/plugins/lazy-service-module.test.ts b/src/plugins/lazy-service-module.test.ts index 2d7aee3016b..d6d21b37d46 100644 --- a/src/plugins/lazy-service-module.test.ts +++ b/src/plugins/lazy-service-module.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; +import { withMockedWindowsPlatform } from "../test-utils/vitest-spies.js"; import { defaultLoadOverrideModule, startLazyPluginServiceModule } from "./lazy-service-module.js"; type LazyPluginServiceHandle = NonNullable< @@ -103,34 +104,28 @@ describe("startLazyPluginServiceModule", () => { }); it("normalizes Windows absolute paths in the default override loader", async () => { - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const start = createAsyncHookMock(); const importModule = vi.fn(async () => ({ startOverride: start })); - try { + await withMockedWindowsPlatform(async () => { await defaultLoadOverrideModule("C:\\Users\\alice\\plugin folder\\x#y.mjs", importModule); - } finally { - platformSpy.mockRestore(); - } + }); expect(importModule).toHaveBeenCalledWith("file:///C:/Users/alice/plugin%20folder/x%23y.mjs"); }); it("leaves caller-supplied override loaders responsible for their own specifiers", async () => { process.env.OPENCLAW_LAZY_SERVICE_OVERRIDE = "C:\\Users\\alice\\browser-service.mjs"; - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const start = createAsyncHookMock(); const loadOverrideModule = vi.fn(async () => ({ startOverride: start })); - try { + await withMockedWindowsPlatform(async () => { await expectLifecycleStarted({ overrideEnvVar: "OPENCLAW_LAZY_SERVICE_OVERRIDE", loadOverrideModule, startExportNames: ["startOverride"], }); - } finally { - platformSpy.mockRestore(); - } + }); expect(loadOverrideModule).toHaveBeenCalledWith("C:\\Users\\alice\\browser-service.mjs"); expect(start).toHaveBeenCalledTimes(1); diff --git a/src/plugins/public-surface-loader.test.ts b/src/plugins/public-surface-loader.test.ts index ba160883b7a..d382b76f324 100644 --- a/src/plugins/public-surface-loader.test.ts +++ b/src/plugins/public-surface-loader.test.ts @@ -3,6 +3,7 @@ import os from "node:os"; import path from "node:path"; import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { withMockedWindowsPlatform } from "../test-utils/vitest-spies.js"; const tempDirs: string[] = []; const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; @@ -47,9 +48,8 @@ describe("bundled plugin public surface loader", () => { moduleExport: { marker: "windows-dist-ok" }, }), })); - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); - try { + await withMockedWindowsPlatform(async () => { const publicSurfaceLoader = await importFreshModule< typeof import("./public-surface-loader.js") >(import.meta.url, "./public-surface-loader.js?scope=windows-dist-jiti"); @@ -68,9 +68,7 @@ describe("bundled plugin public surface loader", () => { }).marker, ).toBe("windows-dist-ok"); expect(createJiti).not.toHaveBeenCalled(); - } finally { - platformSpy.mockRestore(); - } + }); }); it("prefers source require for bundled source public artifacts when a ts require hook exists", async () => { diff --git a/src/plugins/setup-registry.test.ts b/src/plugins/setup-registry.test.ts index 261648025e3..84f00850f43 100644 --- a/src/plugins/setup-registry.test.ts +++ b/src/plugins/setup-registry.test.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { withMockedWindowsPlatform } from "../test-utils/vitest-spies.js"; import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js"; import { getRegistryJitiMocks, @@ -227,17 +228,17 @@ describe("setup-registry module loader", () => { plugins: [{ id: "test-plugin", rootDir: pluginRoot }], diagnostics: [], }); - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const restoreVersions = forceNodeRuntimeVersionsForTest(); try { - resolvePluginSetupRegistry({ - workspaceDir: pluginRoot, - env: {}, + withMockedWindowsPlatform(() => { + resolvePluginSetupRegistry({ + workspaceDir: pluginRoot, + env: {}, + }); }); } finally { restoreVersions(); - platformSpy.mockRestore(); } expect(mocks.createJiti).toHaveBeenCalledTimes(1); diff --git a/src/plugins/stage-bundled-plugin-runtime.test.ts b/src/plugins/stage-bundled-plugin-runtime.test.ts index e45f159e66e..8af37ce6122 100644 --- a/src/plugins/stage-bundled-plugin-runtime.test.ts +++ b/src/plugins/stage-bundled-plugin-runtime.test.ts @@ -4,6 +4,7 @@ import { pathToFileURL } from "node:url"; import { bundledDistPluginFile } from "openclaw/plugin-sdk/test-fixtures"; import { afterEach, describe, expect, it, vi } from "vitest"; import { stageBundledPluginRuntime } from "../../scripts/stage-bundled-plugin-runtime.mjs"; +import { withMockedWindowsPlatform, withRestoredMocks } from "../test-utils/vitest-spies.js"; import { discoverOpenClawPlugins } from "./discovery.js"; import { loadPluginManifestRegistry } from "./manifest-registry.js"; import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js"; @@ -547,7 +548,6 @@ describe("stageBundledPluginRuntime", () => { [bundledDistPluginFile("feishu", "assets/fixture.txt")]: "# Feishu Doc\n", }); - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const realSymlinkSync = fs.symlinkSync.bind(fs); const symlinkSpy = vi.spyOn(fs, "symlinkSync").mockImplementation(((target, link, type) => { const linkPath = String(link); @@ -557,7 +557,11 @@ describe("stageBundledPluginRuntime", () => { return realSymlinkSync(String(target), linkPath, type); }) as typeof fs.symlinkSync); - stageBundledPluginRuntime({ repoRoot }); + withRestoredMocks([symlinkSpy], () => { + withMockedWindowsPlatform(() => { + stageBundledPluginRuntime({ repoRoot }); + }); + }); const runtimeAssetPath = path.join( repoRoot, @@ -569,9 +573,6 @@ describe("stageBundledPluginRuntime", () => { ); expect(fs.lstatSync(runtimeAssetPath).isSymbolicLink()).toBe(false); expect(fs.readFileSync(runtimeAssetPath, "utf8")).toBe("# Feishu Doc\n"); - - symlinkSpy.mockRestore(); - platformSpy.mockRestore(); }, ); }); diff --git a/src/process/exec.windows.test.ts b/src/process/exec.windows.test.ts index 1d3992eeadb..dbe08186514 100644 --- a/src/process/exec.windows.test.ts +++ b/src/process/exec.windows.test.ts @@ -7,6 +7,7 @@ import { _resetWindowsInstallRootsForTests, getWindowsInstallRoots, } from "../infra/windows-install-roots.js"; +import { withMockedWindowsPlatform, withRestoredMocks } from "../test-utils/vitest-spies.js"; const { spawnMock, spawnSyncMock, execFileMock, execFilePromisifyMock } = vi.hoisted(() => { const execFilePromisifyMock = vi.fn(); @@ -123,7 +124,6 @@ function expectedTrustedCmdExe(): string { } async function expectShimmedWindowsCommandWithoutExitCodeSucceeds(params?: { killed?: boolean }) { - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const child = createMockChild({ closeCode: null, exitCode: null, @@ -132,14 +132,12 @@ async function expectShimmedWindowsCommandWithoutExitCodeSucceeds(params?: { kil spawnMock.mockImplementation(() => child); - try { + await withMockedWindowsPlatform(async () => { const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 1000 }); expect(result.code).toBe(0); expect(result.signal).toBeNull(); expect(result.termination).toBe("exit"); - } finally { - platformSpy.mockRestore(); - } + }); } describe("windows command wrapper behavior", () => { @@ -181,25 +179,21 @@ describe("windows command wrapper behavior", () => { }); it("wraps .cmd commands via cmd.exe in runCommandWithTimeout", async () => { - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const expectedComSpec = expectedTrustedCmdExe(); spawnMock.mockImplementation( (_command: string, _args: string[], _options: Record) => createMockChild(), ); - try { + await withMockedWindowsPlatform(async () => { const result = await runCommandWithTimeout(["pnpm", "--version"], { timeoutMs: 1000 }); expect(result.code).toBe(0); const captured = requireSpawnCall(0); expectCmdWrappedInvocation({ captured, expectedComSpec }); - } finally { - platformSpy.mockRestore(); - } + }); }); it("ignores ComSpec when selecting the Windows command wrapper", async () => { - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const previousComSpec = process.env.ComSpec; const previousSystemRoot = process.env.SystemRoot; process.env.ComSpec = "C:\\workspace\\evil\\cmd.exe"; @@ -210,12 +204,14 @@ describe("windows command wrapper behavior", () => { ); try { - const result = await runCommandWithTimeout(["pnpm", "--version"], { timeoutMs: 1000 }); - expect(result.code).toBe(0); - const captured = requireSpawnCall(0); - expectCmdWrappedInvocation({ - captured, - expectedComSpec: path.win32.join("C:\\Windows", "System32", "cmd.exe"), + await withMockedWindowsPlatform(async () => { + const result = await runCommandWithTimeout(["pnpm", "--version"], { timeoutMs: 1000 }); + expect(result.code).toBe(0); + const captured = requireSpawnCall(0); + expectCmdWrappedInvocation({ + captured, + expectedComSpec: path.win32.join("C:\\Windows", "System32", "cmd.exe"), + }); }); } finally { if (previousComSpec === undefined) { @@ -228,12 +224,10 @@ describe("windows command wrapper behavior", () => { } else { process.env.SystemRoot = previousSystemRoot; } - platformSpy.mockRestore(); } }); it("rejects unsafe Windows root values when selecting the command wrapper", async () => { - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const previousSystemRoot = process.env.SystemRoot; const previousWindir = process.env.WINDIR; @@ -242,29 +236,31 @@ describe("windows command wrapper behavior", () => { ); try { - for (const unsafeRoot of [ - "\\\\evil\\share", - "C:\\Windows;C:\\evil", - "\\Windows", - "relative\\path", - ]) { - _resetWindowsInstallRootsForTests({ queryRegistryValue: () => null }); - // Set every install-root env source to the unsafe value so the - // resolver rejects each one and falls through to the safe default. - // Deleting WINDIR here is unreliable on real Windows runners, so - // overwrite it with the same rejected payload. - process.env.SystemRoot = unsafeRoot; - process.env.WINDIR = unsafeRoot; - spawnMock.mockClear(); + await withMockedWindowsPlatform(async () => { + for (const unsafeRoot of [ + "\\\\evil\\share", + "C:\\Windows;C:\\evil", + "\\Windows", + "relative\\path", + ]) { + _resetWindowsInstallRootsForTests({ queryRegistryValue: () => null }); + // Set every install-root env source to the unsafe value so the + // resolver rejects each one and falls through to the safe default. + // Deleting WINDIR here is unreliable on real Windows runners, so + // overwrite it with the same rejected payload. + process.env.SystemRoot = unsafeRoot; + process.env.WINDIR = unsafeRoot; + spawnMock.mockClear(); - const result = await runCommandWithTimeout(["pnpm", "--version"], { timeoutMs: 1000 }); - expect(result.code).toBe(0); - const captured = requireSpawnCall(0); - expectCmdWrappedInvocation({ - captured, - expectedComSpec: path.win32.join("C:\\Windows", "System32", "cmd.exe"), - }); - } + const result = await runCommandWithTimeout(["pnpm", "--version"], { timeoutMs: 1000 }); + expect(result.code).toBe(0); + const captured = requireSpawnCall(0); + expectCmdWrappedInvocation({ + captured, + expectedComSpec: path.win32.join("C:\\Windows", "System32", "cmd.exe"), + }); + } + }); } finally { if (previousSystemRoot === undefined) { delete process.env.SystemRoot; @@ -276,19 +272,17 @@ describe("windows command wrapper behavior", () => { } else { process.env.WINDIR = previousWindir; } - platformSpy.mockRestore(); } }); it("wraps corepack.cmd via cmd.exe in runCommandWithTimeout", async () => { - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const expectedComSpec = expectedTrustedCmdExe(); spawnMock.mockImplementation( (_command: string, _args: string[], _options: Record) => createMockChild(), ); - try { + await withMockedWindowsPlatform(async () => { const result = await runCommandWithTimeout(["corepack", "--version"], { timeoutMs: 1000 }); expect(result.code).toBe(0); const captured = requireSpawnCall(0); @@ -297,52 +291,44 @@ describe("windows command wrapper behavior", () => { expect(captured[1][3]).toContain("corepack.cmd --version"); expect(captured[2].windowsHide).toBe(true); expect(captured[2].windowsVerbatimArguments).toBe(true); - } finally { - platformSpy.mockRestore(); - } + }); }); it("keeps child exitCode when close reports null on Windows npm shims", async () => { - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const child = createMockChild({ closeCode: null, exitCode: 0 }); spawnMock.mockImplementation(() => child); - try { + await withMockedWindowsPlatform(async () => { const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 1000 }); expect(result.code).toBe(0); - } finally { - platformSpy.mockRestore(); - } + }); }); it("spawns node + npm-cli.js for npm argv to avoid direct .cmd execution", async () => { - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const existsSpy = vi.spyOn(fs, "existsSync").mockReturnValue(true); const child = createMockChild({ closeCode: 0, exitCode: 0 }); spawnMock.mockImplementation(() => child); - try { - const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 1000 }); - expect(result.code).toBe(0); - const captured = requireSpawnCall(0); - expect(captured[0]).toBe(process.execPath); - expect(captured[1][0]).toBe( - path.join(path.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"), - ); - expect(captured[1][1]).toBe("--version"); - expect(captured[2].windowsHide).toBe(true); - expect(captured[2].windowsVerbatimArguments).toBeUndefined(); - expect(captured[2].stdio).toEqual(["inherit", "pipe", "pipe"]); - } finally { - existsSpy.mockRestore(); - platformSpy.mockRestore(); - } + await withRestoredMocks([existsSpy], async () => { + await withMockedWindowsPlatform(async () => { + const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 1000 }); + expect(result.code).toBe(0); + const captured = requireSpawnCall(0); + expect(captured[0]).toBe(process.execPath); + expect(captured[1][0]).toBe( + path.join(path.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"), + ); + expect(captured[1][1]).toBe("--version"); + expect(captured[2].windowsHide).toBe(true); + expect(captured[2].windowsVerbatimArguments).toBeUndefined(); + expect(captured[2].stdio).toEqual(["inherit", "pipe", "pipe"]); + }); + }); }); it("falls back to npm.cmd when npm-cli.js is unavailable", async () => { - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const existsSpy = vi.spyOn(fs, "existsSync").mockReturnValue(false); const expectedComSpec = expectedTrustedCmdExe(); @@ -350,24 +336,22 @@ describe("windows command wrapper behavior", () => { (_command: string, _args: string[], _options: Record) => createMockChild(), ); - try { - const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 1000 }); - expect(result.code).toBe(0); - const captured = requireSpawnCall(0); - expect(captured[0]).toBe(expectedComSpec); - expect(captured[1].slice(0, 3)).toEqual(["/d", "/s", "/c"]); - expect(captured[1][3]).toContain("npm.cmd --version"); - expect(captured[2].windowsHide).toBe(true); - expect(captured[2].windowsVerbatimArguments).toBe(true); - expect(captured[2].stdio).toEqual(["inherit", "pipe", "pipe"]); - } finally { - existsSpy.mockRestore(); - platformSpy.mockRestore(); - } + await withRestoredMocks([existsSpy], async () => { + await withMockedWindowsPlatform(async () => { + const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 1000 }); + expect(result.code).toBe(0); + const captured = requireSpawnCall(0); + expect(captured[0]).toBe(expectedComSpec); + expect(captured[1].slice(0, 3)).toEqual(["/d", "/s", "/c"]); + expect(captured[1][3]).toContain("npm.cmd --version"); + expect(captured[2].windowsHide).toBe(true); + expect(captured[2].windowsVerbatimArguments).toBe(true); + expect(captured[2].stdio).toEqual(["inherit", "pipe", "pipe"]); + }); + }); }); it("waits for Windows exitCode settlement after close reports null", async () => { - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const child = createMockChild({ closeCode: null, exitCode: null, @@ -377,12 +361,10 @@ describe("windows command wrapper behavior", () => { spawnMock.mockImplementation(() => child); - try { + await withMockedWindowsPlatform(async () => { const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 1000 }); expect(result.code).toBe(0); - } finally { - platformSpy.mockRestore(); - } + }); }); it("treats shimmed Windows commands without a reported exit code as success when they close cleanly", async () => { @@ -394,7 +376,6 @@ describe("windows command wrapper behavior", () => { }); it("uses cmd.exe wrapper with windowsVerbatimArguments in runExec for .cmd shims", async () => { - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const expectedComSpec = expectedTrustedCmdExe(); execFileMock.mockImplementation( @@ -408,18 +389,14 @@ describe("windows command wrapper behavior", () => { }, ); - try { + await withMockedWindowsPlatform(async () => { await runExec("pnpm", ["--version"], 1000); const captured = requireExecFileCall(0); expectCmdWrappedInvocation({ captured, expectedComSpec }); - } finally { - platformSpy.mockRestore(); - } + }); }); it("sets windowsHide on direct runExec invocations too", async () => { - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); - execFileMock.mockImplementation( ( _command: string, @@ -431,25 +408,21 @@ describe("windows command wrapper behavior", () => { }, ); - try { + await withMockedWindowsPlatform(async () => { await runExec("node", ["--version"], 1000); const captured = requireExecFileCall(0); expect(captured[0]).toBe("node"); expect(captured[1]).toEqual(["--version"]); expect(captured[2].windowsHide).toBe(true); - } finally { - platformSpy.mockRestore(); - } + }); }); it("sets windowsHide on direct runCommandWithTimeout invocations too", async () => { - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); - spawnMock.mockImplementation( (_command: string, _args: string[], _options: Record) => createMockChild(), ); - try { + await withMockedWindowsPlatform(async () => { const result = await runCommandWithTimeout(["node", "--version"], { timeoutMs: 1000 }); expect(result.code).toBe(0); const captured = requireSpawnCall(0); @@ -457,45 +430,42 @@ describe("windows command wrapper behavior", () => { expect(captured[1]).toEqual(["--version"]); expect(captured[2].windowsHide).toBe(true); expect(captured[2].windowsVerbatimArguments).toBeUndefined(); - } finally { - platformSpy.mockRestore(); - } + }); }); it("kills the Windows process tree when the overall timeout elapses", async () => { vi.useFakeTimers(); - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const child = createMockChild({ autoClose: false }); const taskkillChild = createMockChild(); spawnMock.mockImplementationOnce(() => child).mockImplementationOnce(() => taskkillChild); try { - const resultPromise = runCommandWithTimeout(["node", "idle.js"], { timeoutMs: 80 }); + await withMockedWindowsPlatform(async () => { + const resultPromise = runCommandWithTimeout(["node", "idle.js"], { timeoutMs: 80 }); - await vi.advanceTimersByTimeAsync(81); - expect(child.kill).not.toHaveBeenCalled(); - expect(spawnMock).toHaveBeenCalledTimes(2); - const taskkillCall = requireSpawnCall(1); - expect(taskkillCall[0]).toBe("taskkill"); - expect(taskkillCall[1]).toEqual(["/PID", "1234", "/T", "/F"]); - expect(taskkillCall[2]).toEqual({ - stdio: "ignore", - windowsHide: true, + await vi.advanceTimersByTimeAsync(81); + expect(child.kill).not.toHaveBeenCalled(); + expect(spawnMock).toHaveBeenCalledTimes(2); + const taskkillCall = requireSpawnCall(1); + expect(taskkillCall[0]).toBe("taskkill"); + expect(taskkillCall[1]).toEqual(["/PID", "1234", "/T", "/F"]); + expect(taskkillCall[2]).toEqual({ + stdio: "ignore", + windowsHide: true, + }); + + child.emit("close", null, "SIGKILL"); + const result = await resultPromise; + expect(result.termination).toBe("timeout"); + expect(result.code).not.toBe(0); }); - - child.emit("close", null, "SIGKILL"); - const result = await resultPromise; - expect(result.termination).toBe("timeout"); - expect(result.code).not.toBe(0); } finally { - platformSpy.mockRestore(); vi.useRealTimers(); } }); it("decodes GBK stdout and stderr from runExec on Windows", async () => { - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const stdout = Buffer.from([0xb2, 0xe2, 0xca, 0xd4]); const stderr = Buffer.from([0xa3, 0xbb]); @@ -510,20 +480,16 @@ describe("windows command wrapper behavior", () => { }, ); - try { + await withMockedWindowsPlatform(async () => { const result = await runExec("node", ["gbk-output.js"], 1000); expect(result.stdout).toBe("测试"); expect(result.stderr).toBe(";"); const captured = requireExecFileCall(0); expect(captured[2].encoding).toBe("buffer"); - } finally { - platformSpy.mockRestore(); - } + }); }); it("prefers valid UTF-8 stdout from runExec on Windows", async () => { - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); - execFileMock.mockImplementation( ( _command: string, @@ -535,18 +501,15 @@ describe("windows command wrapper behavior", () => { }, ); - try { + await withMockedWindowsPlatform(async () => { await expect(runExec("node", ["utf8-output.js"], 1000)).resolves.toEqual({ stdout: "测试", stderr: "", }); - } finally { - platformSpy.mockRestore(); - } + }); }); it("decodes spawn stdout once so GBK characters split across chunks survive", async () => { - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); const child = createMockChild({ autoClose: false }); spawnMock.mockImplementation(() => { queueMicrotask(() => { @@ -558,7 +521,7 @@ describe("windows command wrapper behavior", () => { return child; }); - try { + await withMockedWindowsPlatform(async () => { await expect( runCommandWithTimeout(["node", "gbk-output.js"], { timeoutMs: 1000 }), ).resolves.toEqual({ @@ -571,8 +534,6 @@ describe("windows command wrapper behavior", () => { termination: "exit", noOutputTimedOut: false, }); - } finally { - platformSpy.mockRestore(); - } + }); }); }); diff --git a/src/secrets/resolve.test.ts b/src/secrets/resolve.test.ts index c7511b555c0..bfc8c1c2cd6 100644 --- a/src/secrets/resolve.test.ts +++ b/src/secrets/resolve.test.ts @@ -4,6 +4,7 @@ import path from "node:path"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { INVALID_EXEC_SECRET_REF_IDS } from "../test-utils/secret-ref-test-vectors.js"; +import { withMockedWindowsPlatform } from "../test-utils/vitest-spies.js"; import { resolveSecretRefString, resolveSecretRefValue, @@ -503,9 +504,7 @@ describe("secret ref resolver", () => { }); it("fails closed on Windows when file provider ACL source is unknown", async () => { - vi.spyOn(process, "platform", "get").mockReturnValue("win32" as unknown as NodeJS.Platform); - - try { + await withMockedWindowsPlatform(async () => { const dir = await createCaseDir("win-acl"); const filePath = path.join(dir, "secrets.json"); await writeSecureFile(filePath, '{"token":"abc123"}'); @@ -524,15 +523,11 @@ describe("secret ref resolver", () => { }, ), ).rejects.toThrow(/ACL verification unavailable on Windows/); - } finally { - vi.restoreAllMocks(); - } + }); }); it("allows trusted file provider opt-out when Windows ACL source is unknown", async () => { - vi.spyOn(process, "platform", "get").mockReturnValue("win32" as unknown as NodeJS.Platform); - - try { + await withMockedWindowsPlatform(async () => { const dir = await createCaseDir("win-acl-opt-out"); const filePath = path.join(dir, "secrets.json"); await writeSecureFile(filePath, '{"token":"abc123"}'); @@ -550,20 +545,14 @@ describe("secret ref resolver", () => { }, ); expect(value).toBe("abc123"); - } finally { - vi.restoreAllMocks(); - } + }); }); it("fails closed on Windows when exec provider ACL source is unknown", async () => { - vi.spyOn(process, "platform", "get").mockReturnValue("win32" as unknown as NodeJS.Platform); - - try { + await withMockedWindowsPlatform(async () => { await expect(resolveExecSecret(execProtocolV1ScriptPath)).rejects.toThrow( /ACL verification unavailable on Windows/, ); - } finally { - vi.restoreAllMocks(); - } + }); }); }); diff --git a/src/test-utils/vitest-spies.ts b/src/test-utils/vitest-spies.ts index a3525284b7f..0745531b6e1 100644 --- a/src/test-utils/vitest-spies.ts +++ b/src/test-utils/vitest-spies.ts @@ -48,3 +48,9 @@ export function withMockedPlatform( ): T | Promise { return withRestoredMocks([vi.spyOn(process, "platform", "get").mockReturnValue(platform)], run); } + +export function withMockedWindowsPlatform(run: () => Promise): Promise; +export function withMockedWindowsPlatform(run: () => T): T; +export function withMockedWindowsPlatform(run: () => T | Promise): T | Promise { + return withMockedPlatform("win32", run); +}