2026-07-21 20:52:20 +03:00
|
|
|
import { describe, expect, test } from "bun:test"
|
|
|
|
|
import type { Message, OpencodeClient, Part } from "@opencode-ai/sdk/v2/client"
|
|
|
|
|
import { ChildStoreManager } from "./child-store"
|
|
|
|
|
import { SessionMessageLoader } from "./session-message-loader"
|
|
|
|
|
|
|
|
|
|
const createRecord = (sessionID: string, id = "msg_1") => ({
|
|
|
|
|
info: { id, sessionID, role: "user", time: { created: 1 } } as Message,
|
|
|
|
|
parts: [{ id: `part_${id}`, messageID: id, sessionID, type: "text", text: "hello" }] as Part[],
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const deferred = <T>() => {
|
|
|
|
|
let resolve!: (value: T) => void
|
|
|
|
|
const promise = new Promise<T>((next) => {
|
|
|
|
|
resolve = next
|
|
|
|
|
})
|
|
|
|
|
return { promise, resolve }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const response = (data: ReturnType<typeof createRecord>[], cursor?: string) => ({
|
|
|
|
|
data,
|
|
|
|
|
response: { headers: { get: (name: string) => name === "x-next-cursor" ? cursor ?? null : null } },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const createLoader = (messages: (input: {
|
|
|
|
|
sessionID: string
|
|
|
|
|
directory?: string
|
|
|
|
|
limit?: number
|
|
|
|
|
before?: string
|
|
|
|
|
}) => Promise<unknown>) => {
|
|
|
|
|
const childStores = new ChildStoreManager()
|
|
|
|
|
const sdk = { session: { messages } } as unknown as OpencodeClient
|
|
|
|
|
const loader = new SessionMessageLoader(childStores, { sdk, runtimeKey: "runtime-a" })
|
|
|
|
|
return { childStores, loader }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe("SessionMessageLoader", () => {
|
|
|
|
|
test("deduplicates navigation and reactive loading for the same target", async () => {
|
|
|
|
|
const pending = deferred<ReturnType<typeof response>>()
|
|
|
|
|
let calls = 0
|
|
|
|
|
const { childStores, loader } = createLoader(async () => {
|
|
|
|
|
calls += 1
|
|
|
|
|
return pending.promise
|
|
|
|
|
})
|
|
|
|
|
const target = { directory: "/repo", sessionID: "session-a" }
|
|
|
|
|
|
|
|
|
|
const navigation = loader.ensure(target, { reason: "navigation" })
|
|
|
|
|
const reactive = loader.ensure(target, { reason: "reactive" })
|
|
|
|
|
expect(calls).toBe(1)
|
|
|
|
|
|
|
|
|
|
pending.resolve(response([createRecord(target.sessionID)]))
|
|
|
|
|
await Promise.all([navigation, reactive])
|
|
|
|
|
|
|
|
|
|
expect(loader.getSnapshot(target).status).toBe("ready")
|
|
|
|
|
expect(childStores.getChild(target.directory)?.getState().message[target.sessionID]?.length).toBe(1)
|
|
|
|
|
loader.dispose()
|
|
|
|
|
childStores.disposeAll()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("runs a requested tail refresh after an older in-flight load", async () => {
|
|
|
|
|
const initial = deferred<ReturnType<typeof response>>()
|
|
|
|
|
const refresh = deferred<ReturnType<typeof response>>()
|
|
|
|
|
let calls = 0
|
|
|
|
|
const limits: number[] = []
|
|
|
|
|
const { childStores, loader } = createLoader(async ({ limit }) => {
|
|
|
|
|
calls += 1
|
|
|
|
|
limits.push(limit ?? 0)
|
|
|
|
|
return calls === 1 ? initial.promise : refresh.promise
|
|
|
|
|
})
|
|
|
|
|
const target = { directory: "/repo", sessionID: "session-a" }
|
|
|
|
|
|
|
|
|
|
const loading = loader.ensure(target, { reason: "navigation" })
|
|
|
|
|
const refreshing = loader.refreshTail(target, 30)
|
|
|
|
|
const duplicateRefresh = loader.refreshTail(target, 80)
|
|
|
|
|
expect(calls).toBe(1)
|
|
|
|
|
expect(duplicateRefresh).toBe(refreshing)
|
|
|
|
|
|
|
|
|
|
initial.resolve(response([createRecord(target.sessionID, "msg_1")]))
|
|
|
|
|
await loading
|
|
|
|
|
await Promise.resolve()
|
|
|
|
|
expect(calls).toBe(2)
|
|
|
|
|
expect(limits).toEqual([50, 80])
|
|
|
|
|
|
|
|
|
|
refresh.resolve(response([createRecord(target.sessionID, "msg_2")]))
|
|
|
|
|
await Promise.all([refreshing, duplicateRefresh])
|
|
|
|
|
|
|
|
|
|
expect(childStores.getChild(target.directory)?.getState().message[target.sessionID]?.map((message) => message.id))
|
|
|
|
|
.toEqual(["msg_1", "msg_2"])
|
|
|
|
|
loader.dispose()
|
|
|
|
|
childStores.disposeAll()
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-29 17:53:55 +03:00
|
|
|
test("preserves complete history coverage across a tail refresh", async () => {
|
|
|
|
|
let calls = 0
|
|
|
|
|
const { childStores, loader } = createLoader(async ({ sessionID }) => {
|
|
|
|
|
calls += 1
|
|
|
|
|
return calls === 1
|
|
|
|
|
? response([createRecord(sessionID, "msg_1")])
|
|
|
|
|
: response([createRecord(sessionID, "msg_2")], "stale-tail-cursor")
|
|
|
|
|
})
|
|
|
|
|
const target = { directory: "/repo", sessionID: "session-a" }
|
|
|
|
|
|
|
|
|
|
await loader.ensure(target)
|
|
|
|
|
expect(loader.getSnapshot(target).complete).toBe(true)
|
|
|
|
|
expect(loader.getSnapshot(target).cursor).toBe(undefined)
|
|
|
|
|
|
|
|
|
|
await loader.refreshTail(target, 2)
|
|
|
|
|
|
|
|
|
|
expect(loader.getSnapshot(target).complete).toBe(true)
|
|
|
|
|
expect(loader.getSnapshot(target).cursor).toBe(undefined)
|
|
|
|
|
loader.dispose()
|
|
|
|
|
childStores.disposeAll()
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-21 20:52:20 +03:00
|
|
|
test("does not deduplicate identical session IDs across directories", async () => {
|
|
|
|
|
const calls: string[] = []
|
|
|
|
|
const { childStores, loader } = createLoader(async ({ directory, sessionID }) => {
|
|
|
|
|
calls.push(directory ?? "")
|
|
|
|
|
return response([createRecord(sessionID)])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
loader.ensure({ directory: "/repo-a", sessionID: "shared" }),
|
|
|
|
|
loader.ensure({ directory: "/repo-b", sessionID: "shared" }),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
expect(calls.sort()).toEqual(["/repo-a", "/repo-b"])
|
|
|
|
|
loader.dispose()
|
|
|
|
|
childStores.disposeAll()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("exposes a retryable error without clearing an existing snapshot", async () => {
|
|
|
|
|
let fail = true
|
|
|
|
|
const { childStores, loader } = createLoader(async ({ sessionID }) => {
|
|
|
|
|
if (fail) return { error: { message: "rejected" }, response: { status: 400 } }
|
|
|
|
|
return response([createRecord(sessionID)])
|
|
|
|
|
})
|
|
|
|
|
const target = { directory: "/repo", sessionID: "session-a" }
|
|
|
|
|
const store = childStores.ensureChild(target.directory, { bootstrap: false })
|
|
|
|
|
store.setState({ message: { [target.sessionID]: [{ id: "cached", sessionID: target.sessionID, role: "user", time: { created: 0 } } as Message] } })
|
|
|
|
|
|
|
|
|
|
await loader.ensure(target, { force: true })
|
|
|
|
|
expect(loader.getSnapshot(target).status).toBe("error")
|
|
|
|
|
expect(store.getState().message[target.sessionID]?.[0]?.id).toBe("cached")
|
|
|
|
|
|
|
|
|
|
fail = false
|
|
|
|
|
await loader.ensure(target, { force: true })
|
|
|
|
|
expect(loader.getSnapshot(target).status).toBe("ready")
|
|
|
|
|
loader.dispose()
|
|
|
|
|
childStores.disposeAll()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("prevents an evicted in-flight request from repopulating the store", async () => {
|
|
|
|
|
const pending = deferred<ReturnType<typeof response>>()
|
|
|
|
|
const { childStores, loader } = createLoader(async () => pending.promise)
|
|
|
|
|
const target = { directory: "/repo", sessionID: "session-a" }
|
|
|
|
|
|
|
|
|
|
const loading = loader.ensure(target)
|
|
|
|
|
loader.invalidateSession(target)
|
|
|
|
|
pending.resolve(response([createRecord(target.sessionID)]))
|
|
|
|
|
await loading
|
|
|
|
|
|
|
|
|
|
expect(childStores.getChild(target.directory)?.getState().message[target.sessionID]).toBe(undefined)
|
|
|
|
|
expect(loader.getSnapshot(target).status).toBe("idle")
|
|
|
|
|
loader.dispose()
|
|
|
|
|
childStores.disposeAll()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("treats an empty successful response as resolved authoritative state", async () => {
|
|
|
|
|
const { childStores, loader } = createLoader(async () => response([]))
|
|
|
|
|
const target = { directory: "/repo", sessionID: "empty" }
|
|
|
|
|
|
|
|
|
|
await loader.ensure(target)
|
|
|
|
|
|
|
|
|
|
expect(loader.getSnapshot(target).resolved).toBe(true)
|
|
|
|
|
expect(loader.getSnapshot(target).complete).toBe(true)
|
|
|
|
|
expect(childStores.getChild(target.directory)?.getState().message[target.sessionID]).toEqual([])
|
|
|
|
|
loader.dispose()
|
|
|
|
|
childStores.disposeAll()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test("retries a missing message payload instead of treating it as an empty snapshot", async () => {
|
|
|
|
|
let calls = 0
|
|
|
|
|
const { childStores, loader } = createLoader(async ({ sessionID }) => {
|
|
|
|
|
calls += 1
|
|
|
|
|
return calls === 1 ? {} : response([createRecord(sessionID)])
|
|
|
|
|
})
|
|
|
|
|
const target = { directory: "/repo", sessionID: "session-a" }
|
|
|
|
|
|
|
|
|
|
await loader.ensure(target)
|
|
|
|
|
|
|
|
|
|
expect(calls).toBe(2)
|
|
|
|
|
expect(loader.getSnapshot(target).status).toBe("ready")
|
|
|
|
|
expect(childStores.getChild(target.directory)?.getState().message[target.sessionID]?.length).toBe(1)
|
|
|
|
|
loader.dispose()
|
|
|
|
|
childStores.disposeAll()
|
|
|
|
|
})
|
|
|
|
|
})
|