test: speed up gateway suite setup
This commit is contained in:
@@ -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" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user