From c8b15af97966c81272817d265fd76f9b0b2459d1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 9 Jan 2026 16:48:24 +0100 Subject: [PATCH] refactor(test): centralize temp home + polling --- src/auto-reply/reply.directive.test.ts | 5 - src/auto-reply/reply.triggers.test.ts | 19 +--- src/infra/bridge/server.test.ts | 135 ++++++++++++------------- test/helpers/poll.ts | 25 +++++ test/helpers/temp-home.ts | 8 ++ 5 files changed, 98 insertions(+), 94 deletions(-) create mode 100644 test/helpers/poll.ts diff --git a/src/auto-reply/reply.directive.test.ts b/src/auto-reply/reply.directive.test.ts index 409a7cad6..bd93e17dc 100644 --- a/src/auto-reply/reply.directive.test.ts +++ b/src/auto-reply/reply.directive.test.ts @@ -30,18 +30,13 @@ vi.mock("../agents/model-catalog.js", () => ({ async function withTempHome(fn: (home: string) => Promise): Promise { return withTempHomeBase( async (home) => { - const previousStateDir = process.env.CLAWDBOT_STATE_DIR; const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR; const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR; - process.env.CLAWDBOT_STATE_DIR = path.join(home, ".clawdbot"); process.env.CLAWDBOT_AGENT_DIR = path.join(home, ".clawdbot", "agent"); process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR; try { return await fn(home); } finally { - if (previousStateDir === undefined) - delete process.env.CLAWDBOT_STATE_DIR; - else process.env.CLAWDBOT_STATE_DIR = previousStateDir; if (previousAgentDir === undefined) delete process.env.CLAWDBOT_AGENT_DIR; else process.env.CLAWDBOT_AGENT_DIR = previousAgentDir; diff --git a/src/auto-reply/reply.triggers.test.ts b/src/auto-reply/reply.triggers.test.ts index 49ea6309e..48308adc3 100644 --- a/src/auto-reply/reply.triggers.test.ts +++ b/src/auto-reply/reply.triggers.test.ts @@ -55,22 +55,9 @@ vi.mock("../web/session.js", () => webMocks); async function withTempHome(fn: (home: string) => Promise): Promise { return withTempHomeBase( async (home) => { - const previousStateDir = process.env.CLAWDBOT_STATE_DIR; - const previousClawdisStateDir = process.env.CLAWDIS_STATE_DIR; - process.env.CLAWDBOT_STATE_DIR = join(home, ".clawdbot"); - process.env.CLAWDIS_STATE_DIR = join(home, ".clawdbot"); - try { - vi.mocked(runEmbeddedPiAgent).mockClear(); - vi.mocked(abortEmbeddedPiRun).mockClear(); - return await fn(home); - } finally { - if (previousStateDir === undefined) - delete process.env.CLAWDBOT_STATE_DIR; - else process.env.CLAWDBOT_STATE_DIR = previousStateDir; - if (previousClawdisStateDir === undefined) - delete process.env.CLAWDIS_STATE_DIR; - else process.env.CLAWDIS_STATE_DIR = previousClawdisStateDir; - } + vi.mocked(runEmbeddedPiAgent).mockClear(); + vi.mocked(abortEmbeddedPiRun).mockClear(); + return await fn(home); }, { prefix: "clawdbot-triggers-" }, ); diff --git a/src/infra/bridge/server.test.ts b/src/infra/bridge/server.test.ts index fe320d8f1..0e93b8869 100644 --- a/src/infra/bridge/server.test.ts +++ b/src/infra/bridge/server.test.ts @@ -5,6 +5,7 @@ import path from "node:path"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import { pollUntil } from "../../../test/helpers/poll.js"; import { approveNodePairing, listNodePairing } from "../node-pairing.js"; import { configureNodeBridgeSocket, startNodeBridgeServer } from "./server.js"; @@ -169,19 +170,16 @@ describe("node bridge server", () => { sendLine(socket, { type: "pair-request", nodeId: "n2", platform: "ios" }); // Approve the pending request from the gateway side. - let reqId: string | undefined; - for (let i = 0; i < 40; i += 1) { - const list = await listNodePairing(baseDir); - const req = list.pending.find((p) => p.nodeId === "n2"); - if (req) { - reqId = req.requestId; - break; - } - await new Promise((r) => setTimeout(r, 25)); - } - expect(reqId).toBeTruthy(); - if (!reqId) throw new Error("expected a pending requestId"); - await approveNodePairing(reqId, baseDir); + const pending = await pollUntil( + async () => { + const list = await listNodePairing(baseDir); + return list.pending.find((p) => p.nodeId === "n2"); + }, + { timeoutMs: 3000 }, + ); + expect(pending).toBeTruthy(); + if (!pending) throw new Error("expected a pending request"); + await approveNodePairing(pending.requestId, baseDir); const line1 = JSON.parse(await readLine()) as { type: string; @@ -220,12 +218,10 @@ describe("node bridge server", () => { }); const socket = net.connect({ host: "127.0.0.1", port: server.port }); + await waitForSocketConnect(socket); sendLine(socket, { type: "pair-request", nodeId: "n3", platform: "ios" }); - for (let i = 0; i < 40; i += 1) { - if (requested) break; - await new Promise((r) => setTimeout(r, 25)); - } + await pollUntil(async () => requested, { timeoutMs: 3000 }); expect(requested?.nodeId).toBe("n3"); expect(typeof requested?.requestId).toBe("string"); @@ -258,19 +254,16 @@ describe("node bridge server", () => { }); // Approve the pending request from the gateway side. - let reqId: string | undefined; - for (let i = 0; i < 120; i += 1) { - const list = await listNodePairing(baseDir); - const req = list.pending.find((p) => p.nodeId === "n3-rpc"); - if (req) { - reqId = req.requestId; - break; - } - await new Promise((r) => setTimeout(r, 25)); - } - expect(reqId).toBeTruthy(); - if (!reqId) throw new Error("expected a pending requestId"); - await approveNodePairing(reqId, baseDir); + const pending = await pollUntil( + async () => { + const list = await listNodePairing(baseDir); + return list.pending.find((p) => p.nodeId === "n3-rpc"); + }, + { timeoutMs: 3000 }, + ); + expect(pending).toBeTruthy(); + if (!pending) throw new Error("expected a pending request"); + await approveNodePairing(pending.requestId, baseDir); const line1 = JSON.parse(await readLine()) as { type: string }; expect(line1.type).toBe("pair-ok"); @@ -343,6 +336,7 @@ describe("node bridge server", () => { }); const socket = net.connect({ host: "127.0.0.1", port: server.port }); + await waitForSocketConnect(socket); const readLine = createLineReader(socket); sendLine(socket, { type: "pair-request", @@ -356,19 +350,16 @@ describe("node bridge server", () => { }); // Approve the pending request from the gateway side. - let reqId: string | undefined; - for (let i = 0; i < 40; i += 1) { - const list = await listNodePairing(baseDir); - const req = list.pending.find((p) => p.nodeId === "n4"); - if (req) { - reqId = req.requestId; - break; - } - await new Promise((r) => setTimeout(r, 25)); - } - expect(reqId).toBeTruthy(); - if (!reqId) throw new Error("expected a pending requestId"); - const approved = await approveNodePairing(reqId, baseDir); + const pending = await pollUntil( + async () => { + const list = await listNodePairing(baseDir); + return list.pending.find((p) => p.nodeId === "n4"); + }, + { timeoutMs: 3000 }, + ); + expect(pending).toBeTruthy(); + if (!pending) throw new Error("expected a pending request"); + const approved = await approveNodePairing(pending.requestId, baseDir); const token = approved?.node?.token ?? ""; expect(token.length).toBeGreaterThan(0); @@ -379,6 +370,7 @@ describe("node bridge server", () => { socket.destroy(); const socket2 = net.connect({ host: "127.0.0.1", port: server.port }); + await waitForSocketConnect(socket2); const readLine2 = createLineReader(socket2); sendLine(socket2, { type: "hello", @@ -394,10 +386,10 @@ describe("node bridge server", () => { const line3 = JSON.parse(await readLine2()) as { type: string }; expect(line3.type).toBe("hello-ok"); - for (let i = 0; i < 40; i += 1) { - if (lastAuthed?.nodeId === "n4") break; - await new Promise((r) => setTimeout(r, 25)); - } + await pollUntil( + async () => (lastAuthed?.nodeId === "n4" ? lastAuthed : null), + { timeoutMs: 3000 }, + ); expect(lastAuthed?.nodeId).toBe("n4"); // Prefer paired metadata over hello payload (token verifies the stored node record). @@ -428,23 +420,21 @@ describe("node bridge server", () => { }); const socket = net.connect({ host: "127.0.0.1", port: server.port }); + await waitForSocketConnect(socket); const readLine = createLineReader(socket); sendLine(socket, { type: "pair-request", nodeId: "n5", platform: "ios" }); // Approve the pending request from the gateway side. - let reqId: string | undefined; - for (let i = 0; i < 40; i += 1) { - const list = await listNodePairing(baseDir); - const req = list.pending.find((p) => p.nodeId === "n5"); - if (req) { - reqId = req.requestId; - break; - } - await new Promise((r) => setTimeout(r, 25)); - } - expect(reqId).toBeTruthy(); - if (!reqId) throw new Error("expected a pending requestId"); - await approveNodePairing(reqId, baseDir); + const pending = await pollUntil( + async () => { + const list = await listNodePairing(baseDir); + return list.pending.find((p) => p.nodeId === "n5"); + }, + { timeoutMs: 3000 }, + ); + expect(pending).toBeTruthy(); + if (!pending) throw new Error("expected a pending request"); + await approveNodePairing(pending.requestId, baseDir); const pairOk = JSON.parse(await readLine()) as { type: string; @@ -494,6 +484,7 @@ describe("node bridge server", () => { // Ensure invoke works only for connected nodes (hello with token on a new socket). const socket2 = net.connect({ host: "127.0.0.1", port: server.port }); + await waitForSocketConnect(socket2); const readLine2 = createLineReader(socket2); sendLine(socket2, { type: "hello", nodeId: "n5", token }); const hello2 = JSON.parse(await readLine2()) as { type: string }; @@ -511,6 +502,7 @@ describe("node bridge server", () => { }); const socket = net.connect({ host: "127.0.0.1", port: server.port }); + await waitForSocketConnect(socket); const readLine = createLineReader(socket); sendLine(socket, { type: "pair-request", @@ -526,19 +518,16 @@ describe("node bridge server", () => { }); // Approve the pending request from the gateway side. - let reqId: string | undefined; - for (let i = 0; i < 40; i += 1) { - const list = await listNodePairing(baseDir); - const req = list.pending.find((p) => p.nodeId === "n-caps"); - if (req) { - reqId = req.requestId; - break; - } - await new Promise((r) => setTimeout(r, 25)); - } - expect(reqId).toBeTruthy(); - if (!reqId) throw new Error("expected a pending requestId"); - await approveNodePairing(reqId, baseDir); + const pending = await pollUntil( + async () => { + const list = await listNodePairing(baseDir); + return list.pending.find((p) => p.nodeId === "n-caps"); + }, + { timeoutMs: 3000 }, + ); + expect(pending).toBeTruthy(); + if (!pending) throw new Error("expected a pending request"); + await approveNodePairing(pending.requestId, baseDir); const pairOk = JSON.parse(await readLine()) as { type: string }; expect(pairOk.type).toBe("pair-ok"); diff --git a/test/helpers/poll.ts b/test/helpers/poll.ts new file mode 100644 index 000000000..3aed881e8 --- /dev/null +++ b/test/helpers/poll.ts @@ -0,0 +1,25 @@ +export type PollOptions = { + timeoutMs?: number; + intervalMs?: number; +}; + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export async function pollUntil( + fn: () => Promise, + opts: PollOptions = {}, +): Promise { + const timeoutMs = opts.timeoutMs ?? 2000; + const intervalMs = opts.intervalMs ?? 25; + const start = Date.now(); + + while (Date.now() - start < timeoutMs) { + const value = await fn(); + if (value !== null && value !== undefined) return value; + await sleep(intervalMs); + } + + return undefined; +} diff --git a/test/helpers/temp-home.ts b/test/helpers/temp-home.ts index 2a07512eb..5c7320a4a 100644 --- a/test/helpers/temp-home.ts +++ b/test/helpers/temp-home.ts @@ -7,6 +7,8 @@ type EnvSnapshot = { userProfile: string | undefined; homeDrive: string | undefined; homePath: string | undefined; + stateDir: string | undefined; + legacyStateDir: string | undefined; }; function snapshotEnv(): EnvSnapshot { @@ -15,6 +17,8 @@ function snapshotEnv(): EnvSnapshot { userProfile: process.env.USERPROFILE, homeDrive: process.env.HOMEDRIVE, homePath: process.env.HOMEPATH, + stateDir: process.env.CLAWDBOT_STATE_DIR, + legacyStateDir: process.env.CLAWDIS_STATE_DIR, }; } @@ -27,11 +31,15 @@ function restoreEnv(snapshot: EnvSnapshot) { restoreKey("USERPROFILE", snapshot.userProfile); restoreKey("HOMEDRIVE", snapshot.homeDrive); restoreKey("HOMEPATH", snapshot.homePath); + restoreKey("CLAWDBOT_STATE_DIR", snapshot.stateDir); + restoreKey("CLAWDIS_STATE_DIR", snapshot.legacyStateDir); } function setTempHome(base: string) { process.env.HOME = base; process.env.USERPROFILE = base; + process.env.CLAWDBOT_STATE_DIR = path.join(base, ".clawdbot"); + process.env.CLAWDIS_STATE_DIR = path.join(base, ".clawdbot"); if (process.platform !== "win32") return; const match = base.match(/^([A-Za-z]:)(.*)$/);