test: harden wsl2 fixtures

This commit is contained in:
Peter Steinberger
2026-05-17 13:45:17 +01:00
parent c80cb5986f
commit e43a2efcdb
10 changed files with 77 additions and 39 deletions

View File

@@ -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]));

View File

@@ -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();

View File

@@ -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();
}
});

View File

@@ -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<OpenClawConfig> = {
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<OpenClawConfig> = {
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();
}
});

View File

@@ -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) => {

View File

@@ -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", () => {

View File

@@ -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();
});

View File

@@ -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<typeof fsPromises.open>) => 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<Buffer | { error: string }> {
let handle: Awaited<ReturnType<typeof open>> | undefined;
let handle: Awaited<ReturnType<typeof fsPromises.open>> | 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);

View File

@@ -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();
}
},
);

View File

@@ -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[] {