opencode/packages/tui/test/cli/tui/data.test.tsx
2026-07-11 17:56:25 -04:00

2477 lines
77 KiB
TypeScript

/** @jsxImportSource @opentui/solid */
import { expect, test } from "bun:test"
import { testRender } from "@opentui/solid"
import type { OpenCodeEvent } from "@opencode-ai/client"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { EventV2 } from "@opencode-ai/core/event"
import { onMount } from "solid-js"
import { ProjectProvider } from "../../../src/context/project"
import { SDKProvider, useSDK } from "../../../src/context/sdk"
import { DataProvider, useData } from "../../../src/context/data"
import { createSessionRows, type SessionRow } from "../../../src/routes/session/rows"
import { createApi, createClient, createEventStream, createFetch, directory, json } from "../../fixture/tui-sdk"
import { TestTuiContexts } from "../../fixture/tui-environment"
const formFields = [{ key: "authorization", type: "external", url: "https://example.com" }] satisfies [
{
key: string
type: "external"
url: string
},
]
async function wait(fn: () => boolean, timeout = 2000) {
const start = Date.now()
while (!fn()) {
if (Date.now() - start > timeout) throw new Error("timed out waiting for condition")
await Bun.sleep(10)
}
}
function emitEvent(events: ReturnType<typeof createEventStream>, event: OpenCodeEvent) {
events.emit({ ...event, location: { directory } })
}
function durable(sessionID: string, seq?: number): { aggregateID: string; seq: number; version: 1 }
function durable<const Version extends number>(
sessionID: string,
seq: number,
version: Version,
): { aggregateID: string; seq: number; version: Version }
function durable(sessionID: string, seq = 0, version = 1) {
return { aggregateID: sessionID, seq, version }
}
test("refreshes resources into reactive getters", async () => {
const events = createEventStream()
const location = {
directory,
project: { id: "proj_test", directory },
}
const calls = createFetch((url) => {
if (url.pathname === "/api/session/ses_test")
return json({
data: {
id: "ses_test",
projectID: "proj_test",
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 0, updated: 0 },
title: "Test session",
location: { directory },
},
})
if (url.pathname === "/api/session/ses_test/message")
return json({
data: [
{ id: "msg_second", created: 0, type: "user", text: "Second", time: { created: 2 } },
{ id: "msg_first", created: 0, type: "user", text: "First", time: { created: 1 } },
],
cursor: {},
})
if (url.pathname === "/api/agent")
return json({
location,
data: [{ id: "build", request: { headers: {}, body: {} }, mode: "primary", hidden: false, permissions: [] }],
})
return undefined
}, events)
let data!: ReturnType<typeof useData>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
data = useData()
onMount(ready)
return <text>{data.session.message.get("ses_test", "msg_second")?.id ?? "missing"}</text>
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await mounted
expect(data.location.default()).toEqual({ directory: process.cwd() })
expect(data.session.get("ses_test")).toBeUndefined()
expect(data.location.agent.list(location)).toBeUndefined()
await data.session.refresh("ses_test")
await data.session.message.refresh("ses_test")
await data.location.agent.refresh()
expect(data.session.get("ses_test")?.title).toBe("Test session")
expect(data.session.message.list("ses_test").map((message) => message.id)).toEqual(["msg_first", "msg_second"])
expect(data.session.message.get("ses_test", "msg_second")?.id).toBe("msg_second")
await app.renderOnce()
expect(app.captureCharFrame()).toContain("msg_second")
expect(data.location.default()).toEqual({ directory, workspaceID: undefined })
expect(data.location.agent.list(location)?.map((agent) => agent.id)).toEqual(["build"])
} finally {
app.renderer.destroy()
}
})
test("applies absolute usage events to session info", async () => {
const events = createEventStream()
const sessionID = "ses_usage_refresh"
const calls = createFetch((url) => {
if (url.pathname === `/api/session/${sessionID}`)
return json({
data: {
id: sessionID,
projectID: "proj_test",
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 0, updated: 0 },
title: "Usage",
location: { directory },
},
})
}, events)
let data!: ReturnType<typeof useData>
function Probe() {
data = useData()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await data.session.refresh(sessionID)
emitEvent(events, {
id: "evt_usage_2",
created: 2,
type: "session.usage.updated",
data: {
sessionID,
cost: 0.5,
tokens: { input: 5, output: 2, reasoning: 1, cache: { read: 1, write: 1 } },
},
})
await wait(() => data.session.get(sessionID)?.cost === 0.5)
expect(data.session.get(sessionID)?.tokens).toEqual({
input: 5,
output: 2,
reasoning: 1,
cache: { read: 1, write: 1 },
})
emitEvent(events, {
id: "evt_usage_3",
created: 3,
type: "session.usage.updated",
data: {
sessionID,
cost: 1,
tokens: { input: 10, output: 4, reasoning: 1, cache: { read: 1, write: 1 } },
},
})
await wait(() => data.session.get(sessionID)?.cost === 1)
expect(data.session.get(sessionID)?.title).toBe("Usage")
emitEvent(events, {
id: "evt_usage_deleted",
created: 9,
type: "session.deleted",
durable: durable(sessionID, 9, 2),
data: { sessionID },
})
await wait(() => data.session.get(sessionID) === undefined)
} finally {
app.renderer.destroy()
}
})
test("truncates committed revert messages without changing lifetime usage", async () => {
const events = createEventStream()
const sessionID = "ses_revert_usage"
let cost = 0
let tokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
const calls = createFetch((url) => {
if (url.pathname === `/api/session/${sessionID}/message`) return json({ data: [], cursor: {} })
if (url.pathname !== `/api/session/${sessionID}`) return
return json({
data: {
id: sessionID,
projectID: "proj_test",
cost,
tokens,
time: { created: 0, updated: 0 },
title: "Revert usage",
location: { directory },
},
})
}, events)
let data!: ReturnType<typeof useData>
function Probe() {
data = useData()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await data.session.refresh(sessionID)
emitEvent(events, {
id: "evt_revert_boundary_started",
created: 1,
type: "session.step.started",
durable: durable(sessionID, 1),
data: {
sessionID,
assistantMessageID: "msg_revert_boundary",
agent: "build",
model: { providerID: "provider", id: "model" },
},
})
cost = 0.5
tokens = { input: 5, output: 2, reasoning: 1, cache: { read: 1, write: 1 } }
emitEvent(events, {
id: "evt_revert_boundary_ended",
created: 2,
type: "session.step.ended",
durable: durable(sessionID, 2),
data: {
sessionID,
assistantMessageID: "msg_revert_boundary",
finish: "stop",
cost: 0.5,
tokens,
},
})
emitEvent(events, {
id: "evt_revert_boundary_usage",
created: 2,
type: "session.usage.updated",
data: { sessionID, cost, tokens },
})
await wait(() => data.session.get(sessionID)?.cost === 0.5)
emitEvent(events, {
id: "evt_revert_later_started",
created: 3,
type: "session.step.started",
durable: durable(sessionID, 3),
data: {
sessionID,
assistantMessageID: "msg_revert_later",
agent: "build",
model: { providerID: "provider", id: "model" },
},
})
cost = 0.75
tokens = { input: 8, output: 3, reasoning: 1, cache: { read: 1, write: 1 } }
emitEvent(events, {
id: "evt_revert_later_ended",
created: 4,
type: "session.step.ended",
durable: durable(sessionID, 4),
data: {
sessionID,
assistantMessageID: "msg_revert_later",
finish: "stop",
cost: 0.25,
tokens: { input: 3, output: 1, reasoning: 0, cache: { read: 0, write: 0 } },
},
})
emitEvent(events, {
id: "evt_revert_later_usage",
created: 4,
type: "session.usage.updated",
data: { sessionID, cost, tokens },
})
await wait(() => data.session.get(sessionID)?.cost === 0.75)
emitEvent(events, {
id: "evt_revert_staged",
created: 5,
type: "session.revert.staged",
durable: durable(sessionID, 5),
data: { sessionID, revert: { messageID: "msg_revert_later" } },
})
await wait(() => data.session.get(sessionID)?.revert?.messageID === "msg_revert_later")
emitEvent(events, {
id: "evt_revert_committed",
created: 6,
type: "session.revert.committed",
durable: durable(sessionID, 6),
data: { sessionID, to: "msg_revert_later" },
})
await wait(() => data.session.message.list(sessionID).length === 1)
expect(data.session.get(sessionID)?.cost).toBe(0.75)
expect(data.session.message.list(sessionID).map((message) => message.id)).toEqual(["msg_revert_boundary"])
expect(data.session.get(sessionID)?.revert).toBeUndefined()
expect(data.session.get(sessionID)?.tokens).toEqual(tokens)
} finally {
app.renderer.destroy()
}
})
test("updates session location when moved", async () => {
const events = createEventStream()
const destination = "/tmp/opencode-moved"
const calls = createFetch((url) => {
if (url.pathname === "/api/session/ses_test")
return json({
data: {
id: "ses_test",
projectID: "proj_test",
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 0, updated: 0 },
title: "Test session",
location: { directory },
},
})
}, events)
let data!: ReturnType<typeof useData>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
data = useData()
onMount(ready)
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await mounted
await data.session.refresh("ses_test")
emitEvent(events, {
id: "evt_moved_1",
created: 1,
type: "session.moved",
durable: durable("ses_test"),
data: {
sessionID: "ses_test",
location: { directory: destination },
subpath: "packages/cli",
},
})
await wait(() => data.session.get("ses_test")?.location.directory === destination)
expect(data.session.get("ses_test")?.subpath).toBe("packages/cli")
} finally {
app.renderer.destroy()
}
})
test("restores running manual compaction before applying live deltas", async () => {
const events = createEventStream()
const calls = createFetch((url) => {
if (url.pathname === "/api/session/session-compaction/message")
return json({
data: [
{
id: "message-compaction",
type: "compaction",
status: "running",
reason: "manual",
summary: "Existing ",
recent: "",
time: { created: 1 },
},
],
cursor: {},
})
}, events)
let data!: ReturnType<typeof useData>
function Probe() {
data = useData()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await data.session.message.refresh("session-compaction")
expect(data.session.message.get("session-compaction", "message-compaction")).toMatchObject({
type: "compaction",
status: "running",
summary: "Existing ",
})
emitEvent(events, {
id: "evt_compaction_delta",
created: 2,
type: "session.compaction.delta",
data: { sessionID: "session-compaction", text: "summary" },
})
await wait(() => {
const message = data.session.message.get("session-compaction", "message-compaction")
return message?.type === "compaction" && message.status === "running" && message.summary === "Existing summary"
})
} finally {
app.renderer.destroy()
}
})
test("reconnects the event stream and bootstraps fresh data", async () => {
const events = createEventStream()
const requests = { active: 0, event: 0, message: 0, model: 0 }
let resolveActive!: (response: Response) => void
let resolveMessages!: (response: Response) => void
const calls = createFetch((url) => {
if (url.pathname === "/api/event") {
requests.event++
return events.v2()
}
if (url.pathname === "/api/session/active") {
requests.active++
if (requests.active === 1) return json({ data: { "session-stale": { type: "running" } } })
return new Promise<Response>((resolve) => {
resolveActive = resolve
})
}
if (url.pathname === "/api/session/session-stale/message") {
requests.message++
if (requests.message === 1)
return json({
data: [{ id: "message-stale", type: "user", text: "Stale", time: { created: 1 } }],
cursor: {},
})
return new Promise<Response>((resolve) => {
resolveMessages = resolve
})
}
if (url.pathname !== "/api/model") return
requests.model++
return json({
location: { directory, project: { id: "proj_test", directory } },
data: [
{
id: `model-${requests.model}`,
providerID: "provider",
name: `Model ${requests.model}`,
api: { type: "native" },
capabilities: { tools: false, input: [], output: [] },
cost: [],
limit: { context: 1, output: 1 },
request: { headers: {}, body: {} },
status: "active",
time: { released: 0 },
variants: [],
},
],
})
}, events)
let data!: ReturnType<typeof useData>
let sdk!: ReturnType<typeof useSDK>
function Probe() {
data = useData()
sdk = useSDK()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await wait(() => data.location.model.list()?.[0]?.id === "model-1")
await wait(() => data.session.status("session-stale") === "running")
await data.session.message.refresh("session-stale")
expect(data.session.message.get("session-stale", "message-stale")?.id).toBe("message-stale")
expect(sdk.connection.status()).toBe("connected")
expect(sdk.connection.attempt()).toBe(0)
events.disconnect()
await wait(() => sdk.connection.status() === "reconnecting")
expect(sdk.connection.attempt()).toBe(1)
expect(sdk.connection.error()).toBe("Event stream disconnected")
await wait(() => requests.active === 2 && sdk.connection.status() === "connected", 4000)
resolveActive(json({ data: { "session-new": { type: "running" } } }))
await wait(() => data.location.model.list()?.[0]?.id === "model-2", 4000)
await wait(() => data.session.status("session-stale") === "idle")
await wait(() => requests.message === 2)
expect(data.session.message.get("session-stale", "message-stale")?.id).toBe("message-stale")
resolveMessages(
json({
data: [{ id: "message-fresh", type: "user", text: "Fresh", time: { created: 2 } }],
cursor: {},
}),
)
await wait(() => data.session.message.get("session-stale", "message-fresh") !== undefined)
expect(data.session.message.get("session-stale", "message-stale")).toBeUndefined()
await wait(() => data.session.status("session-new") === "running")
expect(requests.event).toBe(2)
expect(requests.message).toBe(2)
expect(sdk.connection.status()).toBe("connected")
expect(sdk.connection.attempt()).toBe(0)
expect(sdk.connection.error()).toBeUndefined()
} finally {
app.renderer.destroy()
}
})
test("completes exploration when a queued prompt is promoted", async () => {
const events = createEventStream()
const sessionID = "session-promotion"
const calls = createFetch((url) => {
if (url.pathname === `/api/session/${sessionID}/message`) return json({ data: [], cursor: {} })
}, events)
let rows!: ReturnType<typeof createSessionRows>
function Probe() {
rows = createSessionRows(() => sessionID)
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
emitEvent(events, {
id: "evt_step_started",
created: 1,
type: "session.step.started",
durable: durable(sessionID),
data: {
sessionID,
assistantMessageID: "message-assistant",
agent: "build",
model: { id: "model", providerID: "provider" },
},
})
emitEvent(events, {
id: "evt_tool_started",
created: 2,
type: "session.tool.input.started",
durable: durable(sessionID, 1),
data: {
sessionID,
assistantMessageID: "message-assistant",
callID: "call-read",
name: "read",
},
})
await wait(() => rows.some((row) => row.type === "group" && !row.completed))
emitEvent(events, {
id: "evt_prompt_admitted",
created: 3,
type: "session.input.admitted",
durable: durable(sessionID, 2),
data: {
sessionID,
inputID: "message-user",
input: { type: "user", data: { text: "Continue" }, delivery: "steer" },
},
})
await wait(() => rows.at(-1)?.type === "message")
expect(rows.find((row) => row.type === "group")?.completed).toBe(false)
emitEvent(events, {
id: "evt_prompt_promoted",
created: 4,
type: "session.input.promoted",
durable: durable(sessionID, 3),
data: { sessionID, inputID: "message-user" },
})
await wait(() => rows.find((row) => row.type === "group")?.completed === true)
expect(rows.at(-1)).toEqual({ type: "message", messageID: "message-user" })
} finally {
app.renderer.destroy()
}
})
test("removes committed revert messages from local state", async () => {
const events = createEventStream()
const sessionID = "session-revert"
const calls = createFetch((url) => {
if (url.pathname === `/api/session/${sessionID}/message`) return json({ data: [], cursor: {} })
}, events)
let data!: ReturnType<typeof useData>
function Probe() {
data = useData()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
for (const [seq, inputID] of ["msg_001", "msg_002", "msg_003"].entries()) {
emitEvent(events, {
id: EventV2.ID.create(),
created: seq,
type: "session.input.admitted",
durable: durable(sessionID, seq),
data: { sessionID, inputID, input: { type: "user", data: { text: inputID }, delivery: "steer" } },
})
}
await wait(() => data.session.message.list(sessionID).length === 3)
emitEvent(events, {
id: EventV2.ID.create(),
created: 3,
type: "session.revert.committed",
durable: durable(sessionID, 3),
data: { sessionID, to: "msg_002" },
})
await wait(() => data.session.message.list(sessionID).length === 1)
expect(data.session.message.list(sessionID).map((message) => message.id)).toEqual(["msg_001"])
expect(data.session.message.get(sessionID, "msg_002")).toBeUndefined()
expect(data.session.message.get(sessionID, "msg_003")).toBeUndefined()
} finally {
app.renderer.destroy()
}
})
test("distinguishes initial connection from reconnection", async () => {
const encoder = new TextEncoder()
let stream: ReadableStreamDefaultController<Uint8Array> | undefined
const eventResponse = () =>
new Response(
new ReadableStream<Uint8Array>({
start(controller) {
stream = controller
},
}),
{ headers: { "content-type": "text/event-stream" } },
)
const connect = () =>
stream?.enqueue(
encoder.encode(
`data: ${JSON.stringify({ id: "evt_connected", created: 0, type: "server.connected", data: {} })}\n\n`,
),
)
const disconnect = () => {
stream?.close()
stream = undefined
}
const calls = createFetch((url) => {
if (url.pathname === "/api/event") return eventResponse()
})
let sdk!: ReturnType<typeof useSDK>
function Probe() {
sdk = useSDK()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await wait(() => stream !== undefined)
expect(sdk.connection.status()).toBe("connecting")
connect()
await wait(() => sdk.connection.status() === "connected")
disconnect()
await wait(() => sdk.connection.status() === "reconnecting")
} finally {
app.renderer.destroy()
}
})
test("tracks session status from active sessions and execution events", async () => {
const events = createEventStream()
let settled = false
const calls = createFetch((url) => {
if (url.pathname === "/api/session/active") return json({ data: { "session-active": { type: "running" } } })
if (url.pathname === "/api/session/session-live")
return json({
data: {
id: "session-live",
projectID: "proj_test",
cost: settled ? 0.75 : 0,
tokens: settled
? { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 1 } }
: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 0, updated: 0 },
title: "Live session",
location: { directory },
},
})
if (url.pathname === "/api/session/session-failed")
return json({
data: {
id: "session-failed",
projectID: "proj_test",
cost: 0.25,
tokens: { input: 5, output: 1, reasoning: 1, cache: { read: 1, write: 0 } },
time: { created: 0, updated: 0 },
title: "Failed session",
location: { directory },
},
})
}, events)
let data!: ReturnType<typeof useData>
let rows!: SessionRow[]
let manualRows!: SessionRow[]
function Probe() {
data = useData()
rows = createSessionRows(() => "session-retry")
manualRows = createSessionRows(() => "session-manual")
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await wait(() => data.session.status("session-active") === "running")
expect(data.session.status("session-idle")).toBe("idle")
await data.session.refresh("session-live")
settled = true
emitEvent(events, {
id: "evt_execution_started",
created: 0,
type: "session.execution.started",
durable: durable("session-live"),
data: { sessionID: "session-live" },
})
await wait(() => data.session.status("session-live") === "running")
emitEvent(events, {
id: "evt_step_started",
created: 0,
type: "session.step.started",
durable: durable("session-live"),
data: {
sessionID: "session-live",
assistantMessageID: "message-live",
agent: "build",
model: { id: "model", providerID: "provider" },
},
})
emitEvent(events, {
id: "evt_step_ended",
created: 0,
type: "session.step.ended",
durable: durable("session-live", 1),
data: {
sessionID: "session-live",
assistantMessageID: "message-live",
finish: "stop",
cost: 0.75,
tokens: { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 1 } },
},
})
emitEvent(events, {
id: "evt_step_usage",
created: 0,
type: "session.usage.updated",
data: {
sessionID: "session-live",
cost: 0.75,
tokens: { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 1 } },
},
})
await wait(() => {
const assistant = data.session.message.get("session-live", "message-live")
return assistant?.type === "assistant" && assistant.finish === "stop"
})
await wait(() => data.session.get("session-live")?.cost === 0.75)
expect(data.session.status("session-live")).toBe("running")
expect(data.session.get("session-live")).toMatchObject({
cost: 0.75,
tokens: { input: 10, output: 4, reasoning: 2, cache: { read: 3, write: 1 } },
})
emitEvent(events, {
id: "evt_execution_succeeded",
created: 0,
type: "session.execution.succeeded",
durable: durable("session-live", 1),
data: { sessionID: "session-live" },
})
await wait(() => data.session.status("session-live") === "idle")
await data.session.refresh("session-failed")
emitEvent(events, {
id: "evt_failed_execution_started",
created: 0,
type: "session.execution.started",
durable: durable("session-failed"),
data: { sessionID: "session-failed" },
})
await wait(() => data.session.status("session-failed") === "running")
emitEvent(events, {
id: "evt_failed_step_started",
created: 0,
type: "session.step.started",
durable: durable("session-failed"),
data: {
sessionID: "session-failed",
assistantMessageID: "message-failed",
agent: "build",
model: { id: "model", providerID: "provider" },
},
})
emitEvent(events, {
id: "evt_step_failed",
created: 0,
type: "session.step.failed",
durable: durable("session-failed", 1),
data: {
sessionID: "session-failed",
assistantMessageID: "message-failed",
error: { type: "provider.content-filter", message: "Provider blocked the response" },
cost: 0.25,
tokens: { input: 5, output: 1, reasoning: 1, cache: { read: 1, write: 0 } },
},
})
emitEvent(events, {
id: "evt_failed_step_usage",
created: 0,
type: "session.usage.updated",
data: {
sessionID: "session-failed",
cost: 0.25,
tokens: { input: 5, output: 1, reasoning: 1, cache: { read: 1, write: 0 } },
},
})
await wait(() => {
const assistant = data.session.message.get("session-failed", "message-failed")
return (
assistant?.type === "assistant" &&
assistant.finish === "error" &&
assistant.error?.type === "provider.content-filter"
)
})
await wait(() => data.session.get("session-failed")?.cost === 0.25)
expect(data.session.get("session-failed")?.tokens).toEqual({
input: 5,
output: 1,
reasoning: 1,
cache: { read: 1, write: 0 },
})
expect(data.session.status("session-failed")).toBe("running")
emitEvent(events, {
id: "evt_failed_execution_failed",
created: 0,
type: "session.execution.failed",
durable: durable("session-failed", 1),
data: {
sessionID: "session-failed",
error: { type: "provider.content-filter", message: "Provider blocked the response" },
},
})
await wait(() => data.session.status("session-failed") === "idle")
emitEvent(events, {
id: "evt_retry_execution_started",
created: 0,
type: "session.execution.started",
durable: durable("session-retry"),
data: { sessionID: "session-retry" },
})
emitEvent(events, {
id: "evt_retry_step_started",
created: 0,
type: "session.step.started",
durable: durable("session-retry", 1),
data: {
sessionID: "session-retry",
assistantMessageID: "message-retry",
agent: "build",
model: { id: "model", providerID: "provider" },
},
})
emitEvent(events, {
id: "evt_retry_scheduled",
created: 0,
type: "session.retry.scheduled",
durable: durable("session-retry", 1),
data: {
sessionID: "session-retry",
assistantMessageID: "message-retry",
attempt: 2,
at: 2_000,
error: { type: "provider.transport", message: "Disconnected" },
},
})
await wait(() => {
const assistant = data.session.message.get("session-retry", "message-retry")
return assistant?.type === "assistant" && assistant.retry?.attempt === 2
})
await wait(() => rows.some((row) => row.type === "assistant-footer" && row.messageID === "message-retry"))
emitEvent(events, {
id: "evt_retry_next_step",
created: 2_000,
type: "session.step.started",
durable: durable("session-retry", 1),
data: {
sessionID: "session-retry",
assistantMessageID: "message-retry",
agent: "build",
model: { id: "model", providerID: "provider" },
},
})
await wait(() => {
const assistant = data.session.message.get("session-retry", "message-retry")
return assistant?.type === "assistant" && assistant.retry === undefined
})
await wait(() => !rows.some((row) => row.type === "assistant-footer" && row.messageID === "message-retry"))
expect(data.session.message.list("session-retry").filter((message) => message.type === "assistant")).toHaveLength(1)
emitEvent(events, {
id: "evt_retry_scheduled_again",
created: 2_000,
type: "session.retry.scheduled",
durable: durable("session-retry", 1),
data: {
sessionID: "session-retry",
assistantMessageID: "message-retry",
attempt: 3,
at: 6_000,
error: { type: "provider.transport", message: "Disconnected again" },
},
})
await wait(() => {
const assistant = data.session.message.get("session-retry", "message-retry")
return assistant?.type === "assistant" && assistant.retry?.attempt === 3
})
emitEvent(events, {
id: "evt_retry_interrupted",
created: 2_000,
type: "session.execution.interrupted",
durable: durable("session-retry", 1),
data: { sessionID: "session-retry", reason: "shutdown" },
})
await wait(() => data.session.status("session-retry") === "idle")
expect(data.session.message.get("session-retry", "message-retry")).not.toHaveProperty("retry")
emitEvent(events, {
id: "evt_manual_compaction_admitted",
created: 0,
type: "session.compaction.admitted",
durable: durable("session-manual", 1),
data: { sessionID: "session-manual", inputID: "message-compaction" },
})
await wait(() => data.session.compaction.list("session-manual").includes("message-compaction"))
emitEvent(events, {
id: "evt_manual_compaction_started",
created: 1,
type: "session.compaction.started",
durable: durable("session-manual", 2),
data: { sessionID: "session-manual", reason: "manual", recent: "", inputID: "message-compaction" },
})
emitEvent(events, {
id: "evt_manual_compaction_delta",
created: 2,
type: "session.compaction.delta",
data: { sessionID: "session-manual", text: "Streamed summary" },
})
await wait(() => {
const message = data.session.message.get("session-manual", "message-compaction")
return message?.type === "compaction" && message.status === "running" && message.summary === "Streamed summary"
})
expect(data.session.compaction.list("session-manual")).toEqual([])
const compactionRow = manualRows.find(
(row) => row.type === "message" && row.messageID === "message-compaction",
)
emitEvent(events, {
id: "evt_manual_compaction_ended",
created: 3,
type: "session.compaction.ended",
durable: durable("session-manual", 4),
data: { sessionID: "session-manual", reason: "manual", text: "Streamed summary", recent: "recent" },
})
await wait(() => {
const message = data.session.message.get("session-manual", "message-compaction")
return message?.type === "compaction" && message.status === "completed"
})
expect(manualRows.filter((row) => row.type === "message")).toEqual([
{ type: "message", messageID: "message-compaction" },
])
expect(manualRows.find((row) => row.type === "message" && row.messageID === "message-compaction")).toBe(
compactionRow,
)
emitEvent(events, {
id: "evt_compaction_started",
created: 0,
type: "session.compaction.started",
durable: durable("session-live", 2),
data: { sessionID: "session-live", reason: "auto", recent: "" },
})
emitEvent(events, {
id: "evt_compaction_delta_1",
created: 0,
type: "session.compaction.delta",
data: { sessionID: "session-live", text: "Live " },
})
emitEvent(events, {
id: "evt_compaction_delta_2",
created: 0,
type: "session.compaction.delta",
data: { sessionID: "session-live", text: "summary" },
})
await wait(() => {
const message = data.session.message.get("session-live", "msg_compaction_started")
return message?.type === "compaction" && message.status === "running" && message.summary === "Live summary"
})
const autoCompactionRow = rows.find(
(row) => row.type === "message" && row.messageID === "msg_compaction_started",
)
emitEvent(events, {
id: "evt_compaction_ended",
created: 0,
type: "session.compaction.ended",
durable: durable("session-live", 5),
data: { sessionID: "session-live", reason: "auto", text: "Live summary", recent: "recent" },
})
await wait(() => {
const message = data.session.message.get("session-live", "msg_compaction_started")
return message?.type === "compaction" && message.status === "completed"
})
expect(data.session.message.get("session-live", "msg_compaction_started")).toMatchObject({
type: "compaction",
status: "completed",
summary: "Live summary",
})
expect(rows.find((row) => row.type === "message" && row.messageID === "msg_compaction_started")).toBe(
autoCompactionRow,
)
expect(rows.some((row) => row.type === "message" && row.messageID === "msg_compaction_ended")).toBeFalse()
} finally {
app.renderer.destroy()
}
})
test("restores queued compaction from durable pending input", async () => {
const events = createEventStream()
const sessionID = "session-compaction-queued"
let pending = [
{
admittedSeq: 3,
id: "message-compaction-queued",
sessionID,
timeCreated: 1,
type: "compaction" as const,
},
{
admittedSeq: 4,
id: "message-compaction-later",
sessionID,
timeCreated: 2,
type: "compaction" as const,
},
]
const calls = createFetch((url) => {
if (url.pathname !== `/api/session/${sessionID}/pending`) return
return json({ data: pending })
}, events)
let data!: ReturnType<typeof useData>
let rows!: ReturnType<typeof createSessionRows>
function Probe() {
data = useData()
rows = createSessionRows(() => sessionID)
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await wait(() => data.session.compaction.list(sessionID).length === 2)
expect(data.session.compaction.list(sessionID)).toEqual([
"message-compaction-queued",
"message-compaction-later",
])
await wait(() => rows.filter((row) => row.type === "compaction-queued").length === 2)
expect(rows.filter((row) => row.type === "compaction-queued")).toEqual([
{ type: "compaction-queued", inputID: "message-compaction-queued" },
{ type: "compaction-queued", inputID: "message-compaction-later" },
])
emitEvent(events, {
id: "evt_compaction_started",
created: 2,
type: "session.compaction.started",
durable: durable(sessionID, 4),
data: {
sessionID,
reason: "manual",
recent: "",
inputID: "message-compaction-queued",
},
})
await wait(() => data.session.compaction.list(sessionID).length === 1)
expect(data.session.compaction.list(sessionID)).toEqual(["message-compaction-later"])
emitEvent(events, {
id: "evt_compaction_ended",
created: 3,
type: "session.compaction.ended",
durable: durable(sessionID, 5),
data: { sessionID, reason: "manual", text: "Summary", recent: "" },
})
expect(data.session.compaction.list(sessionID)).toEqual(["message-compaction-later"])
pending = []
emitEvent(events, {
id: "evt_reconnected",
type: "server.connected",
data: {},
})
await wait(() => data.session.compaction.list(sessionID).length === 0)
} finally {
app.renderer.destroy()
}
})
test("refreshes integrations after integration updates", async () => {
const events = createEventStream()
const requests = { integration: 0, model: 0, provider: 0 }
const calls = createFetch((url) => {
if (url.pathname === "/api/model") {
requests.model++
return json({ location: { directory, project: { id: "proj_test", directory } }, data: [] })
}
if (url.pathname === "/api/provider") {
requests.provider++
return json({ location: { directory, project: { id: "proj_test", directory } }, data: [] })
}
if (url.pathname !== "/api/integration") return
requests.integration++
return json({
location: { directory, project: { id: "proj_test", directory } },
data:
requests.integration === 1
? []
: [
{
id: "openai",
name: "OpenAI",
methods: [{ type: "key" }],
},
],
})
}, events)
let data!: ReturnType<typeof useData>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
data = useData()
onMount(ready)
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await mounted
await wait(() => data.location.integration.list() !== undefined)
expect(data.location.integration.list()).toEqual([])
const before = { ...requests }
emitEvent(events, { id: "evt_integration", created: 0, type: "integration.updated", data: {} })
await wait(() => data.location.integration.list()?.length === 1)
await wait(() => requests.model > before.model && requests.provider > before.provider)
expect(data.location.integration.list()?.[0]).toMatchObject({ id: "openai", name: "OpenAI" })
} finally {
app.renderer.destroy()
}
})
test("refreshes MCP resources after catalog updates", async () => {
const events = createEventStream()
let requests = 0
const calls = createFetch((url) => {
if (url.pathname !== "/api/mcp/resource") return
requests++
return json({
location: { directory, project: { id: "proj_test", directory } },
data: {
resources:
requests === 1
? []
: [{ server: "docs", name: "API reference", uri: "https://example.com/api", description: "API docs" }],
templates: [],
},
})
}, events)
let data!: ReturnType<typeof useData>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
data = useData()
onMount(ready)
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await mounted
await wait(() => data.location.mcp.resource.list() !== undefined)
expect(data.location.mcp.resource.list()).toEqual([])
emitEvent(events, {
id: "evt_mcp_resources",
created: 0,
type: "mcp.resources.changed",
data: { server: "docs" },
})
await wait(() => data.location.mcp.resource.list()?.length === 1)
expect(data.location.mcp.resource.list()?.[0]).toEqual({
server: "docs",
name: "API reference",
uri: "https://example.com/api",
description: "API docs",
})
} finally {
app.renderer.destroy()
}
})
test("refreshes effective catalog data after catalog updates", async () => {
const events = createEventStream()
const requests = { model: 0, provider: 0 }
const calls = createFetch((url) => {
if (url.pathname === "/api/model") {
requests.model++
return json({ location: { directory, project: { id: "proj_test", directory } }, data: [] })
}
if (url.pathname === "/api/provider") {
requests.provider++
return json({ location: { directory, project: { id: "proj_test", directory } }, data: [] })
}
}, events)
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<box />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await wait(() => requests.model > 0 && requests.provider > 0)
const before = { ...requests }
emitEvent(events, { id: "evt_catalog", created: 0, type: "catalog.updated", data: {} })
await wait(() => requests.model > before.model && requests.provider > before.provider)
} finally {
app.renderer.destroy()
}
})
test("refreshes agents after agent updates", async () => {
const events = createEventStream()
let requests = 0
const calls = createFetch((url) => {
if (url.pathname !== "/api/agent") return
requests++
return json({
location: { directory, project: { id: "proj_test", directory } },
data: [
{
id: requests === 1 ? "build" : "reviewer",
request: { headers: {}, body: {} },
mode: "primary",
hidden: false,
permissions: [],
},
],
})
}, events)
let data!: ReturnType<typeof useData>
function Probe() {
data = useData()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await wait(() => data.location.agent.list()?.[0]?.id === "build")
emitEvent(events, { id: "evt_agent", created: 0, type: "agent.updated", data: {} })
await wait(() => data.location.agent.list()?.[0]?.id === "reviewer")
} finally {
app.renderer.destroy()
}
})
test("refreshes references after updates", async () => {
const events = createEventStream()
let requests = 0
const calls = createFetch((url) => {
if (url.pathname !== "/api/reference") return
requests++
return json({
location: { directory, project: { id: "proj_test", directory } },
data: requests === 1 ? [] : [{ name: "docs", path: "/docs", source: { type: "local", path: "/docs" } }],
})
}, events)
let data!: ReturnType<typeof useData>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
data = useData()
onMount(ready)
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await mounted
await wait(() => requests === 1)
emitEvent(events, { id: "evt_reference_1", created: 0, type: "reference.updated", data: {} })
await wait(() => data.location.reference.list()?.length === 1)
expect(data.location.reference.list()?.[0]?.name).toBe("docs")
} finally {
app.renderer.destroy()
}
})
test("keeps shell state scoped to location", async () => {
const events = createEventStream()
const other = "/tmp/opencode/other"
const calls = createFetch((url) => {
if (url.pathname !== "/api/shell") return
const requestDirectory = url.searchParams.get("location[directory]")
return json({
location: {
directory: requestDirectory ?? directory,
project: { id: "proj_test", directory: requestDirectory ?? directory },
},
data: [
{
id: requestDirectory === other ? "sh_other" : "sh_default",
status: "running",
command: requestDirectory === other ? "pnpm dev" : "bun test",
cwd: requestDirectory ?? directory,
shell: "/bin/sh",
file: "/tmp/opencode-shell",
metadata: { sessionID: requestDirectory === other ? "ses_other" : "ses_default" },
time: { started: 1 },
},
],
})
}, events)
let data!: ReturnType<typeof useData>
function Probe() {
data = useData()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await wait(() => data.shell.list().some((shell) => shell.id === "sh_default"))
await data.shell.refresh({ directory: other })
expect(data.shell.list().map((shell) => shell.id)).toEqual(["sh_default"])
expect(data.shell.list({ directory: other }).map((shell) => shell.id)).toEqual(["sh_other"])
events.emit({
id: "evt_shell_created",
created: 0,
type: "shell.created",
location: { directory: other },
data: {
info: {
id: "sh_live_other",
status: "running",
command: "npm run watch",
cwd: other,
shell: "/bin/sh",
file: "/tmp/opencode-shell-live",
metadata: { sessionID: "ses_other" },
time: { started: 2 },
},
},
})
await wait(() => data.shell.list({ directory: other }).some((shell) => shell.id === "sh_live_other"))
expect(data.shell.list().map((shell) => shell.id)).toEqual(["sh_default"])
} finally {
app.renderer.destroy()
}
})
test("adds and dismisses permission requests from live events", async () => {
const events = createEventStream()
const calls = createFetch(undefined, events)
let data!: ReturnType<typeof useData>
let sdk!: ReturnType<typeof useSDK>
function Probe() {
data = useData()
sdk = useSDK()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await wait(() => sdk.connection.status() === "connected")
emitEvent(events, {
id: "evt_permission_asked_1",
created: 0,
type: "permission.v2.asked",
data: {
id: "per_1",
sessionID: "ses_1",
action: "bash",
resources: ["bun test"],
},
})
emitEvent(events, {
id: "evt_permission_asked_2",
created: 0,
type: "permission.v2.asked",
data: {
id: "per_2",
sessionID: "ses_1",
action: "read",
resources: [".env"],
},
})
await wait(() => data.session.permission.list("ses_1")?.length === 2)
emitEvent(events, {
id: "evt_permission_replied_1",
created: 0,
type: "permission.v2.replied",
data: { sessionID: "ses_1", requestID: "per_1", reply: "once" },
})
await wait(() => data.session.permission.list("ses_1")?.length === 1)
expect(data.session.permission.list("ses_1")?.[0]?.id).toBe("per_2")
emitEvent(events, {
id: "evt_permission_replied_2",
created: 0,
type: "permission.v2.replied",
data: { sessionID: "ses_1", requestID: "per_2", reply: "reject" },
})
await wait(() => data.session.permission.list("ses_1")?.length === 0)
} finally {
app.renderer.destroy()
}
})
test("reconciles all pending permission requests when the event stream reconnects", async () => {
const events = createEventStream()
let requests = [
{ id: "per_old", sessionID: "ses_old", action: "read", resources: ["old.txt"] },
{ id: "per_keep", sessionID: "ses_keep", action: "shell", resources: ["bun test"] },
]
let calls = 0
const fetch = createFetch((url) => {
if (url.pathname !== "/api/permission/request") return
calls++
return json({ location: { directory, project: { id: "proj_test", directory } }, data: requests })
}, events)
let data!: ReturnType<typeof useData>
function Probe() {
data = useData()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(fetch.fetch)} api={createApi(fetch.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await wait(() => data.session.permission.list("ses_old")?.[0]?.id === "per_old")
expect(data.session.permission.list("ses_keep")?.[0]?.id).toBe("per_keep")
requests = [{ id: "per_new", sessionID: "ses_new", action: "edit", resources: ["new.txt"] }]
events.disconnect()
await wait(() => calls === 2 && data.session.permission.list("ses_new")?.[0]?.id === "per_new")
expect(data.session.permission.list("ses_old")).toBeUndefined()
expect(data.session.permission.list("ses_keep")).toBeUndefined()
} finally {
app.renderer.destroy()
}
})
test("adds, dismisses, and refreshes form requests", async () => {
const events = createEventStream()
const calls = createFetch((url) => {
if (url.pathname !== "/api/session/ses_1/form") return
return json({
data: [{ id: "frm_remote", sessionID: "ses_1", title: "Input requested", fields: formFields }],
})
}, events)
let data!: ReturnType<typeof useData>
let sdk!: ReturnType<typeof useSDK>
function Probe() {
data = useData()
sdk = useSDK()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await wait(() => sdk.connection.status() === "connected")
emitEvent(events, {
id: "evt_form_created_1",
created: 0,
type: "form.created",
data: { form: { id: "frm_1", sessionID: "ses_1", title: "Input requested", fields: formFields } },
})
emitEvent(events, {
id: "evt_form_created_duplicate",
created: 1,
type: "form.created",
data: { form: { id: "frm_1", sessionID: "ses_1", title: "Input requested", fields: formFields } },
})
await wait(() => data.session.form.list("ses_1")?.length === 1)
emitEvent(events, {
id: "evt_form_replied_1",
created: 2,
type: "form.replied",
data: { sessionID: "ses_1", id: "frm_1", answer: {} },
})
await wait(() => data.session.form.list("ses_1")?.length === 0)
emitEvent(events, {
id: "evt_form_created_2",
created: 3,
type: "form.created",
data: { form: { id: "frm_2", sessionID: "ses_1", title: "Input requested", fields: formFields } },
})
emitEvent(events, {
id: "evt_form_cancelled_2",
created: 4,
type: "form.cancelled",
data: { sessionID: "ses_1", id: "frm_2" },
})
await wait(() => data.session.form.list("ses_1")?.length === 0)
await data.session.form.refresh("ses_1")
expect(data.session.form.list("ses_1")?.map((form) => form.id)).toEqual(["frm_remote"])
} finally {
app.renderer.destroy()
}
})
test("tracks global forms by location", async () => {
const events = createEventStream()
const calls = createFetch(undefined, events)
const other = { directory: "/tmp/opencode-other", workspaceID: "wrk_other" }
let data!: ReturnType<typeof useData>
let sdk!: ReturnType<typeof useSDK>
function Probe() {
data = useData()
sdk = useSDK()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await wait(() => sdk.connection.status() === "connected")
events.emit({
id: "evt_form_created_global_other",
created: 0,
location: other,
type: "form.created",
data: {
form: { id: "frm_other", sessionID: "global", title: "Input requested", fields: formFields },
},
})
await wait(() => data.session.form.list("global", other)?.length === 1)
expect(data.session.form.list("global", { directory }) ?? []).toEqual([])
events.emit({
id: "evt_form_created_global_default",
created: 1,
location: { directory },
type: "form.created",
data: {
form: { id: "frm_default", sessionID: "global", title: "Input requested", fields: formFields },
},
})
await wait(() => data.session.form.list("global", { directory })?.length === 1)
events.emit({
id: "evt_form_replied_global_other",
created: 2,
location: other,
type: "form.replied",
data: { id: "frm_other", sessionID: "global", answer: {} },
})
await wait(() => data.session.form.list("global", other)?.length === 0)
expect(data.session.form.list("global", { directory })?.map((form) => form.id)).toEqual(["frm_default"])
} finally {
app.renderer.destroy()
}
})
test("refreshes global forms for the requested location", async () => {
const events = createEventStream()
const requests: URL[] = []
const other = { directory: "/tmp/opencode-other", workspaceID: "wrk_other" }
const calls = createFetch((url) => {
if (url.pathname !== "/api/form/request") return
requests.push(url)
const requestedDirectory = url.searchParams.get("location[directory]") ?? directory
const requestedWorkspace = url.searchParams.get("location[workspace]") ?? undefined
return json({
location: {
directory: requestedDirectory,
workspaceID: requestedWorkspace,
project: { id: "proj_test", directory: requestedDirectory },
},
data: [
{
id: requestedDirectory === other.directory ? "frm_other" : "frm_default",
sessionID: "global",
title: "Input requested",
fields: formFields,
},
],
})
}, events)
let data!: ReturnType<typeof useData>
let sdk!: ReturnType<typeof useSDK>
function Probe() {
data = useData()
sdk = useSDK()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await wait(() => sdk.connection.status() === "connected" && requests.length > 0)
requests.length = 0
await data.session.form.refresh("global", { directory })
await data.session.form.refresh("global", other)
expect(requests).toHaveLength(2)
expect(requests[1]?.searchParams.get("location[directory]")).toBe(other.directory)
expect(requests[1]?.searchParams.get("location[workspace]")).toBe(other.workspaceID)
expect(data.session.form.list("global", other)?.map((form) => form.id)).toEqual(["frm_other"])
expect(data.session.form.list("global", { directory })?.map((form) => form.id)).toEqual(["frm_default"])
} finally {
app.renderer.destroy()
}
})
test("refreshes global forms once per loaded location after reconnect", async () => {
const events = createEventStream()
const requests: URL[] = []
const counts = new Map<string, number>()
const home = { directory: process.cwd() }
const other = { directory: "/tmp/opencode-other", workspaceID: "wrk_other" }
const calls = createFetch((url) => {
if (url.pathname === "/api/location")
return json({ ...home, project: { id: "proj_test", directory: home.directory } })
if (url.pathname === "/api/session")
return json({
data: [
{ id: "ses_default", title: "Default", location: home, time: { created: 0, updated: 0 } },
{ id: "ses_other_1", title: "Other one", location: other, time: { created: 0, updated: 0 } },
{ id: "ses_other_2", title: "Other two", location: other, time: { created: 0, updated: 0 } },
],
cursor: {},
})
if (url.pathname !== "/api/form/request") return
requests.push(url)
const requestedDirectory = url.searchParams.get("location[directory]") ?? home.directory
const requestedWorkspace = url.searchParams.get("location[workspace]") ?? undefined
const count = (counts.get(requestedDirectory) ?? 0) + 1
counts.set(requestedDirectory, count)
return json({
location: {
directory: requestedDirectory,
workspaceID: requestedWorkspace,
project: { id: "proj_test", directory: requestedDirectory },
},
data: [
{
id: `frm_${requestedDirectory === other.directory ? "other" : "default"}_${count}`,
sessionID: "global",
title: "Input requested",
fields: formFields,
},
],
})
}, events)
let data!: ReturnType<typeof useData>
function Probe() {
data = useData()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await wait(
() =>
data.session.form.list("global", home)?.[0]?.id === "frm_default_1" &&
data.session.form.list("global", other)?.[0]?.id === "frm_other_1",
)
expect(requests).toHaveLength(2)
requests.length = 0
events.disconnect()
await wait(
() =>
data.session.form.list("global", home)?.[0]?.id === "frm_default_2" &&
data.session.form.list("global", other)?.[0]?.id === "frm_other_2",
4000,
)
expect(requests).toHaveLength(2)
expect(
requests.map((url) => [
url.searchParams.get("location[directory]") ?? directory,
url.searchParams.get("location[workspace]") ?? undefined,
]),
).toEqual([
[home.directory, undefined],
[other.directory, other.workspaceID],
])
} finally {
app.renderer.destroy()
}
})
test("reconciles all pending form requests when the event stream reconnects", async () => {
const events = createEventStream()
let requests = [
{ id: "frm_old", sessionID: "ses_old", title: "Input requested", fields: formFields },
{
id: "frm_keep",
sessionID: "ses_keep",
title: "Input requested",
fields: [{ key: "authorization", type: "external" as const, url: "https://example.com" }],
},
]
let calls = 0
const fetch = createFetch((url) => {
if (url.pathname !== "/api/form/request") return
calls++
return json({ location: { directory, project: { id: "proj_test", directory } }, data: requests })
}, events)
let data!: ReturnType<typeof useData>
function Probe() {
data = useData()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(fetch.fetch)} api={createApi(fetch.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await wait(() => data.session.form.list("ses_old")?.[0]?.id === "frm_old")
expect(data.session.form.list("ses_keep")?.[0]?.id).toBe("frm_keep")
requests = [{ id: "frm_new", sessionID: "ses_new", title: "Input requested", fields: formFields }]
events.disconnect()
await wait(() => calls === 2 && data.session.form.list("ses_new")?.[0]?.id === "frm_new")
expect(data.session.form.list("ses_old")).toBeUndefined()
expect(data.session.form.list("ses_keep")).toBeUndefined()
} finally {
app.renderer.destroy()
}
})
test("settles pending tools when a live failure arrives", async () => {
const events = createEventStream()
const calls = createFetch((url) => {
if (url.pathname === "/api/session/session-1/message/msg_model_1")
return json({
data: {
id: "msg_model_1",
type: "model-switched",
previous: { id: "model-1", providerID: "provider-1", variant: "medium" },
model: { id: "model-1", providerID: "provider-1", variant: "high" },
time: { created: 0 },
},
})
}, events)
let sync!: ReturnType<typeof useData>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
sync = useData()
onMount(ready)
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await mounted
emitEvent(events, {
id: "evt_agent_1",
created: 0,
type: "session.agent.selected",
durable: durable("session-1"),
data: { sessionID: "session-1", agent: "build" },
})
emitEvent(events, {
id: "evt_model_1",
created: 0,
type: "session.model.selected",
durable: durable("session-1", 1),
data: {
sessionID: "session-1",
model: { id: "model-1", providerID: "provider-1", variant: "high" },
},
})
emitEvent(events, {
id: "evt_step_started_1",
created: 0,
type: "session.step.started",
durable: durable("session-1", 2),
data: {
sessionID: "session-1",
assistantMessageID: "msg_explicit_assistant_9",
agent: "build",
model: { id: "model-1", providerID: "provider-1" },
},
})
emitEvent(events, {
id: "evt_input_1",
created: 0,
type: "session.tool.input.started",
durable: durable("session-1", 3),
data: {
sessionID: "session-1",
assistantMessageID: "msg_explicit_assistant_9",
callID: "call-1",
name: "bash",
},
})
emitEvent(events, {
id: "evt_called_1",
created: 0,
type: "session.tool.called",
durable: durable("session-1", 4),
data: {
sessionID: "session-1",
assistantMessageID: "msg_explicit_assistant_9",
callID: "call-1",
input: {},
executed: false,
state: { call: true },
},
})
emitEvent(events, {
id: "evt_failed_1",
created: 0,
type: "session.tool.failed",
durable: durable("session-1", 5),
data: {
sessionID: "session-1",
assistantMessageID: "msg_explicit_assistant_9",
callID: "call-1",
error: { type: "unknown", message: "aborted" },
executed: false,
resultState: { result: true },
},
})
await wait(() => {
const assistant = sync.session.message.get("session-1", "msg_explicit_assistant_9")
return (
assistant?.type === "assistant" &&
assistant.content[0]?.type === "tool" &&
assistant.content[0].state.status === "error"
)
})
const assistant = sync.session.message.get("session-1", "msg_explicit_assistant_9")
expect(assistant?.type).toBe("assistant")
if (assistant?.type !== "assistant") return
expect(assistant.id).toBe("msg_explicit_assistant_9")
const tool = assistant.content[0]
expect(tool?.type).toBe("tool")
if (tool?.type !== "tool") return
expect(tool.state.status).toBe("error")
if (tool.state.status !== "error") return
expect(tool.state.error).toEqual({ type: "unknown", message: "aborted" })
expect(tool.state.input).toEqual({})
expect(tool.state.structured).toEqual({})
expect(tool.state.content).toEqual([])
expect(tool.executed).toBe(false)
expect(tool.providerState).toEqual({ call: true })
expect(tool.providerResultState).toEqual({ result: true })
expect(sync.session.message.list("session-1").map((message) => message.type)).toEqual([
"agent-switched",
"model-switched",
"assistant",
])
expect(sync.session.message.get("session-1", "msg_model_1")).toMatchObject({
type: "model-switched",
previous: { id: "model-1", providerID: "provider-1", variant: "medium" },
model: { id: "model-1", providerID: "provider-1", variant: "high" },
})
} finally {
app.renderer.destroy()
}
})
test("renders admitted prompts immediately and tracks them until promoted", async () => {
const events = createEventStream()
const sessionID = "session-1"
const messageID = "msg_user_1"
const calls = createFetch((url) => {
if (url.pathname === `/api/session/${sessionID}/message`)
return json({
data: [{ id: messageID, type: "user", text: "hello", time: { created: 0 } }],
cursor: {},
})
}, events)
let sync!: ReturnType<typeof useData>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
sync = useData()
onMount(ready)
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await mounted
const received: string[] = []
const unsubscribe = sync.listen((event) => received.push(event.name))
emitEvent(events, {
id: "evt_admitted_1",
created: 0,
type: "session.input.admitted",
durable: durable(sessionID),
data: {
sessionID,
inputID: messageID,
input: { type: "user", data: { text: "hello" }, delivery: "steer" },
},
})
await wait(() => sync.session.message.list(sessionID)?.length === 1)
const admitted = sync.session.message.list(sessionID)?.[0]
expect(admitted).toMatchObject({ id: messageID, type: "user", text: "hello" })
expect(admitted?.metadata).toBeUndefined()
expect(sync.session.pending.list(sessionID)).toEqual([
{
id: messageID,
sessionID,
admittedSeq: 0,
timeCreated: 0,
type: "user",
data: { text: "hello" },
delivery: "steer",
},
])
expect(sync.session.input.list(sessionID)).toEqual([messageID])
await sync.session.message.refresh(sessionID)
expect(sync.session.message.list(sessionID)?.[0]?.metadata).toBeUndefined()
emitEvent(events, {
id: "evt_prompted_1",
created: 0,
type: "session.input.promoted",
durable: durable(sessionID, 1),
data: {
sessionID,
inputID: messageID,
},
})
await wait(() => received.at(-1) === "session.input.promoted")
expect(received.slice(-2)).toEqual(["session.input.admitted", "session.input.promoted"])
unsubscribe()
const message = sync.session.message.list(sessionID)?.[0]
expect(message?.type).toBe("user")
if (message?.type !== "user") return
expect(message).toMatchObject({ id: messageID, text: "hello" })
expect(message.metadata).toBeUndefined()
expect(sync.session.pending.list(sessionID)).toEqual([])
expect(sync.session.input.list(sessionID)).toEqual([])
expect(sync.session.message.list(sessionID).map((message) => message.id)).toEqual([messageID])
expect(sync.session.message.list("missing")).toEqual([])
expect(sync.session.message.get(sessionID, messageID)).toBe(message)
expect(sync.session.message.get(sessionID, "missing")).toBeUndefined()
expect(received).toHaveLength(3)
} finally {
app.renderer.destroy()
}
})
test("projects live instruction updates with their message ID", async () => {
const events = createEventStream()
const calls = createFetch(undefined, events)
let sync!: ReturnType<typeof useData>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
sync = useData()
onMount(ready)
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
try {
await mounted
emitEvent(events, {
id: "evt_instructions_1",
created: 0,
type: "session.instructions.updated",
durable: durable("session-1", 0, 2),
data: {
sessionID: "session-1",
delta: { "core/date": "0".repeat(64) },
},
})
await wait(() => sync.session.message.list("session-1")?.length === 1)
expect(sync.session.message.list("session-1")?.[0]).toMatchObject({
id: SessionMessage.ID.fromEvent(EventV2.ID.make("evt_instructions_1")),
type: "system",
text: "Instructions updated: core/date",
time: { created: 0 },
})
} finally {
app.renderer.destroy()
}
})
function sessionInfo(id: string, parentID: string | undefined, cost = 0) {
return {
id,
parentID,
projectID: "proj_test",
cost,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 0, updated: 0 },
title: id,
location: { directory },
}
}
// Mounts a DataProvider whose `/api/session/:id` responses are driven by the
// given parent map (sessionID -> parentID). Roots omit the entry. Reused across
// the family-index tests below.
async function mountData(parents: Record<string, string>, costs: Record<string, number> = {}) {
const calls = createFetch((url) => {
const match = url.pathname.match(/^\/api\/session\/([^/]+)$/)
if (match && match[1] !== "active")
return json({ data: sessionInfo(match[1], parents[match[1]], costs[match[1]]) })
})
let data!: ReturnType<typeof useData>
let ready!: () => void
const mounted = new Promise<void>((resolve) => {
ready = resolve
})
function Probe() {
data = useData()
onMount(ready)
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
await mounted
return { data, app }
}
test("groups an orphan child under its missing parent until the root arrives", async () => {
const { data, app } = await mountData({ child: "root" })
try {
await data.session.refresh("child")
// Parent info is absent, so the missing parent is the furthest-known ancestor.
expect(data.session.root("child")).toBe("root")
expect(data.session.family("child")).toEqual(["child"])
expect(data.session.family("root")).toEqual(["child"])
await data.session.refresh("root")
expect(data.session.root("root")).toBe("root")
// The tentative root entry folds into the now-known root's family.
expect(data.session.family("child")).toEqual(["child", "root"])
expect(data.session.family("root")).toEqual(["child", "root"])
} finally {
app.renderer.destroy()
}
})
test("indexes arbitrarily deep nesting under a single root", async () => {
const { data, app } = await mountData({ grandchild: "child", child: "root" })
try {
await data.session.refresh("grandchild")
expect(data.session.root("grandchild")).toBe("child")
expect(data.session.family("grandchild")).toEqual(["grandchild"])
await data.session.refresh("child")
// grandchild's tentative family (keyed by the missing "child") merges up
// toward the still-missing "root".
expect(data.session.root("child")).toBe("root")
expect(data.session.family("grandchild")).toEqual(["grandchild", "child"])
await data.session.refresh("root")
expect(data.session.root("grandchild")).toBe("root")
expect(data.session.root("child")).toBe("root")
expect(data.session.family("root")).toEqual(["grandchild", "child", "root"])
} finally {
app.renderer.destroy()
}
})
test("totals family cost for roots and keeps subagent cost scoped", async () => {
const { data, app } = await mountData(
{ grandchild: "child", child: "root" },
{ root: 1, child: 2, grandchild: 3 },
)
try {
await data.session.refresh("grandchild")
await data.session.refresh("child")
await data.session.refresh("root")
expect(data.session.cost("root")).toBe(6)
expect(data.session.cost("child")).toBe(2)
expect(data.session.cost("grandchild")).toBe(3)
} finally {
app.renderer.destroy()
}
})
test("re-registering an existing session is idempotent", async () => {
const { data, app } = await mountData({ grandchild: "child", child: "root" })
try {
await data.session.refresh("grandchild")
await data.session.refresh("child")
await data.session.refresh("root")
const before = data.session.family("root")
expect(before).toEqual(["grandchild", "child", "root"])
await data.session.refresh("child")
await data.session.refresh("root")
await data.session.refresh("grandchild")
expect(data.session.family("root")).toEqual(before)
expect(data.session.family("root")).toHaveLength(3)
} finally {
app.renderer.destroy()
}
})
test("stops at the last non-repeating ancestor on a parent cycle", async () => {
const { data, app } = await mountData({ x: "y", y: "x" })
try {
await data.session.refresh("x")
await data.session.refresh("y")
// Does not hang; walking up from "y" stops before re-entering "x".
expect(data.session.root("y")).toBe("x")
expect(data.session.family("y")).toEqual(["x", "y"])
} finally {
app.renderer.destroy()
}
})