| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262 |
- /** @jsxImportSource @opentui/solid */
- import { expect, test } from "bun:test"
- import type { GlobalEvent } from "@kirincode-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/kirincode/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 () => {
- await using tmp = await tmpdir()
- await Bun.write(`${tmp.path}/kv.json`, "{}")
- let resolveMessages!: (response: Response) => void
- const messages = new Promise<Response>((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
- }, tmp.path)
- 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()
- }
- })
- test("orphan live deltas do not suppress hydrated parts", async () => {
- await using tmp = await tmpdir()
- await Bun.write(`${tmp.path}/kv.json`, "{}")
- let resolveMessages!: (response: Response) => void
- const messages = new Promise<Response>((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
- }, tmp.path)
- 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()
- }
- })
- test("hydration does not clear text streamed before it starts", async () => {
- await using tmp = await tmpdir()
- await Bun.write(`${tmp.path}/kv.json`, "{}")
- let resolveMessages!: (response: Response) => void
- const messages = new Promise<Response>((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
- }, tmp.path)
- 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()
- }
- })
- test("live messages merged during hydration retain the 100 message window", async () => {
- await using tmp = await tmpdir()
- await Bun.write(`${tmp.path}/kv.json`, "{}")
- let resolveMessages!: (response: Response) => void
- const messages = new Promise<Response>((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
- }, tmp.path)
- 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()
- }
- })
- test("a message removed during hydration does not regain stale parts", async () => {
- await using tmp = await tmpdir()
- await Bun.write(`${tmp.path}/kv.json`, "{}")
- let resolveMessages!: (response: Response) => void
- const messages = new Promise<Response>((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
- }, tmp.path)
- 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()
- }
- })
|