summaryrefslogtreecommitdiffhomepage
path: root/packages/api/tests/routes.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/api/tests/routes.test.ts')
-rw-r--r--packages/api/tests/routes.test.ts106
1 files changed, 106 insertions, 0 deletions
diff --git a/packages/api/tests/routes.test.ts b/packages/api/tests/routes.test.ts
new file mode 100644
index 0000000..d5384b3
--- /dev/null
+++ b/packages/api/tests/routes.test.ts
@@ -0,0 +1,106 @@
+import { describe, expect, it, vi } from "vitest";
+
+// Mock @dispatch/core's Agent to avoid real LLM calls
+vi.mock("@dispatch/core", async () => {
+ const actual = await vi.importActual<typeof import("@dispatch/core")>("@dispatch/core");
+ return {
+ ...actual,
+ Agent: class MockAgent {
+ status = "idle";
+ messages: unknown[] = [];
+ async *run(_message: string) {
+ yield { type: "status", status: "running" } as const;
+ // Simulate some processing time so status stays "running"
+ await new Promise<void>((r) => setTimeout(r, 100));
+ yield { type: "text-delta", delta: "Hello " } as const;
+ yield { type: "text-delta", delta: "world" } as const;
+ yield {
+ type: "done",
+ message: { role: "assistant", content: "Hello world" },
+ } as const;
+ yield { type: "status", status: "idle" } as const;
+ }
+ },
+ };
+});
+
+const { app } = await import("../src/app.js");
+
+describe("GET /health", () => {
+ it("returns 200 with ok: true", async () => {
+ const res = await app.request("/health");
+ expect(res.status).toBe(200);
+ const body = await res.json();
+ expect(body).toEqual({ ok: true });
+ });
+});
+
+describe("GET /status", () => {
+ it("returns idle status initially", async () => {
+ const res = await app.request("/status");
+ expect(res.status).toBe(200);
+ const body = await res.json();
+ expect(body.status).toBe("idle");
+ expect(typeof body.messageCount).toBe("number");
+ });
+});
+
+describe("POST /chat", () => {
+ it("returns 200 with valid message", async () => {
+ const res = await app.request("/chat", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ message: "hello world" }),
+ });
+ expect(res.status).toBe(200);
+ const body = await res.json();
+ expect(body).toEqual({ status: "ok" });
+ });
+
+ it("returns 400 with empty message", async () => {
+ const res = await app.request("/chat", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ message: "" }),
+ });
+ expect(res.status).toBe(400);
+ });
+
+ it("returns 400 with whitespace-only message", async () => {
+ const res = await app.request("/chat", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ message: " " }),
+ });
+ expect(res.status).toBe(400);
+ });
+
+ it("returns 400 with missing message field", async () => {
+ const res = await app.request("/chat", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({}),
+ });
+ expect(res.status).toBe(400);
+ });
+
+ it("returns 409 when agent is already running", async () => {
+ // Start a message (non-blocking)
+ await app.request("/chat", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ message: "first message" }),
+ });
+
+ // Small delay to let the async generator start and emit "running" status
+ await new Promise<void>((r) => setTimeout(r, 20));
+
+ // Immediately send a second — agent should be running
+ const res = await app.request("/chat", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ message: "second message" }),
+ });
+ expect(res.status).toBe(409);
+ });
+});