fix(ci): stabilize windows tests

This commit is contained in:
Peter Steinberger
2026-01-08 02:44:09 +00:00
parent f3f5e49d94
commit fbeb9e6775
10 changed files with 148 additions and 62 deletions

View File

@@ -327,14 +327,14 @@ export async function handleDirectiveOnly(params: {
aliasIndex,
allowedModelKeys,
allowedModelCatalog,
resetModelOverride,
initialModelLabel,
formatModelSwitchEvent,
currentThinkLevel,
currentVerboseLevel,
currentReasoningLevel,
currentElevatedLevel,
} = params;
resetModelOverride,
initialModelLabel,
formatModelSwitchEvent,
currentThinkLevel,
currentVerboseLevel,
currentReasoningLevel,
currentElevatedLevel,
} = params;
if (directives.hasModelDirective) {
const modelDirective = directives.rawModelDirective?.trim().toLowerCase();

View File

@@ -7,11 +7,33 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
const base = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-config-"));
const previousHome = process.env.HOME;
const previousUserProfile = process.env.USERPROFILE;
const previousHomeDrive = process.env.HOMEDRIVE;
const previousHomePath = process.env.HOMEPATH;
process.env.HOME = base;
process.env.USERPROFILE = base;
if (process.platform === "win32") {
const parsed = path.parse(base);
process.env.HOMEDRIVE = parsed.root.replace(/\\$/, "");
process.env.HOMEPATH = base.slice(Math.max(parsed.root.length - 1, 0));
}
try {
return await fn(base);
} finally {
process.env.HOME = previousHome;
process.env.USERPROFILE = previousUserProfile;
if (process.platform === "win32") {
if (previousHomeDrive === undefined) {
delete process.env.HOMEDRIVE;
} else {
process.env.HOMEDRIVE = previousHomeDrive;
}
if (previousHomePath === undefined) {
delete process.env.HOMEPATH;
} else {
process.env.HOMEPATH = previousHomePath;
}
}
await fs.rm(base, { recursive: true, force: true });
}
}
@@ -402,7 +424,7 @@ describe("Nix integration (U3, U5, U9)", () => {
{ CLAWDBOT_STATE_DIR: "/custom/state/dir" },
async () => {
const { STATE_DIR_CLAWDBOT } = await import("./config.js");
expect(STATE_DIR_CLAWDBOT).toBe("/custom/state/dir");
expect(STATE_DIR_CLAWDBOT).toBe(path.resolve("/custom/state/dir"));
},
);
});
@@ -412,7 +434,9 @@ describe("Nix integration (U3, U5, U9)", () => {
{ CLAWDBOT_CONFIG_PATH: undefined, CLAWDBOT_STATE_DIR: undefined },
async () => {
const { CONFIG_PATH_CLAWDBOT } = await import("./config.js");
expect(CONFIG_PATH_CLAWDBOT).toMatch(/\.clawdbot\/clawdbot\.json$/);
expect(CONFIG_PATH_CLAWDBOT).toMatch(
/\.clawdbot[\\/]clawdbot\.json$/,
);
},
);
});
@@ -435,7 +459,9 @@ describe("Nix integration (U3, U5, U9)", () => {
},
async () => {
const { CONFIG_PATH_CLAWDBOT } = await import("./config.js");
expect(CONFIG_PATH_CLAWDBOT).toBe("/custom/state/clawdbot.json");
expect(CONFIG_PATH_CLAWDBOT).toBe(
path.join(path.resolve("/custom/state"), "clawdbot.json"),
);
},
);
});

View File

@@ -1,3 +1,4 @@
import path from "node:path";
import { describe, expect, it } from "vitest";
import { resolveOAuthDir, resolveOAuthPath } from "./paths.js";
@@ -9,9 +10,11 @@ describe("oauth paths", () => {
CLAWDBOT_STATE_DIR: "/custom/state",
} as NodeJS.ProcessEnv;
expect(resolveOAuthDir(env, "/custom/state")).toBe("/custom/oauth");
expect(resolveOAuthDir(env, "/custom/state")).toBe(
path.resolve("/custom/oauth"),
);
expect(resolveOAuthPath(env, "/custom/state")).toBe(
"/custom/oauth/oauth.json",
path.join(path.resolve("/custom/oauth"), "oauth.json"),
);
});
@@ -21,10 +24,10 @@ describe("oauth paths", () => {
} as NodeJS.ProcessEnv;
expect(resolveOAuthDir(env, "/custom/state")).toBe(
"/custom/state/credentials",
path.join("/custom/state", "credentials"),
);
expect(resolveOAuthPath(env, "/custom/state")).toBe(
"/custom/state/credentials/oauth.json",
path.join("/custom/state", "credentials", "oauth.json"),
);
});
});

View File

@@ -138,7 +138,9 @@ describe("sessions", () => {
{ CLAWDBOT_STATE_DIR: "/custom/state" } as NodeJS.ProcessEnv,
() => "/home/ignored",
);
expect(dir).toBe("/custom/state/agents/main/sessions");
expect(dir).toBe(
path.join(path.resolve("/custom/state"), "agents", "main", "sessions"),
);
});
it("falls back to CLAWDIS_STATE_DIR for session transcripts dir", () => {
@@ -146,7 +148,9 @@ describe("sessions", () => {
{ CLAWDIS_STATE_DIR: "/legacy/state" } as NodeJS.ProcessEnv,
() => "/home/ignored",
);
expect(dir).toBe("/legacy/state/agents/main/sessions");
expect(dir).toBe(
path.join(path.resolve("/legacy/state"), "agents", "main", "sessions"),
);
});
it("includes topic ids in session transcript filenames", () => {
@@ -155,7 +159,13 @@ describe("sessions", () => {
try {
const sessionFile = resolveSessionTranscriptPath("sess-1", "main", 123);
expect(sessionFile).toBe(
"/custom/state/agents/main/sessions/sess-1-topic-123.jsonl",
path.join(
path.resolve("/custom/state"),
"agents",
"main",
"sessions",
"sess-1-topic-123.jsonl",
),
);
} finally {
if (prev === undefined) {

View File

@@ -1,3 +1,4 @@
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
const fsMocks = vi.hoisted(() => ({
@@ -22,14 +23,16 @@ afterEach(() => {
describe("resolveGatewayProgramArguments", () => {
it("uses realpath-resolved dist entry when running via npx shim", async () => {
process.argv = ["node", "/tmp/.npm/_npx/63c3/node_modules/.bin/clawdbot"];
fsMocks.realpath.mockResolvedValue(
const argv1 = path.resolve(
"/tmp/.npm/_npx/63c3/node_modules/.bin/clawdbot",
);
const entryPath = path.resolve(
"/tmp/.npm/_npx/63c3/node_modules/clawdbot/dist/entry.js",
);
process.argv = ["node", argv1];
fsMocks.realpath.mockResolvedValue(entryPath);
fsMocks.access.mockImplementation(async (target: string) => {
if (
target === "/tmp/.npm/_npx/63c3/node_modules/clawdbot/dist/entry.js"
) {
if (target === entryPath) {
return;
}
throw new Error("missing");
@@ -39,7 +42,7 @@ describe("resolveGatewayProgramArguments", () => {
expect(result.programArguments).toEqual([
process.execPath,
"/tmp/.npm/_npx/63c3/node_modules/clawdbot/dist/entry.js",
entryPath,
"gateway-daemon",
"--port",
"18789",
@@ -47,12 +50,16 @@ describe("resolveGatewayProgramArguments", () => {
});
it("falls back to node_modules package dist when .bin path is not resolved", async () => {
process.argv = ["node", "/tmp/.npm/_npx/63c3/node_modules/.bin/clawdbot"];
const argv1 = path.resolve(
"/tmp/.npm/_npx/63c3/node_modules/.bin/clawdbot",
);
const indexPath = path.resolve(
"/tmp/.npm/_npx/63c3/node_modules/clawdbot/dist/index.js",
);
process.argv = ["node", argv1];
fsMocks.realpath.mockRejectedValue(new Error("no realpath"));
fsMocks.access.mockImplementation(async (target: string) => {
if (
target === "/tmp/.npm/_npx/63c3/node_modules/clawdbot/dist/index.js"
) {
if (target === indexPath) {
return;
}
throw new Error("missing");
@@ -62,7 +69,7 @@ describe("resolveGatewayProgramArguments", () => {
expect(result.programArguments).toEqual([
process.execPath,
"/tmp/.npm/_npx/63c3/node_modules/clawdbot/dist/index.js",
indexPath,
"gateway-daemon",
"--port",
"18789",

View File

@@ -17,6 +17,44 @@ import {
installGatewayTestHooks();
describe("gateway server models + voicewake", () => {
const setTempHome = (homeDir: string) => {
const prevHome = process.env.HOME;
const prevUserProfile = process.env.USERPROFILE;
const prevHomeDrive = process.env.HOMEDRIVE;
const prevHomePath = process.env.HOMEPATH;
process.env.HOME = homeDir;
process.env.USERPROFILE = homeDir;
if (process.platform === "win32") {
const parsed = path.parse(homeDir);
process.env.HOMEDRIVE = parsed.root.replace(/\\$/, "");
process.env.HOMEPATH = homeDir.slice(Math.max(parsed.root.length - 1, 0));
}
return () => {
if (prevHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = prevHome;
}
if (prevUserProfile === undefined) {
delete process.env.USERPROFILE;
} else {
process.env.USERPROFILE = prevUserProfile;
}
if (process.platform === "win32") {
if (prevHomeDrive === undefined) {
delete process.env.HOMEDRIVE;
} else {
process.env.HOMEDRIVE = prevHomeDrive;
}
if (prevHomePath === undefined) {
delete process.env.HOMEPATH;
} else {
process.env.HOMEPATH = prevHomePath;
}
}
};
};
test(
"voicewake.get returns defaults and voicewake.set broadcasts",
{ timeout: 15_000 },
@@ -24,8 +62,7 @@ describe("gateway server models + voicewake", () => {
const homeDir = await fs.mkdtemp(
path.join(os.tmpdir(), "clawdbot-home-"),
);
const prevHome = process.env.HOME;
process.env.HOME = homeDir;
const restoreHome = setTempHome(homeDir);
const { server, ws } = await startServerWithClient();
await connectOk(ws);
@@ -72,18 +109,13 @@ describe("gateway server models + voicewake", () => {
ws.close();
await server.close();
if (prevHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = prevHome;
}
restoreHome();
},
);
test("pushes voicewake.changed to nodes on connect and on updates", async () => {
const homeDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-home-"));
const prevHome = process.env.HOME;
process.env.HOME = homeDir;
const restoreHome = setTempHome(homeDir);
bridgeSendEvent.mockClear();
bridgeListConnected.mockReturnValue([{ nodeId: "n1" }]);
@@ -124,11 +156,7 @@ describe("gateway server models + voicewake", () => {
ws.close();
await server.close();
if (prevHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = prevHome;
}
restoreHome();
});
test("models.list returns model catalog", async () => {

View File

@@ -49,11 +49,11 @@ describe("brew helpers", () => {
it("includes Linuxbrew bin/sbin in path candidates", () => {
const env: NodeJS.ProcessEnv = { HOMEBREW_PREFIX: "/custom/prefix" };
const dirs = resolveBrewPathDirs({ homeDir: "/home/test", env });
expect(dirs).toContain("/custom/prefix/bin");
expect(dirs).toContain("/custom/prefix/sbin");
expect(dirs).toContain(path.join("/custom/prefix", "bin"));
expect(dirs).toContain(path.join("/custom/prefix", "sbin"));
expect(dirs).toContain("/home/linuxbrew/.linuxbrew/bin");
expect(dirs).toContain("/home/linuxbrew/.linuxbrew/sbin");
expect(dirs).toContain("/home/test/.linuxbrew/bin");
expect(dirs).toContain("/home/test/.linuxbrew/sbin");
expect(dirs).toContain(path.join("/home/test", ".linuxbrew", "bin"));
expect(dirs).toContain(path.join("/home/test", ".linuxbrew", "sbin"));
});
});

View File

@@ -51,9 +51,10 @@ describe("control UI assets helpers", () => {
});
it("resolves dist control-ui index path for dist argv1", () => {
const argv1 = path.join("/tmp", "pkg", "dist", "index.js");
const argv1 = path.resolve("/tmp", "pkg", "dist", "index.js");
const distDir = path.dirname(argv1);
expect(resolveControlUiDistIndexPath(argv1)).toBe(
path.join("/tmp", "pkg", "dist", "control-ui", "index.html"),
path.join(distDir, "control-ui", "index.html"),
);
});
});

View File

@@ -5,6 +5,11 @@ import { describe, expect, it, vi } from "vitest";
import { HEARTBEAT_PROMPT } from "../auto-reply/heartbeat.js";
import * as replyModule from "../auto-reply/reply.js";
import type { ClawdbotConfig } from "../config/config.js";
import {
resolveAgentIdFromSessionKey,
resolveMainSessionKey,
resolveStorePath,
} from "../config/sessions.js";
import {
resolveHeartbeatIntervalMs,
resolveHeartbeatPrompt,
@@ -192,15 +197,24 @@ describe("runHeartbeatOnce", () => {
"{agentId}",
"sessions.json",
);
const storePath = path.join(tmpDir, "agents", "work", "sessions.json");
const replySpy = vi.spyOn(replyModule, "getReplyFromConfig");
try {
const cfg: ClawdbotConfig = {
routing: { defaultAgentId: "work" },
agent: { heartbeat: { every: "5m" } },
whatsapp: { allowFrom: ["*"] },
session: { store: storeTemplate },
};
const sessionKey = resolveMainSessionKey(cfg);
const agentId = resolveAgentIdFromSessionKey(sessionKey);
const storePath = resolveStorePath(storeTemplate, { agentId });
await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.writeFile(
storePath,
JSON.stringify(
{
"agent:work:main": {
[sessionKey]: {
sessionId: "sid",
updatedAt: Date.now(),
lastProvider: "whatsapp",
@@ -212,13 +226,6 @@ describe("runHeartbeatOnce", () => {
),
);
const cfg: ClawdbotConfig = {
routing: { defaultAgentId: "work" },
agent: { heartbeat: { every: "5m" } },
whatsapp: { allowFrom: ["*"] },
session: { store: storeTemplate },
};
replySpy.mockResolvedValue({ text: "Hello from heartbeat" });
const sendWhatsApp = vi.fn().mockResolvedValue({
messageId: "m1",

View File

@@ -1,4 +1,6 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { DisconnectReason } from "@whiskeysockets/baileys";
import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -7,12 +9,14 @@ vi.useFakeTimers();
const rmMock = vi.spyOn(fs, "rm");
const authDir = path.join(os.tmpdir(), "wa-creds");
vi.mock("../config/config.js", () => ({
loadConfig: () =>
({
whatsapp: {
accounts: {
default: { enabled: true, authDir: "/tmp/wa-creds" },
default: { enabled: true, authDir },
},
},
}) as never,
@@ -29,9 +33,9 @@ vi.mock("./session.js", () => {
createWaSocket,
waitForWaConnection,
formatError,
WA_WEB_AUTH_DIR: "/tmp/wa-creds",
WA_WEB_AUTH_DIR: authDir,
logoutWeb: vi.fn(async (params: { authDir?: string }) => {
await fs.rm(params.authDir ?? "/tmp/wa-creds", {
await fs.rm(params.authDir ?? authDir, {
recursive: true,
force: true,
});
@@ -75,7 +79,7 @@ describe("loginWeb coverage", () => {
await expect(
loginWeb(false, "web", waitForWaConnection as never),
).rejects.toThrow(/cache cleared/i);
expect(rmMock).toHaveBeenCalledWith("/tmp/wa-creds", {
expect(rmMock).toHaveBeenCalledWith(authDir, {
recursive: true,
force: true,
});