diff options
| author | Adam Malczewski <[email protected]> | 2026-06-27 01:13:28 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-27 01:13:31 +0900 |
| commit | 2fa03f8d7410c2b8d6be8e10ad088863e83d7177 (patch) | |
| tree | 94e1923180ae38d571d34b578afecb0a18913c24 /scripts/probe-cache-warming.ts | |
| parent | 80f99665034a0e510300793205c162fc7a46769f (diff) | |
| parent | 08b12478636f4a5c86a1f3c40a843f2906b7c82f (diff) | |
| download | dispatch-web-2fa03f8d7410c2b8d6be8e10ad088863e83d7177.tar.gz dispatch-web-2fa03f8d7410c2b8d6be8e10ad088863e83d7177.zip | |
Merge branch 'dev' into feature/heartbeat
# Conflicts:
# src/app/App.svelte
# src/app/store.svelte.ts
# src/app/store.test.ts
# src/features/workspaces/ui/WorkspaceCard.test.ts
Diffstat (limited to 'scripts/probe-cache-warming.ts')
| -rw-r--r-- | scripts/probe-cache-warming.ts | 414 |
1 files changed, 207 insertions, 207 deletions
diff --git a/scripts/probe-cache-warming.ts b/scripts/probe-cache-warming.ts index 470e43b..1bf1f9b 100644 --- a/scripts/probe-cache-warming.ts +++ b/scripts/probe-cache-warming.ts @@ -14,9 +14,9 @@ * bun scripts/probe-cache-warming.ts */ import type { - ChatDeltaMessage, - ChatErrorMessage, - CloseConversationResponse, + ChatDeltaMessage, + ChatErrorMessage, + CloseConversationResponse, } from "@dispatch/transport-contract"; import type { SurfaceServerMessage, SurfaceSpec } from "@dispatch/ui-contract"; import { createSurfaceSocket } from "../src/adapters/ws/index.ts"; @@ -28,17 +28,17 @@ const SURFACE_ID = "cache-warming"; const checks: { name: string; ok: boolean }[] = []; const record = (name: string, ok: boolean, detail?: string) => { - checks.push({ name, ok }); - console.log(` ${ok ? "✅" : "❌"} ${name}${detail ? ` — ${detail}` : ""}`); + checks.push({ name, ok }); + console.log(` ${ok ? "✅" : "❌"} ${name}${detail ? ` — ${detail}` : ""}`); }; const log = (msg: string) => console.log(`[${new Date().toISOString().slice(11, 19)}] ${msg}`); const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); function summarize(spec: SurfaceSpec | null): string { - const c = parseControls(spec); - const next = - c.nextWarmAt === null ? "null" : `${Math.round((c.nextWarmAt - Date.now()) / 1000)}s`; - return `enabled=${c.enabled} interval=${c.intervalSeconds}s lastPct=${c.lastPct} next=${next} lastWarmAt=${c.lastWarmAt}`; + const c = parseControls(spec); + const next = + c.nextWarmAt === null ? "null" : `${Math.round((c.nextWarmAt - Date.now()) / 1000)}s`; + return `enabled=${c.enabled} interval=${c.intervalSeconds}s lastPct=${c.lastPct} next=${next} lastWarmAt=${c.lastWarmAt}`; } let catalog: { id: string; scope?: string }[] = []; @@ -49,229 +49,229 @@ let specWaiter: (() => void) | null = null; const chatHandlers = new Map<string, (msg: ChatDeltaMessage | ChatErrorMessage) => void>(); const socket = createSurfaceSocket({ - url: WS_URL, - onMessage: (m: SurfaceServerMessage) => { - if (m.type === "catalog") { - catalog = [...m.catalog]; - log(`catalog: ${m.catalog.map((e) => `${e.id}(scope=${e.scope ?? "—"})`).join(", ")}`); - } else if (m.type === "surface") { - latestSpec = m.spec; - latestSpecConv = m.conversationId; - log(`surface(initial) conv=${m.conversationId ?? "—"}: ${summarize(m.spec)}`); - specWaiter?.(); - } else if (m.type === "update") { - if (m.update.surfaceId !== SURFACE_ID) return; - latestSpec = m.update.spec; - latestSpecConv = m.update.conversationId; - log(`update conv=${m.update.conversationId ?? "—"}: ${summarize(m.update.spec)}`); - specWaiter?.(); - } else if (m.type === "error") { - log(`surface ERROR: ${m.surfaceId ?? "—"}: ${m.message}`); - } - }, - onChat: (msg) => { - const id = msg.type === "chat.error" ? msg.conversationId : msg.event.conversationId; - if (id !== undefined) chatHandlers.get(id)?.(msg); - }, + url: WS_URL, + onMessage: (m: SurfaceServerMessage) => { + if (m.type === "catalog") { + catalog = [...m.catalog]; + log(`catalog: ${m.catalog.map((e) => `${e.id}(scope=${e.scope ?? "—"})`).join(", ")}`); + } else if (m.type === "surface") { + latestSpec = m.spec; + latestSpecConv = m.conversationId; + log(`surface(initial) conv=${m.conversationId ?? "—"}: ${summarize(m.spec)}`); + specWaiter?.(); + } else if (m.type === "update") { + if (m.update.surfaceId !== SURFACE_ID) return; + latestSpec = m.update.spec; + latestSpecConv = m.update.conversationId; + log(`update conv=${m.update.conversationId ?? "—"}: ${summarize(m.update.spec)}`); + specWaiter?.(); + } else if (m.type === "error") { + log(`surface ERROR: ${m.surfaceId ?? "—"}: ${m.message}`); + } + }, + onChat: (msg) => { + const id = msg.type === "chat.error" ? msg.conversationId : msg.event.conversationId; + if (id !== undefined) chatHandlers.get(id)?.(msg); + }, }); /** Wait for the next surface/update message (or time out). */ function nextSpec(timeoutMs: number): Promise<boolean> { - return new Promise((resolve) => { - const t = setTimeout(() => { - specWaiter = null; - resolve(false); - }, timeoutMs); - specWaiter = () => { - clearTimeout(t); - specWaiter = null; - resolve(true); - }; - }); + return new Promise((resolve) => { + const t = setTimeout(() => { + specWaiter = null; + resolve(false); + }, timeoutMs); + specWaiter = () => { + clearTimeout(t); + specWaiter = null; + resolve(true); + }; + }); } async function runTinyTurn(conversationId: string, prompt: string): Promise<boolean> { - const done = Promise.withResolvers<boolean>(); - chatHandlers.set(conversationId, (msg) => { - if (msg.type === "chat.error") { - log(`chat.error: ${msg.message}`); - done.resolve(false); - } else if (msg.event.type === "turn-sealed") { - done.resolve(true); - } - }); - socket.send({ type: "chat.send", conversationId, message: prompt }); - const t = setTimeout(() => done.resolve(false), 90_000); - const ok = await done.promise; - clearTimeout(t); - chatHandlers.delete(conversationId); - return ok; + const done = Promise.withResolvers<boolean>(); + chatHandlers.set(conversationId, (msg) => { + if (msg.type === "chat.error") { + log(`chat.error: ${msg.message}`); + done.resolve(false); + } else if (msg.event.type === "turn-sealed") { + done.resolve(true); + } + }); + socket.send({ type: "chat.send", conversationId, message: prompt }); + const t = setTimeout(() => done.resolve(false), 90_000); + const ok = await done.promise; + clearTimeout(t); + chatHandlers.delete(conversationId); + return ok; } function invoke(actionId: string, conversationId: string, payload?: unknown): void { - socket.send( - payload === undefined - ? { type: "invoke", surfaceId: SURFACE_ID, actionId, conversationId } - : { type: "invoke", surfaceId: SURFACE_ID, actionId, payload, conversationId }, - ); + socket.send( + payload === undefined + ? { type: "invoke", surfaceId: SURFACE_ID, actionId, conversationId } + : { type: "invoke", surfaceId: SURFACE_ID, actionId, payload, conversationId }, + ); } async function main() { - await sleep(600); - record( - "catalog includes cache-warming with scope=conversation", - catalog.some((e) => e.id === SURFACE_ID && e.scope === "conversation"), - ); + await sleep(600); + record( + "catalog includes cache-warming with scope=conversation", + catalog.some((e) => e.id === SURFACE_ID && e.scope === "conversation"), + ); - // ── A: the DRAFT/new-tab path — subscribe with NO conversationId ─────────── - log("PHASE A: subscribe with NO conversationId (draft / new tab)"); - socket.send({ type: "subscribe", surfaceId: SURFACE_ID }); - await nextSpec(3000); - record( - "draft subscribe → degenerate spec (no toggle parsed)", - !parseControls(latestSpec).enabled, - ); - socket.send({ type: "unsubscribe", surfaceId: SURFACE_ID }); - await sleep(300); + // ── A: the DRAFT/new-tab path — subscribe with NO conversationId ─────────── + log("PHASE A: subscribe with NO conversationId (draft / new tab)"); + socket.send({ type: "subscribe", surfaceId: SURFACE_ID }); + await nextSpec(3000); + record( + "draft subscribe → degenerate spec (no toggle parsed)", + !parseControls(latestSpec).enabled, + ); + socket.send({ type: "unsubscribe", surfaceId: SURFACE_ID }); + await sleep(300); - // ── B: a FRESH conversation defaults OFF (CR-4a) + echo (CR-4d) ──────────── - const conv = crypto.randomUUID(); - log(`PHASE B: creating conversation ${conv}`); - if (!(await runTinyTurn(conv, "Reply with exactly: ok"))) { - log("FATAL: could not create a conversation"); - process.exit(1); - } - socket.send({ type: "subscribe", surfaceId: SURFACE_ID, conversationId: conv }); - await nextSpec(3000); - const fresh = parseControls(latestSpec); - record("CR-4d: initial surface message echoes conversationId", latestSpecConv === conv); - record("CR-4a: fresh conversation defaults to warming OFF", fresh.enabled === false); - record("CR-4a: nothing scheduled while off (nextWarmAt null)", fresh.nextWarmAt === null); + // ── B: a FRESH conversation defaults OFF (CR-4a) + echo (CR-4d) ──────────── + const conv = crypto.randomUUID(); + log(`PHASE B: creating conversation ${conv}`); + if (!(await runTinyTurn(conv, "Reply with exactly: ok"))) { + log("FATAL: could not create a conversation"); + process.exit(1); + } + socket.send({ type: "subscribe", surfaceId: SURFACE_ID, conversationId: conv }); + await nextSpec(3000); + const fresh = parseControls(latestSpec); + record("CR-4d: initial surface message echoes conversationId", latestSpecConv === conv); + record("CR-4a: fresh conversation defaults to warming OFF", fresh.enabled === false); + record("CR-4a: nothing scheduled while off (nextWarmAt null)", fresh.nextWarmAt === null); - // ── C: opt in + 10s interval → repeated warms, FUTURE nextWarmAt (CR-4b) ─── - log("PHASE C: toggling warming ON"); - const toggleId = fresh.toggleActionId; - if (toggleId === null) { - record("toggle action present", false); - process.exit(1); - } - invoke(toggleId, conv); - await nextSpec(3000); - let c = parseControls(latestSpec); - record("toggle-on update arrived (enabled)", c.enabled === true); - record( - "CR-4b: enable schedules a FUTURE nextWarmAt", - c.nextWarmAt !== null && c.nextWarmAt > Date.now(), - ); + // ── C: opt in + 10s interval → repeated warms, FUTURE nextWarmAt (CR-4b) ─── + log("PHASE C: toggling warming ON"); + const toggleId = fresh.toggleActionId; + if (toggleId === null) { + record("toggle action present", false); + process.exit(1); + } + invoke(toggleId, conv); + await nextSpec(3000); + let c = parseControls(latestSpec); + record("toggle-on update arrived (enabled)", c.enabled === true); + record( + "CR-4b: enable schedules a FUTURE nextWarmAt", + c.nextWarmAt !== null && c.nextWarmAt > Date.now(), + ); - const setIntervalId = c.setIntervalActionId; - if (setIntervalId !== null) { - log("PHASE C: set-interval = 10s"); - invoke(setIntervalId, conv, 10); - await nextSpec(3000); - c = parseControls(latestSpec); - record( - "set-interval update: interval=10 + FUTURE nextWarmAt", - c.intervalSeconds === 10 && c.nextWarmAt !== null && c.nextWarmAt > Date.now(), - ); - } + const setIntervalId = c.setIntervalActionId; + if (setIntervalId !== null) { + log("PHASE C: set-interval = 10s"); + invoke(setIntervalId, conv, 10); + await nextSpec(3000); + c = parseControls(latestSpec); + record( + "set-interval update: interval=10 + FUTURE nextWarmAt", + c.intervalSeconds === 10 && c.nextWarmAt !== null && c.nextWarmAt > Date.now(), + ); + } - log("PHASE C: waiting up to 45s for 2 automatic warms…"); - const deadline = Date.now() + 45_000; - let lastSeen = c.lastWarmAt; - let warms = 0; - let allFuture = true; - while (Date.now() < deadline && warms < 2) { - await nextSpec(Math.max(1, deadline - Date.now())); - const now = parseControls(latestSpec); - if (now.lastWarmAt !== null && now.lastWarmAt !== lastSeen) { - lastSeen = now.lastWarmAt; - warms++; - const future = now.nextWarmAt !== null && now.nextWarmAt > Date.now() - 1000; - if (!future) allFuture = false; - log( - ` automatic warm #${warms}: pct=${now.lastPct} retention=${now.retentionPct} ` + - `nextWarmAt ${future ? "FUTURE" : "STALE/PAST"}`, - ); - } - } - record("automatic warms repeat (2 observed @10s)", warms >= 2, `${warms} warm(s)`); - record("CR-4b: every post-warm update carries a FUTURE nextWarmAt", warms >= 2 && allFuture); + log("PHASE C: waiting up to 45s for 2 automatic warms…"); + const deadline = Date.now() + 45_000; + let lastSeen = c.lastWarmAt; + let warms = 0; + let allFuture = true; + while (Date.now() < deadline && warms < 2) { + await nextSpec(Math.max(1, deadline - Date.now())); + const now = parseControls(latestSpec); + if (now.lastWarmAt !== null && now.lastWarmAt !== lastSeen) { + lastSeen = now.lastWarmAt; + warms++; + const future = now.nextWarmAt !== null && now.nextWarmAt > Date.now() - 1000; + if (!future) allFuture = false; + log( + ` automatic warm #${warms}: pct=${now.lastPct} retention=${now.retentionPct} ` + + `nextWarmAt ${future ? "FUTURE" : "STALE/PAST"}`, + ); + } + } + record("automatic warms repeat (2 observed @10s)", warms >= 2, `${warms} warm(s)`); + record("CR-4b: every post-warm update carries a FUTURE nextWarmAt", warms >= 2 && allFuture); - // ── D: close mid-turn → abort + warming disabled (CR-4c) ─────────────────── - log("PHASE D: starting a long turn, then closing the conversation mid-turn…"); - const seenDone = Promise.withResolvers<string>(); // resolves with done.reason - const seenSealed = Promise.withResolvers<void>(); - let turnStarted = false; - const started = Promise.withResolvers<void>(); - chatHandlers.set(conv, (msg) => { - if (msg.type === "chat.error") { - log(`chat.error: ${msg.message}`); - return; - } - const ev = msg.event; - if (ev.type === "turn-start") { - turnStarted = true; - started.resolve(); - } else if (ev.type === "done") { - seenDone.resolve(ev.reason); - } else if (ev.type === "turn-sealed") { - seenSealed.resolve(); - } - }); - socket.send({ - type: "chat.send", - conversationId: conv, - message: - "Write a detailed 1000-word essay about the history of computing. Take your time and be thorough.", - }); - const startTimeout = setTimeout(() => started.resolve(), 15_000); - await started.promise; - clearTimeout(startTimeout); - record("turn started (watcher saw turn-start)", turnStarted); - await sleep(1000); // let it generate a moment + // ── D: close mid-turn → abort + warming disabled (CR-4c) ─────────────────── + log("PHASE D: starting a long turn, then closing the conversation mid-turn…"); + const seenDone = Promise.withResolvers<string>(); // resolves with done.reason + const seenSealed = Promise.withResolvers<void>(); + let turnStarted = false; + const started = Promise.withResolvers<void>(); + chatHandlers.set(conv, (msg) => { + if (msg.type === "chat.error") { + log(`chat.error: ${msg.message}`); + return; + } + const ev = msg.event; + if (ev.type === "turn-start") { + turnStarted = true; + started.resolve(); + } else if (ev.type === "done") { + seenDone.resolve(ev.reason); + } else if (ev.type === "turn-sealed") { + seenSealed.resolve(); + } + }); + socket.send({ + type: "chat.send", + conversationId: conv, + message: + "Write a detailed 1000-word essay about the history of computing. Take your time and be thorough.", + }); + const startTimeout = setTimeout(() => started.resolve(), 15_000); + await started.promise; + clearTimeout(startTimeout); + record("turn started (watcher saw turn-start)", turnStarted); + await sleep(1000); // let it generate a moment - const res = await fetch(`${HTTP_BASE}/conversations/${encodeURIComponent(conv)}/close`, { - method: "POST", - headers: { Origin: "http://localhost:24204" }, - }); - record("POST /conversations/:id/close → 200", res.ok, `HTTP ${res.status}`); - const body = (await res.json()) as CloseConversationResponse; - record("close aborted the in-flight turn (abortedTurn)", body.abortedTurn === true); + const res = await fetch(`${HTTP_BASE}/conversations/${encodeURIComponent(conv)}/close`, { + method: "POST", + headers: { Origin: "http://localhost:24204" }, + }); + record("POST /conversations/:id/close → 200", res.ok, `HTTP ${res.status}`); + const body = (await res.json()) as CloseConversationResponse; + record("close aborted the in-flight turn (abortedTurn)", body.abortedTurn === true); - const doneReason = await Promise.race([seenDone.promise, sleep(15_000).then(() => "(timeout)")]); - record('watcher received done with reason "aborted"', doneReason === "aborted", doneReason); - const sealed = await Promise.race([ - seenSealed.promise.then(() => true), - sleep(15_000).then(() => false), - ]); - record("turn sealed normally after abort", sealed); - chatHandlers.delete(conv); + const doneReason = await Promise.race([seenDone.promise, sleep(15_000).then(() => "(timeout)")]); + record('watcher received done with reason "aborted"', doneReason === "aborted", doneReason); + const sealed = await Promise.race([ + seenSealed.promise.then(() => true), + sleep(15_000).then(() => false), + ]); + record("turn sealed normally after abort", sealed); + chatHandlers.delete(conv); - // The close also pushed a surface update: warming disabled + unscheduled. - await sleep(1500); - const closed = parseControls(latestSpec); - record( - "CR-4c: close disabled warming + cleared the schedule", - closed.enabled === false && closed.nextWarmAt === null, - summarize(latestSpec), - ); + // The close also pushed a surface update: warming disabled + unscheduled. + await sleep(1500); + const closed = parseControls(latestSpec); + record( + "CR-4c: close disabled warming + cleared the schedule", + closed.enabled === false && closed.nextWarmAt === null, + summarize(latestSpec), + ); - // Idempotency: closing again (now idle) succeeds with abortedTurn false. - const res2 = await fetch(`${HTTP_BASE}/conversations/${encodeURIComponent(conv)}/close`, { - method: "POST", - headers: { Origin: "http://localhost:24204" }, - }); - const body2 = (await res2.json()) as CloseConversationResponse; - record("close is idempotent (200 + abortedTurn:false)", res2.ok && body2.abortedTurn === false); + // Idempotency: closing again (now idle) succeeds with abortedTurn false. + const res2 = await fetch(`${HTTP_BASE}/conversations/${encodeURIComponent(conv)}/close`, { + method: "POST", + headers: { Origin: "http://localhost:24204" }, + }); + const body2 = (await res2.json()) as CloseConversationResponse; + record("close is idempotent (200 + abortedTurn:false)", res2.ok && body2.abortedTurn === false); - socket.close(); - const passed = checks.filter((x) => x.ok).length; - console.log(`\n[probe-cache-warming] ${passed}/${checks.length} checks passed`); - process.exit(passed === checks.length ? 0 : 1); + socket.close(); + const passed = checks.filter((x) => x.ok).length; + console.log(`\n[probe-cache-warming] ${passed}/${checks.length} checks passed`); + process.exit(passed === checks.length ? 0 : 1); } main().catch((e) => { - console.error(`[probe] FATAL: ${e}`); - process.exit(1); + console.error(`[probe] FATAL: ${e}`); + process.exit(1); }); |
