From fbeb9e6775dfe32b42381810bfac4670419cd80f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 8 Jan 2026 02:44:09 +0000 Subject: [PATCH] fix(ci): stabilize windows tests --- src/auto-reply/reply/directive-handling.ts | 16 +++--- src/config/config.test.ts | 32 ++++++++++-- src/config/paths.test.ts | 11 ++-- src/config/sessions.test.ts | 16 ++++-- src/daemon/program-args.test.ts | 29 +++++++---- src/gateway/server.models-voicewake.test.ts | 56 +++++++++++++++------ src/infra/brew.test.ts | 8 +-- src/infra/control-ui-assets.test.ts | 5 +- src/infra/heartbeat-runner.test.ts | 25 +++++---- src/web/login.coverage.test.ts | 12 +++-- 10 files changed, 148 insertions(+), 62 deletions(-) diff --git a/src/auto-reply/reply/directive-handling.ts b/src/auto-reply/reply/directive-handling.ts index 5cbb04e7b..1248d7bcb 100644 --- a/src/auto-reply/reply/directive-handling.ts +++ b/src/auto-reply/reply/directive-handling.ts @@ -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(); diff --git a/src/config/config.test.ts b/src/config/config.test.ts index 7e375579e..121e0802e 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -7,11 +7,33 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; async function withTempHome(fn: (home: string) => Promise): Promise { 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"), + ); }, ); }); diff --git a/src/config/paths.test.ts b/src/config/paths.test.ts index fd40ce3d6..c9d9cbc3a 100644 --- a/src/config/paths.test.ts +++ b/src/config/paths.test.ts @@ -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"), ); }); }); diff --git a/src/config/sessions.test.ts b/src/config/sessions.test.ts index 0a62e50cc..bb1789839 100644 --- a/src/config/sessions.test.ts +++ b/src/config/sessions.test.ts @@ -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) { diff --git a/src/daemon/program-args.test.ts b/src/daemon/program-args.test.ts index 7b7b0f891..dc1589422 100644 --- a/src/daemon/program-args.test.ts +++ b/src/daemon/program-args.test.ts @@ -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", diff --git a/src/gateway/server.models-voicewake.test.ts b/src/gateway/server.models-voicewake.test.ts index 00fc69cb3..048f90c18 100644 --- a/src/gateway/server.models-voicewake.test.ts +++ b/src/gateway/server.models-voicewake.test.ts @@ -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 () => { diff --git a/src/infra/brew.test.ts b/src/infra/brew.test.ts index ddbec8b7a..2139b04d9 100644 --- a/src/infra/brew.test.ts +++ b/src/infra/brew.test.ts @@ -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")); }); }); diff --git a/src/infra/control-ui-assets.test.ts b/src/infra/control-ui-assets.test.ts index f25db3cde..2b009e68d 100644 --- a/src/infra/control-ui-assets.test.ts +++ b/src/infra/control-ui-assets.test.ts @@ -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"), ); }); }); diff --git a/src/infra/heartbeat-runner.test.ts b/src/infra/heartbeat-runner.test.ts index b3def41a5..54ad17b62 100644 --- a/src/infra/heartbeat-runner.test.ts +++ b/src/infra/heartbeat-runner.test.ts @@ -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", diff --git a/src/web/login.coverage.test.ts b/src/web/login.coverage.test.ts index 6933feff9..a74413a01 100644 --- a/src/web/login.coverage.test.ts +++ b/src/web/login.coverage.test.ts @@ -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, });