Files
openchamber/packages/ui/src/sync/__tests__/event-reducer.test.ts
T

349 lines
11 KiB
TypeScript
Raw Normal View History

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> = {}): 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<SessionStatus, { type: "retry" }>).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([])
})
})