import { expect, test } from "bun:test" import { createRoot } from "solid-js" import { createData, type CreateDataInput } from "../src/solid" import { OpenCode, type OpenCodeEvent, type SessionInfo } from "../src/promise" test("evicts a whole family while preserving metadata and attention", async () => { const setup = fixture() try { for (const id of ["ses_parent", "ses_child", "ses_grandchild", "ses_other"]) { await setup.data.session.message.sync(id) await setup.data.session.pending.sync(id) } setup.data.session.setStatus("ses_child", "running") setup.emit({ id: "evt_permission", created: 1, type: "permission.asked", data: { id: "per_test", sessionID: "ses_child", action: "bash", resources: ["bun test"] }, }) setup.data.session.evict("ses_parent") for (const id of ["ses_parent", "ses_child", "ses_grandchild"]) { expect(setup.data.session.message.list(id)).toEqual([]) expect(setup.data.session.message.get(id, "msg_page")).toBeUndefined() expect(setup.data.session.message.more(id)).toBe(false) expect(setup.data.session.message.loading(id)).toBe(false) expect(setup.data.session.pending.list(id)).toEqual([]) expect(setup.data.session.input.list(id)).toEqual([]) expect(setup.data.session.get(id)?.id).toBe(id) } expect(setup.data.session.family("ses_parent")).toEqual(["ses_parent", "ses_child", "ses_grandchild"]) expect(setup.data.session.status("ses_child")).toBe("running") expect(setup.data.session.permission.list("ses_child")?.[0]?.id).toBe("per_test") expect(setup.data.session.message.list("ses_other")).toHaveLength(2) setup.emit({ id: "evt_agent", created: 1, type: "session.agent.selected", durable: { aggregateID: "ses_parent", seq: 1, version: 1 }, data: { sessionID: "ses_parent", agent: "build" }, }) expect(setup.data.session.message.list("ses_parent")).toMatchObject([{ type: "agent-switched" }]) await setup.data.session.message.sync("ses_parent") expect(setup.data.session.message.list("ses_parent").map((item) => item.id)).toEqual(["msg_page"]) await setup.data.session.message.sync("ses_grandchild") await setup.data.session.pending.sync("ses_grandchild") expect(setup.data.session.message.list("ses_grandchild")).toHaveLength(2) expect(setup.data.session.message.list("ses_child")).toEqual([]) } finally { setup.dispose() } }) test("evicting an in-flight read leaves the next sync invalidated", async () => { const requested = Promise.withResolvers() const release = Promise.withResolvers() let requests = 0 const setup = fixture(async () => { if (++requests !== 1) return requested.resolve() await release.promise }) try { const initial = setup.data.session.message.sync("ses_child") await requested.promise setup.data.session.evict("ses_parent") release.resolve() await initial await setup.data.session.message.sync("ses_child") expect(requests).toBe(2) expect(setup.data.session.message.list("ses_child")).toHaveLength(1) } finally { release.resolve() setup.dispose() } }) test.each(["echo", "reject"] as const)("eviction preserves optimistic submissions until %s", async (mode) => { const release = Promise.withResolvers() const setup = fixture(async (url) => { if (!url.pathname.endsWith("/prompt")) return undefined await release.promise return mode === "reject" ? Response.json({ message: "rejected" }, { status: 400 }) : Response.json({ id: "msg_local" }) }) try { const request = setup.data.session.prompt({ sessionID: "ses_child", id: "msg_local", text: "local" }) const settled = Promise.allSettled([request]) setup.data.session.evict("ses_parent") expect(setup.data.session.message.list("ses_child")).toMatchObject([{ id: "msg_local", text: "local" }]) if (mode === "echo") setup.emit(enqueued("ses_child", "msg_local")) release.resolve() await settled expect(setup.data.session.message.list("ses_child")).toHaveLength(mode === "echo" ? 1 : 0) expect(setup.data.session.pending.list("ses_child")).toHaveLength(mode === "echo" ? 1 : 0) } finally { release.resolve() setup.dispose() } }) function info(id: string, parentID?: string): SessionInfo { return { id, parentID, projectID: "project", cost: 0, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, time: { created: 0, updated: 0 }, location: { directory: "/project" }, } } function enqueued(sessionID: string, inboxID = "msg_pending"): OpenCodeEvent { return { id: `evt_${inboxID}`, created: 1, type: "session.inbox.enqueued", durable: { aggregateID: sessionID, seq: 1, version: 1 }, data: { sessionID, inboxID, item: { type: "user", delivery: "steer", payload: { text: "pending" } } }, } } function fixture(read?: (url: URL) => Promise) { const listeners = new Set[0]>() const api = OpenCode.make({ baseUrl: "http://opencode.local", fetch: async (input) => { const url = new URL(input instanceof Request ? input.url : String(input)) const response = await read?.(url) if (response) return response const sessionID = url.pathname.split("/")[3] if (url.pathname.endsWith("/inbox")) return Response.json({ data: [ { id: "msg_pending", sessionID, type: "user", delivery: "steer", payload: { text: "pending" }, time: { created: 1 }, }, ], }) return Response.json({ data: [{ id: "msg_page", type: "user", text: "page", time: { created: 0 } }], cursor: url.searchParams.has("cursor") ? {} : { next: "older" }, }) }, }) return createRoot((dispose) => { const data = createData({ api: () => api, directory: "/project", event: { on: () => () => {}, listen(handler) { listeners.add(handler) return () => listeners.delete(handler) }, }, }) data.session.remember(info("ses_parent")) data.session.remember(info("ses_child", "ses_parent")) data.session.remember(info("ses_grandchild", "ses_child")) data.session.remember(info("ses_other")) return { data, dispose, emit: (details: OpenCodeEvent) => listeners.forEach((listener) => listener({ name: details.type, details })), } }) }