summaryrefslogtreecommitdiffhomepage
path: root/packages/cli/src/http.test.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-26 22:03:19 +0900
committerAdam Malczewski <[email protected]>2026-06-26 22:23:39 +0900
commit727c98c9dae516a2070eb950410314380a20c974 (patch)
tree52aa1022c54f11770be7e4e2a324f0a8b8b8deec /packages/cli/src/http.test.ts
parente59dc11f63b1df51142259bb2c406af8c9c8c2bb (diff)
downloaddispatch-727c98c9dae516a2070eb950410314380a20c974.tar.gz
dispatch-727c98c9dae516a2070eb950410314380a20c974.zip
style: switch from tabs to 2-space indentation
Diffstat (limited to 'packages/cli/src/http.test.ts')
-rw-r--r--packages/cli/src/http.test.ts970
1 files changed, 485 insertions, 485 deletions
diff --git a/packages/cli/src/http.test.ts b/packages/cli/src/http.test.ts
index ab39813..18de728 100644
--- a/packages/cli/src/http.test.ts
+++ b/packages/cli/src/http.test.ts
@@ -1,517 +1,517 @@
import type {
- AgentEvent,
- ChatRequest,
- ConversationListResponse,
+ AgentEvent,
+ ChatRequest,
+ ConversationListResponse,
} from "@dispatch/transport-contract";
import { describe, expect, it } from "vitest";
import {
- enqueueMessage,
- fetchConversations,
- fetchLastMessage,
- fetchModels,
- openConversation,
- resolveConversationId,
- streamChat,
+ enqueueMessage,
+ fetchConversations,
+ fetchLastMessage,
+ fetchModels,
+ openConversation,
+ resolveConversationId,
+ streamChat,
} from "./http.js";
function ndjsonLines(...events: AgentEvent[]): string {
- return `${events.map((e) => JSON.stringify(e)).join("\n")}\n`;
+ return `${events.map((e) => JSON.stringify(e)).join("\n")}\n`;
}
function makeFakeFetch(responseBody: string, headers?: Record<string, string>) {
- const fn = async (_url: string | URL | Request, _init?: RequestInit): Promise<Response> => {
- const encoder = new TextEncoder();
- const chunks = responseBody.split("|||");
- let i = 0;
- const stream = new ReadableStream<Uint8Array>({
- pull(controller) {
- if (i < chunks.length) {
- const chunk = chunks[i];
- if (chunk !== undefined) controller.enqueue(encoder.encode(chunk));
- i++;
- } else {
- controller.close();
- }
- },
- });
- return new Response(stream, {
- status: 200,
- headers: headers ?? {},
- });
- };
- return fn as unknown as typeof fetch;
+ const fn = async (_url: string | URL | Request, _init?: RequestInit): Promise<Response> => {
+ const encoder = new TextEncoder();
+ const chunks = responseBody.split("|||");
+ let i = 0;
+ const stream = new ReadableStream<Uint8Array>({
+ pull(controller) {
+ if (i < chunks.length) {
+ const chunk = chunks[i];
+ if (chunk !== undefined) controller.enqueue(encoder.encode(chunk));
+ i++;
+ } else {
+ controller.close();
+ }
+ },
+ });
+ return new Response(stream, {
+ status: 200,
+ headers: headers ?? {},
+ });
+ };
+ return fn as unknown as typeof fetch;
}
describe("streamChat", () => {
- it("parses NDJSON events and returns conversationId", async () => {
- const event1: AgentEvent = {
- type: "text-delta",
- conversationId: "c1",
- turnId: "t1",
- delta: "Hello",
- };
- const event2: AgentEvent = {
- type: "done",
- conversationId: "c1",
- turnId: "t1",
- reason: "completed",
- };
-
- const body = ndjsonLines(event1, event2);
- const fakeFetch = makeFakeFetch(body, { "X-Conversation-Id": "c1" });
-
- const { conversationId, events } = await streamChat(
- { fetchImpl: fakeFetch },
- {
- server: "http://localhost:24203",
- request: { message: "hi", model: "openai/gpt-4" },
- },
- );
-
- expect(conversationId).toBe("c1");
-
- const collected: AgentEvent[] = [];
- for await (const e of events) {
- collected.push(e);
- }
-
- expect(collected).toEqual([event1, event2]);
- });
-
- it("handles NDJSON split across chunks", async () => {
- const event1: AgentEvent = {
- type: "text-delta",
- conversationId: "c",
- turnId: "t",
- delta: "Hi",
- };
- const event2: AgentEvent = {
- type: "usage",
- conversationId: "c",
- turnId: "t",
- usage: { inputTokens: 10, outputTokens: 5 },
- };
-
- const fullNdjson = ndjsonLines(event1, event2);
- // Split mid-line: after 20 chars
- const mid = 20;
- const chunk1 = fullNdjson.slice(0, mid);
- const chunk2 = fullNdjson.slice(mid);
-
- const fakeFetch = makeFakeFetch(`${chunk1}|||${chunk2}`, {
- "X-Conversation-Id": "c",
- });
-
- const { events } = await streamChat(
- { fetchImpl: fakeFetch },
- {
- server: "http://localhost:24203",
- request: { message: "hi" },
- },
- );
-
- const collected: AgentEvent[] = [];
- for await (const e of events) {
- collected.push(e);
- }
-
- expect(collected).toEqual([event1, event2]);
- });
-
- it("throws on non-OK status", async () => {
- const fakeFetch = (async (): Promise<Response> =>
- new Response("not found", { status: 404 })) as unknown as typeof fetch;
-
- await expect(
- streamChat(
- { fetchImpl: fakeFetch },
- {
- server: "http://localhost:24203",
- request: { message: "hi" },
- },
- ),
- ).rejects.toThrow("POST /chat failed with status 404");
- });
-
- it("throws when response has no body", async () => {
- const fakeFetch = (async (): Promise<Response> =>
- new Response(null, { status: 200 })) as unknown as typeof fetch;
-
- await expect(
- streamChat(
- { fetchImpl: fakeFetch },
- {
- server: "http://localhost:24203",
- request: { message: "hi" },
- },
- ),
- ).rejects.toThrow("no body");
- });
-
- it("includes reasoningEffort in request body when set", async () => {
- let capturedBody: string | undefined;
- const doneEvent: AgentEvent = {
- type: "done",
- conversationId: "c",
- turnId: "t",
- reason: "completed",
- };
- const fakeFetch = async (
- _url: string | URL | Request,
- init?: RequestInit,
- ): Promise<Response> => {
- capturedBody = init?.body as string;
- const encoder = new TextEncoder();
- const stream = new ReadableStream<Uint8Array>({
- pull(controller) {
- controller.enqueue(encoder.encode(`${JSON.stringify(doneEvent)}\n`));
- controller.close();
- },
- });
- return new Response(stream, { status: 200 });
- };
-
- await streamChat(
- { fetchImpl: fakeFetch as unknown as typeof fetch },
- {
- server: "http://localhost:24203",
- request: { message: "hi", reasoningEffort: "xhigh" },
- },
- );
-
- expect(capturedBody).toBeDefined();
- const parsed = JSON.parse(capturedBody as string) as ChatRequest;
- expect(parsed.reasoningEffort).toBe("xhigh");
- });
-
- it("omits reasoningEffort from request body when not set", async () => {
- let capturedBody: string | undefined;
- const doneEvent: AgentEvent = {
- type: "done",
- conversationId: "c",
- turnId: "t",
- reason: "completed",
- };
- const fakeFetch = async (
- _url: string | URL | Request,
- init?: RequestInit,
- ): Promise<Response> => {
- capturedBody = init?.body as string;
- const encoder = new TextEncoder();
- const stream = new ReadableStream<Uint8Array>({
- pull(controller) {
- controller.enqueue(encoder.encode(`${JSON.stringify(doneEvent)}\n`));
- controller.close();
- },
- });
- return new Response(stream, { status: 200 });
- };
-
- await streamChat(
- { fetchImpl: fakeFetch as unknown as typeof fetch },
- {
- server: "http://localhost:24203",
- request: { message: "hi" },
- },
- );
-
- expect(capturedBody).toBeDefined();
- const parsed = JSON.parse(capturedBody as string) as ChatRequest;
- expect(parsed).not.toHaveProperty("reasoningEffort");
- });
+ it("parses NDJSON events and returns conversationId", async () => {
+ const event1: AgentEvent = {
+ type: "text-delta",
+ conversationId: "c1",
+ turnId: "t1",
+ delta: "Hello",
+ };
+ const event2: AgentEvent = {
+ type: "done",
+ conversationId: "c1",
+ turnId: "t1",
+ reason: "completed",
+ };
+
+ const body = ndjsonLines(event1, event2);
+ const fakeFetch = makeFakeFetch(body, { "X-Conversation-Id": "c1" });
+
+ const { conversationId, events } = await streamChat(
+ { fetchImpl: fakeFetch },
+ {
+ server: "http://localhost:24203",
+ request: { message: "hi", model: "openai/gpt-4" },
+ },
+ );
+
+ expect(conversationId).toBe("c1");
+
+ const collected: AgentEvent[] = [];
+ for await (const e of events) {
+ collected.push(e);
+ }
+
+ expect(collected).toEqual([event1, event2]);
+ });
+
+ it("handles NDJSON split across chunks", async () => {
+ const event1: AgentEvent = {
+ type: "text-delta",
+ conversationId: "c",
+ turnId: "t",
+ delta: "Hi",
+ };
+ const event2: AgentEvent = {
+ type: "usage",
+ conversationId: "c",
+ turnId: "t",
+ usage: { inputTokens: 10, outputTokens: 5 },
+ };
+
+ const fullNdjson = ndjsonLines(event1, event2);
+ // Split mid-line: after 20 chars
+ const mid = 20;
+ const chunk1 = fullNdjson.slice(0, mid);
+ const chunk2 = fullNdjson.slice(mid);
+
+ const fakeFetch = makeFakeFetch(`${chunk1}|||${chunk2}`, {
+ "X-Conversation-Id": "c",
+ });
+
+ const { events } = await streamChat(
+ { fetchImpl: fakeFetch },
+ {
+ server: "http://localhost:24203",
+ request: { message: "hi" },
+ },
+ );
+
+ const collected: AgentEvent[] = [];
+ for await (const e of events) {
+ collected.push(e);
+ }
+
+ expect(collected).toEqual([event1, event2]);
+ });
+
+ it("throws on non-OK status", async () => {
+ const fakeFetch = (async (): Promise<Response> =>
+ new Response("not found", { status: 404 })) as unknown as typeof fetch;
+
+ await expect(
+ streamChat(
+ { fetchImpl: fakeFetch },
+ {
+ server: "http://localhost:24203",
+ request: { message: "hi" },
+ },
+ ),
+ ).rejects.toThrow("POST /chat failed with status 404");
+ });
+
+ it("throws when response has no body", async () => {
+ const fakeFetch = (async (): Promise<Response> =>
+ new Response(null, { status: 200 })) as unknown as typeof fetch;
+
+ await expect(
+ streamChat(
+ { fetchImpl: fakeFetch },
+ {
+ server: "http://localhost:24203",
+ request: { message: "hi" },
+ },
+ ),
+ ).rejects.toThrow("no body");
+ });
+
+ it("includes reasoningEffort in request body when set", async () => {
+ let capturedBody: string | undefined;
+ const doneEvent: AgentEvent = {
+ type: "done",
+ conversationId: "c",
+ turnId: "t",
+ reason: "completed",
+ };
+ const fakeFetch = async (
+ _url: string | URL | Request,
+ init?: RequestInit,
+ ): Promise<Response> => {
+ capturedBody = init?.body as string;
+ const encoder = new TextEncoder();
+ const stream = new ReadableStream<Uint8Array>({
+ pull(controller) {
+ controller.enqueue(encoder.encode(`${JSON.stringify(doneEvent)}\n`));
+ controller.close();
+ },
+ });
+ return new Response(stream, { status: 200 });
+ };
+
+ await streamChat(
+ { fetchImpl: fakeFetch as unknown as typeof fetch },
+ {
+ server: "http://localhost:24203",
+ request: { message: "hi", reasoningEffort: "xhigh" },
+ },
+ );
+
+ expect(capturedBody).toBeDefined();
+ const parsed = JSON.parse(capturedBody as string) as ChatRequest;
+ expect(parsed.reasoningEffort).toBe("xhigh");
+ });
+
+ it("omits reasoningEffort from request body when not set", async () => {
+ let capturedBody: string | undefined;
+ const doneEvent: AgentEvent = {
+ type: "done",
+ conversationId: "c",
+ turnId: "t",
+ reason: "completed",
+ };
+ const fakeFetch = async (
+ _url: string | URL | Request,
+ init?: RequestInit,
+ ): Promise<Response> => {
+ capturedBody = init?.body as string;
+ const encoder = new TextEncoder();
+ const stream = new ReadableStream<Uint8Array>({
+ pull(controller) {
+ controller.enqueue(encoder.encode(`${JSON.stringify(doneEvent)}\n`));
+ controller.close();
+ },
+ });
+ return new Response(stream, { status: 200 });
+ };
+
+ await streamChat(
+ { fetchImpl: fakeFetch as unknown as typeof fetch },
+ {
+ server: "http://localhost:24203",
+ request: { message: "hi" },
+ },
+ );
+
+ expect(capturedBody).toBeDefined();
+ const parsed = JSON.parse(capturedBody as string) as ChatRequest;
+ expect(parsed).not.toHaveProperty("reasoningEffort");
+ });
});
describe("fetchModels", () => {
- it("returns ModelsResponse on success", async () => {
- const models = { models: ["openai/gpt-4", "anthropic/claude-3"] };
- const fakeFetch = (async (): Promise<Response> =>
- new Response(JSON.stringify(models), {
- status: 200,
- headers: { "Content-Type": "application/json" },
- })) as unknown as typeof fetch;
-
- const result = await fetchModels(
- { fetchImpl: fakeFetch },
- { server: "http://localhost:24203" },
- );
- expect(result).toEqual(models);
- });
-
- it("throws on non-OK status", async () => {
- const fakeFetch = (async (): Promise<Response> =>
- new Response("server error", { status: 500 })) as unknown as typeof fetch;
-
- await expect(
- fetchModels({ fetchImpl: fakeFetch }, { server: "http://localhost:24203" }),
- ).rejects.toThrow("GET /models failed with status 500");
- });
+ it("returns ModelsResponse on success", async () => {
+ const models = { models: ["openai/gpt-4", "anthropic/claude-3"] };
+ const fakeFetch = (async (): Promise<Response> =>
+ new Response(JSON.stringify(models), {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ })) as unknown as typeof fetch;
+
+ const result = await fetchModels(
+ { fetchImpl: fakeFetch },
+ { server: "http://localhost:24203" },
+ );
+ expect(result).toEqual(models);
+ });
+
+ it("throws on non-OK status", async () => {
+ const fakeFetch = (async (): Promise<Response> =>
+ new Response("server error", { status: 500 })) as unknown as typeof fetch;
+
+ await expect(
+ fetchModels({ fetchImpl: fakeFetch }, { server: "http://localhost:24203" }),
+ ).rejects.toThrow("GET /models failed with status 500");
+ });
});
describe("fetchConversations", () => {
- it("requests GET /conversations with no query when query omitted", async () => {
- let calledUrl: string | undefined;
- const list: ConversationListResponse = {
- conversations: [
- {
- id: "abcdef1234567890",
- title: "first",
- createdAt: 1,
- lastActivityAt: 2,
- status: "idle",
- workspaceId: "default",
- },
- ],
- };
- const fakeFetch = (async (url: string | URL | Request): Promise<Response> => {
- calledUrl = String(url);
- return new Response(JSON.stringify(list), { status: 200 });
- }) as unknown as typeof fetch;
-
- const result = await fetchConversations(
- { fetchImpl: fakeFetch },
- { server: "http://localhost:24203" },
- );
- expect(calledUrl).toBe("http://localhost:24203/conversations");
- expect(result).toEqual(list);
- });
-
- it("appends ?q=<encoded> when a query is given", async () => {
- let calledUrl: string | undefined;
- const fakeFetch = (async (url: string | URL | Request): Promise<Response> => {
- calledUrl = String(url);
- return new Response(JSON.stringify({ conversations: [] }), { status: 200 });
- }) as unknown as typeof fetch;
-
- await fetchConversations(
- { fetchImpl: fakeFetch },
- { server: "http://localhost:24203", query: "abc def" },
- );
- expect(calledUrl).toBe("http://localhost:24203/conversations?q=abc+def");
- });
-
- it("appends ?workspaceId=<value> when a workspaceId is given", async () => {
- let calledUrl: string | undefined;
- const fakeFetch = (async (url: string | URL | Request): Promise<Response> => {
- calledUrl = String(url);
- return new Response(JSON.stringify({ conversations: [] }), { status: 200 });
- }) as unknown as typeof fetch;
-
- await fetchConversations(
- { fetchImpl: fakeFetch },
- { server: "http://localhost:24203", workspaceId: "proj" },
- );
- expect(calledUrl).toBe("http://localhost:24203/conversations?workspaceId=proj");
- });
-
- it("combines ?status= and ?workspaceId= when both are given", async () => {
- let calledUrl: string | undefined;
- const fakeFetch = (async (url: string | URL | Request): Promise<Response> => {
- calledUrl = String(url);
- return new Response(JSON.stringify({ conversations: [] }), { status: 200 });
- }) as unknown as typeof fetch;
-
- await fetchConversations(
- { fetchImpl: fakeFetch },
- { server: "http://localhost:24203", status: "active,idle", workspaceId: "proj" },
- );
- expect(calledUrl).toBe(
- "http://localhost:24203/conversations?status=active%2Cidle&workspaceId=proj",
- );
- });
-
- it("throws on non-OK status", async () => {
- const fakeFetch = (async (): Promise<Response> =>
- new Response("boom", { status: 500 })) as unknown as typeof fetch;
- await expect(
- fetchConversations({ fetchImpl: fakeFetch }, { server: "http://localhost:24203" }),
- ).rejects.toThrow("GET /conversations failed with status 500");
- });
+ it("requests GET /conversations with no query when query omitted", async () => {
+ let calledUrl: string | undefined;
+ const list: ConversationListResponse = {
+ conversations: [
+ {
+ id: "abcdef1234567890",
+ title: "first",
+ createdAt: 1,
+ lastActivityAt: 2,
+ status: "idle",
+ workspaceId: "default",
+ },
+ ],
+ };
+ const fakeFetch = (async (url: string | URL | Request): Promise<Response> => {
+ calledUrl = String(url);
+ return new Response(JSON.stringify(list), { status: 200 });
+ }) as unknown as typeof fetch;
+
+ const result = await fetchConversations(
+ { fetchImpl: fakeFetch },
+ { server: "http://localhost:24203" },
+ );
+ expect(calledUrl).toBe("http://localhost:24203/conversations");
+ expect(result).toEqual(list);
+ });
+
+ it("appends ?q=<encoded> when a query is given", async () => {
+ let calledUrl: string | undefined;
+ const fakeFetch = (async (url: string | URL | Request): Promise<Response> => {
+ calledUrl = String(url);
+ return new Response(JSON.stringify({ conversations: [] }), { status: 200 });
+ }) as unknown as typeof fetch;
+
+ await fetchConversations(
+ { fetchImpl: fakeFetch },
+ { server: "http://localhost:24203", query: "abc def" },
+ );
+ expect(calledUrl).toBe("http://localhost:24203/conversations?q=abc+def");
+ });
+
+ it("appends ?workspaceId=<value> when a workspaceId is given", async () => {
+ let calledUrl: string | undefined;
+ const fakeFetch = (async (url: string | URL | Request): Promise<Response> => {
+ calledUrl = String(url);
+ return new Response(JSON.stringify({ conversations: [] }), { status: 200 });
+ }) as unknown as typeof fetch;
+
+ await fetchConversations(
+ { fetchImpl: fakeFetch },
+ { server: "http://localhost:24203", workspaceId: "proj" },
+ );
+ expect(calledUrl).toBe("http://localhost:24203/conversations?workspaceId=proj");
+ });
+
+ it("combines ?status= and ?workspaceId= when both are given", async () => {
+ let calledUrl: string | undefined;
+ const fakeFetch = (async (url: string | URL | Request): Promise<Response> => {
+ calledUrl = String(url);
+ return new Response(JSON.stringify({ conversations: [] }), { status: 200 });
+ }) as unknown as typeof fetch;
+
+ await fetchConversations(
+ { fetchImpl: fakeFetch },
+ { server: "http://localhost:24203", status: "active,idle", workspaceId: "proj" },
+ );
+ expect(calledUrl).toBe(
+ "http://localhost:24203/conversations?status=active%2Cidle&workspaceId=proj",
+ );
+ });
+
+ it("throws on non-OK status", async () => {
+ const fakeFetch = (async (): Promise<Response> =>
+ new Response("boom", { status: 500 })) as unknown as typeof fetch;
+ await expect(
+ fetchConversations({ fetchImpl: fakeFetch }, { server: "http://localhost:24203" }),
+ ).rejects.toThrow("GET /conversations failed with status 500");
+ });
});
describe("fetchLastMessage", () => {
- it("requests GET /conversations/:id/last and returns the body", async () => {
- let calledUrl: string | undefined;
- const fakeFetch = (async (url: string | URL | Request): Promise<Response> => {
- calledUrl = String(url);
- return new Response(
- JSON.stringify({ conversationId: "c1", content: "hello back", turnId: "t1" }),
- { status: 200 },
- );
- }) as unknown as typeof fetch;
-
- const result = await fetchLastMessage(
- { fetchImpl: fakeFetch },
- { server: "http://localhost:24203", conversationId: "c1" },
- );
- expect(calledUrl).toBe("http://localhost:24203/conversations/c1/last");
- expect(result).toEqual({ conversationId: "c1", content: "hello back", turnId: "t1" });
- });
-
- it("throws on non-OK status", async () => {
- const fakeFetch = (async (): Promise<Response> =>
- new Response("nope", { status: 404 })) as unknown as typeof fetch;
- await expect(
- fetchLastMessage(
- { fetchImpl: fakeFetch },
- { server: "http://localhost:24203", conversationId: "c1" },
- ),
- ).rejects.toThrow("GET /conversations/:id/last failed with status 404");
- });
+ it("requests GET /conversations/:id/last and returns the body", async () => {
+ let calledUrl: string | undefined;
+ const fakeFetch = (async (url: string | URL | Request): Promise<Response> => {
+ calledUrl = String(url);
+ return new Response(
+ JSON.stringify({ conversationId: "c1", content: "hello back", turnId: "t1" }),
+ { status: 200 },
+ );
+ }) as unknown as typeof fetch;
+
+ const result = await fetchLastMessage(
+ { fetchImpl: fakeFetch },
+ { server: "http://localhost:24203", conversationId: "c1" },
+ );
+ expect(calledUrl).toBe("http://localhost:24203/conversations/c1/last");
+ expect(result).toEqual({ conversationId: "c1", content: "hello back", turnId: "t1" });
+ });
+
+ it("throws on non-OK status", async () => {
+ const fakeFetch = (async (): Promise<Response> =>
+ new Response("nope", { status: 404 })) as unknown as typeof fetch;
+ await expect(
+ fetchLastMessage(
+ { fetchImpl: fakeFetch },
+ { server: "http://localhost:24203", conversationId: "c1" },
+ ),
+ ).rejects.toThrow("GET /conversations/:id/last failed with status 404");
+ });
});
describe("enqueueMessage", () => {
- it("POSTs to /conversations/:id/queue with { text } and returns the body", async () => {
- let calledUrl: string | undefined;
- let calledInit: RequestInit | undefined;
- const fakeFetch = (async (
- url: string | URL | Request,
- init?: RequestInit,
- ): Promise<Response> => {
- calledUrl = String(url);
- calledInit = init;
- return new Response(JSON.stringify({ conversationId: "c1", startedTurn: false, queue: [] }), {
- status: 200,
- });
- }) as unknown as typeof fetch;
-
- const result = await enqueueMessage(
- { fetchImpl: fakeFetch },
- { server: "http://localhost:24203", conversationId: "c1", text: "hi" },
- );
- expect(calledUrl).toBe("http://localhost:24203/conversations/c1/queue");
- expect(calledInit?.method).toBe("POST");
- expect(JSON.parse(calledInit?.body as string)).toEqual({ text: "hi" });
- expect(result).toEqual({ conversationId: "c1", startedTurn: false, queue: [] });
- });
-
- it("throws on non-OK status", async () => {
- const fakeFetch = (async (): Promise<Response> =>
- new Response("bad", { status: 400 })) as unknown as typeof fetch;
- await expect(
- enqueueMessage(
- { fetchImpl: fakeFetch },
- { server: "http://localhost:24203", conversationId: "c1", text: "hi" },
- ),
- ).rejects.toThrow("POST /conversations/:id/queue failed with status 400");
- });
+ it("POSTs to /conversations/:id/queue with { text } and returns the body", async () => {
+ let calledUrl: string | undefined;
+ let calledInit: RequestInit | undefined;
+ const fakeFetch = (async (
+ url: string | URL | Request,
+ init?: RequestInit,
+ ): Promise<Response> => {
+ calledUrl = String(url);
+ calledInit = init;
+ return new Response(JSON.stringify({ conversationId: "c1", startedTurn: false, queue: [] }), {
+ status: 200,
+ });
+ }) as unknown as typeof fetch;
+
+ const result = await enqueueMessage(
+ { fetchImpl: fakeFetch },
+ { server: "http://localhost:24203", conversationId: "c1", text: "hi" },
+ );
+ expect(calledUrl).toBe("http://localhost:24203/conversations/c1/queue");
+ expect(calledInit?.method).toBe("POST");
+ expect(JSON.parse(calledInit?.body as string)).toEqual({ text: "hi" });
+ expect(result).toEqual({ conversationId: "c1", startedTurn: false, queue: [] });
+ });
+
+ it("throws on non-OK status", async () => {
+ const fakeFetch = (async (): Promise<Response> =>
+ new Response("bad", { status: 400 })) as unknown as typeof fetch;
+ await expect(
+ enqueueMessage(
+ { fetchImpl: fakeFetch },
+ { server: "http://localhost:24203", conversationId: "c1", text: "hi" },
+ ),
+ ).rejects.toThrow("POST /conversations/:id/queue failed with status 400");
+ });
});
describe("openConversation", () => {
- it("POSTs to /conversations/:id/open and returns the body", async () => {
- let calledUrl: string | undefined;
- let calledInit: RequestInit | undefined;
- const fakeFetch = (async (
- url: string | URL | Request,
- init?: RequestInit,
- ): Promise<Response> => {
- calledUrl = String(url);
- calledInit = init;
- return new Response(JSON.stringify({ conversationId: "c1" }), { status: 200 });
- }) as unknown as typeof fetch;
-
- const result = await openConversation(
- { fetchImpl: fakeFetch },
- { server: "http://localhost:24203", conversationId: "c1" },
- );
- expect(calledUrl).toBe("http://localhost:24203/conversations/c1/open");
- expect(calledInit?.method).toBe("POST");
- expect(result).toEqual({ conversationId: "c1" });
- });
-
- it("throws on non-OK status", async () => {
- const fakeFetch = (async (): Promise<Response> =>
- new Response("bad", { status: 500 })) as unknown as typeof fetch;
- await expect(
- openConversation(
- { fetchImpl: fakeFetch },
- { server: "http://localhost:24203", conversationId: "c1" },
- ),
- ).rejects.toThrow("POST /conversations/:id/open failed with status 500");
- });
+ it("POSTs to /conversations/:id/open and returns the body", async () => {
+ let calledUrl: string | undefined;
+ let calledInit: RequestInit | undefined;
+ const fakeFetch = (async (
+ url: string | URL | Request,
+ init?: RequestInit,
+ ): Promise<Response> => {
+ calledUrl = String(url);
+ calledInit = init;
+ return new Response(JSON.stringify({ conversationId: "c1" }), { status: 200 });
+ }) as unknown as typeof fetch;
+
+ const result = await openConversation(
+ { fetchImpl: fakeFetch },
+ { server: "http://localhost:24203", conversationId: "c1" },
+ );
+ expect(calledUrl).toBe("http://localhost:24203/conversations/c1/open");
+ expect(calledInit?.method).toBe("POST");
+ expect(result).toEqual({ conversationId: "c1" });
+ });
+
+ it("throws on non-OK status", async () => {
+ const fakeFetch = (async (): Promise<Response> =>
+ new Response("bad", { status: 500 })) as unknown as typeof fetch;
+ await expect(
+ openConversation(
+ { fetchImpl: fakeFetch },
+ { server: "http://localhost:24203", conversationId: "c1" },
+ ),
+ ).rejects.toThrow("POST /conversations/:id/open failed with status 500");
+ });
});
describe("resolveConversationId", () => {
- function listFetch(list: ConversationListResponse): typeof fetch {
- return (async (): Promise<Response> =>
- new Response(JSON.stringify(list), { status: 200 })) as unknown as typeof fetch;
- }
-
- it("returns the full id on a single match", async () => {
- const fetchImpl = listFetch({
- conversations: [
- {
- id: "abcdef1234567890abcdef1234567890",
- title: "only",
- createdAt: 1,
- lastActivityAt: 2,
- status: "idle",
- workspaceId: "default",
- },
- ],
- });
- const result = await resolveConversationId(
- { fetchImpl },
- { server: "http://localhost:24203", shortId: "abcdef" },
- );
- expect(result).toBe("abcdef1234567890abcdef1234567890");
- });
-
- it("returns an error object on no match", async () => {
- const fetchImpl = listFetch({ conversations: [] });
- const result = await resolveConversationId(
- { fetchImpl },
- { server: "http://localhost:24203", shortId: "abcdef" },
- );
- expect(typeof result).toBe("object");
- if (typeof result !== "string") {
- expect(result.error).toContain("No conversation matching");
- expect(result.error).toContain("abcdef");
- }
- });
-
- it("returns an error object listing matches on multiple matches", async () => {
- const fetchImpl = listFetch({
- conversations: [
- {
- id: "abcdef1234567890aaaaaaaaaaaaaaaa",
- title: "first",
- createdAt: 1,
- lastActivityAt: 2,
- status: "idle",
- workspaceId: "default",
- },
- {
- id: "abcdef1234567890bbbbbbbbbbbbbbbb",
- title: "second",
- createdAt: 1,
- lastActivityAt: 3,
- status: "idle",
- workspaceId: "default",
- },
- ],
- });
- const result = await resolveConversationId(
- { fetchImpl },
- { server: "http://localhost:24203", shortId: "abcdef" },
- );
- expect(typeof result).toBe("object");
- if (typeof result !== "string") {
- expect(result.error).toContain("Multiple conversations matching");
- expect(result.error).toContain("abcdef12");
- expect(result.error).toContain("first");
- expect(result.error).toContain("second");
- }
- });
-
- it("passes through a full UUID (32+ chars) without calling fetch", async () => {
- const fetchImpl = (async (): Promise<Response> => {
- throw new Error("fetch must not be called for a full UUID");
- }) as unknown as typeof fetch;
- const fullId = "abcdef1234567890abcdef1234567890";
- const result = await resolveConversationId(
- { fetchImpl },
- { server: "http://localhost:24203", shortId: fullId },
- );
- expect(result).toBe(fullId);
- });
+ function listFetch(list: ConversationListResponse): typeof fetch {
+ return (async (): Promise<Response> =>
+ new Response(JSON.stringify(list), { status: 200 })) as unknown as typeof fetch;
+ }
+
+ it("returns the full id on a single match", async () => {
+ const fetchImpl = listFetch({
+ conversations: [
+ {
+ id: "abcdef1234567890abcdef1234567890",
+ title: "only",
+ createdAt: 1,
+ lastActivityAt: 2,
+ status: "idle",
+ workspaceId: "default",
+ },
+ ],
+ });
+ const result = await resolveConversationId(
+ { fetchImpl },
+ { server: "http://localhost:24203", shortId: "abcdef" },
+ );
+ expect(result).toBe("abcdef1234567890abcdef1234567890");
+ });
+
+ it("returns an error object on no match", async () => {
+ const fetchImpl = listFetch({ conversations: [] });
+ const result = await resolveConversationId(
+ { fetchImpl },
+ { server: "http://localhost:24203", shortId: "abcdef" },
+ );
+ expect(typeof result).toBe("object");
+ if (typeof result !== "string") {
+ expect(result.error).toContain("No conversation matching");
+ expect(result.error).toContain("abcdef");
+ }
+ });
+
+ it("returns an error object listing matches on multiple matches", async () => {
+ const fetchImpl = listFetch({
+ conversations: [
+ {
+ id: "abcdef1234567890aaaaaaaaaaaaaaaa",
+ title: "first",
+ createdAt: 1,
+ lastActivityAt: 2,
+ status: "idle",
+ workspaceId: "default",
+ },
+ {
+ id: "abcdef1234567890bbbbbbbbbbbbbbbb",
+ title: "second",
+ createdAt: 1,
+ lastActivityAt: 3,
+ status: "idle",
+ workspaceId: "default",
+ },
+ ],
+ });
+ const result = await resolveConversationId(
+ { fetchImpl },
+ { server: "http://localhost:24203", shortId: "abcdef" },
+ );
+ expect(typeof result).toBe("object");
+ if (typeof result !== "string") {
+ expect(result.error).toContain("Multiple conversations matching");
+ expect(result.error).toContain("abcdef12");
+ expect(result.error).toContain("first");
+ expect(result.error).toContain("second");
+ }
+ });
+
+ it("passes through a full UUID (32+ chars) without calling fetch", async () => {
+ const fetchImpl = (async (): Promise<Response> => {
+ throw new Error("fetch must not be called for a full UUID");
+ }) as unknown as typeof fetch;
+ const fullId = "abcdef1234567890abcdef1234567890";
+ const result = await resolveConversationId(
+ { fetchImpl },
+ { server: "http://localhost:24203", shortId: fullId },
+ );
+ expect(result).toBe(fullId);
+ });
});