test: speed up gateway suite setup

This commit is contained in:
Peter Steinberger
2026-01-23 04:28:02 +00:00
parent dd2400fb2a
commit 64be2b2cd1
7 changed files with 606 additions and 623 deletions

View File

@@ -49,469 +49,372 @@ function parseSseDataLines(text: string): string[] {
} }
describe("OpenAI-compatible HTTP API (e2e)", () => { describe("OpenAI-compatible HTTP API (e2e)", () => {
it("is disabled by default (requires config)", { timeout: 120_000 }, async () => { it("rejects when disabled (default + config)", { timeout: 120_000 }, async () => {
const port = await getFreePort(); {
const server = await startServerWithDefaultConfig(port); const port = await getFreePort();
try { const server = await startServerWithDefaultConfig(port);
const res = await postChatCompletions(port, { try {
model: "clawdbot", const res = await postChatCompletions(port, {
messages: [{ role: "user", content: "hi" }], model: "clawdbot",
messages: [{ role: "user", content: "hi" }],
});
expect(res.status).toBe(404);
} finally {
await server.close({ reason: "test done" });
}
}
{
const port = await getFreePort();
const server = await startServer(port, {
openAiChatCompletionsEnabled: false,
}); });
expect(res.status).toBe(404); try {
} finally { const res = await postChatCompletions(port, {
await server.close({ reason: "test done" }); model: "clawdbot",
messages: [{ role: "user", content: "hi" }],
});
expect(res.status).toBe(404);
} finally {
await server.close({ reason: "test done" });
}
} }
}); });
it("can be disabled via config (404)", async () => { it("handles request validation and routing", async () => {
const port = await getFreePort();
const server = await startServer(port, {
openAiChatCompletionsEnabled: false,
});
try {
const res = await postChatCompletions(port, {
model: "clawdbot",
messages: [{ role: "user", content: "hi" }],
});
expect(res.status).toBe(404);
} finally {
await server.close({ reason: "test done" });
}
});
it("rejects non-POST", async () => {
const port = await getFreePort(); const port = await getFreePort();
const server = await startServer(port); const server = await startServer(port);
const mockAgentOnce = (payloads: Array<{ text: string }>) => {
agentCommand.mockReset();
agentCommand.mockResolvedValueOnce({ payloads } as never);
};
try { try {
const res = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { {
method: "GET", const res = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
headers: { authorization: "Bearer secret" }, method: "GET",
}); headers: { authorization: "Bearer secret" },
expect(res.status).toBe(405); });
} finally { expect(res.status).toBe(405);
await server.close({ reason: "test done" }); await res.text();
} }
});
it("rejects missing auth", async () => { {
const port = await getFreePort(); const res = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
const server = await startServer(port); method: "POST",
try { headers: { "content-type": "application/json" },
const res = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, { body: JSON.stringify({ messages: [{ role: "user", content: "hi" }] }),
method: "POST", });
headers: { "content-type": "application/json" }, expect(res.status).toBe(401);
body: JSON.stringify({ messages: [{ role: "user", content: "hi" }] }), await res.text();
}); }
expect(res.status).toBe(401);
} finally {
await server.close({ reason: "test done" });
}
});
it("routes to a specific agent via header", async () => { {
agentCommand.mockResolvedValueOnce({ mockAgentOnce([{ text: "hello" }]);
payloads: [{ text: "hello" }], const res = await postChatCompletions(
} as never); port,
{ model: "clawdbot", messages: [{ role: "user", content: "hi" }] },
{ "x-clawdbot-agent-id": "beta" },
);
expect(res.status).toBe(200);
const port = await getFreePort(); expect(agentCommand).toHaveBeenCalledTimes(1);
const server = await startServer(port); const [opts] = agentCommand.mock.calls[0] ?? [];
try { expect((opts as { sessionKey?: string } | undefined)?.sessionKey ?? "").toMatch(
const res = await postChatCompletions( /^agent:beta:/,
port, );
{ model: "clawdbot", messages: [{ role: "user", content: "hi" }] }, await res.text();
{ "x-clawdbot-agent-id": "beta" }, }
);
expect(res.status).toBe(200);
expect(agentCommand).toHaveBeenCalledTimes(1); {
const [opts] = agentCommand.mock.calls[0] ?? []; mockAgentOnce([{ text: "hello" }]);
expect((opts as { sessionKey?: string } | undefined)?.sessionKey ?? "").toMatch( const res = await postChatCompletions(port, {
/^agent:beta:/,
);
} finally {
await server.close({ reason: "test done" });
}
});
it("routes to a specific agent via model (no custom headers)", async () => {
agentCommand.mockResolvedValueOnce({
payloads: [{ text: "hello" }],
} as never);
const port = await getFreePort();
const server = await startServer(port);
try {
const res = await postChatCompletions(port, {
model: "clawdbot:beta",
messages: [{ role: "user", content: "hi" }],
});
expect(res.status).toBe(200);
expect(agentCommand).toHaveBeenCalledTimes(1);
const [opts] = agentCommand.mock.calls[0] ?? [];
expect((opts as { sessionKey?: string } | undefined)?.sessionKey ?? "").toMatch(
/^agent:beta:/,
);
} finally {
await server.close({ reason: "test done" });
}
});
it("prefers explicit header agent over model agent", async () => {
agentCommand.mockResolvedValueOnce({
payloads: [{ text: "hello" }],
} as never);
const port = await getFreePort();
const server = await startServer(port);
try {
const res = await postChatCompletions(
port,
{
model: "clawdbot:beta", model: "clawdbot:beta",
messages: [{ role: "user", content: "hi" }], messages: [{ role: "user", content: "hi" }],
}, });
{ "x-clawdbot-agent-id": "alpha" }, expect(res.status).toBe(200);
);
expect(res.status).toBe(200);
expect(agentCommand).toHaveBeenCalledTimes(1); expect(agentCommand).toHaveBeenCalledTimes(1);
const [opts] = agentCommand.mock.calls[0] ?? []; const [opts] = agentCommand.mock.calls[0] ?? [];
expect((opts as { sessionKey?: string } | undefined)?.sessionKey ?? "").toMatch( expect((opts as { sessionKey?: string } | undefined)?.sessionKey ?? "").toMatch(
/^agent:alpha:/, /^agent:beta:/,
); );
} finally { await res.text();
await server.close({ reason: "test done" }); }
}
});
it("honors x-clawdbot-session-key override", async () => { {
agentCommand.mockResolvedValueOnce({ mockAgentOnce([{ text: "hello" }]);
payloads: [{ text: "hello" }], const res = await postChatCompletions(
} as never); port,
const port = await getFreePort();
const server = await startServer(port);
try {
const res = await postChatCompletions(
port,
{ model: "clawdbot", messages: [{ role: "user", content: "hi" }] },
{
"x-clawdbot-agent-id": "beta",
"x-clawdbot-session-key": "agent:beta:openai:custom",
},
);
expect(res.status).toBe(200);
const [opts] = agentCommand.mock.calls[0] ?? [];
expect((opts as { sessionKey?: string } | undefined)?.sessionKey).toBe(
"agent:beta:openai:custom",
);
} finally {
await server.close({ reason: "test done" });
}
});
it("uses OpenAI user for a stable session key", async () => {
agentCommand.mockResolvedValueOnce({
payloads: [{ text: "hello" }],
} as never);
const port = await getFreePort();
const server = await startServer(port);
try {
const res = await postChatCompletions(port, {
user: "alice",
model: "clawdbot",
messages: [{ role: "user", content: "hi" }],
});
expect(res.status).toBe(200);
const [opts] = agentCommand.mock.calls[0] ?? [];
expect((opts as { sessionKey?: string } | undefined)?.sessionKey ?? "").toContain(
"openai-user:alice",
);
} finally {
await server.close({ reason: "test done" });
}
});
it("extracts user message text from array content", async () => {
agentCommand.mockResolvedValueOnce({
payloads: [{ text: "hello" }],
} as never);
const port = await getFreePort();
const server = await startServer(port);
try {
const res = await postChatCompletions(port, {
model: "clawdbot",
messages: [
{ {
role: "user", model: "clawdbot:beta",
content: [ messages: [{ role: "user", content: "hi" }],
{ type: "text", text: "hello" },
{ type: "input_text", text: "world" },
],
}, },
], { "x-clawdbot-agent-id": "alpha" },
}); );
expect(res.status).toBe(200); expect(res.status).toBe(200);
const [opts] = agentCommand.mock.calls[0] ?? []; expect(agentCommand).toHaveBeenCalledTimes(1);
expect((opts as { message?: string } | undefined)?.message).toBe("hello\nworld"); const [opts] = agentCommand.mock.calls[0] ?? [];
expect((opts as { sessionKey?: string } | undefined)?.sessionKey ?? "").toMatch(
/^agent:alpha:/,
);
await res.text();
}
{
mockAgentOnce([{ text: "hello" }]);
const res = await postChatCompletions(
port,
{ model: "clawdbot", messages: [{ role: "user", content: "hi" }] },
{
"x-clawdbot-agent-id": "beta",
"x-clawdbot-session-key": "agent:beta:openai:custom",
},
);
expect(res.status).toBe(200);
const [opts] = agentCommand.mock.calls[0] ?? [];
expect((opts as { sessionKey?: string } | undefined)?.sessionKey).toBe(
"agent:beta:openai:custom",
);
await res.text();
}
{
mockAgentOnce([{ text: "hello" }]);
const res = await postChatCompletions(port, {
user: "alice",
model: "clawdbot",
messages: [{ role: "user", content: "hi" }],
});
expect(res.status).toBe(200);
const [opts] = agentCommand.mock.calls[0] ?? [];
expect((opts as { sessionKey?: string } | undefined)?.sessionKey ?? "").toContain(
"openai-user:alice",
);
await res.text();
}
{
mockAgentOnce([{ text: "hello" }]);
const res = await postChatCompletions(port, {
model: "clawdbot",
messages: [
{
role: "user",
content: [
{ type: "text", text: "hello" },
{ type: "input_text", text: "world" },
],
},
],
});
expect(res.status).toBe(200);
const [opts] = agentCommand.mock.calls[0] ?? [];
expect((opts as { message?: string } | undefined)?.message).toBe("hello\nworld");
await res.text();
}
{
mockAgentOnce([{ text: "I am Claude" }]);
const res = await postChatCompletions(port, {
model: "clawdbot",
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "Hello, who are you?" },
{ role: "assistant", content: "I am Claude." },
{ role: "user", content: "What did I just ask you?" },
],
});
expect(res.status).toBe(200);
const [opts] = agentCommand.mock.calls[0] ?? [];
const message = (opts as { message?: string } | undefined)?.message ?? "";
expect(message).toContain(HISTORY_CONTEXT_MARKER);
expect(message).toContain("User: Hello, who are you?");
expect(message).toContain("Assistant: I am Claude.");
expect(message).toContain(CURRENT_MESSAGE_MARKER);
expect(message).toContain("User: What did I just ask you?");
await res.text();
}
{
mockAgentOnce([{ text: "hello" }]);
const res = await postChatCompletions(port, {
model: "clawdbot",
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "Hello" },
],
});
expect(res.status).toBe(200);
const [opts] = agentCommand.mock.calls[0] ?? [];
const message = (opts as { message?: string } | undefined)?.message ?? "";
expect(message).not.toContain(HISTORY_CONTEXT_MARKER);
expect(message).not.toContain(CURRENT_MESSAGE_MARKER);
expect(message).toBe("Hello");
await res.text();
}
{
mockAgentOnce([{ text: "hello" }]);
const res = await postChatCompletions(port, {
model: "clawdbot",
messages: [
{ role: "developer", content: "You are a helpful assistant." },
{ role: "user", content: "Hello" },
],
});
expect(res.status).toBe(200);
const [opts] = agentCommand.mock.calls[0] ?? [];
const extraSystemPrompt =
(opts as { extraSystemPrompt?: string } | undefined)?.extraSystemPrompt ?? "";
expect(extraSystemPrompt).toBe("You are a helpful assistant.");
await res.text();
}
{
mockAgentOnce([{ text: "ok" }]);
const res = await postChatCompletions(port, {
model: "clawdbot",
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "What's the weather?" },
{ role: "assistant", content: "Checking the weather." },
{ role: "tool", content: "Sunny, 70F." },
],
});
expect(res.status).toBe(200);
const [opts] = agentCommand.mock.calls[0] ?? [];
const message = (opts as { message?: string } | undefined)?.message ?? "";
expect(message).toContain(HISTORY_CONTEXT_MARKER);
expect(message).toContain("User: What's the weather?");
expect(message).toContain("Assistant: Checking the weather.");
expect(message).toContain(CURRENT_MESSAGE_MARKER);
expect(message).toContain("Tool: Sunny, 70F.");
await res.text();
}
{
mockAgentOnce([{ text: "hello" }]);
const res = await postChatCompletions(port, {
stream: false,
model: "clawdbot",
messages: [{ role: "user", content: "hi" }],
});
expect(res.status).toBe(200);
const json = (await res.json()) as Record<string, unknown>;
expect(json.object).toBe("chat.completion");
expect(Array.isArray(json.choices)).toBe(true);
const choice0 = (json.choices as Array<Record<string, unknown>>)[0] ?? {};
const msg = (choice0.message as Record<string, unknown> | undefined) ?? {};
expect(msg.role).toBe("assistant");
expect(msg.content).toBe("hello");
}
{
const res = await postChatCompletions(port, {
model: "clawdbot",
messages: [{ role: "system", content: "yo" }],
});
expect(res.status).toBe(400);
const missingUserJson = (await res.json()) as Record<string, unknown>;
expect((missingUserJson.error as Record<string, unknown> | undefined)?.type).toBe(
"invalid_request_error",
);
}
} finally { } finally {
await server.close({ reason: "test done" }); await server.close({ reason: "test done" });
} }
}); });
it("includes conversation history when multiple messages are provided", async () => { it("streams SSE chunks when stream=true", async () => {
agentCommand.mockResolvedValueOnce({
payloads: [{ text: "I am Claude" }],
} as never);
const port = await getFreePort(); const port = await getFreePort();
const server = await startServer(port); const server = await startServer(port);
try { try {
const res = await postChatCompletions(port, { {
model: "clawdbot", agentCommand.mockReset();
messages: [ agentCommand.mockImplementationOnce(async (opts: unknown) => {
{ role: "system", content: "You are a helpful assistant." }, const runId = (opts as { runId?: string } | undefined)?.runId ?? "";
{ role: "user", content: "Hello, who are you?" }, emitAgentEvent({ runId, stream: "assistant", data: { delta: "he" } });
{ role: "assistant", content: "I am Claude." }, emitAgentEvent({ runId, stream: "assistant", data: { delta: "llo" } });
{ role: "user", content: "What did I just ask you?" }, return { payloads: [{ text: "hello" }] } as never;
], });
});
expect(res.status).toBe(200);
const [opts] = agentCommand.mock.calls[0] ?? []; const res = await postChatCompletions(port, {
const message = (opts as { message?: string } | undefined)?.message ?? ""; stream: true,
expect(message).toContain(HISTORY_CONTEXT_MARKER); model: "clawdbot",
expect(message).toContain("User: Hello, who are you?"); messages: [{ role: "user", content: "hi" }],
expect(message).toContain("Assistant: I am Claude."); });
expect(message).toContain(CURRENT_MESSAGE_MARKER); expect(res.status).toBe(200);
expect(message).toContain("User: What did I just ask you?"); expect(res.headers.get("content-type") ?? "").toContain("text/event-stream");
} finally {
await server.close({ reason: "test done" });
}
});
it("does not include history markers for single message", async () => { const text = await res.text();
agentCommand.mockResolvedValueOnce({ const data = parseSseDataLines(text);
payloads: [{ text: "hello" }], expect(data[data.length - 1]).toBe("[DONE]");
} as never);
const port = await getFreePort(); const jsonChunks = data
const server = await startServer(port); .filter((d) => d !== "[DONE]")
try { .map((d) => JSON.parse(d) as Record<string, unknown>);
const res = await postChatCompletions(port, { expect(jsonChunks.some((c) => c.object === "chat.completion.chunk")).toBe(true);
model: "clawdbot", const allContent = jsonChunks
messages: [ .flatMap((c) => (c.choices as Array<Record<string, unknown>> | undefined) ?? [])
{ role: "system", content: "You are a helpful assistant." }, .map((choice) => (choice.delta as Record<string, unknown> | undefined)?.content)
{ role: "user", content: "Hello" }, .filter((v): v is string => typeof v === "string")
], .join("");
}); expect(allContent).toBe("hello");
expect(res.status).toBe(200); }
const [opts] = agentCommand.mock.calls[0] ?? []; {
const message = (opts as { message?: string } | undefined)?.message ?? ""; agentCommand.mockReset();
expect(message).not.toContain(HISTORY_CONTEXT_MARKER); agentCommand.mockImplementationOnce(async (opts: unknown) => {
expect(message).not.toContain(CURRENT_MESSAGE_MARKER); const runId = (opts as { runId?: string } | undefined)?.runId ?? "";
expect(message).toBe("Hello"); emitAgentEvent({ runId, stream: "assistant", data: { delta: "hi" } });
} finally { emitAgentEvent({ runId, stream: "assistant", data: { delta: "hi" } });
await server.close({ reason: "test done" }); return { payloads: [{ text: "hihi" }] } as never;
} });
});
it("treats developer role same as system role", async () => { const repeatedRes = await postChatCompletions(port, {
agentCommand.mockResolvedValueOnce({ stream: true,
payloads: [{ text: "hello" }], model: "clawdbot",
} as never); messages: [{ role: "user", content: "hi" }],
});
expect(repeatedRes.status).toBe(200);
const repeatedText = await repeatedRes.text();
const repeatedData = parseSseDataLines(repeatedText);
const repeatedChunks = repeatedData
.filter((d) => d !== "[DONE]")
.map((d) => JSON.parse(d) as Record<string, unknown>);
const repeatedContent = repeatedChunks
.flatMap((c) => (c.choices as Array<Record<string, unknown>> | undefined) ?? [])
.map((choice) => (choice.delta as Record<string, unknown> | undefined)?.content)
.filter((v): v is string => typeof v === "string")
.join("");
expect(repeatedContent).toBe("hihi");
}
const port = await getFreePort(); {
const server = await startServer(port); agentCommand.mockReset();
try { agentCommand.mockResolvedValueOnce({
const res = await postChatCompletions(port, { payloads: [{ text: "hello" }],
model: "clawdbot", } as never);
messages: [
{ role: "developer", content: "You are a helpful assistant." },
{ role: "user", content: "Hello" },
],
});
expect(res.status).toBe(200);
const [opts] = agentCommand.mock.calls[0] ?? []; const fallbackRes = await postChatCompletions(port, {
const extraSystemPrompt = stream: true,
(opts as { extraSystemPrompt?: string } | undefined)?.extraSystemPrompt ?? ""; model: "clawdbot",
expect(extraSystemPrompt).toBe("You are a helpful assistant."); messages: [{ role: "user", content: "hi" }],
} finally { });
await server.close({ reason: "test done" }); expect(fallbackRes.status).toBe(200);
} const fallbackText = await fallbackRes.text();
}); expect(fallbackText).toContain("[DONE]");
expect(fallbackText).toContain("hello");
it("includes tool output when it is the latest message", async () => { }
agentCommand.mockResolvedValueOnce({
payloads: [{ text: "ok" }],
} as never);
const port = await getFreePort();
const server = await startServer(port);
try {
const res = await postChatCompletions(port, {
model: "clawdbot",
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "What's the weather?" },
{ role: "assistant", content: "Checking the weather." },
{ role: "tool", content: "Sunny, 70F." },
],
});
expect(res.status).toBe(200);
const [opts] = agentCommand.mock.calls[0] ?? [];
const message = (opts as { message?: string } | undefined)?.message ?? "";
expect(message).toContain(HISTORY_CONTEXT_MARKER);
expect(message).toContain("User: What's the weather?");
expect(message).toContain("Assistant: Checking the weather.");
expect(message).toContain(CURRENT_MESSAGE_MARKER);
expect(message).toContain("Tool: Sunny, 70F.");
} finally {
await server.close({ reason: "test done" });
}
});
it("returns a non-streaming OpenAI chat.completion response", async () => {
agentCommand.mockResolvedValueOnce({
payloads: [{ text: "hello" }],
} as never);
const port = await getFreePort();
const server = await startServer(port);
try {
const res = await postChatCompletions(port, {
stream: false,
model: "clawdbot",
messages: [{ role: "user", content: "hi" }],
});
expect(res.status).toBe(200);
const json = (await res.json()) as Record<string, unknown>;
expect(json.object).toBe("chat.completion");
expect(Array.isArray(json.choices)).toBe(true);
const choice0 = (json.choices as Array<Record<string, unknown>>)[0] ?? {};
const msg = (choice0.message as Record<string, unknown> | undefined) ?? {};
expect(msg.role).toBe("assistant");
expect(msg.content).toBe("hello");
} finally {
await server.close({ reason: "test done" });
}
});
it("requires a user message", async () => {
const port = await getFreePort();
const server = await startServer(port);
try {
const res = await postChatCompletions(port, {
model: "clawdbot",
messages: [{ role: "system", content: "yo" }],
});
expect(res.status).toBe(400);
const json = (await res.json()) as Record<string, unknown>;
expect((json.error as Record<string, unknown> | undefined)?.type).toBe(
"invalid_request_error",
);
} finally {
await server.close({ reason: "test done" });
}
});
it("streams SSE chunks when stream=true (delta events)", async () => {
agentCommand.mockImplementationOnce(async (opts: unknown) => {
const runId = (opts as { runId?: string } | undefined)?.runId ?? "";
emitAgentEvent({ runId, stream: "assistant", data: { delta: "he" } });
emitAgentEvent({ runId, stream: "assistant", data: { delta: "llo" } });
return { payloads: [{ text: "hello" }] } as never;
});
const port = await getFreePort();
const server = await startServer(port);
try {
const res = await postChatCompletions(port, {
stream: true,
model: "clawdbot",
messages: [{ role: "user", content: "hi" }],
});
expect(res.status).toBe(200);
expect(res.headers.get("content-type") ?? "").toContain("text/event-stream");
const text = await res.text();
const data = parseSseDataLines(text);
expect(data[data.length - 1]).toBe("[DONE]");
const jsonChunks = data
.filter((d) => d !== "[DONE]")
.map((d) => JSON.parse(d) as Record<string, unknown>);
expect(jsonChunks.some((c) => c.object === "chat.completion.chunk")).toBe(true);
const allContent = jsonChunks
.flatMap((c) => (c.choices as Array<Record<string, unknown>> | undefined) ?? [])
.map((choice) => (choice.delta as Record<string, unknown> | undefined)?.content)
.filter((v): v is string => typeof v === "string")
.join("");
expect(allContent).toBe("hello");
} finally {
await server.close({ reason: "test done" });
}
});
it("preserves repeated identical deltas when streaming SSE", async () => {
agentCommand.mockImplementationOnce(async (opts: unknown) => {
const runId = (opts as { runId?: string } | undefined)?.runId ?? "";
emitAgentEvent({ runId, stream: "assistant", data: { delta: "hi" } });
emitAgentEvent({ runId, stream: "assistant", data: { delta: "hi" } });
return { payloads: [{ text: "hihi" }] } as never;
});
const port = await getFreePort();
const server = await startServer(port);
try {
const res = await postChatCompletions(port, {
stream: true,
model: "clawdbot",
messages: [{ role: "user", content: "hi" }],
});
expect(res.status).toBe(200);
const text = await res.text();
const data = parseSseDataLines(text);
const jsonChunks = data
.filter((d) => d !== "[DONE]")
.map((d) => JSON.parse(d) as Record<string, unknown>);
const allContent = jsonChunks
.flatMap((c) => (c.choices as Array<Record<string, unknown>> | undefined) ?? [])
.map((choice) => (choice.delta as Record<string, unknown> | undefined)?.content)
.filter((v): v is string => typeof v === "string")
.join("");
expect(allContent).toBe("hihi");
} finally {
await server.close({ reason: "test done" });
}
});
it("streams SSE chunks when stream=true (fallback when no deltas)", async () => {
agentCommand.mockResolvedValueOnce({
payloads: [{ text: "hello" }],
} as never);
const port = await getFreePort();
const server = await startServer(port);
try {
const res = await postChatCompletions(port, {
stream: true,
model: "clawdbot",
messages: [{ role: "user", content: "hi" }],
});
expect(res.status).toBe(200);
const text = await res.text();
expect(text).toContain("[DONE]");
expect(text).toContain("hello");
} finally { } finally {
await server.close({ reason: "test done" }); await server.close({ reason: "test done" });
} }

View File

@@ -1,95 +1,106 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { afterEach, describe, expect, it } from "vitest"; import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { WebSocket } from "ws";
import { import {
connectOk, connectOk,
getFreePort,
installGatewayTestHooks, installGatewayTestHooks,
onceMessage, onceMessage,
startServerWithClient, startGatewayServer,
} from "./test-helpers.js"; } from "./test-helpers.js";
installGatewayTestHooks(); installGatewayTestHooks({ scope: "suite" });
const servers: Array<Awaited<ReturnType<typeof startServerWithClient>>> = []; let server: Awaited<ReturnType<typeof startGatewayServer>>;
let port = 0;
let previousToken: string | undefined;
afterEach(async () => { beforeAll(async () => {
for (const { server, ws } of servers) { previousToken = process.env.CLAWDBOT_GATEWAY_TOKEN;
try { delete process.env.CLAWDBOT_GATEWAY_TOKEN;
ws.close(); port = await getFreePort();
await server.close(); server = await startGatewayServer(port);
} catch {
/* ignore */
}
}
servers.length = 0;
await new Promise((resolve) => setTimeout(resolve, 50));
}); });
afterAll(async () => {
await server.close();
if (previousToken === undefined) delete process.env.CLAWDBOT_GATEWAY_TOKEN;
else process.env.CLAWDBOT_GATEWAY_TOKEN = previousToken;
});
const openClient = async () => {
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
await new Promise<void>((resolve) => ws.once("open", resolve));
await connectOk(ws);
return ws;
};
describe("gateway config.apply", () => { describe("gateway config.apply", () => {
it("writes config, stores sentinel, and schedules restart", async () => { it("writes config, stores sentinel, and schedules restart", async () => {
const result = await startServerWithClient(); const ws = await openClient();
servers.push(result);
const { ws } = result;
await connectOk(ws);
const id = "req-1";
ws.send(
JSON.stringify({
type: "req",
id,
method: "config.apply",
params: {
raw: '{ "agents": { "list": [{ "id": "main", "workspace": "~/clawd" }] } }',
sessionKey: "agent:main:whatsapp:dm:+15555550123",
restartDelayMs: 0,
},
}),
);
const res = await onceMessage<{ ok: boolean; payload?: unknown }>(
ws,
(o) => o.type === "res" && o.id === id,
);
expect(res.ok).toBe(true);
// Verify sentinel file was created (restart was scheduled)
const sentinelPath = path.join(os.homedir(), ".clawdbot", "restart-sentinel.json");
// Wait for file to be written
await new Promise((resolve) => setTimeout(resolve, 100));
try { try {
const raw = await fs.readFile(sentinelPath, "utf-8"); const id = "req-1";
const parsed = JSON.parse(raw) as { payload?: { kind?: string } }; ws.send(
expect(parsed.payload?.kind).toBe("config-apply"); JSON.stringify({
} catch { type: "req",
// File may not exist if signal delivery is mocked, verify response was ok instead id,
method: "config.apply",
params: {
raw: '{ "agents": { "list": [{ "id": "main", "workspace": "~/clawd" }] } }',
sessionKey: "agent:main:whatsapp:dm:+15555550123",
restartDelayMs: 0,
},
}),
);
const res = await onceMessage<{ ok: boolean; payload?: unknown }>(
ws,
(o) => o.type === "res" && o.id === id,
);
expect(res.ok).toBe(true); expect(res.ok).toBe(true);
// Verify sentinel file was created (restart was scheduled)
const sentinelPath = path.join(os.homedir(), ".clawdbot", "restart-sentinel.json");
// Wait for file to be written
await new Promise((resolve) => setTimeout(resolve, 100));
try {
const raw = await fs.readFile(sentinelPath, "utf-8");
const parsed = JSON.parse(raw) as { payload?: { kind?: string } };
expect(parsed.payload?.kind).toBe("config-apply");
} catch {
// File may not exist if signal delivery is mocked, verify response was ok instead
expect(res.ok).toBe(true);
}
} finally {
ws.close();
} }
}); });
it("rejects invalid raw config", async () => { it("rejects invalid raw config", async () => {
const result = await startServerWithClient(); const ws = await openClient();
servers.push(result); try {
const { ws } = result; const id = "req-2";
await connectOk(ws); ws.send(
JSON.stringify({
const id = "req-2"; type: "req",
ws.send( id,
JSON.stringify({ method: "config.apply",
type: "req", params: {
id, raw: "{",
method: "config.apply", },
params: { }),
raw: "{", );
}, const res = await onceMessage<{ ok: boolean; error?: unknown }>(
}), ws,
); (o) => o.type === "res" && o.id === id,
const res = await onceMessage<{ ok: boolean; error?: unknown }>( );
ws, expect(res.ok).toBe(false);
(o) => o.type === "res" && o.id === id, } finally {
); ws.close();
expect(res.ok).toBe(false); }
}); });
}); });

View File

@@ -12,7 +12,7 @@ import {
waitForSystemEvent, waitForSystemEvent,
} from "./test-helpers.js"; } from "./test-helpers.js";
installGatewayTestHooks(); installGatewayTestHooks({ scope: "suite" });
async function yieldToEventLoop() { async function yieldToEventLoop() {
// Avoid relying on timers (fake timers can leak between tests). // Avoid relying on timers (fake timers can leak between tests).

View File

@@ -1,7 +1,7 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { describe, expect, test } from "vitest"; import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { WebSocket } from "ws"; import { WebSocket } from "ws";
import { emitAgentEvent } from "../infra/agent-events.js"; import { emitAgentEvent } from "../infra/agent-events.js";
import { import {
@@ -21,12 +21,35 @@ import {
} from "./test-helpers.js"; } from "./test-helpers.js";
import { buildDeviceAuthPayload } from "./device-auth.js"; import { buildDeviceAuthPayload } from "./device-auth.js";
installGatewayTestHooks(); installGatewayTestHooks({ scope: "suite" });
let server: Awaited<ReturnType<typeof startGatewayServer>>;
let port = 0;
let previousToken: string | undefined;
beforeAll(async () => {
previousToken = process.env.CLAWDBOT_GATEWAY_TOKEN;
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
port = await getFreePort();
server = await startGatewayServer(port);
});
afterAll(async () => {
await server.close();
if (previousToken === undefined) delete process.env.CLAWDBOT_GATEWAY_TOKEN;
else process.env.CLAWDBOT_GATEWAY_TOKEN = previousToken;
});
const openClient = async (opts?: Parameters<typeof connectOk>[1]) => {
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
await new Promise<void>((resolve) => ws.once("open", resolve));
await connectOk(ws, opts);
return ws;
};
describe("gateway server health/presence", () => { describe("gateway server health/presence", () => {
test("connect + health + presence + status succeed", { timeout: 60_000 }, async () => { test("connect + health + presence + status succeed", { timeout: 60_000 }, async () => {
const { server, ws } = await startServerWithClient(); const ws = await openClient();
await connectOk(ws);
const healthP = onceMessage(ws, (o) => o.type === "res" && o.id === "health1"); const healthP = onceMessage(ws, (o) => o.type === "res" && o.id === "health1");
const statusP = onceMessage(ws, (o) => o.type === "res" && o.id === "status1"); const statusP = onceMessage(ws, (o) => o.type === "res" && o.id === "status1");
@@ -51,7 +74,6 @@ describe("gateway server health/presence", () => {
expect(Array.isArray(presence.payload)).toBe(true); expect(Array.isArray(presence.payload)).toBe(true);
ws.close(); ws.close();
await server.close();
}); });
test("broadcasts heartbeat events and serves last-heartbeat", async () => { test("broadcasts heartbeat events and serves last-heartbeat", async () => {
@@ -76,8 +98,7 @@ describe("gateway server health/presence", () => {
payload?: unknown; payload?: unknown;
}; };
const { server, ws } = await startServerWithClient(); const ws = await openClient();
await connectOk(ws);
const waitHeartbeat = onceMessage<EventFrame>( const waitHeartbeat = onceMessage<EventFrame>(
ws, ws,
@@ -117,12 +138,10 @@ describe("gateway server health/presence", () => {
expect((toggle.payload as { enabled?: boolean } | undefined)?.enabled).toBe(false); expect((toggle.payload as { enabled?: boolean } | undefined)?.enabled).toBe(false);
ws.close(); ws.close();
await server.close();
}); });
test("presence events carry seq + stateVersion", { timeout: 8000 }, async () => { test("presence events carry seq + stateVersion", { timeout: 8000 }, async () => {
const { server, ws } = await startServerWithClient(); const ws = await openClient();
await connectOk(ws);
const presenceEventP = onceMessage(ws, (o) => o.type === "event" && o.event === "presence"); const presenceEventP = onceMessage(ws, (o) => o.type === "event" && o.event === "presence");
ws.send( ws.send(
@@ -140,12 +159,10 @@ describe("gateway server health/presence", () => {
expect(Array.isArray(evt.payload?.presence)).toBe(true); expect(Array.isArray(evt.payload?.presence)).toBe(true);
ws.close(); ws.close();
await server.close();
}); });
test("agent events stream with seq", { timeout: 8000 }, async () => { test("agent events stream with seq", { timeout: 8000 }, async () => {
const { server, ws } = await startServerWithClient(); const ws = await openClient();
await connectOk(ws);
const runId = randomUUID(); const runId = randomUUID();
const evtPromise = onceMessage( const evtPromise = onceMessage(
@@ -163,7 +180,6 @@ describe("gateway server health/presence", () => {
expect(evt.payload.data.msg).toBe("hi"); expect(evt.payload.data.msg).toBe("hi");
ws.close(); ws.close();
await server.close();
}); });
test("shutdown event is broadcast on close", { timeout: 8000 }, async () => { test("shutdown event is broadcast on close", { timeout: 8000 }, async () => {
@@ -177,16 +193,7 @@ describe("gateway server health/presence", () => {
}); });
test("presence broadcast reaches multiple clients", { timeout: 8000 }, async () => { test("presence broadcast reaches multiple clients", { timeout: 8000 }, async () => {
const port = await getFreePort(); const clients = await Promise.all([openClient(), openClient(), openClient()]);
const server = await startGatewayServer(port);
const mkClient = async () => {
const c = new WebSocket(`ws://127.0.0.1:${port}`);
await new Promise<void>((resolve) => c.once("open", resolve));
await connectOk(c);
return c;
};
const clients = await Promise.all([mkClient(), mkClient(), mkClient()]);
const waits = clients.map((c) => const waits = clients.map((c) =>
onceMessage(c, (o) => o.type === "event" && o.event === "presence"), onceMessage(c, (o) => o.type === "event" && o.event === "presence"),
); );
@@ -204,7 +211,6 @@ describe("gateway server health/presence", () => {
expect(typeof evt.seq).toBe("number"); expect(typeof evt.seq).toBe("number");
} }
for (const c of clients) c.close(); for (const c of clients) c.close();
await server.close();
}); });
test("presence includes client fingerprint", async () => { test("presence includes client fingerprint", async () => {
@@ -222,8 +228,7 @@ describe("gateway server health/presence", () => {
signedAtMs, signedAtMs,
token: null, token: null,
}); });
const { server, ws } = await startServerWithClient(); const ws = await openClient({
await connectOk(ws, {
role, role,
scopes, scopes,
client: { client: {
@@ -264,13 +269,11 @@ describe("gateway server health/presence", () => {
expect(clientEntry?.modelIdentifier).toBe("iPad16,6"); expect(clientEntry?.modelIdentifier).toBe("iPad16,6");
ws.close(); ws.close();
await server.close();
}); });
test("cli connections are not tracked as instances", async () => { test("cli connections are not tracked as instances", async () => {
const { server, ws } = await startServerWithClient();
const cliId = `cli-${randomUUID()}`; const cliId = `cli-${randomUUID()}`;
await connectOk(ws, { const ws = await openClient({
client: { client: {
id: GATEWAY_CLIENT_NAMES.CLI, id: GATEWAY_CLIENT_NAMES.CLI,
version: "dev", version: "dev",
@@ -294,6 +297,5 @@ describe("gateway server health/presence", () => {
expect(entries.some((e) => e.instanceId === cliId)).toBe(false); expect(entries.some((e) => e.instanceId === cliId)).toBe(false);
ws.close(); ws.close();
await server.close();
}); });
}); });

View File

@@ -1,9 +1,21 @@
import { test } from "vitest"; import { afterAll, beforeAll, test } from "vitest";
import WebSocket from "ws"; import WebSocket from "ws";
import { PROTOCOL_VERSION } from "./protocol/index.js"; import { PROTOCOL_VERSION } from "./protocol/index.js";
import { getFreePort, onceMessage, startGatewayServer } from "./test-helpers.server.js"; import { getFreePort, onceMessage, startGatewayServer } from "./test-helpers.server.js";
let server: Awaited<ReturnType<typeof startGatewayServer>>;
let port = 0;
beforeAll(async () => {
port = await getFreePort();
server = await startGatewayServer(port);
});
afterAll(async () => {
await server.close();
});
function connectReq( function connectReq(
ws: WebSocket, ws: WebSocket,
params: { clientId: string; platform: string; token?: string; password?: string }, params: { clientId: string; platform: string; token?: string; password?: string },
@@ -43,8 +55,6 @@ function connectReq(
} }
test("accepts clawdbot-ios as a valid gateway client id", async () => { test("accepts clawdbot-ios as a valid gateway client id", async () => {
const port = await getFreePort();
const server = await startGatewayServer(port);
const ws = new WebSocket(`ws://127.0.0.1:${port}`); const ws = new WebSocket(`ws://127.0.0.1:${port}`);
await new Promise<void>((resolve) => ws.once("open", resolve)); await new Promise<void>((resolve) => ws.once("open", resolve));
@@ -61,12 +71,9 @@ test("accepts clawdbot-ios as a valid gateway client id", async () => {
} }
ws.close(); ws.close();
await server.close();
}); });
test("accepts clawdbot-android as a valid gateway client id", async () => { test("accepts clawdbot-android as a valid gateway client id", async () => {
const port = await getFreePort();
const server = await startGatewayServer(port);
const ws = new WebSocket(`ws://127.0.0.1:${port}`); const ws = new WebSocket(`ws://127.0.0.1:${port}`);
await new Promise<void>((resolve) => ws.once("open", resolve)); await new Promise<void>((resolve) => ws.once("open", resolve));
@@ -83,5 +90,4 @@ test("accepts clawdbot-android as a valid gateway client id", async () => {
} }
ws.close(); ws.close();
await server.close();
}); });

View File

@@ -1,14 +1,16 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { beforeEach, describe, expect, test, vi } from "vitest"; import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
import { WebSocket } from "ws";
import { import {
connectOk, connectOk,
embeddedRunMock, embeddedRunMock,
getFreePort,
installGatewayTestHooks, installGatewayTestHooks,
piSdkMock, piSdkMock,
rpcReq, rpcReq,
startServerWithClient, startGatewayServer,
testState, testState,
writeSessionStore, writeSessionStore,
} from "./test-helpers.js"; } from "./test-helpers.js";
@@ -39,7 +41,31 @@ vi.mock("../auto-reply/reply/abort.js", async () => {
}; };
}); });
installGatewayTestHooks(); installGatewayTestHooks({ scope: "suite" });
let server: Awaited<ReturnType<typeof startGatewayServer>>;
let port = 0;
let previousToken: string | undefined;
beforeAll(async () => {
previousToken = process.env.CLAWDBOT_GATEWAY_TOKEN;
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
port = await getFreePort();
server = await startGatewayServer(port);
});
afterAll(async () => {
await server.close();
if (previousToken === undefined) delete process.env.CLAWDBOT_GATEWAY_TOKEN;
else process.env.CLAWDBOT_GATEWAY_TOKEN = previousToken;
});
const openClient = async (opts?: Parameters<typeof connectOk>[1]) => {
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
await new Promise<void>((resolve) => ws.once("open", resolve));
const hello = await connectOk(ws, opts);
return { ws, hello };
};
describe("gateway server sessions", () => { describe("gateway server sessions", () => {
beforeEach(() => { beforeEach(() => {
@@ -98,8 +124,7 @@ describe("gateway server sessions", () => {
}, },
}); });
const { server, ws } = await startServerWithClient(); const { ws, hello } = await openClient();
const hello = await connectOk(ws);
expect((hello as unknown as { features?: { methods?: string[] } }).features?.methods).toEqual( expect((hello as unknown as { features?: { methods?: string[] } }).features?.methods).toEqual(
expect.arrayContaining([ expect.arrayContaining([
"sessions.list", "sessions.list",
@@ -336,7 +361,6 @@ describe("gateway server sessions", () => {
); );
ws.close(); ws.close();
await server.close();
}); });
test("sessions.preview returns transcript previews", async () => { test("sessions.preview returns transcript previews", async () => {
@@ -365,8 +389,7 @@ describe("gateway server sessions", () => {
}, },
}); });
const { server, ws } = await startServerWithClient(); const { ws } = await openClient();
await connectOk(ws);
const preview = await rpcReq<{ const preview = await rpcReq<{
previews: Array<{ previews: Array<{
key: string; key: string;
@@ -383,7 +406,6 @@ describe("gateway server sessions", () => {
expect(entry?.items[1]?.text).toContain("call weather"); expect(entry?.items[1]?.text).toContain("call weather");
ws.close(); ws.close();
await server.close();
}); });
test("sessions.delete rejects main and aborts active runs", async () => { test("sessions.delete rejects main and aborts active runs", async () => {
@@ -415,8 +437,7 @@ describe("gateway server sessions", () => {
embeddedRunMock.activeIds.add("sess-active"); embeddedRunMock.activeIds.add("sess-active");
embeddedRunMock.waitResults.set("sess-active", true); embeddedRunMock.waitResults.set("sess-active", true);
const { server, ws } = await startServerWithClient(); const { ws } = await openClient();
await connectOk(ws);
const mainDelete = await rpcReq(ws, "sessions.delete", { key: "main" }); const mainDelete = await rpcReq(ws, "sessions.delete", { key: "main" });
expect(mainDelete.ok).toBe(false); expect(mainDelete.ok).toBe(false);
@@ -439,6 +460,5 @@ describe("gateway server sessions", () => {
expect(embeddedRunMock.waitCalls).toEqual(["sess-active"]); expect(embeddedRunMock.waitCalls).toEqual(["sess-active"]);
ws.close(); ws.close();
await server.close();
}); });
}); });

View File

@@ -3,7 +3,7 @@ import { type AddressInfo, createServer } from "node:net";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { afterEach, beforeEach, expect, vi } from "vitest"; import { afterAll, afterEach, beforeAll, beforeEach, expect, vi } from "vitest";
import { WebSocket } from "ws"; import { WebSocket } from "ws";
import { resolveMainSessionKeyFromConfig, type SessionEntry } from "../config/sessions.js"; import { resolveMainSessionKeyFromConfig, type SessionEntry } from "../config/sessions.js";
@@ -75,67 +75,79 @@ export async function writeSessionStore(params: {
await fs.writeFile(storePath, JSON.stringify(store, null, 2), "utf-8"); await fs.writeFile(storePath, JSON.stringify(store, null, 2), "utf-8");
} }
export function installGatewayTestHooks() { async function setupGatewayTestHome() {
beforeEach(async () => { previousHome = process.env.HOME;
// Some tests intentionally use fake timers; ensure they don't leak into gateway suites. previousUserProfile = process.env.USERPROFILE;
vi.useRealTimers(); previousStateDir = process.env.CLAWDBOT_STATE_DIR;
setLoggerOverride({ level: "silent", consoleLevel: "silent" }); previousConfigPath = process.env.CLAWDBOT_CONFIG_PATH;
previousHome = process.env.HOME; previousSkipBrowserControl = process.env.CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER;
previousUserProfile = process.env.USERPROFILE; previousSkipGmailWatcher = process.env.CLAWDBOT_SKIP_GMAIL_WATCHER;
previousStateDir = process.env.CLAWDBOT_STATE_DIR; previousSkipCanvasHost = process.env.CLAWDBOT_SKIP_CANVAS_HOST;
previousConfigPath = process.env.CLAWDBOT_CONFIG_PATH; tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gateway-home-"));
previousSkipBrowserControl = process.env.CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER; process.env.HOME = tempHome;
previousSkipGmailWatcher = process.env.CLAWDBOT_SKIP_GMAIL_WATCHER; process.env.USERPROFILE = tempHome;
previousSkipCanvasHost = process.env.CLAWDBOT_SKIP_CANVAS_HOST; process.env.CLAWDBOT_STATE_DIR = path.join(tempHome, ".clawdbot");
tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gateway-home-")); delete process.env.CLAWDBOT_CONFIG_PATH;
process.env.HOME = tempHome; }
process.env.USERPROFILE = tempHome;
process.env.CLAWDBOT_STATE_DIR = path.join(tempHome, ".clawdbot");
delete process.env.CLAWDBOT_CONFIG_PATH;
process.env.CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER = "1";
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
tempConfigRoot = path.join(tempHome, ".clawdbot-test");
setTestConfigRoot(tempConfigRoot);
sessionStoreSaveDelayMs.value = 0;
testTailnetIPv4.value = undefined;
testState.gatewayBind = undefined;
testState.gatewayAuth = undefined;
testState.gatewayControlUi = undefined;
testState.hooksConfig = undefined;
testState.canvasHostPort = undefined;
testState.legacyIssues = [];
testState.legacyParsed = {};
testState.migrationConfig = null;
testState.migrationChanges = [];
testState.cronEnabled = false;
testState.cronStorePath = undefined;
testState.sessionConfig = undefined;
testState.sessionStorePath = undefined;
testState.agentConfig = undefined;
testState.agentsConfig = undefined;
testState.bindingsConfig = undefined;
testState.channelsConfig = undefined;
testState.allowFrom = undefined;
testIsNixMode.value = false;
cronIsolatedRun.mockClear();
agentCommand.mockClear();
embeddedRunMock.activeIds.clear();
embeddedRunMock.abortCalls = [];
embeddedRunMock.waitCalls = [];
embeddedRunMock.waitResults.clear();
drainSystemEvents(resolveMainSessionKeyFromConfig());
resetAgentRunContextForTest();
const mod = await serverModulePromise;
mod.__resetModelCatalogCacheForTest();
piSdkMock.enabled = false;
piSdkMock.discoverCalls = 0;
piSdkMock.models = [];
}, 60_000);
afterEach(async () => { function applyGatewaySkipEnv() {
vi.useRealTimers(); process.env.CLAWDBOT_SKIP_BROWSER_CONTROL_SERVER = "1";
resetLogger(); process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
}
async function resetGatewayTestState(options: { uniqueConfigRoot: boolean }) {
// Some tests intentionally use fake timers; ensure they don't leak into gateway suites.
vi.useRealTimers();
setLoggerOverride({ level: "silent", consoleLevel: "silent" });
if (!tempHome) {
throw new Error("resetGatewayTestState called before temp home was initialized");
}
applyGatewaySkipEnv();
tempConfigRoot = options.uniqueConfigRoot
? await fs.mkdtemp(path.join(tempHome, "clawdbot-test-"))
: path.join(tempHome, ".clawdbot-test");
setTestConfigRoot(tempConfigRoot);
sessionStoreSaveDelayMs.value = 0;
testTailnetIPv4.value = undefined;
testState.gatewayBind = undefined;
testState.gatewayAuth = undefined;
testState.gatewayControlUi = undefined;
testState.hooksConfig = undefined;
testState.canvasHostPort = undefined;
testState.legacyIssues = [];
testState.legacyParsed = {};
testState.migrationConfig = null;
testState.migrationChanges = [];
testState.cronEnabled = false;
testState.cronStorePath = undefined;
testState.sessionConfig = undefined;
testState.sessionStorePath = undefined;
testState.agentConfig = undefined;
testState.agentsConfig = undefined;
testState.bindingsConfig = undefined;
testState.channelsConfig = undefined;
testState.allowFrom = undefined;
testIsNixMode.value = false;
cronIsolatedRun.mockClear();
agentCommand.mockClear();
embeddedRunMock.activeIds.clear();
embeddedRunMock.abortCalls = [];
embeddedRunMock.waitCalls = [];
embeddedRunMock.waitResults.clear();
drainSystemEvents(resolveMainSessionKeyFromConfig());
resetAgentRunContextForTest();
const mod = await serverModulePromise;
mod.__resetModelCatalogCacheForTest();
piSdkMock.enabled = false;
piSdkMock.discoverCalls = 0;
piSdkMock.models = [];
}
async function cleanupGatewayTestHome(options: { restoreEnv: boolean }) {
vi.useRealTimers();
resetLogger();
if (options.restoreEnv) {
if (previousHome === undefined) delete process.env.HOME; if (previousHome === undefined) delete process.env.HOME;
else process.env.HOME = previousHome; else process.env.HOME = previousHome;
if (previousUserProfile === undefined) delete process.env.USERPROFILE; if (previousUserProfile === undefined) delete process.env.USERPROFILE;
@@ -151,16 +163,45 @@ export function installGatewayTestHooks() {
else process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = previousSkipGmailWatcher; else process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = previousSkipGmailWatcher;
if (previousSkipCanvasHost === undefined) delete process.env.CLAWDBOT_SKIP_CANVAS_HOST; if (previousSkipCanvasHost === undefined) delete process.env.CLAWDBOT_SKIP_CANVAS_HOST;
else process.env.CLAWDBOT_SKIP_CANVAS_HOST = previousSkipCanvasHost; else process.env.CLAWDBOT_SKIP_CANVAS_HOST = previousSkipCanvasHost;
if (tempHome) { }
await fs.rm(tempHome, { if (options.restoreEnv && tempHome) {
recursive: true, await fs.rm(tempHome, {
force: true, recursive: true,
maxRetries: 20, force: true,
retryDelay: 25, maxRetries: 20,
}); retryDelay: 25,
tempHome = undefined; });
} tempHome = undefined;
tempConfigRoot = undefined; }
tempConfigRoot = undefined;
}
export function installGatewayTestHooks(options?: { scope?: "test" | "suite" }) {
const scope = options?.scope ?? "test";
if (scope === "suite") {
beforeAll(async () => {
await setupGatewayTestHome();
await resetGatewayTestState({ uniqueConfigRoot: true });
});
beforeEach(async () => {
await resetGatewayTestState({ uniqueConfigRoot: true });
}, 60_000);
afterEach(async () => {
await cleanupGatewayTestHome({ restoreEnv: false });
});
afterAll(async () => {
await cleanupGatewayTestHome({ restoreEnv: true });
});
return;
}
beforeEach(async () => {
await setupGatewayTestHome();
await resetGatewayTestState({ uniqueConfigRoot: false });
}, 60_000);
afterEach(async () => {
await cleanupGatewayTestHome({ restoreEnv: true });
}); });
} }