import { describe, expect, test, mock } from "bun:test"; import { createHydrateRoute } from "../routes/hydrate.ts"; import type { Runtime, Action } from "@gizmo-ai/runtime"; import type { PersistenceStore, ActionRecord } from "../persistence/index.ts"; function createMockRuntime(state: Record): Runtime, Action> { return { getState: () => state, dispatch: mock(() => {}), dispatchToReducer: mock(() => {}), resetState: mock(() => {}), execute: mock(() => ({ included: Promise.resolve(), completed: Promise.resolve({ status: "completed" }), cancel: () => {}, })), subscribe: () => () => {}, subscribeToActions: () => () => {}, getPluginGraph: () => ({ nodes: [], edges: [] }), getPlugins: () => [], } as Runtime, Action>; } function createMockPersistence(actions: ActionRecord[]): PersistenceStore { return { appendRun: async () => {}, updateRun: async () => {}, queryRuns: async () => [], getRun: async () => null, appendAction: async () => {}, getActions: mock(async () => actions), appendState: async () => {}, getLatestState: async () => null, queryState: async () => [], init: async () => {}, close: async () => {}, }; } function makeAction(seq: number, type: string, payload: unknown = {}): ActionRecord { return { seq, timestamp: Date.now(), type, payload }; } describe("createHydrateRoute", () => { test("returns 501 when persistence is not enabled", async () => { const runtime = createMockRuntime({ agent: { conversation: [], loopCount: 0 } }); const app = createHydrateRoute({ runtime, persistence: null }); const res = await app.request("/", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ executionId: "abc-123" }), }); expect(res.status).toBe(501); const body = await res.json(); expect(body.error).toContain("Persistence is not enabled"); }); test("returns 400 when executionId is missing", async () => { const runtime = createMockRuntime({ agent: { conversation: [], loopCount: 0 } }); const persistence = createMockPersistence([]); const app = createHydrateRoute({ runtime, persistence }); const res = await app.request("/", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}), }); expect(res.status).toBe(400); }); test("returns 404 when no actions found for executionId", async () => { const runtime = createMockRuntime({ agent: { conversation: [], loopCount: 0 } }); const persistence = createMockPersistence([]); const app = createHydrateRoute({ runtime, persistence }); const res = await app.request("/", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ executionId: "nonexistent" }), }); expect(res.status).toBe(404); }); test("resets state before replaying actions", async () => { const runtime = createMockRuntime({ agent: { conversation: [1, 2, 3], loopCount: 2 }, }); const actions = [ makeAction(1, "RUNTIME_EXECUTION_STARTED", { type: "RUNTIME_EXECUTION_STARTED" }), ]; const persistence = createMockPersistence(actions); const app = createHydrateRoute({ runtime, persistence }); const res = await app.request("/", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ executionId: "abc-123" }), }); expect(res.status).toBe(200); // resetState must be called before any dispatchToReducer calls expect(runtime.resetState).toHaveBeenCalledTimes(1); const resetOrder = (runtime.resetState as ReturnType).mock.calls[0]; const replayOrder = (runtime.dispatchToReducer as ReturnType).mock.calls[0]; // Both were called (reset first, then replay) expect(resetOrder).toBeDefined(); expect(replayOrder).toBeDefined(); }); test("replays all actions via dispatchToReducer (bypassing middleware)", async () => { const runtime = createMockRuntime({ agent: { conversation: [1, 2, 3], loopCount: 2 }, }); const actions = [ makeAction(1, "RUNTIME_EXECUTION_STARTED", { type: "RUNTIME_EXECUTION_STARTED" }), makeAction(2, "agent/messageAdded", { type: "agent/messageAdded", role: "user" }), makeAction(3, "agent/messageAdded", { type: "agent/messageAdded", role: "assistant" }), ]; const persistence = createMockPersistence(actions); const app = createHydrateRoute({ runtime, persistence }); const res = await app.request("/", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ executionId: "abc-123" }), }); expect(res.status).toBe(200); const body = await res.json(); expect(body.status).toBe("hydrated"); expect(body.actionCount).toBe(3); expect(body.state.conversationLength).toBe(3); expect(body.state.loopCount).toBe(2); // Actions go through dispatchToReducer (reducers only), NOT dispatch (middleware chain) expect(runtime.dispatchToReducer).toHaveBeenCalledTimes(3); expect(runtime.dispatch).not.toHaveBeenCalled(); }); test("slices actions by upToSeq", async () => { const runtime = createMockRuntime({ agent: { conversation: [], loopCount: 0 } }); const actions = [ makeAction(1, "RUNTIME_EXECUTION_STARTED"), makeAction(2, "agent/messageAdded"), makeAction(3, "agent/messageAdded"), makeAction(4, "agent/toolCallExecuted"), makeAction(5, "agent/messageAdded"), ]; const persistence = createMockPersistence(actions); const app = createHydrateRoute({ runtime, persistence }); const res = await app.request("/", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ executionId: "abc-123", upToSeq: 3 }), }); expect(res.status).toBe(200); const body = await res.json(); expect(body.actionCount).toBe(3); expect(runtime.dispatchToReducer).toHaveBeenCalledTimes(3); }); test("slices actions by upToTurn", async () => { const runtime = createMockRuntime({ agent: { conversation: [], loopCount: 0 } }); const actions = [ // Turn 1 makeAction(1, "RUNTIME_EXECUTION_STARTED"), makeAction(2, "agent/messageAdded"), makeAction(3, "agent/messageAdded"), // Turn 2 makeAction(4, "RUNTIME_EXECUTION_STARTED"), makeAction(5, "agent/messageAdded"), // Turn 3 makeAction(6, "RUNTIME_EXECUTION_STARTED"), makeAction(7, "agent/messageAdded"), ]; const persistence = createMockPersistence(actions); const app = createHydrateRoute({ runtime, persistence }); const res = await app.request("/", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ executionId: "abc-123", upToTurn: 2 }), }); expect(res.status).toBe(200); const body = await res.json(); // Turn 1 (3 actions) + Turn 2 (2 actions) = 5 actions, stops before turn 3 expect(body.actionCount).toBe(5); expect(runtime.dispatchToReducer).toHaveBeenCalledTimes(5); }); test("slim mode stubs tool output bodies", async () => { const runtime = createMockRuntime({ agent: { conversation: [], loopCount: 0 } }); const actions = [ makeAction(1, "RUNTIME_EXECUTION_STARTED", { type: "RUNTIME_EXECUTION_STARTED" }), makeAction(2, "agent/toolCallExecuted", { type: "agent/toolCallExecuted", name: "bash", output: "A".repeat(50_000), }), makeAction(3, "agent/messageAdded", { type: "agent/messageAdded" }), ]; const persistence = createMockPersistence(actions); const app = createHydrateRoute({ runtime, persistence }); const res = await app.request("/", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ executionId: "abc-123", hydrateMode: "slim" }), }); expect(res.status).toBe(200); // The second dispatchToReducer call should have the stubbed output const secondCall = (runtime.dispatchToReducer as ReturnType).mock.calls[1][0] as any; expect(secondCall.output).toBe("[hydrated — output omitted]"); }); test("replays ALL action types — middleware bypass means no filtering needed", async () => { const runtime = createMockRuntime({ agent: { conversation: [], loopCount: 0 } }); const actions = [ makeAction(1, "RUNTIME_EXECUTION_STARTED"), makeAction(2, "AGENT_LOOP_INCREMENTED"), makeAction(3, "AGENT_MODEL_REQUESTED"), makeAction(4, "RUNTIME_WORK_STARTED"), makeAction(5, "AGENT_MODEL_STREAM_STARTED"), makeAction(6, "AGENT_MODEL_STREAM_CONTENT_DELTA"), makeAction(7, "AGENT_MODEL_STREAM_REASONING_DELTA"), makeAction(8, "AGENT_MODEL_STREAM_COMPLETED"), makeAction(9, "RUNTIME_WORK_COMPLETED"), makeAction(10, "AGENT_MODEL_RESPONDED"), makeAction(11, "AGENT_TOOL_CALL_REQUESTED"), makeAction(12, "AGENT_TOOL_CALL_EXECUTED"), makeAction(13, "AGENT_TURN_COMPLETED"), makeAction(14, "RUNTIME_EXECUTION_COMPLETED"), ]; const persistence = createMockPersistence(actions); const app = createHydrateRoute({ runtime, persistence }); const res = await app.request("/", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ executionId: "abc-123" }), }); expect(res.status).toBe(200); const body = await res.json(); // All 14 actions replayed — no filtering needed since middleware is bypassed expect(body.actionCount).toBe(14); expect(runtime.dispatchToReducer).toHaveBeenCalledTimes(14); // dispatch (middleware path) was never called expect(runtime.dispatch).not.toHaveBeenCalled(); }); test("thenExecute triggers runtime.execute after hydration", async () => { const runtime = createMockRuntime({ agent: { conversation: [1], loopCount: 0 }, execution: { id: "new-exec", turnIds: ["turn-1"] }, }); const actions = [ makeAction(1, "RUNTIME_EXECUTION_STARTED", { type: "RUNTIME_EXECUTION_STARTED" }), ]; const persistence = createMockPersistence(actions); const app = createHydrateRoute({ runtime, persistence }); const res = await app.request("/", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ executionId: "abc-123", thenExecute: "Check the email" }), }); expect(res.status).toBe(200); const body = await res.json(); expect(body.execution).toBeDefined(); expect(body.execution.executionId).toBe("new-exec"); expect(body.execution.turnId).toBe("turn-1"); expect(runtime.execute).toHaveBeenCalledWith("Check the email"); }); });