From e43a2efcdb5dd985f309b72a44807bbe96540718 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 17 May 2026 13:45:17 +0100 Subject: [PATCH] test: harden wsl2 fixtures --- extensions/active-memory/index.test.ts | 5 ++- extensions/lmstudio/src/setup.test.ts | 10 +++++- src/agents/model-fallback.test.ts | 15 ++++---- src/agents/model-selection.test.ts | 34 +++++++++---------- src/daemon/systemd.test.ts | 1 + src/gateway/server-runtime-services.test.ts | 9 +++-- .../session-attachments.contract.test.ts | 14 ++++++-- src/plugins/host-hook-attachments.ts | 10 ++++-- src/plugins/install.test.ts | 12 +++++-- test/scripts/lint-suppressions.test.ts | 6 +++- 10 files changed, 77 insertions(+), 39 deletions(-) diff --git a/extensions/active-memory/index.test.ts b/extensions/active-memory/index.test.ts index 8dc39f96bf3..29108f422a3 100644 --- a/extensions/active-memory/index.test.ts +++ b/extensions/active-memory/index.test.ts @@ -2837,7 +2837,10 @@ describe("active-memory plugin", () => { }, ); - expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(2); + const sessionKeys = runEmbeddedPiAgent.mock.calls.map( + ([params]) => (params as { sessionKey?: string }).sessionKey, + ); + expect(new Set(sessionKeys).size).toBeGreaterThanOrEqual(2); const infoLines = vi .mocked(api.logger.info) .mock.calls.map((call: unknown[]) => String(call[0])); diff --git a/extensions/lmstudio/src/setup.test.ts b/extensions/lmstudio/src/setup.test.ts index 9db6efdaf1f..1a3b7b3f9a0 100644 --- a/extensions/lmstudio/src/setup.test.ts +++ b/extensions/lmstudio/src/setup.test.ts @@ -11,6 +11,8 @@ import type { WizardPrompter } from "openclaw/plugin-sdk/setup"; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { LMSTUDIO_DEFAULT_API_KEY_ENV_VAR, + LMSTUDIO_DEFAULT_INFERENCE_BASE_URL, + LMSTUDIO_DOCKER_HOST_INFERENCE_BASE_URL, LMSTUDIO_LOCAL_API_KEY_PLACEHOLDER, } from "./defaults.js"; import { @@ -702,11 +704,17 @@ describe("lmstudio setup", () => { const ctx = buildNonInteractiveContext({ customModelId: "missing-model", }); + const dockerSetup = ["1", "true", "yes", "on"].includes( + process.env.OPENCLAW_DOCKER_SETUP?.trim().toLowerCase() ?? "", + ); + const expectedBaseUrl = dockerSetup + ? LMSTUDIO_DOCKER_HOST_INFERENCE_BASE_URL + : LMSTUDIO_DEFAULT_INFERENCE_BASE_URL; await expect(configureLmstudioNonInteractive(ctx)).resolves.toBeNull(); expect(ctx.runtime.error).toHaveBeenCalledWith( - "LM Studio model missing-model was not found at http://localhost:1234/v1.\nAvailable models: qwen3-8b-instruct", + `LM Studio model missing-model was not found at ${expectedBaseUrl}.\nAvailable models: qwen3-8b-instruct`, ); expect(ctx.runtime.exit).toHaveBeenCalledWith(1); expect(configureSelfHostedNonInteractiveMock).not.toHaveBeenCalled(); diff --git a/src/agents/model-fallback.test.ts b/src/agents/model-fallback.test.ts index a18d2af3563..1e75e7dd46d 100644 --- a/src/agents/model-fallback.test.ts +++ b/src/agents/model-fallback.test.ts @@ -1278,8 +1278,7 @@ describe("runWithModelFallback", () => { }); it("warns when falling back due to model_not_found", async () => { - setLoggerOverride({ level: "silent", consoleLevel: "warn" }); - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const warnLogs = createWarnLogCapture("openclaw-model-fallback-test"); try { const cfg = makeCfg(); const run = vi @@ -1295,13 +1294,13 @@ describe("runWithModelFallback", () => { }); expect(result.result).toBe("ok"); - expect(warnSpy).toHaveBeenCalledWith( - '[model-fallback] Model "openai/gpt-6" not found. Fell back to "anthropic/claude-haiku-3-5".', - ); + expect( + await warnLogs.findText( + 'Model "openai/gpt-6" not found. Fell back to "anthropic/claude-haiku-3-5".', + ), + ).toBeDefined(); } finally { - warnSpy.mockRestore(); - setLoggerOverride(null); - resetLogger(); + warnLogs.cleanup(); } }); diff --git a/src/agents/model-selection.test.ts b/src/agents/model-selection.test.ts index 8c373a7732d..2f792e6edc7 100644 --- a/src/agents/model-selection.test.ts +++ b/src/agents/model-selection.test.ts @@ -1585,9 +1585,8 @@ describe("model-selection", () => { expect(result).toEqual({ provider: "anthropic", model: "claude-opus-4-6" }); }); - it("should fall back to the configured default provider and warn if provider is missing for non-alias", () => { - setLoggerOverride({ level: "silent", consoleLevel: "warn" }); - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + it("should fall back to the configured default provider and warn if provider is missing for non-alias", async () => { + const warnLogs = createWarnLogCapture("openclaw-model-selection-test"); try { const cfg: Partial = { agents: { @@ -1604,13 +1603,13 @@ describe("model-selection", () => { }); expect(result).toEqual({ provider: "google", model: "claude-3-5-sonnet" }); - expect(warnSpy).toHaveBeenCalledWith( - '[model-selection] Model "claude-3-5-sonnet" specified without provider. Falling back to "google/claude-3-5-sonnet". Please use "google/claude-3-5-sonnet" in your config.', - ); + expect( + await warnLogs.findText( + 'Model "claude-3-5-sonnet" specified without provider. Falling back to "google/claude-3-5-sonnet". Please use "google/claude-3-5-sonnet" in your config.', + ), + ).toBeDefined(); } finally { - warnSpy.mockRestore(); - setLoggerOverride(null); - resetLogger(); + warnLogs.cleanup(); } }); @@ -1868,9 +1867,8 @@ describe("model-selection", () => { expect(result).toEqual({ provider: "openai", model: "gpt-5.4" }); }); - it("should warn when specified model cannot be resolved and falls back to default", () => { - setLoggerOverride({ level: "silent", consoleLevel: "warn" }); - const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + it("should warn when specified model cannot be resolved and falls back to default", async () => { + const warnLogs = createWarnLogCapture("openclaw-model-selection-test"); try { const cfg: Partial = { agents: { @@ -1887,13 +1885,13 @@ describe("model-selection", () => { }); expect(result).toEqual({ provider: "openai", model: "gpt-5.4" }); - expect(warnSpy).toHaveBeenCalledWith( - '[model-selection] Model "openai/" could not be resolved. Falling back to default "openai/gpt-5.4".', - ); + expect( + await warnLogs.findText( + 'Model "openai/" could not be resolved. Falling back to default "openai/gpt-5.4".', + ), + ).toBeDefined(); } finally { - warnSpy.mockRestore(); - setLoggerOverride(null); - resetLogger(); + warnLogs.cleanup(); } }); diff --git a/src/daemon/systemd.test.ts b/src/daemon/systemd.test.ts index 6dd616e8153..9ebc87a71d1 100644 --- a/src/daemon/systemd.test.ts +++ b/src/daemon/systemd.test.ts @@ -1127,6 +1127,7 @@ describe("systemd service install and uninstall", () => { it("uses the sudo-u target user for install activation machine-scope retry", async () => { await withNodeSystemdFixture(async ({ env }) => { + mockEffectiveUid(1000); const installEnv = { ...env, USER: "openclaw", SUDO_USER: "admin" }; execFileMock .mockImplementationOnce((_cmd, args, _opts, cb) => { diff --git a/src/gateway/server-runtime-services.test.ts b/src/gateway/server-runtime-services.test.ts index 800475a572f..2538146768a 100644 --- a/src/gateway/server-runtime-services.test.ts +++ b/src/gateway/server-runtime-services.test.ts @@ -286,6 +286,7 @@ describe("server-runtime-services", () => { const applyMaintenance = vi.fn(); const cron = { start: vi.fn(async () => undefined) }; const recordPostReadyMemory = vi.fn(); + const clearIntervalSpy = vi.spyOn(globalThis, "clearInterval"); scheduleGatewayPostReadyMaintenance( createPostReadyMaintenanceScheduleParams({ @@ -305,14 +306,18 @@ describe("server-runtime-services", () => { if (!resolveMaintenance) { throw new Error("Expected gateway maintenance resolver to be initialized"); } - resolveMaintenance(createMaintenanceHandles()); + const maintenance = createMaintenanceHandles(); + resolveMaintenance(maintenance); await Promise.resolve(); await Promise.resolve(); expect(applyMaintenance).not.toHaveBeenCalled(); expect(cron.start).not.toHaveBeenCalled(); expect(recordPostReadyMemory).not.toHaveBeenCalled(); - expect(vi.getTimerCount()).toBe(0); + expect(clearIntervalSpy).toHaveBeenCalledWith(maintenance.tickInterval); + expect(clearIntervalSpy).toHaveBeenCalledWith(maintenance.healthInterval); + expect(clearIntervalSpy).toHaveBeenCalledWith(maintenance.dedupeCleanup); + expect(clearIntervalSpy).toHaveBeenCalledWith(maintenance.mediaCleanup); }); it("keeps scheduled services disabled for minimal test gateways", () => { diff --git a/src/plugins/contracts/session-attachments.contract.test.ts b/src/plugins/contracts/session-attachments.contract.test.ts index 7b49410d5d8..44ce813aa4b 100644 --- a/src/plugins/contracts/session-attachments.contract.test.ts +++ b/src/plugins/contracts/session-attachments.contract.test.ts @@ -1,4 +1,4 @@ -import fs from "node:fs/promises"; +import * as fs from "node:fs/promises"; import path from "node:path"; import { createPluginRegistryFixture, @@ -11,6 +11,7 @@ import { resolvePreferredOpenClawTmpDir } from "../../infra/tmp-openclaw-dir.js" import { FILE_TYPE_SNIFF_MAX_BYTES } from "../../media/mime.js"; import { createOutboundTestPlugin, createTestRegistry } from "../../test-utils/channel-plugins.js"; import { + attachmentProbeFs, resolveAttachmentDelivery, sendPluginSessionAttachment, } from "../host-hook-attachments.js"; @@ -453,8 +454,15 @@ describe("plugin session attachments", () => { await withSessionStore(async ({ storePath, stateDir }) => { const unreadablePath = path.join(stateDir, "unreadable.pdf"); await fs.writeFile(unreadablePath, "%PDF-1.7\n", "utf8"); - await fs.chmod(unreadablePath, 0o000); await writeSessionEntry(storePath); + const originalOpen = attachmentProbeFs.open.bind(attachmentProbeFs); + const openSpy = vi.spyOn(attachmentProbeFs, "open").mockImplementation((async (...args) => { + const [target] = args; + if (path.resolve(String(target)) === unreadablePath) { + throw new Error("EACCES: permission denied, open 'unreadable.pdf'"); + } + return await originalOpen(...args); + }) as typeof fs.open); try { const result = await sendBundledSessionAttachment({ @@ -468,7 +476,7 @@ describe("plugin session attachments", () => { } expect(result.error).toContain(`attachment file MIME read failed for ${unreadablePath}`); } finally { - await fs.chmod(unreadablePath, 0o600).catch(() => undefined); + openSpy.mockRestore(); } expect(workflowMocks.sendMessage).not.toHaveBeenCalled(); }); diff --git a/src/plugins/host-hook-attachments.ts b/src/plugins/host-hook-attachments.ts index 65717b940ab..3f3b8535eae 100644 --- a/src/plugins/host-hook-attachments.ts +++ b/src/plugins/host-hook-attachments.ts @@ -1,4 +1,5 @@ -import { lstat, open } from "node:fs/promises"; +import * as fsPromises from "node:fs/promises"; +import { lstat } from "node:fs/promises"; import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js"; import { resolvePathFromInput } from "../agents/path-policy.js"; import { resolveWorkspaceRoot } from "../agents/workspace-dir.js"; @@ -18,6 +19,9 @@ import type { import type { PluginOrigin } from "./plugin-origin.types.js"; const DEFAULT_ATTACHMENT_MAX_BYTES = 25 * 1024 * 1024; +export const attachmentProbeFs = { + open: (...args: Parameters) => fsPromises.open(...args), +}; const MAX_ATTACHMENT_FILES = 10; type SendMessage = typeof import("../infra/outbound/message.js").sendMessage; @@ -71,9 +75,9 @@ async function readMimeSniffBuffer( filePath: string, size: number, ): Promise { - let handle: Awaited> | undefined; + let handle: Awaited> | undefined; try { - handle = await open(filePath, "r"); + handle = await attachmentProbeFs.open(filePath, "r"); const length = Math.min(Math.max(0, size), FILE_TYPE_SNIFF_MAX_BYTES); const buffer = Buffer.alloc(length); const { bytesRead } = await handle.read(buffer, 0, length, 0); diff --git a/src/plugins/install.test.ts b/src/plugins/install.test.ts index 09e873db719..61805ae1355 100644 --- a/src/plugins/install.test.ts +++ b/src/plugins/install.test.ts @@ -1,4 +1,5 @@ import fs from "node:fs"; +import fsPromises from "node:fs/promises"; import path from "node:path"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { safePathSegmentHashed } from "../infra/install-safe-path.js"; @@ -2171,7 +2172,14 @@ describe("installPluginFromArchive", () => { path.join(blockedDir, "package.json"), JSON.stringify({ name: "plain-crypto-js" }), ); - fs.chmodSync(blockedDir, 0o000); + const originalReaddir = fsPromises.readdir.bind(fsPromises); + const readdirSpy = vi.spyOn(fsPromises, "readdir").mockImplementation((async (...args) => { + const [target] = args; + if (path.resolve(String(target)) === blockedDir) { + throw new Error("EACCES: permission denied, scandir 'vendor/sealed'"); + } + return await originalReaddir(...args); + }) as typeof fsPromises.readdir); try { const { result } = await installFromDirWithWarnings({ pluginDir, extensionsDir }); @@ -2183,7 +2191,7 @@ describe("installPluginFromArchive", () => { expect(result.error).toContain("vendor/sealed"); } } finally { - fs.chmodSync(blockedDir, 0o755); + readdirSpy.mockRestore(); } }, ); diff --git a/test/scripts/lint-suppressions.test.ts b/test/scripts/lint-suppressions.test.ts index 6e9370b7157..1b2ce4803d4 100644 --- a/test/scripts/lint-suppressions.test.ts +++ b/test/scripts/lint-suppressions.test.ts @@ -33,7 +33,11 @@ function isProductionCodeFile(relativePath: string): boolean { } function listGitCodeFiles(root: string): string[] | null { - return listGitTrackedFiles({ repoRoot, pathspecs: root })?.filter(isProductionCodeFile) ?? null; + return ( + listGitTrackedFiles({ repoRoot, pathspecs: root }) + ?.filter(isProductionCodeFile) + .filter((relativePath) => fs.existsSync(path.join(repoRoot, relativePath))) ?? null + ); } function walkCodeFiles(dir: string, files: string[] = []): string[] {