refactor(test): centralize temp home + polling

This commit is contained in:
Peter Steinberger
2026-01-09 16:48:24 +01:00
parent eb73b4e58e
commit c8b15af979
5 changed files with 98 additions and 94 deletions

View File

@@ -30,18 +30,13 @@ vi.mock("../agents/model-catalog.js", () => ({
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
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;

View File

@@ -55,22 +55,9 @@ vi.mock("../web/session.js", () => webMocks);
async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
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-" },
);

View File

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

25
test/helpers/poll.ts Normal file
View File

@@ -0,0 +1,25 @@
export type PollOptions = {
timeoutMs?: number;
intervalMs?: number;
};
function sleep(ms: number) {
return new Promise<void>((resolve) => setTimeout(resolve, ms));
}
export async function pollUntil<T>(
fn: () => Promise<T | null | undefined>,
opts: PollOptions = {},
): Promise<T | undefined> {
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;
}

View File

@@ -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]:)(.*)$/);