/** @jsxImportSource @opentui/solid */ import { expect, test } from "bun:test" import { Global } from "@opencode-ai/core/global" import type { GlobalEvent } from "@opencode-ai/sdk/v2" import { tmpdir } from "../../../fixture/fixture" import { json, mount, wait } from "./sync-fixture" const sessionID = "ses_hydration_race" const messageID = "msg_hydration_race" const partID = "prt_hydration_race" const session = { id: sessionID, title: "race", time: { created: 0, updated: 0 }, version: "1.15.13", directory: "/tmp/opencode/packages/opencode", } const assistant = { id: messageID, sessionID, role: "assistant" as const, agent: "build", modelID: "model", providerID: "test", mode: "build", parentID: "msg_user", path: { cwd: session.directory, root: session.directory }, cost: 0, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, time: { created: 1, completed: 2 }, } function global(payload: GlobalEvent["payload"]): GlobalEvent { return { directory: "/tmp/other", project: "proj_test", payload } } test("stale session hydration does not overwrite live message parts", async () => { const previous = Global.Path.state await using tmp = await tmpdir() Global.Path.state = tmp.path await Bun.write(`${tmp.path}/kv.json`, "{}") let resolveMessages!: (response: Response) => void const messages = new Promise((resolve) => { resolveMessages = resolve }) let requested = false const { app, emit, sync } = await mount((url) => { if (url.pathname === `/session/${sessionID}`) return json(session) if (url.pathname === `/session/${sessionID}/message`) { requested = true return messages } if (url.pathname === `/session/${sessionID}/todo` || url.pathname === `/session/${sessionID}/diff`) return json([]) return undefined }) try { const hydrate = sync.session.sync(sessionID) await wait(() => requested) emit(global({ id: "evt_message", type: "message.updated", properties: { sessionID, info: assistant } })) emit( global({ id: "evt_part", type: "message.part.updated", properties: { sessionID, time: 2, part: { id: partID, sessionID, messageID, type: "text", text: "visible live content" }, }, }), ) await wait(() => sync.data.part[messageID]?.[0]?.type === "text") resolveMessages( json([ { info: assistant, parts: [{ id: partID, sessionID, messageID, type: "text", text: "" }], }, ]), ) await hydrate expect(sync.data.part[messageID][0]).toMatchObject({ text: "visible live content" }) } finally { app.renderer.destroy() Global.Path.state = previous } }) test("orphan live deltas do not suppress hydrated parts", async () => { const previous = Global.Path.state await using tmp = await tmpdir() Global.Path.state = tmp.path await Bun.write(`${tmp.path}/kv.json`, "{}") let resolveMessages!: (response: Response) => void const messages = new Promise((resolve) => { resolveMessages = resolve }) let requested = false const { app, emit, sync } = await mount((url) => { if (url.pathname === `/session/${sessionID}`) return json(session) if (url.pathname === `/session/${sessionID}/message`) { requested = true return messages } if (url.pathname === `/session/${sessionID}/todo` || url.pathname === `/session/${sessionID}/diff`) return json([]) return undefined }) try { const hydrate = sync.session.sync(sessionID) await wait(() => requested) emit( global({ id: "evt_delta", type: "message.part.delta", properties: { sessionID, messageID, partID, field: "text", delta: "ignored until part exists" }, }), ) resolveMessages( json([{ info: assistant, parts: [{ id: partID, sessionID, messageID, type: "text", text: "hydrated" }] }]), ) await hydrate expect(sync.data.part[messageID][0]).toMatchObject({ text: "hydrated" }) } finally { app.renderer.destroy() Global.Path.state = previous } }) test("hydration does not clear text streamed before it starts", async () => { const previous = Global.Path.state await using tmp = await tmpdir() Global.Path.state = tmp.path await Bun.write(`${tmp.path}/kv.json`, "{}") let resolveMessages!: (response: Response) => void const messages = new Promise((resolve) => { resolveMessages = resolve }) let requested = false const { app, emit, sync } = await mount((url) => { if (url.pathname === `/session/${sessionID}`) return json(session) if (url.pathname === `/session/${sessionID}/message`) { requested = true return messages } if (url.pathname === `/session/${sessionID}/todo` || url.pathname === `/session/${sessionID}/diff`) return json([]) return undefined }) try { emit(global({ id: "evt_message", type: "message.updated", properties: { sessionID, info: assistant } })) emit( global({ id: "evt_part", type: "message.part.updated", properties: { sessionID, time: 1, part: { id: partID, sessionID, messageID, type: "text", text: "" }, }, }), ) emit( global({ id: "evt_delta", type: "message.part.delta", properties: { sessionID, messageID, partID, field: "text", delta: "visible streamed content" }, }), ) await wait(() => sync.data.part[messageID]?.[0]?.type === "text" && sync.data.part[messageID][0].text !== "") const hydrate = sync.session.sync(sessionID) await wait(() => requested) resolveMessages(json([{ info: assistant, parts: [{ id: partID, sessionID, messageID, type: "text", text: "" }] }])) await hydrate expect(sync.data.part[messageID][0]).toMatchObject({ text: "visible streamed content" }) } finally { app.renderer.destroy() Global.Path.state = previous } }) test("live messages merged during hydration retain the 100 message window", async () => { const previous = Global.Path.state await using tmp = await tmpdir() Global.Path.state = tmp.path await Bun.write(`${tmp.path}/kv.json`, "{}") let resolveMessages!: (response: Response) => void const messages = new Promise((resolve) => { resolveMessages = resolve }) let requested = false const { app, emit, sync } = await mount((url) => { if (url.pathname === `/session/${sessionID}`) return json(session) if (url.pathname === `/session/${sessionID}/message`) { requested = true return messages } if (url.pathname === `/session/${sessionID}/todo` || url.pathname === `/session/${sessionID}/diff`) return json([]) return undefined }) try { const hydrate = sync.session.sync(sessionID) await wait(() => requested) const live = { ...assistant, id: "msg_z_live" } emit(global({ id: "evt_live", type: "message.updated", properties: { sessionID, info: live } })) await wait(() => sync.data.message[sessionID]?.some((message) => message.id === live.id) ?? false) resolveMessages( json( Array.from({ length: 100 }, (_, index) => { const id = `msg_${String(index).padStart(3, "0")}` return { info: { ...assistant, id }, parts: [{ id: `prt_${id}`, sessionID, messageID: id, type: "text", text: id }], } }), ), ) await hydrate expect(sync.data.message[sessionID]).toHaveLength(100) expect(sync.data.message[sessionID].at(-1)?.id).toBe(live.id) expect(sync.data.message[sessionID].some((message) => message.id === "msg_000")).toBe(false) expect(sync.data.part.msg_000).toBeUndefined() } finally { app.renderer.destroy() Global.Path.state = previous } }) test("a message removed during hydration does not regain stale parts", async () => { const previous = Global.Path.state await using tmp = await tmpdir() Global.Path.state = tmp.path await Bun.write(`${tmp.path}/kv.json`, "{}") let resolveMessages!: (response: Response) => void const messages = new Promise((resolve) => { resolveMessages = resolve }) let requested = false const { app, emit, sync } = await mount((url) => { if (url.pathname === `/session/${sessionID}`) return json(session) if (url.pathname === `/session/${sessionID}/message`) { requested = true return messages } if (url.pathname === `/session/${sessionID}/todo` || url.pathname === `/session/${sessionID}/diff`) return json([]) return undefined }) try { emit(global({ id: "evt_message", type: "message.updated", properties: { sessionID, info: assistant } })) await wait(() => sync.data.message[sessionID]?.length === 1) const hydrate = sync.session.sync(sessionID) await wait(() => requested) emit(global({ id: "evt_removed", type: "message.removed", properties: { sessionID, messageID } })) await wait(() => sync.data.message[sessionID]?.length === 0) resolveMessages( json([{ info: assistant, parts: [{ id: partID, sessionID, messageID, type: "text", text: "stale" }] }]), ) await hydrate expect(sync.data.message[sessionID]).toEqual([]) expect(sync.data.part[messageID]).toBeUndefined() } finally { app.renderer.destroy() Global.Path.state = previous } })