summaryrefslogtreecommitdiffhomepage
path: root/packages/transport-http/src/app.test.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-11 12:45:21 +0900
committerAdam Malczewski <[email protected]>2026-06-11 12:45:21 +0900
commit27fd0be36b2f6395249de5aacc86e41fe4e0207f (patch)
tree67ae766c1985344878d6a2e71da18834fa73e47d /packages/transport-http/src/app.test.ts
parentc2b4c05d91fa88b8d02c055a0e15c22abd8e21f3 (diff)
downloaddispatch-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.ts106
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" } },