import { describe, expect, test } from "bun:test" import type { Session } from "@opencode-ai/sdk/v2" import type { Event, Message, Part, PermissionRequest, QuestionRequest, SessionStatus } from "@opencode-ai/sdk/v2/client" import { applyDirectoryEvent } from "../event-reducer" import { INITIAL_STATE, type State } from "../types" function state(overrides: Partial = {}): State { return { ...INITIAL_STATE, message: {}, part: {}, session_status: {}, ...overrides, } } function deltaEvent(): Event { return { type: "message.part.delta", properties: { messageID: "msg_1", partID: "prt_1", field: "text", delta: "hello", }, } as Event } function partUpdatedEvent(): Event { return { type: "message.part.updated", properties: { part: { id: "prt_1", messageID: "msg_1", sessionID: "ses_1", type: "text", text: "hello", }, }, } as Event } function topLevelSessionOnlyPartUpdatedEvent(): Event { return { type: "message.part.updated", properties: { sessionID: "ses_1", part: { id: "prt_1", messageID: "msg_1", type: "text", text: "hello", }, }, } as Event } function buildSession(title: string, time: Session["time"]): Session { return { id: "ses_1", title, time, } as Session } describe("applyDirectoryEvent", () => { test("inserts post-rollover message events by creation time rather than ID", () => { const legacy = { id: "msg_ffffffffffffLegacy", sessionID: "ses_1", role: "user", time: { created: 100 }, } as Message const current = { id: "msg_000000000000Current", sessionID: "ses_1", role: "assistant", time: { created: 200 }, } as Message const draft = state({ message: { ses_1: [legacy] } }) expect(applyDirectoryEvent(draft, { type: "message.updated", properties: { info: current }, } as Event)).toBe(true) expect(draft.message.ses_1).toEqual([legacy, current]) }) test("preserves part event order across the part ID rollover", () => { const legacyPart = { id: "prt_ffffffffffffLegacy", messageID: "msg_1", sessionID: "ses_1", type: "text", text: "legacy", } as Part const currentPart = { id: "prt_000000000000Current", messageID: "msg_1", sessionID: "ses_1", type: "text", text: "current", } as Part const draft = state({ message: { ses_1: [{ id: "msg_1", sessionID: "ses_1", role: "assistant", time: { created: 1 } } as Message] }, part: { msg_1: [legacyPart] }, }) expect(applyDirectoryEvent(draft, { type: "message.part.updated", properties: { part: currentPart }, } as Event)).toBe(true) expect(draft.part.msg_1).toEqual([legacyPart, currentPart]) }) test("replaces an optimistic user part in place instead of appending it", () => { const optimisticText = { id: "prt_optimistic_text", messageID: "msg_1", type: "text", text: "hi" } as Part const optimisticFile = { id: "prt_optimistic_file", messageID: "msg_1", type: "file", filename: "a.png" } as Part const serverText = { id: "prt_server_text", messageID: "msg_1", sessionID: "ses_1", type: "text", text: "hi" } as Part const draft = state({ message: { ses_1: [{ id: "msg_1", sessionID: "ses_1", role: "user", time: { created: 1 } } as Message] }, part: { msg_1: [optimisticText, optimisticFile] }, }) expect(applyDirectoryEvent(draft, { type: "message.part.updated", properties: { part: serverText }, } as Event)).toBe(true) expect(draft.part.msg_1).toEqual([serverText, optimisticFile]) }) test("returns typed materialization when delta arrives before parts", () => { const result = applyDirectoryEvent(state(), deltaEvent()) expect(result).toEqual({ changed: false, materialization: { type: "incomplete-session-snapshot", reason: "orphan-delta", messageID: "msg_1", partID: "prt_1" }, }) }) test("returns typed materialization when delta part is missing", () => { const result = applyDirectoryEvent( state({ part: { msg_1: [{ id: "prt_2", messageID: "msg_1", type: "text", text: "" } as Part] } }), deltaEvent(), ) expect(result).toEqual({ changed: false, materialization: { type: "incomplete-session-snapshot", reason: "missing-delta-part", messageID: "msg_1", partID: "prt_1" }, }) }) test("applies part update and requests materialization when owning message is absent", () => { const draft = state() const result = applyDirectoryEvent(draft, partUpdatedEvent()) expect(draft.part.msg_1.map((item) => item.id)).toEqual(["prt_1"]) expect(result).toEqual({ changed: true, materialization: { type: "incomplete-session-snapshot", reason: "missing-owning-message", sessionID: "ses_1", messageID: "msg_1", partID: "prt_1", }, }) }) test("uses top-level session id and part message id for part update materialization", () => { const draft = state() const result = applyDirectoryEvent(draft, topLevelSessionOnlyPartUpdatedEvent()) expect(draft.part.msg_1.map((item) => item.id)).toEqual(["prt_1"]) expect(result).toEqual({ changed: true, materialization: { type: "incomplete-session-snapshot", reason: "missing-owning-message", sessionID: "ses_1", messageID: "msg_1", partID: "prt_1", }, }) }) test("uses top-level session id for delta materialization", () => { const result = applyDirectoryEvent(state(), { type: "message.part.delta", properties: { sessionID: "ses_1", messageID: "msg_1", partID: "prt_1", field: "text", delta: "hello", }, } as Event) expect(result).toEqual({ changed: false, materialization: { type: "incomplete-session-snapshot", reason: "orphan-delta", sessionID: "ses_1", messageID: "msg_1", partID: "prt_1" }, }) }) test("skips stale session.updated events so a newer title survives", () => { const draft = state({ session: [buildSession("New Title", { created: 1, updated: 20 })] }) const result = applyDirectoryEvent(draft, { type: "session.updated", properties: { info: buildSession("Old Title", { created: 1, updated: 10 }), }, } as Event) expect(result).toBe(false) expect(draft.session[0]?.title).toBe("New Title") }) test("applies part update without materialization when owning message exists", () => { const draft = state({ message: { ses_1: [{ id: "msg_1", sessionID: "ses_1", role: "assistant", time: { created: 1 } } as never] }, }) const result = applyDirectoryEvent(draft, partUpdatedEvent()) expect(draft.part.msg_1.map((item) => item.id)).toEqual(["prt_1"]) expect(result).toBe(true) }) test("skips duplicate session status events", () => { const draft = state() const busyStatus = { type: "busy" } as SessionStatus const event = { type: "session.status", properties: { sessionID: "ses_1", status: busyStatus }, } as Event expect(applyDirectoryEvent(draft, event)).toBe(true) const statusRef = draft.session_status.ses_1 expect(applyDirectoryEvent(draft, event)).toBe(false) expect(draft.session_status.ses_1).toBe(statusRef) }) test("skips duplicate session idle events", () => { const draft = state() const event = { type: "session.idle", properties: { sessionID: "ses_1" }, } as Event expect(applyDirectoryEvent(draft, event)).toBe(true) const statusRef = draft.session_status.ses_1 expect(applyDirectoryEvent(draft, event)).toBe(false) expect(draft.session_status.ses_1).toBe(statusRef) }) test("skips duplicate session error idle-state events", () => { const draft = state() const event = { type: "session.error", properties: { sessionID: "ses_1" }, } as Event expect(applyDirectoryEvent(draft, event)).toBe(true) const statusRef = draft.session_status.ses_1 expect(applyDirectoryEvent(draft, event)).toBe(false) expect(draft.session_status.ses_1).toBe(statusRef) }) test("detects retry status metadata changes", () => { const draft = state({ session_status: { ses_1: { type: "retry", attempt: 1, message: "rate limited", next: 10 } as SessionStatus, }, }) const event = { type: "session.status", properties: { sessionID: "ses_1", status: { type: "retry", attempt: 2, message: "rate limited", next: 20 } as SessionStatus, }, } as Event expect(applyDirectoryEvent(draft, event)).toBe(true) expect((draft.session_status.ses_1 as Extract).attempt).toBe(2) }) test("updates permission request arrays immutably", () => { const initialPermissions = [ { id: "perm_1", sessionID: "ses_1" } as PermissionRequest, ] const draft = state({ permission: { ses_1: initialPermissions } }) applyDirectoryEvent(draft, { type: "permission.asked", properties: { id: "perm_2", sessionID: "ses_1" } as PermissionRequest, } as Event) expect(draft.permission.ses_1).not.toBe(initialPermissions) expect(draft.permission.ses_1.map((item) => item.id)).toEqual(["perm_1", "perm_2"]) const afterAsk = draft.permission.ses_1 applyDirectoryEvent(draft, { type: "permission.replied", properties: { sessionID: "ses_1", requestID: "perm_1" }, } as Event) expect(draft.permission.ses_1).not.toBe(afterAsk) expect(draft.permission.ses_1.map((item) => item.id)).toEqual(["perm_2"]) }) test("updates question request arrays immutably", () => { const initialQuestions = [ { id: "ques_1", sessionID: "ses_1" } as QuestionRequest, ] const draft = state({ question: { ses_1: initialQuestions } }) applyDirectoryEvent(draft, { type: "question.asked", properties: { id: "ques_2", sessionID: "ses_1" } as QuestionRequest, } as Event) expect(draft.question.ses_1).not.toBe(initialQuestions) expect(draft.question.ses_1.map((item) => item.id)).toEqual(["ques_1", "ques_2"]) const afterAsk = draft.question.ses_1 applyDirectoryEvent(draft, { type: "question.replied", properties: { sessionID: "ses_1", requestID: "ques_1" }, } as Event) expect(draft.question.ses_1).not.toBe(afterAsk) expect(draft.question.ses_1.map((item) => item.id)).toEqual(["ques_2"]) const afterReply = draft.question.ses_1 applyDirectoryEvent(draft, { type: "question.rejected", properties: { sessionID: "ses_1", requestID: "ques_2" }, } as Event) expect(draft.question.ses_1).not.toBe(afterReply) expect(draft.question.ses_1).toEqual([]) }) })