import { FetchRepoTransport, parseAtUri } from '@radial/atproto' import { COLLECTIONS, type StoredRecord } from '@radial/core' import { FIXTURE_DIDS, fixtureSpace } from '@radial/core/fixture' import { afterEach, describe, expect, it, vi } from 'vitest' import { adopt, signOut } from './auth.svelte.js' import { FAKE_PDS, FakePds, latestVersions, memoryStorage } from './fake-pds.js' import { memoryDatabase, type SpaceDatabase } from './idb.js' import { Identities } from './identity.js' import { bootstrap, openSpace, session, settle, stop, sync, type OpenOptions, } from './session.svelte.js' import { openSpaceStores } from './store.js' import { goalsOf, isEnded, projectByName, unitsOf } from './space.js' // The live read path, end to end, against a PDS that only exists in this file: DID documents, // handle resolution, `getLatestCommit`, `listRecords`. Everything between the stub `fetch` and the // index — `Identities`, `FetchRepoTransport`, `RepoPoller`, `SpaceIngestor`, the persistent store, // `materialize()` — is the code the browser actually runs. // // The three things 6.2 claims are asserted here: a live space folds to the same views the fixture // does, a record written elsewhere shows up within one poll, and a warm reload costs one head check // per member and no rescan at all. const fixture = fixtureSpace() const SPACE_DID = parseAtUri(fixture.spaceUri).did const HANDLES: Record = { [FIXTURE_DIDS.tim]: 'tim.test', [FIXTURE_DIDS.ana]: 'ana.test', [FIXTURE_DIDS.planner]: 'planner.agents.test', [FIXTURE_DIDS.builder]: 'builder.agents.test', [FIXTURE_DIDS.reviewer]: 'reviewer.agents.test', } const currentVersions = (): StoredRecord[] => latestVersions(fixture.records) const versionsFor = (spaceUri: string): StoredRecord[] => currentVersions().map((record) => JSON.parse(JSON.stringify(record).replaceAll(fixture.spaceUri, spaceUri)) as StoredRecord, ) interface Harness { network: FakePds database: SpaceDatabase storage: ReturnType } const open = (harness: Harness): Promise => openSpace(fixture.spaceUri, options(harness)) const options = (harness: Harness): OpenOptions => ({ identities: new Identities({ fetcher: harness.network.fetch, storage: harness.storage }), transport: (identities) => new FetchRepoTransport(identities.pdsResolver, harness.network.fetch), openStores: (uri) => openSpaceStores(uri, async () => harness.database), poll: false, }) const harness = ( records = currentVersions(), handles = HANDLES, profiles: Record = {}, ): Harness => ({ network: new FakePds(records, handles, profiles), database: memoryDatabase(), storage: memoryStorage(), }) afterEach(async () => { stop() await signOut() session.space = undefined session.status = 'choosing' session.error = '' }) describe('opening a live space', () => { it('prunes the cold collection sweep to collections each repository actually holds', async () => { const live = harness() await open(live) const members = session.space?.index.members.filter((member) => member.active).length ?? 0 expect(live.network.counts('describeRepo')).toBe(members) expect(live.network.counts('listRecords')).toBeLessThan(members * 26) }) it('preserves the published Space on an unchanged tick while advancing its clocks', async () => { const live = harness() await open(live) const published = session.space if (!published) throw new Error('no space') published.asOf = '2000-01-01T00:00:00.000Z' published.syncedAt = published.asOf await sync() expect(session.space).toBe(published) expect(session.space?.asOf).not.toBe('2000-01-01T00:00:00.000Z') expect(session.space?.syncedAt).toBe(session.space?.asOf) }) it('publishes and remembers the latest distinct space', async () => { const uriB = `${fixture.spaceUri}-b` const spaceA = harness() const spaceB = harness(versionsFor(uriB)) await openSpace(fixture.spaceUri, options(spaceA)) spaceA.network.reset() await openSpace(uriB, options(spaceB)) expect(session.space?.uri).toBe(uriB) expect(session.status).toBe('ready') expect(session.recents[0]?.uri).toBe(uriB) await sync() expect(spaceA.network.calls).toHaveLength(0) }) it('does not let a superseded open publish or fail after the latest one', async () => { const uriB = `${fixture.spaceUri}-b` const slow = harness() const latest = harness(versionsFor(uriB)) let release: (() => void) | undefined const gate = new Promise((resolve) => (release = resolve)) const slowOptions = options(slow) const slowOpenStores = slowOptions.openStores if (!slowOpenStores) throw new Error('missing store opener') const openingA = openSpace(fixture.spaceUri, { ...slowOptions, openStores: async (uri) => { await gate return slowOpenStores(uri) }, }) await openSpace(uriB, options(latest)) const published = session.space release?.() await openingA expect(session.space).toBe(published) expect(session.space?.uri).toBe(uriB) expect(session.status).toBe('ready') expect(session.error).toBe('') }) it('draws the same views the fixture does, from repos over the wire', async () => { const live = harness() await open(live) expect(session.status).toBe('ready') expect(session.error).toBe('') const space = session.space if (!space) throw new Error('no space') expect(space.fixture).toBe(false) expect(space.uri).toBe(fixture.spaceUri) expect(space.index.ignored).toEqual([]) // The same nine system documents 6.1 asserts against the fixture, folded from a live scan. const project = projectByName(space.index, 'radial-ng') if (!project) throw new Error('no project') expect(unitsOf(space.index, project)).toHaveLength(9) expect(goalsOf(space.index, project).filter(isEnded)).toHaveLength(5) // Every `getLatestCommit` resolves a PDS first. Asking the directory once per member rather // than once per request is the difference between a poll tick costing one round trip and three. const members = space.index.members.filter((member) => member.active).length expect(live.network.counts('didDocument')).toBe(members) }) it('names members by a handle it resolved, not by one a record claimed', async () => { const live = harness() await open(live) const space = session.space if (!space) throw new Error('no space') // A human publishes no handle anywhere; the DID document is the only place one exists. expect(space.directory.get(FIXTURE_DIDS.tim).handle).toBe('tim.test') // An agent publishes one in its own record, which is a self-assertion — the resolved, // round-tripped handle is the one shown. expect(space.directory.get(FIXTURE_DIDS.planner).handle).toBe('planner.agents.test') }) it('drops a handle that does not round-trip back to the DID that claimed it', async () => { // Ana's document claims a handle that belongs to Tim. Showing it would be showing somebody // else's name, so she renders as a DID instead — the one thing that is always true. const live = harness(currentVersions(), { ...HANDLES, [FIXTURE_DIDS.ana]: 'tim.test' }) await open(live) const space = session.space if (!space) throw new Error('no space') expect(space.directory.get(FIXTURE_DIDS.ana).handle).toBeUndefined() expect(space.directory.get(FIXTURE_DIDS.ana).name).toBe('a1s2d3f4…') }) it('shows the picture a member publishes, and the derived disc for one who does not', async () => { // A profile picture is not a Radial record: it is read from the member's own repo, the same // place their handle comes from, and it is an overlay on the directory rather than a field the // index carries. An identity with no profile record keeps the colour-and-initials disc, so the // fallback is not an error path — it is what most agents will always look like. const live = harness(currentVersions(), HANDLES, { [FIXTURE_DIDS.tim]: 'bafyavatartim' }) await open(live) const space = session.space if (!space) throw new Error('no space') expect(space.directory.get(FIXTURE_DIDS.tim).avatar).toBe( `https://cdn.bsky.app/img/avatar_thumbnail/plain/${encodeURIComponent(FIXTURE_DIDS.tim)}/bafyavatartim@jpeg`, ) const planner = space.directory.get(FIXTURE_DIDS.planner) expect(planner.avatar).toBeUndefined() expect(planner.initials).toBe('PL') // One profile read per member on a cold scan, and none on the next tick: the answer is cached // beside the handle and the PDS. const profileReads = live.network.calls.filter((call) => call.detail.startsWith('app.bsky.actor.profile'), ) expect(profileReads).toHaveLength(space.index.members.filter((member) => member.active).length) live.network.reset() await sync() expect(live.network.counts('getRecord')).toBe(0) }) it('refuses a URI that does not name a space record', async () => { await openSpace('at://did:plc:qv7hjr2mzk4x/com.disnetdev.radial.goal/abc') expect(session.status).toBe('choosing') expect(session.error).toContain('space') expect(session.space).toBeUndefined() }) it('says so when the space record is not there', async () => { const live = harness([]) await open(live) expect(session.status).toBe('failed') expect(session.error).toContain('Space record not found') }) }) describe('opening a deep link', () => { it('keeps a private bookmark deep link off public stores and asks for a fresh ticket', async () => { const live = harness() const did = FIXTURE_DIDS.tim adopt(did, (path, init) => live.network.fetch(new URL(path, FAKE_PDS), init)) const openStores = vi.fn() await bootstrap( new URL(`/?space=${encodeURIComponent(fixture.spaceUri)}`, 'https://radl.app'), { ...options(live), privateMeta: async () => undefined, privateBookmark: async (owner, uri) => owner === did && uri === fixture.spaceUri, openStores, }, ) expect(session.status).toBe('needs-ticket') expect(session.error).toContain('fresh transfer ticket') expect(openStores).not.toHaveBeenCalled() }) it('opens a public space when the account’s own PDS will not answer the bookmark check', async () => { const live = harness() const did = FIXTURE_DIDS.tim adopt(did, (path, init) => live.network.fetch(new URL(path, FAKE_PDS), init)) // The check reads the USER's repo on the way into every space, including one hosted somewhere // else entirely. Their PDS being down is not a fact about this space. await bootstrap( new URL(`/?space=${encodeURIComponent(fixture.spaceUri)}`, 'https://radl.app'), { ...options(live), privateMeta: async () => undefined, privateBookmark: async () => { throw new Error('XrpcError: upstream is down') }, }, ) expect(session.status).toBe('ready') expect(session.space?.uri).toBe(fixture.spaceUri) }) it('opens an explicit public space without requiring an account', async () => { const live = harness() await bootstrap( new URL(`/p/radial?space=${encodeURIComponent(fixture.spaceUri)}`, 'https://radl.app'), options(live), ) expect(session.status).toBe('ready') expect(session.space?.uri).toBe(fixture.spaceUri) }) it('recovers an older goal link from the public goal record', async () => { const live = harness() const goal = currentVersions().find((record) => record.collection === COLLECTIONS.goal) if (!goal) throw new Error('fixture goal missing') const parts = parseAtUri(goal.uri) await bootstrap(new URL(`/g/${parts.did}/${parts.rkey}`, 'https://radl.app'), options(live)) expect(session.status).toBe('ready') expect(session.space?.uri).toBe(fixture.spaceUri) }) it('does not let a superseded goal lookup replace a newer explicit space', async () => { const uriB = `${fixture.spaceUri}-b` const slow = harness() const latest = harness(versionsFor(uriB)) const goal = currentVersions().find((record) => record.collection === COLLECTIONS.goal) if (!goal) throw new Error('fixture goal missing') const parts = parseAtUri(goal.uri) let release: (() => void) | undefined const gate = new Promise((resolve) => (release = resolve)) const slowOptions = options(slow) const slowTransport = slowOptions.transport if (!slowTransport) throw new Error('missing transport') const older = bootstrap(new URL(`/g/${parts.did}/${parts.rkey}`, 'https://radl.app'), { ...slowOptions, transport: (identities) => { const transport = slowTransport(identities) return { resolvePds: transport.resolvePds.bind(transport), getLatestCommit: transport.getLatestCommit.bind(transport), listRecords: transport.listRecords.bind(transport), async getRecord(uri) { await gate return transport.getRecord(uri) }, } }, }) await bootstrap( new URL(`/p/radial?space=${encodeURIComponent(uriB)}`, 'https://radl.app'), options(latest), ) const published = session.space release?.() await older expect(session.space).toBe(published) expect(session.space?.uri).toBe(uriB) expect(session.status).toBe('ready') expect(session.error).toBe('') }) it('does not let a superseded goal lookup failure overwrite a newer space', async () => { const uriB = `${fixture.spaceUri}-b` const slow = harness() const latest = harness(versionsFor(uriB)) const goal = currentVersions().find((record) => record.collection === COLLECTIONS.goal) if (!goal) throw new Error('fixture goal missing') const parts = parseAtUri(goal.uri) let reject: ((error: Error) => void) | undefined const failure = new Promise((_resolve, rejectPromise) => (reject = rejectPromise)) const slowOptions = options(slow) const slowTransport = slowOptions.transport if (!slowTransport) throw new Error('missing transport') const older = bootstrap(new URL(`/g/${parts.did}/${parts.rkey}`, 'https://radl.app'), { ...slowOptions, transport: (identities) => { const transport = slowTransport(identities) return { resolvePds: transport.resolvePds.bind(transport), getLatestCommit: transport.getLatestCommit.bind(transport), listRecords: transport.listRecords.bind(transport), getRecord: () => failure, } }, }) await bootstrap( new URL(`/p/radial?space=${encodeURIComponent(uriB)}`, 'https://radl.app'), options(latest), ) const published = session.space reject?.(new Error('stale lookup failed')) await older expect(session.space).toBe(published) expect(session.space?.uri).toBe(uriB) expect(session.status).toBe('ready') expect(session.error).toBe('') }) it('does not interpret an ambiguous older project link against a remembered space', async () => { await bootstrap(new URL('/p/radial', 'https://radl.app')) expect(session.status).toBe('choosing') expect(session.space).toBeUndefined() expect(session.error).toContain('which space') }) }) describe('staying live', () => { it('picks up a record written elsewhere within one poll', async () => { const live = harness() await open(live) const before = session.space if (!before) throw new Error('no space') const reviewQueue = before.index.goals.find( (goal) => goal.target.value.title === 'Review queue in the web UI', ) if (!reviewQueue) throw new Error('fixture goal missing') const threadBefore = reviewQueue.messages.length // What `radial message post` does from a container, or a human from the CLI. live.network.append({ did: FIXTURE_DIDS.tim, collection: COLLECTIONS.message, rkey: 'msg-live-1', uri: `at://${FIXTURE_DIDS.tim}/${COLLECTIONS.message}/msg-live-1`, cid: 'cid-msg-live-1', rev: '9999999999999', value: { $type: COLLECTIONS.message, goal: { uri: reviewQueue.target.uri, cid: reviewQueue.target.cid }, body: 'Written from the CLI while the tab was open.', mentions: [], createdAt: '2026-07-25T14:20:00Z', }, } as StoredRecord) await sync() const after = session.space if (!after) throw new Error('no space') const refreshed = after.index.goals.find((goal) => goal.target.uri === reviewQueue.target.uri) expect(refreshed?.messages).toHaveLength(threadBefore + 1) expect(after.syncedAt).not.toBe(before.syncedAt) }) it('keeps the last good index on screen when a poll fails', async () => { const live = harness() await open(live) const before = session.space // A member's PDS goes away. That makes the view stale, not wrong: the records already folded // are still the records, and replacing the screen with an error would throw away work the human // is reading. live.network.offline = true await sync() expect(session.error).toBe('offline') expect(session.status).toBe('ready') expect(session.space).toBe(before) // …and it recovers on the next tick without anything being re-opened. live.network.offline = false await sync() expect(session.error).toBe('') }) }) describe('a warm reload', () => { it('costs one head check per member and no rescan', async () => { const live = harness() await open(live) const members = session.space?.index.members.filter((member) => member.active).length ?? 0 expect(members).toBeGreaterThan(1) await settle() stop() live.network.reset() await open(live) expect(session.status).toBe('ready') expect(live.network.counts('listRecords')).toBe(0) expect(live.network.counts('getLatestCommit')).toBe(members) // The identity cache came back from storage too, so no directory was asked anything. expect(live.network.counts('didDocument')).toBe(0) expect(live.network.counts('resolveHandle')).toBe(0) }) it('does rescan a repo whose head moved', async () => { const live = harness() await open(live) await settle() stop() live.network.heads.set(FIXTURE_DIDS.planner, (live.network.heads.get(FIXTURE_DIDS.planner) ?? 0) + 1) live.network.reset() await open(live) const listed = live.network.calls.filter((call) => call.method === 'listRecords') expect(listed.length).toBeGreaterThan(0) expect(listed.every((call) => call.did === FIXTURE_DIDS.planner)).toBe(true) }) })