diff options
| author | Adam Malczewski <[email protected]> | 2026-06-11 12:45:21 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-11 12:45:21 +0900 |
| commit | 27fd0be36b2f6395249de5aacc86e41fe4e0207f (patch) | |
| tree | 67ae766c1985344878d6a2e71da18834fa73e47d /packages/transport-http/src/app.test.ts | |
| parent | c2b4c05d91fa88b8d02c055a0e15c22abd8e21f3 (diff) | |
| download | dispatch-27fd0be36b2f6395249de5aacc86e41fe4e0207f.tar.gz dispatch-27fd0be36b2f6395249de5aacc86e41fe4e0207f.zip | |
feat(cache-warming): manual POST /chat/warm trigger endpoint
A frontend 'warm now' button (and fast tests) can trigger a warm on demand
instead of waiting for the automatic timer.
- transport-contract: WarmRequest / WarmResponse wire types
- transport-http: POST /chat/warm → cacheWarmHandle.warm(); 200 with cachePct,
409 when the conversation is generating, 400 on missing conversationId
Live-verified vs claude haiku: seed turn cacheWrite=6799 → POST /chat/warm
returns cacheReadTokens=6799 cachePct=100 (100% hit). 760 vitest + 109 bun green.
Diffstat (limited to 'packages/transport-http/src/app.test.ts')
| -rw-r--r-- | packages/transport-http/src/app.test.ts | 106 |
1 files changed, 105 insertions, 1 deletions
diff --git a/packages/transport-http/src/app.test.ts b/packages/transport-http/src/app.test.ts index e634e19..7352b5d 100644 --- a/packages/transport-http/src/app.test.ts +++ b/packages/transport-http/src/app.test.ts @@ -10,7 +10,12 @@ import { createThroughputStore, dayKeyOf } from "@dispatch/throughput-store"; import type { ThroughputResponse } from "@dispatch/transport-contract"; import { describe, expect, it } from "vitest"; import { createApp } from "./app.js"; -import type { ConversationStore, CredentialStore, SessionOrchestrator } from "./seam.js"; +import type { + ConversationStore, + CredentialStore, + SessionOrchestrator, + WarmService, +} from "./seam.js"; function createMemStorage(): StorageNamespace { const map = new Map<string, string>(); @@ -147,6 +152,23 @@ function createThrowingCredentialStore(error: Error): CredentialStore { }; } +function createFakeWarmService( + result: + | { + inputTokens: number; + outputTokens: number; + cacheReadTokens: number; + cacheWriteTokens: number; + } + | { error: string }, +): WarmService { + return { + async warm() { + return result; + }, + }; +} + const noopLogger = createFakeLogger(); describe("GET /health", () => { @@ -399,6 +421,88 @@ describe("POST /chat", () => { }); }); +describe("POST /chat/warm", () => { + it("POST /chat/warm returns 200 with cachePct from the warm usage", async () => { + const app = createApp({ + conversationStore: createFakeConversationStore(), + orchestrator: createFakeOrchestrator([]), + credentialStore: createFakeCredentialStore([]), + warmService: createFakeWarmService({ + inputTokens: 1000, + outputTokens: 200, + cacheReadTokens: 800, + cacheWriteTokens: 100, + }), + logger: noopLogger, + }); + + const res = await app.request("/chat/warm", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ conversationId: "conv1" }), + }); + + expect(res.status).toBe(200); + const body = (await res.json()) as { + inputTokens: number; + outputTokens: number; + cacheReadTokens: number; + cacheWriteTokens: number; + cachePct: number; + }; + expect(body.inputTokens).toBe(1000); + expect(body.outputTokens).toBe(200); + expect(body.cacheReadTokens).toBe(800); + expect(body.cacheWriteTokens).toBe(100); + expect(body.cachePct).toBe(80); + }); + + it("POST /chat/warm returns 409 when the warm service reports the conversation is generating", async () => { + const app = createApp({ + conversationStore: createFakeConversationStore(), + orchestrator: createFakeOrchestrator([]), + credentialStore: createFakeCredentialStore([]), + warmService: createFakeWarmService({ error: "conversation is generating" }), + logger: noopLogger, + }); + + const res = await app.request("/chat/warm", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ conversationId: "conv1" }), + }); + + expect(res.status).toBe(409); + const body = (await res.json()) as { error: string }; + expect(body.error).toBe("conversation is generating"); + }); + + it("POST /chat/warm returns 400 when conversationId is missing", async () => { + const app = createApp({ + conversationStore: createFakeConversationStore(), + orchestrator: createFakeOrchestrator([]), + credentialStore: createFakeCredentialStore([]), + warmService: createFakeWarmService({ + inputTokens: 0, + outputTokens: 0, + cacheReadTokens: 0, + cacheWriteTokens: 0, + }), + logger: noopLogger, + }); + + const res = await app.request("/chat/warm", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), + }); + + expect(res.status).toBe(400); + const body = (await res.json()) as { error: string }; + expect(body.error).toContain("conversationId"); + }); +}); + describe("GET /conversations/:id", () => { const sampleChunks: StoredChunk[] = [ { seq: 1, role: "user", chunk: { type: "text", text: "hello" } }, |
