Files
openchamber/packages/ui/src/sync/event-pipeline.test.ts
T
Bohdan Triapitsyn 85400459e9 perf: overhaul session loading, caching, and runtime isolation (#2360)
Improve OpenChamber responsiveness under large session workloads while fixing
cache, synchronization, and persistence correctness across runtimes, projects,
directories, and worktrees.

- prioritize selected and visible sessions during bootstrap and defer
  non-critical enrichment work
- reduce redundant message loading, event processing, store publication, and
  hidden sidebar work
- prevent stale session and message requests from overwriting newer
  authoritative state
- preserve existing data when authoritative fetches fail instead of treating
  failures as successful empty responses
- scope session materialization, messages, drafts, queues, todos, pins,
  permissions, folders, tabs, Git state, and pull request data by runtime and
  directory identity
- harden runtime switching, reconnect, cleanup, mutation reconciliation, and
  persisted-state ordering
- preserve live subagent Task linkage when metadata arrives after an older
  message request or while streaming parts are suspended
- coalesce overlapping tail refreshes without losing newer refresh demand
- improve cold-session loading by moving deferrable work out of the critical
  bootstrap path
- isolate URL authentication, mobile credentials, native secrets, and other
  runtime-owned state across endpoint changes
- bound long-lived caches and remove avoidable allocations from event and
  rendering hot paths
- limit virtualization to archive collections where it improves rendering
  without disrupting active sidebar layout
- stabilize session folders, pin ordering, expanded state, and persisted
  sidebar behavior
- open skill files through the same secure editor and outside-workspace grant
  flow used by file navigation, including worktree sessions
- expand regression coverage for stale completions, runtime collisions,
  reconnect behavior, persistence races, authoritative empty results, and
  subagent refresh ordering
- document the updated synchronization, cache ownership, performance, and
  runtime-isolation invariants
2026-07-21 20:52:20 +03:00

263 lines
7.8 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import type { Event, OpencodeClient } from "@opencode-ai/sdk/v2/client"
import { createEventPipeline } from "./event-pipeline"
const failAfter = (ms: number) => new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error("Timed out waiting for event pipeline flush")), ms)
})
function partUpdatedEvent(text: string): Event {
return {
type: "message.part.updated",
properties: {
part: {
id: "prt_1",
messageID: "msg_1",
sessionID: "ses_1",
type: "text",
text,
},
},
} as Event
}
function deltaEvent(delta: string): Event {
return {
type: "message.part.delta",
properties: {
messageID: "msg_1",
partID: "prt_1",
field: "text",
delta,
},
} as Event
}
function statusEvent(type: "busy" | "retry"): Event {
return {
type: "session.status",
properties: {
sessionID: "ses_1",
status: type === "busy"
? { type }
: { type, attempt: 1, message: "retrying", next: 1 },
},
} as Event
}
function createSdk(events: Event[], streamFinished: () => void): OpencodeClient {
return {
global: {
event: async ({ signal }: { signal: AbortSignal }) => ({
stream: (async function* () {
for (const payload of events) {
yield { directory: "/repo", payload }
}
streamFinished()
await new Promise<void>((resolve) => {
if (signal.aborted) {
resolve()
return
}
signal.addEventListener("abort", () => resolve(), { once: true })
})
})(),
}),
},
} as unknown as OpencodeClient
}
describe("createEventPipeline", () => {
test("delivers one ordered batch per directory flush", async () => {
let resolveStreamFinished!: () => void
const streamFinished = new Promise<void>((resolve) => {
resolveStreamFinished = resolve
})
let resolveDelivered!: (events: readonly Event[]) => void
const deliveredBatch = new Promise<readonly Event[]>((resolve) => {
resolveDelivered = resolve
})
const pipeline = createEventPipeline({
sdk: createSdk([
partUpdatedEvent("a"),
deltaEvent("b"),
partUpdatedEvent("ab"),
], resolveStreamFinished),
onEvents: (_directory, events) => resolveDelivered([...events]),
transport: "sse",
heartbeatTimeoutMs: 1_000,
})
try {
await streamFinished
const delivered = await Promise.race([deliveredBatch, failAfter(500)])
expect(delivered.map((event) => event.type)).toEqual([
"message.part.updated",
"message.part.delta",
"message.part.updated",
])
} finally {
pipeline.cleanup()
}
})
test("does not coalesce session status across an idle barrier", async () => {
let resolveStreamFinished!: () => void
const streamFinished = new Promise<void>((resolve) => {
resolveStreamFinished = resolve
})
let resolveDelivered!: (events: readonly Event[]) => void
const deliveredBatch = new Promise<readonly Event[]>((resolve) => {
resolveDelivered = resolve
})
const pipeline = createEventPipeline({
sdk: createSdk([
statusEvent("busy"),
{ type: "session.idle", properties: { sessionID: "ses_1" } } as Event,
statusEvent("retry"),
], resolveStreamFinished),
onEvents: (_directory, events) => resolveDelivered([...events]),
transport: "sse",
heartbeatTimeoutMs: 1_000,
})
try {
await streamFinished
const delivered = await Promise.race([deliveredBatch, failAfter(500)])
expect(delivered.map((event) => event.type)).toEqual([
"session.status",
"session.idle",
"session.status",
])
expect((delivered[2]?.properties as { status?: { type?: string } }).status?.type).toBe("retry")
} finally {
pipeline.cleanup()
}
})
test("preserves part update order around text deltas", async () => {
let resolveStreamFinished!: () => void
const streamFinished = new Promise<void>((resolve) => {
resolveStreamFinished = resolve
})
let resolveDelivered!: () => void
const deliveredAll = new Promise<void>((resolve) => {
resolveDelivered = resolve
})
const delivered: Event[] = []
const pipeline = createEventPipeline({
sdk: createSdk([
partUpdatedEvent("a"),
deltaEvent("b"),
partUpdatedEvent("ab"),
], resolveStreamFinished),
onEvent: (_directory, payload) => {
delivered.push(payload)
if (delivered.length === 3) {
resolveDelivered()
}
},
transport: "sse",
heartbeatTimeoutMs: 1_000,
})
try {
await streamFinished
await Promise.race([deliveredAll, failAfter(500)])
} finally {
pipeline.cleanup()
}
expect(delivered.map((event) => {
if (event.type === "message.part.delta") {
return `delta:${(event.properties as { delta: string }).delta}`
}
return `updated:${((event.properties as { part: { text: string } }).part).text}`
})).toEqual(["updated:a", "delta:b", "updated:ab"])
})
test("does not merge deltas across an intervening part snapshot", async () => {
let resolveStreamFinished!: () => void
const streamFinished = new Promise<void>((resolve) => {
resolveStreamFinished = resolve
})
let resolveDelivered!: () => void
const deliveredAll = new Promise<void>((resolve) => {
resolveDelivered = resolve
})
const delivered: Event[] = []
const pipeline = createEventPipeline({
sdk: createSdk([
partUpdatedEvent("a"),
deltaEvent("b"),
partUpdatedEvent("ab"),
deltaEvent("c"),
], resolveStreamFinished),
onEvent: (_directory, payload) => {
delivered.push(payload)
if (delivered.length === 4) {
resolveDelivered()
}
},
transport: "sse",
heartbeatTimeoutMs: 1_000,
})
try {
await streamFinished
await Promise.race([deliveredAll, new Promise<void>((resolve) => setTimeout(resolve, 300))])
} finally {
pipeline.cleanup()
}
// The "ab" snapshot is a coalescing barrier: the trailing "c" delta must
// stay a separate event after it, not merge into the "b" delta queued
// before the snapshot (which the snapshot would then overwrite).
expect(delivered.map((event) => {
if (event.type === "message.part.delta") {
return `delta:${(event.properties as { delta: string }).delta}`
}
return `updated:${((event.properties as { part: { text: string } }).part).text}`
})).toEqual(["updated:a", "delta:b", "updated:ab", "delta:c"])
})
test("normalizes openchamber session status events", async () => {
let resolveStreamFinished!: () => void
const streamFinished = new Promise<void>((resolve) => {
resolveStreamFinished = resolve
})
let resolveDelivered!: (event: Event) => void
const deliveredEvent = new Promise<Event>((resolve) => {
resolveDelivered = resolve
})
const pipeline = createEventPipeline({
sdk: createSdk([
{
type: "openchamber:session-status",
properties: {
sessionID: "ses_1",
status: "idle",
},
} as unknown as Event,
], resolveStreamFinished),
onEvent: (_directory, payload) => {
resolveDelivered(payload)
},
transport: "sse",
heartbeatTimeoutMs: 1_000,
})
try {
await streamFinished
const delivered = await Promise.race([deliveredEvent, failAfter(500)])
expect(delivered.type).toBe("session.status")
expect(delivered.properties).toEqual({
sessionID: "ses_1",
status: { type: "idle" },
})
} finally {
pipeline.cleanup()
}
})
})