Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
TypeScript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127import { expect, test } from "bun:test"import { getEventListeners } from "node:events"import { createRoot } from "solid-js"import { createData, type CreateDataInput } from "../src/solid"import { OpenCode, type ModelInfo, type OpenCodeEvent, type Project, type SessionInfo } from "../src/promise"
const session = (viewed: number): SessionInfo => ({ id: "ses_refresh", projectID: "project", cost: 0, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, outcome: "succeeded", time: { created: 0, updated: 0, idle: 2, viewed }, location: { directory: "/project" },})
test("uses the configured initial window and retains normal cursor page sizes", async () => { const requests: { limit: string | null; cursor: string | null }[] = [] const api = OpenCode.make({ baseUrl: "http://opencode.local", fetch: async (input, init) => { const url = new URL((input instanceof Request ? input : new Request(input, init)).url) const cursor = url.searchParams.get("cursor") requests.push({ limit: url.searchParams.get("limit"), cursor }) return Response.json({ data: [{ id: cursor ? "msg_1" : "msg_2", type: "user", text: "History", time: { created: cursor ? 1 : 2 } }], cursor: cursor ? {} : { next: "older" }, }) }, }) const setup = createRoot((dispose) => ({ data: createData({ api: () => api, directory: "/project", initialMessageLimit: () => 40, event: { on: () => () => {}, listen: () => () => {} }, }), dispose, })) try { await setup.data.session.message.sync("ses_refresh") await setup.data.session.message.sync("ses_refresh") expect(requests).toEqual([{ limit: "40", cursor: null }]) await setup.data.session.message.loadMore("ses_refresh") expect(requests).toEqual([ { limit: "40", cursor: null }, { limit: "20", cursor: "older" }, ]) expect(setup.data.session.message.list("ses_refresh").map((message) => message.id)).toEqual(["msg_1", "msg_2"]) } finally { setup.dispose() }})
test("reconciles a stale running tool when execution settles", async () => { const listeners = new Set<Parameters<CreateDataInput["event"]["listen"]>[0]>() let completed = false let requests = 0 const api = OpenCode.make({ baseUrl: "http://opencode.local", fetch: async () => { requests++ return Response.json({ data: [ { id: "msg_assistant", type: "assistant", agent: "build", model: { providerID: "provider", id: "model" }, time: { created: 1, ...(completed ? { completed: 2 } : {}) }, content: [ { type: "tool", id: "call_execute", name: "execute", time: { created: 1, ran: 1, ...(completed ? { completed: 2 } : {}) }, state: completed ? { status: "completed", input: {}, metadata: {}, content: [] } : { status: "running", input: {}, metadata: {} }, }, ], }, ], cursor: {}, }) }, }) const setup = createRoot((dispose) => ({ data: createData({ api: () => api, directory: "/project", event: { on: () => () => {}, listen(handler) { listeners.add(handler) return () => listeners.delete(handler) }, }, connection: { status: () => "connected" }, }), dispose, })) try { await setup.data.session.message.sync("ses_refresh") completed = true const interrupted: OpenCodeEvent = { id: "evt_interrupted", created: 3, type: "session.execution.interrupted", durable: { aggregateID: "ses_refresh", seq: 1, version: 1 }, data: { sessionID: "ses_refresh", reason: "user" }, } listeners.forEach((listener) => listener({ name: interrupted.type, details: interrupted }))
await wait( () => setup.data.session.message.get("ses_refresh", "msg_assistant")?.content[0]?.type === "tool" && setup.data.session.message.get("ses_refresh", "msg_assistant")?.content[0]?.state.status === "completed", ) expect(requests).toBe(2) expect(setup.data.session.message.get("ses_refresh", "msg_assistant")?.content[0]).toMatchObject({ state: { status: "completed" }, }) } finally { setup.dispose() }})
test("revalidates after an event overtakes an active session read", async () => { let release!: () => void const gate = new Promise<void>((resolve) => (release = resolve)) const listeners = new Set<Parameters<CreateDataInput["event"]["listen"]>[0]>() let requests = 0 const api = OpenCode.make({ baseUrl: "http://opencode.local", fetch: async (input, init) => { const request = input instanceof Request ? input : new Request(input, init) if (!request.url.endsWith("/api/session/ses_refresh")) throw new Error(`Unexpected request: ${request.url}`) requests++ if (requests === 1) { await gate return Response.json({ data: session(1) }) } return Response.json({ data: session(2) }) }, }) const event: CreateDataInput["event"] = { on: <Type extends OpenCodeEvent["type"]>( _type: Type, _handler: (event: Extract<OpenCodeEvent, { type: Type }>) => void, ) => () => {}, listen(handler) { listeners.add(handler) return () => listeners.delete(handler) }, } const setup = createRoot((dispose) => ({ data: createData({ api: () => api, directory: "/project", event, connection: { status: () => "connected" } }), dispose, }))
try { setup.data.session.remember(session(1)) setup.data.session.invalidate("ses_refresh") const initial = setup.data.session.sync("ses_refresh") await wait(() => requests === 1)
const viewed: OpenCodeEvent = { id: "evt_viewed", created: 2, type: "session.viewed", durable: { aggregateID: "ses_refresh", seq: 1, version: 1 }, data: { sessionID: "ses_refresh", idle: 2 }, } listeners.forEach((listener) => listener({ name: viewed.type, details: viewed })) await Bun.sleep(20) release() await initial
await wait(() => requests === 2 && setup.data.session.get("ses_refresh")?.time.viewed === 2) } finally { setup.dispose() }})
test("preserves a live session rename across concurrent session and family reads", async () => { const family = Promise.withResolvers<void>() const renamed = Promise.withResolvers<void>() const listeners = new Set<Parameters<CreateDataInput["event"]["listen"]>[0]>() let requests = 0 const stale = { ...session(0), title: undefined } const api = OpenCode.make({ baseUrl: "http://opencode.local", fetch: async (input, init) => { const request = input instanceof Request ? input : new Request(input, init) if (!request.url.endsWith(`/api/session/${stale.id}`)) return Response.json({ data: [], cursor: {} }) requests++ await (requests === 1 ? family.promise : renamed.promise) return Response.json({ data: stale }) }, }) const setup = createRoot((dispose) => ({ data: createData({ api: () => api, directory: "/project", event: { on: () => () => {}, listen(handler) { listeners.add(handler) return () => listeners.delete(handler) }, }, }), dispose, }))
try { const initial = setup.data.session.sync(stale.id, { children: true }) await wait(() => requests === 1) const event: OpenCodeEvent = { id: "evt_renamed", created: 1, type: "session.renamed", durable: { aggregateID: stale.id, seq: 1, version: 1 }, data: { sessionID: stale.id, title: "Generated title" }, } listeners.forEach((listener) => listener({ name: event.type, details: event })) await wait(() => requests === 2) renamed.resolve() await wait(() => setup.data.session.get(stale.id) !== undefined) family.resolve() await initial await Bun.sleep(0)
expect(setup.data.session.get(stale.id)?.title).toBe("Generated title") } finally { family.resolve() renamed.resolve() setup.dispose() }})
test("updates authoritative cached project metadata from live events", async () => { const listeners = new Set<Parameters<CreateDataInput["event"]["listen"]>[0]>() const original: Project = { id: "project_renamed", canonical: "/projects/original", name: "Original custom name", time: { created: 1, updated: 1 }, sandboxes: [], } const unrelated: Project = { id: "project_unrelated", canonical: "/projects/unrelated", name: "Unrelated project", time: { created: 1, updated: 1 }, sandboxes: [], } let requests = 0 const api = OpenCode.make({ baseUrl: "http://opencode.local", fetch: async (input, init) => { const request = input instanceof Request ? input : new Request(input, init) if (!request.url.endsWith("/api/project")) throw new Error(`Unexpected request: ${request.url}`) requests++ return Response.json([original, unrelated]) }, }) const event: CreateDataInput["event"] = { on: () => () => {}, listen(handler) { listeners.add(handler) return () => listeners.delete(handler) }, } const setup = createRoot((dispose) => ({ data: createData({ api: () => api, directory: "/projects/original", event }), dispose, }))
try { await setup.data.project.sync() expect(setup.data.project.get(original.id)).toEqual(original)
const updated: OpenCodeEvent = { id: "evt_project_renamed", created: 2, type: "project.updated", data: { ...original, canonical: "/projects/renamed", name: "Updated custom name", time: { ...original.time, updated: 2 }, }, } listeners.forEach((listener) => listener({ name: updated.type, details: updated }))
expect(setup.data.project.get(original.id)?.canonical).toBe("/projects/renamed") expect(setup.data.project.get(original.id)?.name).toBe("Updated custom name") expect(setup.data.project.get(unrelated.id)).toEqual(unrelated) expect(requests).toBe(1)
const reset: OpenCodeEvent = { id: "evt_project_name_reset", created: 3, type: "project.updated", data: { id: original.id, canonical: "/projects/renamed-again", time: { ...original.time, updated: 3 }, sandboxes: [], }, } listeners.forEach((listener) => listener({ name: reset.type, details: reset }))
expect(setup.data.project.get(original.id)?.canonical).toBe("/projects/renamed-again") expect(setup.data.project.get(original.id)?.name).toBeUndefined() expect(setup.data.project.get(unrelated.id)).toEqual(unrelated) expect(requests).toBe(1) } finally { setup.dispose() }})
test("adopts cached directory-project sessions when their repository is resolved", async () => { const listeners = new Set<Parameters<CreateDataInput["event"]["listen"]>[0]>() const refreshed: SessionInfo = { ...session(0), id: "ses_uncached", projectID: "repository", location: { directory: "/unknown-alias" }, subpath: "app", } const api = OpenCode.make({ baseUrl: "http://opencode.local", fetch: async (input, init) => { const request = input instanceof Request ? input : new Request(input, init) if (!request.url.endsWith("/api/session/ses_uncached")) throw new Error(`Unexpected request: ${request.url}`) return Response.json({ data: refreshed }) }, }) const setup = createRoot((dispose) => ({ data: createData({ api: () => api, directory: "/repo", event: { on: () => () => {}, listen(handler) { listeners.add(handler) return () => listeners.delete(handler) }, }, }), dispose, }))
try { const sessions: SessionInfo[] = [ { ...session(0), id: "ses_root", projectID: "directory-root", location: { directory: "/repo" } }, { ...session(0), id: "ses_nested", projectID: "directory-nested", location: { directory: "/repo/app" } }, { ...session(0), id: "ses_alias", projectID: "directory-nested", location: { directory: "/repo/alias/../app" }, }, { ...session(0), id: "ses_symlink", projectID: "directory-nested", location: { directory: "/shortcut" } }, { ...refreshed, projectID: "directory-uncached" }, { ...session(0), id: "ses_global", projectID: "global", location: { directory: "/repo/legacy" } }, { ...session(0), id: "ses_escaped", projectID: "global", location: { directory: "/repo/../other" } }, { ...session(0), id: "ses_other", projectID: "other-repository", location: { directory: "/repo/vendor" } }, { ...session(0), id: "ses_sibling", projectID: "global", location: { directory: "/repo-other" } }, { ...session(0), id: "ses_remote", projectID: "directory-root", location: { directory: "/repo" }, }, ] sessions.forEach((item) => setup.data.session.remember(item)) for (const project of [ { id: "directory-root", canonical: "/repo" }, { id: "directory-nested", canonical: "/repo/app" }, ]) { const updated: OpenCodeEvent = { id: `evt_${project.id}`, created: 0, type: "project.updated", data: { ...project, time: { created: 0, updated: 0 }, sandboxes: [] }, } listeners.forEach((listener) => listener({ name: updated.type, details: updated })) }
const resolved: OpenCodeEvent = { id: "evt_repository_resolved", created: 1, type: "worktree.resolved", durable: { aggregateID: "repository", seq: 0, version: 1 }, data: { projectID: "repository", directory: "/repo", previous: "global", adopted: ["directory-root", "directory-nested", "directory-uncached"], }, } listeners.forEach((listener) => listener({ name: resolved.type, details: resolved }))
expect(setup.data.session.get("ses_root")?.projectID).toBe("repository") expect(setup.data.session.get("ses_root")?.subpath).toBeUndefined() expect(setup.data.session.get("ses_nested")).toMatchObject({ projectID: "repository", subpath: "app" }) expect(setup.data.session.get("ses_alias")).toMatchObject({ projectID: "repository", subpath: "app" }) expect(setup.data.session.get("ses_symlink")).toMatchObject({ projectID: "repository", subpath: "app" }) expect(setup.data.session.get("ses_global")).toMatchObject({ projectID: "repository", subpath: "legacy" }) expect(setup.data.session.get("ses_escaped")?.projectID).toBe("global") expect(setup.data.session.get("ses_other")?.projectID).toBe("other-repository") expect(setup.data.session.get("ses_sibling")?.projectID).toBe("global") expect(setup.data.session.get("ses_remote")?.projectID).toBe("repository") await wait(() => setup.data.session.get("ses_uncached")?.projectID === "repository") expect(setup.data.session.get("ses_uncached")?.subpath).toBe("app") } finally { setup.dispose() }})
test("refreshes global credential events across every loaded location", async () => { const listeners = new Set<Parameters<CreateDataInput["event"]["listen"]>[0]>() const requests: URL[] = [] const api = OpenCode.make({ baseUrl: "http://opencode.local", fetch: async (input, init) => { const request = input instanceof Request ? input : new Request(input, init) const url = new URL(request.url) requests.push(url) const directory = url.searchParams.get("location[directory]") ?? "/project" return Response.json({ location: { directory, project: { id: "project", directory, canonical: directory }, }, data: [], }) }, }) const setup = createRoot((dispose) => ({ data: createData({ api: () => api, directory: "/project", event: { on: () => () => {}, listen(handler) { listeners.add(handler) return () => listeners.delete(handler) }, }, connection: { status: () => "connected" }, }), dispose, })) const locations = [{ directory: "/project" }, { directory: "/other" }]
try { await Promise.all( locations.flatMap((location) => [ setup.data.location.integration.sync(location), setup.data.location.model.sync(location), setup.data.location.provider.sync(location), setup.data.location.reference.sync(location), ]), ) const references = locations.map((location) => setup.data.location.reference.list(location)) requests.length = 0
const updated: OpenCodeEvent = { id: "evt_credential.updated", created: 1, type: "credential.updated", data: {}, } listeners.forEach((listener) => listener({ name: updated.type, details: updated })) await wait(() => requests.length === 2) expect(requests.map((url) => [url.pathname, url.searchParams.get("location[directory]")])).toEqual([ ["/api/integration", "/project"], ["/api/integration", "/other"], ]) requests.length = 0
for (const credentialID of ["credential", null]) { const switched: OpenCodeEvent = { id: `evt_credential.switched.${credentialID}`, created: 2, type: "credential.switched", data: { credentialID, integrationID: "integration" }, } listeners.forEach((listener) => listener({ name: switched.type, details: switched })) await wait(() => requests.length === 4) expect(requests.map((url) => [url.pathname, url.searchParams.get("location[directory]")])).toEqual( expect.arrayContaining([ ["/api/model", "/project"], ["/api/provider", "/project"], ["/api/model", "/other"], ["/api/provider", "/other"], ]), ) locations.forEach((location, index) => expect(setup.data.location.reference.list(location)).toBe(references[index]), ) requests.length = 0 } } finally { setup.dispose() }})
for (const resource of ["provider", "model"] as const) { test(`refreshes ${resource} values only at the location named by ${resource}.updated`, async () => { const listeners = new Set<Parameters<CreateDataInput["event"]["listen"]>[0]>() const requests: URL[] = [] const current = { name: "Before" } const api = OpenCode.make({ baseUrl: "http://opencode.local", fetch: async (input, init) => { const request = input instanceof Request ? input : new Request(input, init) const url = new URL(request.url) requests.push(url) const directory = url.searchParams.get("location[directory]") ?? "/project" if (url.pathname !== "/api/provider" && url.pathname !== "/api/model") throw new Error(`Unexpected request: ${request.url}`) return Response.json({ location: { directory, project: { id: directory, directory, canonical: directory } }, data: url.pathname === "/api/provider" ? [{ id: "company", name: current.name, activation: "enabled", package: "aisdk:@ai-sdk/openai" }] : [modelInfo(current.name)], }) }, }) const setup = createRoot((dispose) => ({ data: createData({ api: () => api, directory: "/project", event: { on: () => () => {}, listen(handler) { listeners.add(handler) return () => listeners.delete(handler) }, }, connection: { status: () => "connected" }, }), dispose, })) const project = { directory: "/project" } const other = { directory: "/other" } const unaffected = resource === "provider" ? "model" : "provider"
try { await Promise.all( [project, other].flatMap((location) => [ setup.data.location.provider.sync(location), setup.data.location.model.sync(location), ]), ) const untouchedResource = setup.data.location[unaffected].list(project) const untouchedLocation = setup.data.location[resource].list(other) requests.length = 0 current.name = "After" const event: OpenCodeEvent = { id: `evt_${resource}_updated`, created: 1, type: `${resource}.updated`, location: project, data: {}, } listeners.forEach((listener) => listener({ name: event.type, details: event }))
await wait(() => setup.data.location[resource].list(project)?.[0]?.name === "After") expect(requests.map((url) => [url.pathname, url.searchParams.get("location[directory]")])).toEqual([ [`/api/${resource}`, "/project"], ]) expect(setup.data.location[unaffected].list(project)).toBe(untouchedResource) expect(setup.data.location[resource].list(other)).toBe(untouchedLocation) expect(setup.data.location[unaffected].list(project)?.[0]?.name).toBe("Before") expect(setup.data.location[resource].list(other)?.[0]?.name).toBe("Before") } finally { setup.dispose() } })}
test("revalidates model discovery when an update overtakes an in-flight model list", async () => { const started = Promise.withResolvers<void>() const release = Promise.withResolvers<void>() const listeners = new Set<Parameters<CreateDataInput["event"]["listen"]>[0]>() let requests = 0 const api = OpenCode.make({ baseUrl: "http://opencode.local", fetch: async (input, init) => { const request = input instanceof Request ? input : new Request(input, init) if (new URL(request.url).pathname !== "/api/model") throw new Error(`Unexpected request: ${request.url}`) const initial = ++requests === 1 if (initial) { started.resolve() await release.promise } return Response.json({ location: { directory: "/project" }, data: [modelInfo(initial ? "Before discovery" : "Discovered model")], }) }, }) const setup = createRoot((dispose) => ({ data: createData({ api: () => api, directory: "/project", event: { on: () => () => {}, listen(handler) { listeners.add(handler) return () => listeners.delete(handler) }, }, connection: { status: () => "connected" }, }), dispose, }))
try { const initial = setup.data.location.model.sync() await started.promise const event: OpenCodeEvent = { id: "evt_models_discovered", created: 1, type: "model.updated", location: { directory: "/project" }, data: {}, } listeners.forEach((listener) => listener({ name: event.type, details: event })) release.resolve() await initial await wait(() => setup.data.location.model.list()?.[0]?.name === "Discovered model") expect(requests).toBe(2) } finally { release.resolve() setup.dispose() }})
test("refreshes references for the location an update names", async () => { const listeners = new Set<Parameters<CreateDataInput["event"]["listen"]>[0]>() const requests: URL[] = [] const api = OpenCode.make({ baseUrl: "http://opencode.local", fetch: async (input, init) => { const request = input instanceof Request ? input : new Request(input, init) const url = new URL(request.url) requests.push(url) const directory = url.searchParams.get("location[directory]") ?? "/project" return Response.json({ location: { directory, project: { id: "project", directory, canonical: directory }, }, data: [], }) }, }) const setup = createRoot((dispose) => ({ data: createData({ api: () => api, directory: "/project", event: { on: () => () => {}, listen(handler) { listeners.add(handler) return () => listeners.delete(handler) }, }, connection: { status: () => "connected" }, }), dispose, })) const other = { directory: "/other" }
try { await Promise.all([setup.data.location.reference.sync(), setup.data.location.reference.sync(other)]) requests.length = 0
const updated: OpenCodeEvent = { id: "evt_reference.updated", created: 1, type: "reference.updated", location: other, data: {}, } listeners.forEach((listener) => listener({ name: updated.type, details: updated })) await wait(() => requests.length === 1) expect([requests[0]!.pathname, requests[0]!.searchParams.get("location[directory]")]).toEqual([ "/api/reference", "/other", ]) } finally { setup.dispose() }})
test("preserves sibling catalogs through location preload, branch, shell, and websearch updates", async () => { const listeners = new Set<Parameters<CreateDataInput["event"]["listen"]>[0]>() const location = { directory: "/project", project: { id: "project", directory: "/project", canonical: "/project" } } const requests: string[] = [] const api = OpenCode.make({ baseUrl: "http://opencode.local", fetch: async (input, init) => { const pathname = new URL((input instanceof Request ? input : new Request(input, init)).url).pathname requests.push(pathname) if (pathname === "/api/session/active") return Response.json({}) if (pathname === "/api/project") return Response.json([]) if (pathname === "/api/location") return Response.json(location) if (pathname === "/api/vcs") return Response.json({ location, data: { branch: { current: "main", default: "main" } } }) if (pathname === "/api/reference") return Response.json({ location, data: [{ name: "docs", path: "/docs", source: { type: "local", path: "/docs" } }], }) if (pathname === "/api/websearch/provider") return Response.json({ location, data: [{ id: "search", name: "Search" }] }) throw new Error(`Unexpected request: ${pathname}`) }, }) const setup = createRoot((dispose) => ({ data: createData({ api: () => api, directory: location.directory, event: { on: () => () => {}, listen(handler) { listeners.add(handler) return () => listeners.delete(handler) }, }, }), dispose, })) const emit = (details: OpenCodeEvent) => listeners.forEach((listener) => listener({ name: details.type, details })) const shell = { id: "sh_first", status: "running" as const, command: "echo hello", cwd: location.directory, shell: "/bin/sh", file: "/shell-output", metadata: {}, time: { started: 1 }, }
try { // A live event may arrive before any location reads have populated this key. emit({ type: "shell.created", location, data: { info: shell } }) expect(setup.data.shell.get(shell.id)).toMatchObject(shell) const first = setup.data.shell.get(shell.id) await Promise.all([setup.data.location.reference.sync(), setup.data.location.vcs.sync()]) const references = setup.data.location.reference.list() expect(references?.map((reference) => [reference.name, reference.path])).toEqual([["docs", "/docs"]]) expect(setup.data.shell.get(shell.id)).toBe(first)
emit({ type: "vcs.branch.updated", location, data: { branch: "feature" } }) expect(setup.data.location.vcs.info()?.branch).toEqual({ current: "feature", default: "main" }) expect(setup.data.location.reference.list()).toBe(references) emit({ type: "shell.created", location, data: { info: { ...shell, id: "sh_second" } } }) expect(setup.data.shell.list().map((shell) => shell.id)).toEqual(["sh_first", "sh_second"]) expect(setup.data.location.reference.list()).toBe(references) emit({ type: "shell.deleted", location, data: { id: "sh_second" } }) expect(setup.data.shell.list().map((shell) => shell.id)).toEqual(["sh_first"]) expect(setup.data.shell.get(shell.id)).toBe(first) expect(setup.data.location.reference.list()).toBe(references)
await setup.data.location.websearch.refresh() expect(setup.data.location.websearch.list()).toEqual([{ id: "search", name: "Search" }]) expect(setup.data.location.reference.list()).toBe(references) emit({ type: "server.connected", data: {} }) await wait(() => setup.data.location.info() !== undefined) expect(setup.data.location.info()).toMatchObject(location) expect(setup.data.location.reference.list()).toBe(references) expect(setup.data.shell.get(shell.id)).toBe(first) expect(setup.data.location.vcs.info()?.branch).toEqual({ current: "feature", default: "main" }) expect(requests.toSorted()).toEqual([ "/api/location", "/api/project", "/api/reference", "/api/session/active", "/api/vcs", "/api/websearch/provider", ]) } finally { setup.dispose() }})
test("reports optimistic sessions as creating until the request settles", async () => { const release = Promise.withResolvers<void>() const api = OpenCode.make({ baseUrl: "http://opencode.local", fetch: async (input, init) => { const request = input instanceof Request ? input : new Request(input, init) if (!request.url.endsWith("/api/session")) throw new Error(`Unexpected request: ${request.url}`) await release.promise return Response.json({ data: session(0) }) }, }) const event: CreateDataInput["event"] = { on: () => () => {}, listen: () => () => {}, } const setup = createRoot((dispose) => ({ data: createData({ api: () => api, directory: "/project", event, connection: { status: () => "connected" } }), dispose, }))
try { const created = setup.data.session.create({ id: "ses_refresh", location: { directory: "/project" } }) expect(setup.data.session.creating(created.id)).toBe(true) release.resolve() await created.request expect(setup.data.session.creating(created.id)).toBe(false) } finally { setup.dispose() }})
test("loads bounded message pages", async () => { const requests: URL[] = [] const api = OpenCode.make({ baseUrl: "http://opencode.local", fetch: async (input, init) => { const request = input instanceof Request ? input : new Request(input, init) const url = new URL(request.url) requests.push(url) return Response.json({ data: [], cursor: requests.length === 1 ? { next: "next" } : {} }) }, }) const setup = createRoot((dispose) => ({ data: createData({ api: () => api, directory: "/project", event: { on: () => () => {}, listen: () => () => {} }, }), dispose, }))
try { await setup.data.session.message.sync("ses_refresh") await setup.data.session.message.loadMore("ses_refresh")
expect(requests).toHaveLength(2) expect(Object.fromEntries(requests[0].searchParams)).toEqual({ limit: "20", order: "desc" }) expect(Object.fromEntries(requests[1].searchParams)).toEqual({ cursor: "next", limit: "20" }) } finally { setup.dispose() }})
test.each(["success", "failure", "cancel", "cancel-retry", "cancel-page", "join-cancel", "join-failure"])( "bulk history (%s)", async (mode) => { const messages = [1, 2, 3].map((index) => ({ id: `msg_${index}`, type: "user", text: `Message ${index}`, time: { created: index }, })) const release = Promise.withResolvers<void>() const controller = new AbortController() const requests: URL[] = [] const publications: string[][] = [] const api = OpenCode.make({ baseUrl: "http://opencode.local", fetch: async (input, init) => { const url = new URL(input instanceof Request ? input.url : String(input)) requests.push(url) const cursor = url.searchParams.get("cursor") if (!cursor) return Response.json({ data: [messages[2]], cursor: { next: "recent" } }) if (cursor === "recent") { if (mode.startsWith("join")) await release.promise if (mode === "join-failure") return Response.json({ message: "offline" }, { status: 503 }) return Response.json({ data: [messages[2], messages[1]], cursor: { next: "oldest" } }) } if (cursor === "oldest") return Response.json({ data: [messages[0]], cursor: { next: "empty" } }) expect(init?.signal).toBe(requests.length === 4 ? controller.signal : undefined) await release.promise if (mode === "failure") return Response.json({ message: "offline" }, { status: 503 }) return Response.json({ data: [], cursor: {} }) }, }) const setup = createRoot((dispose) => { const data = createData({ api: () => api, directory: "/project", event: { on: () => () => {}, listen: () => () => {} }, }) return { data, dispose } })
try { await setup.data.session.message.sync("ses_refresh") const newest = setup.data.session.message.get("ses_refresh", "msg_3") const load = setup.data.session.message.loadMore( "ses_refresh", mode.startsWith("join") ? undefined : { all: true, signal: controller.signal, beforePublish: () => { publications.push(setup.data.session.message.list("ses_refresh").map((message) => message.id)) expect(setup.data.session.message.get("ses_refresh", "msg_3")).toBe(newest) }, }, ) const joined = setup.data.session.message.loadMore("ses_refresh", { all: true, signal: controller.signal }) const settled = Promise.allSettled([load, joined]) if (mode.startsWith("join")) { await wait(() => requests.length === 2) expect(getEventListeners(controller.signal, "abort")).toHaveLength(1) controller.abort() let cancelled = false void joined.then(() => { cancelled = true }) await wait(() => cancelled) expect(setup.data.session.message.loading("ses_refresh")).toBe(true) expect(getEventListeners(controller.signal, "abort")).toHaveLength(0) release.resolve() expect((await settled).map((result) => result.status)).toEqual( mode === "join-failure" ? ["rejected", "fulfilled"] : ["fulfilled", "fulfilled"], ) expect(requests.at(-1)?.searchParams.get("limit")).toBe("20") expect(requests).toHaveLength(2) expect(setup.data.session.message.more("ses_refresh")).toBe(true) expect(setup.data.session.message.list("ses_refresh").map((message) => message.id)).toEqual( mode === "join-failure" ? ["msg_3"] : ["msg_2", "msg_3"], ) return } await wait(() => requests.length === 4) expect(setup.data.session.message.loading("ses_refresh")).toBe(true) expect(setup.data.session.message.list("ses_refresh").map((message) => message.id)).toEqual(["msg_3"]) expect(requests.slice(1).map((url) => url.searchParams.get("limit"))).toEqual(["200", "200", "200"]) if (mode.startsWith("cancel")) controller.abort() const retry = mode === "cancel-retry" || mode === "cancel-page" ? setup.data.session.message.loadMore("ses_refresh", mode === "cancel-retry" ? { all: true } : undefined) : undefined release.resolve() expect((await settled).map((result) => result.status)).toEqual( mode === "failure" ? ["rejected", "rejected"] : ["fulfilled", "fulfilled"], ) await retry const success = mode === "success" || mode === "cancel-retry" expect(setup.data.session.message.loading("ses_refresh")).toBe(false) expect(setup.data.session.message.more("ses_refresh")).toBe(!success) expect(setup.data.session.message.list("ses_refresh").map((message) => message.id)).toEqual( success ? ["msg_1", "msg_2", "msg_3"] : mode === "cancel-page" ? ["msg_2", "msg_3"] : ["msg_3"], ) expect(setup.data.session.message.get("ses_refresh", "msg_3")).toBe(newest) expect(requests).toHaveLength(mode === "cancel-retry" ? 7 : mode === "cancel-page" ? 5 : 4) if (mode === "cancel-page") expect(requests.at(-1)?.searchParams.get("limit")).toBe("20") expect(publications).toEqual(mode === "success" ? [["msg_3"]] : []) expect(getEventListeners(controller.signal, "abort")).toHaveLength(0) } finally { release.resolve() setup.dispose() } },)
test.each([ "session.execution.succeeded", "session.execution.failed", "session.execution.interrupted", "session.execution.started", "session.deleted",] as const)("preserves %s activity when an older snapshot arrives", async (type) => { const release = Promise.withResolvers<void>() const requested = Promise.withResolvers<void>() const setup = activityFixture(async () => { requested.resolve() await release.promise return Response.json({ data: { ...(type === "session.execution.started" ? {} : { ses_refresh: { type: "running" } }), ses_hydrated: { type: "running" }, }, }) })
try { if (type !== "session.execution.started") setup.data.session.setStatus("ses_refresh", "running") setup.emit({ type: "server.connected", data: {} }) await requested.promise setup.emit({ id: "evt_activity", created: 2, type, durable: { aggregateID: "ses_refresh", seq: 2, version: 1 }, data: { sessionID: "ses_refresh", reason: "user" }, }) expect(setup.data.session.status("ses_refresh")).toBe(type === "session.execution.started" ? "running" : "idle") release.resolve() await wait(() => setup.data.session.status("ses_hydrated") === "running") expect(setup.data.session.status("ses_refresh")).toBe(type === "session.execution.started" ? "running" : "idle") } finally { release.resolve() setup.dispose() }})
test("ignores activity snapshots from an older connection", async () => { const reads: ReturnType<typeof Promise.withResolvers<Response>>[] = [] const setup = activityFixture(() => { const read = Promise.withResolvers<Response>() reads.push(read) return read.promise })
try { setup.emit({ type: "server.connected", data: {} }) await wait(() => reads.length === 1) setup.emit({ type: "server.connected", data: {} }) await wait(() => reads.length === 2) reads[1]?.resolve(Response.json({ data: { ses_new: { type: "running" } } })) await wait(() => setup.data.session.status("ses_new") === "running") reads[0]?.resolve(Response.json({ data: { ses_old: { type: "running" } } })) await Bun.sleep(20) expect(setup.data.session.status("ses_new")).toBe("running") expect(setup.data.session.status("ses_old")).toBe("idle") } finally { reads.forEach((read) => read.resolve(Response.json({ data: {} }))) setup.dispose() }})
test("projects background user shell metadata from durable shell data", () => { const setup = activityFixture(() => Response.json({ data: {} })) try { setup.emit({ id: "evt_user_shell", created: 1, type: "session.shell.started", durable: { aggregateID: "ses_refresh", seq: 1, version: 1 }, data: { sessionID: "ses_refresh", shell: { id: "sh_user", status: "running", command: "pwd", cwd: "/project", shell: "/bin/sh", file: "/project/shell.out", metadata: { sessionID: "ses_refresh", background: true }, time: { started: 1 }, }, }, }) expect(setup.data.session.message.list("ses_refresh")).toMatchObject([ { type: "shell", shellID: "sh_user", status: "running", metadata: { background: true } }, ]) } finally { setup.dispose() }})
function modelInfo(name: string): ModelInfo { return { id: "chat", modelID: "chat", providerID: "company", name, capabilities: { tools: true, input: ["text"], output: ["text"] }, variants: [], time: { released: 0 }, cost: [], status: "active", enabled: true, limit: { context: 8192, output: 1024 }, }}
function activityFixture(read: () => Response | Promise<Response>) { const listeners = new Set<Parameters<CreateDataInput["event"]["listen"]>[0]>() const api = OpenCode.make({ baseUrl: "http://opencode.local", fetch: async (input, init) => { const request = input instanceof Request ? input : new Request(input, init) const path = new URL(request.url).pathname if (path === "/api/session/active") return read() if (path === "/api/project") return Response.json([]) if (path === "/api/location") return Response.json({ directory: "/project" }) return Response.json({ location: { directory: "/project" }, data: { branch: "main" } }) }, }) return createRoot((dispose) => ({ data: createData({ api: () => api, directory: "/project", event: { on: () => () => {}, listen(handler) { listeners.add(handler) return () => listeners.delete(handler) }, }, }), emit: (details: OpenCodeEvent) => listeners.forEach((listener) => listener({ name: details.type, details })), dispose, }))}
async function wait(check: () => boolean) { const started = Date.now() while (!check()) { if (Date.now() - started > 2_000) throw new Error("Timed out waiting for condition") await Bun.sleep(10) }}