import { describe, expect, it } from "vitest"; import { runHistorySyncOperation, type HistorySyncCommand, } from "@/features/agents/operations/historySyncOperation"; import type { AgentState } from "@/features/agents/state/store"; type ChatHistoryMessage = Record; const createAgent = (overrides?: Partial): AgentState => { const base: AgentState = { agentId: "agent-1", name: "Agent One", sessionKey: "agent:agent-1:main", status: "idle", sessionCreated: true, awaitingUserInput: false, hasUnseenActivity: false, outputLines: [], lastResult: null, lastDiff: null, runId: null, runStartedAt: null, streamText: null, thinkingTrace: null, latestOverride: null, latestOverrideKind: null, lastAssistantMessageAt: null, lastActivityAt: null, latestPreview: null, lastUserMessage: null, draft: "", sessionSettingsSynced: true, historyLoadedAt: null, historyFetchLimit: null, historyFetchedCount: null, historyMaybeTruncated: false, toolCallingEnabled: true, showThinkingTraces: true, model: "openai/gpt-5", thinkingLevel: "medium", avatarSeed: "seed-1", avatarUrl: null, }; return { ...base, ...(overrides ?? {}) }; }; const getCommandsByKind = ( commands: HistorySyncCommand[], kind: TKind ): Array> => commands.filter((command) => command.kind === kind) as Array< Extract >; describe("historySyncOperation", () => { it("returns noop when request intent resolves to skip", async () => { const commands = await runHistorySyncOperation({ client: { call: async () => ({ messages: [] as ChatHistoryMessage[] }) as T, }, agentId: "agent-1", getAgent: () => null, inFlightSessionKeys: new Set(), requestId: "req-1", loadedAt: 1_234, defaultLimit: 200, maxLimit: 5000, transcriptV2Enabled: true, }); expect(commands).toEqual([{ kind: "noop", reason: "missing-agent" }]); }); it("applies history updates even when latest agent is running with active run", async () => { const agent = createAgent({ status: "running", runId: "run-1", transcriptRevision: 3, outputLines: ["> local question", "assistant draft"], }); const commands = await runHistorySyncOperation({ client: { call: async () => ({ sessionKey: agent.sessionKey, messages: [{ role: "assistant", content: "remote answer" }], }) as T, }, agentId: "agent-1", getAgent: () => agent, inFlightSessionKeys: new Set(), requestId: "req-2", loadedAt: 2_345, defaultLimit: 200, maxLimit: 5000, transcriptV2Enabled: true, }); const updates = getCommandsByKind(commands, "dispatchUpdateAgent"); const metrics = getCommandsByKind(commands, "logMetric"); expect(metrics).toEqual([]); const finalUpdate = updates[updates.length - 1]; if (!finalUpdate) throw new Error("Expected final update command."); const patch = finalUpdate.patch; expect(patch.outputLines).toContain("> local question"); expect(patch.outputLines).toContain("assistant draft"); expect(patch.outputLines).toContain("remote answer"); expect(patch.lastResult).toBe("remote answer"); expect(patch.latestPreview).toBe("remote answer"); expect(patch.lastAppliedHistoryRequestId).toBe("req-2"); }); it("returns transcript merge update commands when disposition is apply and transcript v2 is enabled", async () => { const agent = createAgent({ transcriptRevision: 1, outputLines: ["> local question"], }); const markdownAssistant = [ "- first bullet", "- second bullet", "", "```ts", "console.log('merged answer');", "```", ].join("\n"); const messages: ChatHistoryMessage[] = [{ role: "assistant", content: markdownAssistant }]; const commands = await runHistorySyncOperation({ client: { call: async () => ({ sessionKey: agent.sessionKey, messages, }) as T, }, agentId: "agent-1", getAgent: () => agent, inFlightSessionKeys: new Set(), requestId: "req-3", loadedAt: 3_456, defaultLimit: 200, maxLimit: 5000, transcriptV2Enabled: true, }); const updates = getCommandsByKind(commands, "dispatchUpdateAgent"); expect(updates.length).toBeGreaterThanOrEqual(2); expect(updates).toContainEqual({ kind: "dispatchUpdateAgent", agentId: "agent-1", patch: { lastHistoryRequestRevision: 1 }, }); const finalUpdate = updates[updates.length - 1]; if (!finalUpdate) throw new Error("Expected final update command."); const patch = finalUpdate.patch; expect(Array.isArray(patch.outputLines)).toBe(true); expect(patch.outputLines).toContain("> local question"); expect(patch.outputLines).toContain(markdownAssistant); expect(patch.lastResult).toBe(markdownAssistant); expect(patch.latestPreview).toBe(markdownAssistant); expect(patch.lastAppliedHistoryRequestId).toBe("req-3"); }); it("normalizes assistant text in transcript-v2 history sync patches", async () => { const agent = createAgent({ transcriptRevision: 1, outputLines: ["> local question"], }); const commands = await runHistorySyncOperation({ client: { call: async () => ({ sessionKey: agent.sessionKey, messages: [{ role: "assistant", content: "\n- alpha \n\n\n- beta\t \n\n" }], }) as T, }, agentId: "agent-1", getAgent: () => agent, inFlightSessionKeys: new Set(), requestId: "req-3b", loadedAt: 3_789, defaultLimit: 200, maxLimit: 5000, transcriptV2Enabled: true, }); const updates = getCommandsByKind(commands, "dispatchUpdateAgent"); const finalUpdate = updates[updates.length - 1]; if (!finalUpdate) throw new Error("Expected final update command."); const patch = finalUpdate.patch; expect(Array.isArray(patch.outputLines)).toBe(true); expect(patch.outputLines).toContain("> local question"); expect(patch.outputLines).toContain("- alpha\n\n- beta"); expect(patch.lastResult).toBe("- alpha\n\n- beta"); expect(patch.latestPreview).toBe("- alpha\n\n- beta"); expect(patch.lastAppliedHistoryRequestId).toBe("req-3b"); }); it("infers running state from recent user-terminal history in transcript-v2 mode", async () => { const agent = createAgent({ status: "idle", runId: null, runStartedAt: null, transcriptRevision: 1, outputLines: [], }); const loadedAt = Date.parse("2024-01-01T00:30:00.000Z"); const lastUserAt = Date.parse("2024-01-01T00:29:40.000Z"); const commands = await runHistorySyncOperation({ client: { call: async () => ({ sessionKey: agent.sessionKey, messages: [ { role: "user", timestamp: new Date(lastUserAt).toISOString(), content: "still working?", }, ], }) as T, }, agentId: "agent-1", getAgent: () => agent, inFlightSessionKeys: new Set(), requestId: "req-3c", loadedAt, defaultLimit: 200, maxLimit: 5000, transcriptV2Enabled: true, }); const updates = getCommandsByKind(commands, "dispatchUpdateAgent"); const finalUpdate = updates[updates.length - 1]; if (!finalUpdate) throw new Error("Expected final update command."); const patch = finalUpdate.patch; expect(patch.status).toBe("running"); expect(patch.runId).toBeNull(); expect(patch.runStartedAt).toBe(lastUserAt); expect(patch.streamText).toBeNull(); expect(patch.thinkingTrace).toBeNull(); expect(patch.lastUserMessage).toBe("still working?"); expect(patch.lastAppliedHistoryRequestId).toBe("req-3c"); }); it("does not infer running state when terminal history includes an aborted assistant message", async () => { const agent = createAgent({ status: "idle", runId: null, runStartedAt: null, transcriptRevision: 1, outputLines: [], }); const loadedAt = Date.parse("2024-01-01T00:30:00.000Z"); const commands = await runHistorySyncOperation({ client: { call: async () => ({ sessionKey: agent.sessionKey, messages: [ { role: "user", timestamp: "2024-01-01T00:29:40.000Z", content: "still working?", }, { role: "assistant", timestamp: "2024-01-01T00:29:41.000Z", content: [], stopReason: "aborted", errorMessage: "Request was aborted", }, ], }) as T, }, agentId: "agent-1", getAgent: () => agent, inFlightSessionKeys: new Set(), requestId: "req-3d", loadedAt, defaultLimit: 200, maxLimit: 5000, transcriptV2Enabled: true, }); const updates = getCommandsByKind(commands, "dispatchUpdateAgent"); const finalUpdate = updates[updates.length - 1]; if (!finalUpdate) throw new Error("Expected final update command."); const patch = finalUpdate.patch; expect(patch.status).toBeUndefined(); expect(patch.runId).toBeUndefined(); expect(Array.isArray(patch.outputLines)).toBe(true); expect(patch.outputLines).toContain("Run aborted."); expect(patch.lastResult).toBe("Run aborted."); expect(patch.latestPreview).toBe("Run aborted."); expect(patch.lastAppliedHistoryRequestId).toBe("req-3d"); }); it("returns legacy history sync patch command when transcript v2 is disabled", async () => { const agent = createAgent({ transcriptRevision: 0, outputLines: ["> local question"], }); const commands = await runHistorySyncOperation({ client: { call: async () => ({ sessionKey: agent.sessionKey, messages: [{ role: "assistant", content: "Legacy answer" }], }) as T, }, agentId: "agent-1", getAgent: () => agent, inFlightSessionKeys: new Set(), requestId: "req-4", loadedAt: 4_567, defaultLimit: 200, maxLimit: 5000, transcriptV2Enabled: false, }); const updates = getCommandsByKind(commands, "dispatchUpdateAgent"); const finalUpdate = updates[updates.length - 1]; if (!finalUpdate) throw new Error("Expected final update command."); const patch = finalUpdate.patch; expect(patch.outputLines).toContain("> local question"); expect(patch.outputLines).toContain("Legacy answer"); expect(patch.lastResult).toBe("Legacy answer"); expect(patch.lastAppliedHistoryRequestId).toBe("req-4"); }); it("drops stale history when transcript revision changes during fetch", async () => { const requestAgent = createAgent({ transcriptRevision: 7, outputLines: ["> local question", "assistant current"], }); const latestAgent = createAgent({ transcriptRevision: 8, outputLines: ["> local question", "assistant current"], }); let readCount = 0; const inFlight = new Set(); const commands = await runHistorySyncOperation({ client: { call: async () => ({ sessionKey: requestAgent.sessionKey, messages: [{ role: "assistant", content: "stale remote answer" }], }) as T, }, agentId: "agent-1", getAgent: () => { readCount += 1; return readCount <= 1 ? requestAgent : latestAgent; }, inFlightSessionKeys: inFlight, requestId: "req-5", loadedAt: 5_678, defaultLimit: 200, maxLimit: 5000, transcriptV2Enabled: true, }); const metrics = getCommandsByKind(commands, "logMetric"); expect(metrics).toEqual([ { kind: "logMetric", metric: "history_response_dropped_stale", meta: { reason: "transcript_revision_changed", agentId: "agent-1", requestId: "req-5", }, }, ]); const updates = getCommandsByKind(commands, "dispatchUpdateAgent"); expect(updates).toContainEqual({ kind: "dispatchUpdateAgent", agentId: "agent-1", patch: { lastHistoryRequestRevision: 7 }, }); const finalUpdate = updates[updates.length - 1]; if (!finalUpdate) throw new Error("Expected final update command."); const patch = finalUpdate.patch; expect(patch).not.toHaveProperty("outputLines"); expect(patch).not.toHaveProperty("lastAppliedHistoryRequestId"); expect(patch).not.toHaveProperty("lastResult"); expect(patch).not.toHaveProperty("latestPreview"); expect(inFlight.size).toBe(0); }); });