mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-28 06:20:15 -04:00
227 lines
7.4 KiB
TypeScript
227 lines
7.4 KiB
TypeScript
import { expect, test } from "bun:test"
|
|
import { createRoot } from "solid-js"
|
|
import { createData, type CreateDataInput } from "../src/solid"
|
|
import { OpenCode, 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("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("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("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()
|
|
}
|
|
})
|
|
|
|
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)
|
|
}
|
|
}
|