Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
9.9 kB · 317 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318import { expect, test } from "bun:test"import { createRoot, createSignal } from "solid-js"import { createStore } from "solid-js/store"import { createSessionResolution } from "@/session/session-resolution"
type Session = { id: string; directory: string }
const sessionOf = (id: string): Session => ({ id, directory: `/dir/${id}` })
// Fake session store: get reads a reactive cache, sync returns a// deferred promise the test settles or fails explicitly. The session memo is// live (read below), so it recomputes eagerly on cache/status writes — throws// surface at the write site, which is also where the enclosing ErrorBoundary// would see them in the app. Assertions wrap write + read to cover both.function createFixture(initial: Record<string, Session> = {}) { const [cache, setCache] = createSignal(initial) const deferred = new Map<string, PromiseWithResolvers<unknown>>() const resolves: string[] = [] const messages = { syncs: [] as string[], ...Promise.withResolvers<unknown>() } const pending = { syncs: [] as string[] } return { resolves, messages, pending, sessions: { get: (id: string) => cache()[id], sync: (id: string) => { resolves.push(id) const entry = deferred.get(id) ?? Promise.withResolvers<unknown>() deferred.set(id, entry) return entry.promise }, message: { sync: (id: string) => { messages.syncs.push(id) return messages.promise }, }, pending: { sync: (id: string) => { pending.syncs.push(id) return Promise.resolve() }, }, }, settle(id: string, directory = `/dir/${id}`) { setCache({ ...cache(), [id]: { id, directory } }) deferred.get(id)?.resolve(undefined) deferred.delete(id) }, fail(id: string, error: unknown) { deferred.get(id)?.reject(error) // The real store does not cache failures: the inflight request entry is // dropped on rejection so the next resolve retries. deferred.delete(id) }, remove(id: string) { const next = { ...cache() } delete next[id] setCache(next) }, }}
// Two microtask ticks: one for the resolve promise handed back by the fixture,// one for the .then/.catch chain inside createSessionResolution.const flush = async () => { await Promise.resolve() await Promise.resolve()}
test("refreshes the current session on reconnect while keeping cached content visible", async () => { await createRoot(async (dispose) => { const fixture = createFixture({ ses_a: sessionOf("ses_a") }) const [connection, setConnection] = createStore({ connected: false }) const current = createSessionResolution( () => "ses_a", () => fixture.sessions, { connected: () => connection.connected }, )
expect(current()).toEqual(sessionOf("ses_a")) expect(fixture.resolves).toEqual([]) await flush() setConnection("connected", true) expect(fixture.resolves).toEqual(["ses_a"]) fixture.settle("ses_a") await flush()
setConnection("connected", false) expect(current()).toEqual(sessionOf("ses_a")) expect(fixture.resolves).toEqual(["ses_a"]) setConnection("connected", true) expect(fixture.resolves).toEqual(["ses_a", "ses_a"]) expect(current()).toEqual(sessionOf("ses_a")) expect(fixture.messages.syncs).toEqual(["ses_a", "ses_a"]) expect(fixture.pending.syncs).toEqual(["ses_a", "ses_a"]) fixture.settle("ses_a", "/worktrees/moved") await flush() expect(current()?.directory).toBe("/worktrees/moved") dispose() })})
test("starts metadata and messages in parallel once the route has a session ID", async () => { await createRoot(async (dispose) => { const fixture = createFixture() const [id, setId] = createSignal<string>() const current = createSessionResolution(id, () => fixture.sessions)
expect(current()).toBeUndefined() await flush() expect(fixture.resolves).toEqual([]) expect(fixture.messages.syncs).toEqual([])
setId("ses_a") expect(fixture.resolves).toEqual(["ses_a"]) expect(fixture.messages.syncs).toEqual(["ses_a"])
fixture.messages.resolve(undefined) await flush() expect(current()).toBeUndefined()
fixture.settle("ses_a") await flush() expect(current()?.id).toBe("ses_a")
dispose() })})
test("message failure does not fail metadata resolution", async () => { await createRoot(async (dispose) => { const fixture = createFixture() const current = createSessionResolution( () => "ses_a", () => fixture.sessions, )
await flush() fixture.messages.reject(new Error("message sync failed")) await flush() expect(current()).toBeUndefined()
fixture.settle("ses_a") await flush() expect(current()?.id).toBe("ses_a")
dispose() })})
// Session tabs on the same server share one route instance, so navigating to// another session changes the id in place; resolution must follow it instead// of reporting the new session as missing.test("re-resolves when navigating to an uncached session without a remount", async () => { await createRoot(async (dispose) => { const fixture = createFixture({ ses_a: sessionOf("ses_a") }) const [id, setId] = createSignal("ses_a") const current = createSessionResolution(id, () => fixture.sessions)
await flush() expect(current()?.id).toBe("ses_a") expect(fixture.resolves).toEqual([]) expect(fixture.messages.syncs).toEqual(["ses_a"])
expect(() => { setId("ses_b") current() }).not.toThrow() expect(fixture.resolves).toEqual(["ses_b"]) expect(fixture.messages.syncs).toEqual(["ses_a", "ses_b"])
fixture.settle("ses_b") await flush() expect(current()?.id).toBe("ses_b")
dispose() })})
// A late failure from a session the user already navigated away from must not// poison the currently viewed session.test("ignores a stale resolution failure after the target changes", async () => { await createRoot(async (dispose) => { const fixture = createFixture() const [id, setId] = createSignal("ses_a") const current = createSessionResolution(id, () => fixture.sessions)
await flush() setId("ses_b") fixture.fail("ses_a", new Error("Session not found: ses_a")) await flush()
expect(() => current()).not.toThrow() fixture.settle("ses_b") await flush() expect(current()?.id).toBe("ses_b")
dispose() })})
test("returning to a pruned session re-resolves instead of throwing not found", async () => { await createRoot(async (dispose) => { const fixture = createFixture() const [id, setId] = createSignal("ses_a") const current = createSessionResolution(id, () => fixture.sessions)
await flush() fixture.settle("ses_a") await flush()
setId("ses_b") fixture.settle("ses_b") await flush()
fixture.remove("ses_a") expect(() => { setId("ses_a") current() }).not.toThrow() expect(fixture.resolves).toEqual(["ses_a", "ses_b", "ses_a"]) expect(fixture.messages.syncs).toEqual(["ses_a", "ses_b", "ses_a"])
fixture.settle("ses_a") await flush() expect(current()?.id).toBe("ses_a")
dispose() })})
// A resolution that fails while its session is unfocused must not leave a// poisoned status behind: revisiting that session retries cleanly instead of// rethrowing the stale failure before the retry can start.test("revisiting a session whose resolution failed while unfocused retries cleanly", async () => { await createRoot(async (dispose) => { const fixture = createFixture() const [id, setId] = createSignal("ses_a") const current = createSessionResolution(id, () => fixture.sessions)
await flush() setId("ses_b") fixture.fail("ses_a", new Error("resolve failed")) await flush()
expect(() => { setId("ses_a") current() }).not.toThrow() expect(fixture.resolves).toEqual(["ses_a", "ses_b", "ses_a"]) expect(fixture.messages.syncs).toEqual(["ses_a", "ses_b", "ses_a"])
fixture.settle("ses_a") await flush() expect(current()?.id).toBe("ses_a")
dispose() })})
// The session accessor is reactive: replacing the data store (for example after// the server context is rebuilt) must gate out the old store's status and// re-resolve against the new one instead of fabricating a not-found.test("re-resolves against a replaced session store", async () => { await createRoot(async (dispose) => { const first = createFixture() const second = createFixture() const [store, setStore] = createSignal(first.sessions) const current = createSessionResolution(() => "ses_a", store)
await flush() first.settle("ses_a") await flush() expect(current()?.id).toBe("ses_a")
expect(() => { setStore(second.sessions) current() }).not.toThrow() await flush() expect(second.resolves).toEqual(["ses_a"]) expect(first.messages.syncs).toEqual(["ses_a"]) expect(second.messages.syncs).toEqual(["ses_a"])
second.settle("ses_a") await flush() expect(current()?.id).toBe("ses_a")
dispose() })})
// The viewed session is pinned in the cache, so disappearing after settlement// means it was deleted; the boundary must show the not found fallback.test("throws not found when the settled session is deleted", async () => { await createRoot(async (dispose) => { const fixture = createFixture() const current = createSessionResolution( () => "ses_a", () => fixture.sessions, )
await flush() fixture.settle("ses_a") await flush() expect(current()?.id).toBe("ses_a")
expect(() => { fixture.remove("ses_a") current() }).toThrow("Session not found: ses_a")
dispose() })})