import { FetchRepoTransport, XrpcError, parseAtUri } from '@radial/atproto' import { COLLECTIONS, buildTurnBundle, imageBlobUrl, parseImageLocator, tally, type StoredRecord, } from '@radial/core' import { FIXTURE_DIDS, fixtureSpace } from '@radial/core/fixture' import { afterEach, describe, expect, it } from 'vitest' import { addMemberArgs, autoReviewArgs, closeArgs, memberRows, ownsProject, projectArchiveArgs, projectArgs, projectChanges, projectEditArgs, projectEditDraft, registryRows, removeMemberArgs, spaceArgs, typeArgs, } from './admin.js' import { account, adopt, signOut } from './auth.svelte.js' import { FAKE_PDS, FakePds, latestVersions, memoryStorage } from './fake-pds.js' import { memoryDatabase } from './idb.js' import { Identities } from './identity.js' import { captureDraft, requestableTypes, requestArgs, retractArgs, retractableRequest, } from './requests.js' import { askAnswerArgs, messageArgs, pendingAnswers, pendingFor } from './replies.js' import { openSpace, session, stop } from './session.svelte.js' import { goalsOf, isEnded, liveProjects, projectByName, unitsOf } from './space.js' import { openSpaceStores } from './store.js' import { capturedFrom, distilledInto, liveUnits } from './units.js' import { askReviewArgs, findingsOf, owedBy, reviewQueue, verdictArgs, type FindingDraft, } from './verdicts.js' import { createSpace, uploadImage, write, writeFailure } from './write.js' // The write path, end to end, against the same fake PDS the read tests use. Everything between the // stub `fetch` and the screen is the code a browser runs: `RepoWriter` signing with a session it // cannot read the key of, `runCli` from `@radial/sidecar` building the record, and `materialize()` // folding the acknowledged value. Nothing here is a browser-only // reimplementation of anything — that is the claim 6.3 is making, and this is where it is checked. const fixture = fixtureSpace() 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', } /** What a DPoP-bound session's `fetchHandler` is, as far as anything above it can tell. */ const TOKEN = 'dpop-access-token-that-must-never-be-persisted' interface Harness { network: FakePds storage: ReturnType } async function open(): Promise { const harness: Harness = { network: new FakePds(latestVersions(fixture.records), HANDLES), storage: memoryStorage(), } await point(harness, fixture.spaceUri) return harness } /** * Point the tab at a space served by this harness. A second call gets a second database, the way a * second space in a browser does — `openSpaceStores` keys IndexedDB by space URI. */ async function point(harness: Harness, uri: string): Promise { const database = memoryDatabase() await openSpace(uri, { identities: new Identities({ fetcher: harness.network.fetch, storage: harness.storage }), transport: (identities) => new FetchRepoTransport(identities.pdsResolver, harness.network.fetch), openStores: (spaceUri) => openSpaceStores(spaceUri, async () => database), poll: false, }) } /** Sign in as a member. The session is the only thing that holds a credential, exactly as in a tab. */ function signInAs(did: string, network: FakePds): void { adopt( did, (path, init) => network.fetch(new URL(path, FAKE_PDS), { ...init, headers: { ...init.headers, authorization: `DPoP ${TOKEN}` }, }), { handle: HANDLES[did] as string }, ) } const space = () => { const current = session.space if (!current) throw new Error('no space') return current } const project = () => { const found = projectByName(space().index, 'radial-ng') if (!found) throw new Error('no project') return found } afterEach(async () => { await signOut() stop() session.space = undefined session.status = 'choosing' session.error = '' }) describe('writing a goal from the browser', () => { it('imports a pinned foreign discussion without Radial lexicon validation', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const origin = { uri: 'at://did:plc:feedback/app.userinput.discussion/one', cid: 'cid-discussion-one', } harness.network.append({ did: 'did:plc:feedback', collection: 'app.userinput.discussion', rkey: 'one', uri: origin.uri, cid: origin.cid, rev: '0000000000001', value: { $type: 'app.userinput.discussion', title: 'Foreign feedback' }, } as unknown as StoredRecord) const result = await write([ 'goal', 'create', '--project', project().target.uri, '--title', 'Imported feedback', '--origin', `${origin.uri}#${origin.cid}`, ]) const created = goalsOf(space().index, project()).find((goal) => goal.target.uri === result.primary.uri) expect(created?.target.value.origin).toEqual(origin) expect(harness.network.calls.some((call) => call.method === 'getRecord' && call.detail === 'app.userinput.discussion/one')).toBe(true) }) it('lands in the author’s own repo and is on screen before anything navigates', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const before = goalsOf(space().index, project()).length const result = await write([ 'goal', 'create', '--project', project().target.uri, '--title', 'Write a goal from a browser tab', '--body', 'The whole of 6.3, in one record.', ]) // The record went to the signed-in DID's repo, signed by that session and nothing else. expect(parseAtUri(result.primary.uri).did).toBe(FIXTURE_DIDS.tim) expect(parseAtUri(result.primary.uri).collection).toBe(COLLECTIONS.goal) const created = harness.network.calls.find((call) => call.method === 'createRecord') expect(created?.did).toBe(FIXTURE_DIDS.tim) expect(created?.authorization).toBe(`DPoP ${TOKEN}`) // …and `write` folded the PDS-acknowledged URI, CID and value immediately, so the index a route // renders from already holds it without a second network round trip. const goals = goalsOf(space().index, project()) expect(goals).toHaveLength(before + 1) expect(goals.some((goal) => goal.target.uri === result.primary.uri)).toBe(true) expect(space().index.ignored).toEqual([]) expect(space().index.edits).toEqual([]) }) it('does not re-read the repo after the PDS acknowledges the write', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) harness.network.reset() await write([ 'goal', 'create', '--project', project().target.uri, '--title', 'Only one repo moved', '--body', 'So no repo re-read is needed.', ]) expect(harness.network.counts('createRecord')).toBe(1) expect(harness.network.counts('getLatestCommit')).toBe(0) expect(harness.network.counts('listRecords')).toBe(0) }) it('pins the project to the version it read, rather than to whatever the URI names later', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const result = await write([ 'goal', 'create', '--project', project().target.uri, '--title', 'Pinned', '--body', 'The project ref carries a CID.', ]) const goal = goalsOf(space().index, project()).find((entry) => entry.target.uri === result.primary.uri) expect(goal?.target.value.project).toEqual({ uri: project().target.uri, cid: project().target.cid, }) // The CID came from reading the record, not from the index — a `getRecord` is what pinned it. expect(harness.network.calls.some((call) => call.method === 'getRecord')).toBe(true) }) }) describe('writing a message from the browser', () => { it('appears in the goal’s thread', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const goal = space().index.goals.find( (entry) => entry.target.value.title === 'Review queue in the web UI', ) if (!goal) throw new Error('fixture goal missing') const before = goal.messages.length await write(['message', 'post', '--goal', goal.target.uri, '--body', 'Posted from the tab.']) const after = space().index.goals.find((entry) => entry.target.uri === goal.target.uri) expect(after?.messages).toHaveLength(before + 1) expect(after?.messages.at(-1)?.value.body).toBe('Posted from the tab.') expect(after?.messages.at(-1)?.did).toBe(FIXTURE_DIDS.tim) }) it('answers an agent’s question in the thread, and the request stops awaiting input', async () => { // The whole of the Awaiting-input promise, end to end in a tab: a reply is a message with // `parent`, and the fold clears the block the moment somebody other than the asker writes one. const harness = await open() signInAs(FIXTURE_DIDS.ana, harness.network) const blocked = () => { const found = space().index.goals.find( (entry) => entry.target.value.title === 'Capture requests should distill, not copy', ) if (!found) throw new Error('fixture goal missing') return found } const question = blocked().messages.find((message) => message.did === FIXTURE_DIDS.planner) if (!question) throw new Error('fixture question missing') expect(blocked().awaitingInput).toHaveLength(1) const result = await write( messageArgs({ goal: blocked().target.uri, body: 'Go with (a) — the criteria are the load-bearing claims.', parent: question, author: FIXTURE_DIDS.ana, }), ) const written = blocked().messages.find((message) => message.uri === result.primary.uri) expect(written?.value.parent).toEqual({ uri: question.uri, cid: question.cid }) // The root is derived by the command from the parent, never passed by the caller. expect(written?.value.root).toEqual({ uri: question.uri, cid: question.cid }) expect(written?.value.mentions).toEqual([FIXTURE_DIDS.planner]) expect(blocked().awaitingInput).toEqual([]) }) it('asks an agent to reply with a typed record, which is not a unit and moves no pie', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const queue = () => { const found = space().index.goals.find( (entry) => entry.target.value.title === 'Review queue in the web UI', ) if (!found) throw new Error('fixture goal missing') return found } const before = tally(unitsOf(space().index, queue())) const message = queue().messages[0] if (!message) throw new Error('fixture message missing') const result = await write( askAnswerArgs({ goal: queue().target.uri, message: { uri: message.uri, cid: message.cid }, assignee: FIXTURE_DIDS.reviewer, brief: 'Say which read you meant.', }), ) const request = queue().openRequests.find((entry) => entry.uri === result.primary.uri) expect(request?.value.type).toBe('answer') expect(request?.value.subject).toEqual({ uri: message.uri, cid: message.cid }) expect(request?.value.assignee).toBe(FIXTURE_DIDS.reviewer) // A conversation is not work: no row, and the goal's fraction is exactly where it was. const after = unitsOf(space().index, queue()) expect(after.some((unit) => unit.type === 'answer')).toBe(false) expect(tally(after)).toEqual(before) // It is pending on the message it names, which is where the thread draws it. expect(pendingFor(pendingAnswers(queue()), message).map((entry) => entry.uri)).toEqual([ result.primary.uri, ]) }) }) describe('requesting an artifact from the browser', () => { const reviewQueue = () => { const found = space().index.goals.find( (entry) => entry.target.value.title === 'Review queue in the web UI', ) if (!found) throw new Error('fixture goal missing') return found } it('writes the request the compose card composed, and the unit is on screen at once', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const goal = reviewQueue() const plan = unitsOf(space().index, goal).find((unit) => unit.type === 'plan') const version = plan?.current if (!version) throw new Error('fixture version missing') const result = await write( requestArgs( { kind: 'request', target: goal.target.uri, scope: 'goal', type: 'security-review', basedOn: [{ uri: version.artifact.uri, cid: version.artifact.cid }], }, { brief: 'Look at the session boundary.', assignee: FIXTURE_DIDS.reviewer }, ), ) expect(parseAtUri(result.primary.uri).did).toBe(FIXTURE_DIDS.tim) expect(parseAtUri(result.primary.uri).collection).toBe(COLLECTIONS.artifactRequest) // `timeline()` folds it into a unit of its own, named to the reviewer and holding the exact // version it was asked against — no navigation, no wait for the ten-second tick. const unit = unitsOf(space().index, reviewQueue()).find( (entry) => entry.openRequest?.uri === result.primary.uri, ) expect(unit?.type).toBe('security-review') expect(unit?.state).toBe('assigned') expect(unit?.assignee).toBe(FIXTURE_DIDS.reviewer) expect(unit?.openRequest?.value.brief).toBe('Look at the session boundary.') // The basis is pinned to the CID that was read, not to whatever the URI names later. expect(unit?.openRequest?.value.basedOn).toEqual([ { uri: version.artifact.uri, cid: version.artifact.cid }, ]) expect(space().index.ignored).toEqual([]) }) it('leaves an unassigned request open to any capable agent', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const goal = reviewQueue() const result = await write( requestArgs( { kind: 'request', target: goal.target.uri, scope: 'goal', type: 'plan' }, { brief: '', assignee: '' }, ), ) const unit = unitsOf(space().index, reviewQueue()).find( (entry) => entry.openRequest?.uri === result.primary.uri, ) expect(unit?.state).toBe('open') expect(unit?.assignee).toBeUndefined() expect(unit?.openRequest?.value.brief).toBeUndefined() }) it('asks a project-scoped type against the project, so it lands in System', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const result = await write( requestArgs( { kind: 'request', target: project().target.uri, scope: 'project', type: 'conventions' }, { brief: 'Write down what a review looks for.', assignee: '' }, ), ) const unit = unitsOf(space().index, project()).find( (entry) => entry.openRequest?.uri === result.primary.uri, ) expect(unit?.type).toBe('conventions') expect(unit?.openRequest?.value.project?.uri).toBe(project().target.uri) expect(unit?.openRequest?.value.goal).toBeUndefined() }) }) describe('capturing a goal artifact into System (design §8)', () => { const reviewQueue = () => { const found = space().index.goals.find( (entry) => entry.target.value.title === 'Review queue in the web UI', ) if (!found) throw new Error('fixture goal missing') return found } const planOf = (goal: ReturnType) => { const found = unitsOf(space().index, goal).find((entry) => entry.type === 'plan') if (!found) throw new Error('fixture unit missing') return found } it('writes a project-scoped request pinned to the goal artifact, and the row lands in System', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const goalUri = reviewQueue().target.uri const version = planOf(reviewQueue()).current if (!version) throw new Error('fixture version missing') const result = await write( requestArgs( captureDraft(project().target.uri, goalUri, 'adr', { uri: version.artifact.uri, cid: version.artifact.cid, }), { brief: 'Record the decision the plan settled, not the plan.', assignee: FIXTURE_DIDS.planner }, ), ) // The row is the project's, not the goal's. Nothing about the goal changed: a capture asks for a // new record in the project's standing memory, and the artifact it was distilled from stays // exactly where it was, still judged by whatever judged it. const captured = unitsOf(space().index, project()).find( (entry) => entry.openRequest?.uri === result.primary.uri, ) expect(captured?.type).toBe('adr') expect(captured?.versions).toEqual([]) expect(captured?.openRequest?.value.project?.uri).toBe(project().target.uri) expect(captured?.openRequest?.value.goal).toBeUndefined() expect(captured?.openRequest?.value.basedOn).toEqual([ { uri: version.artifact.uri, cid: version.artifact.cid }, ]) expect( unitsOf(space().index, reviewQueue()).some( (entry) => entry.openRequest?.uri === result.primary.uri, ), ).toBe(false) // …and the pairing resolves in both directions across two targets neither fold can see at once, // which is the whole of what makes this a capture rather than a second plan. if (!captured) throw new Error('captured unit missing') const from = capturedFrom(space().index, captured) expect(from?.unit.type).toBe('plan') expect(from?.target.target.uri).toBe(goalUri) expect(distilledInto(space().index, planOf(reviewQueue())).map((entry) => entry.unit.key)).toContain( captured.key, ) expect(space().index.ignored).toEqual([]) }) }) describe('judging a version from the browser', () => { const ignored = () => { const found = space().index.goals.find( (entry) => entry.target.value.title === 'Surface the records the fold ignored', ) if (!found) throw new Error('fixture goal missing') return found } const unitOf = (type: string) => { const found = unitsOf(space().index, ignored()).find((entry) => entry.type === type) if (!found) throw new Error(`fixture unit missing: ${type}`) return found } const FINDINGS: FindingDraft[] = [ { severity: 'error', path: 'src/lib/components/Diagnostics.svelte', line: '18', body: 'The reason strings are composed here rather than read off the fold, so the UI and `core` can come to disagree about why a record was dropped.', }, { severity: 'warning', path: 'src/lib/components/Diagnostics.svelte', line: '', body: 'Open by default. The plan says closed, and a disclosure that is always open is not a disclosure.', }, { severity: 'info', path: '', line: '', body: 'Say in the body that `index.edits` is a different thing from `index.ignored`, so the next turn does not merge the two lists.', }, ] it('flows a request_changes with three findings into the turn bundle of the v2 somebody then asks for', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const v1 = unitOf('plan').versions[0] if (!v1) throw new Error('fixture version missing') const findings = findingsOf(FINDINGS) expect(findings).toHaveLength(3) await write(verdictArgs(v1, { verdict: 'request_changes', findings })) // The review pins v1 by cid, and the fold reads it back off the version rather than off the // unit: this is what makes "judged" mean the same thing in a tab as it does in the daemon. const judged = unitOf('plan') expect(judged.versions[0]?.verdict).toBe('request_changes') expect(judged.state).toBe('judged') expect(judged.versions[0]?.reviews.at(-1)?.did).toBe(FIXTURE_DIDS.tim) expect(judged.versions[0]?.reviews.at(-1)?.value.findings).toEqual(findings) // A human reads the findings and asks for another version, built on the one that was judged. const asked = await write( requestArgs( { kind: 'request', target: ignored().target.uri, scope: 'goal', type: 'plan', basedOn: [{ uri: v1.artifact.uri, cid: v1.artifact.cid }], }, { brief: 'Address the findings on v1.', assignee: FIXTURE_DIDS.planner }, ), ) // …and the turn that answers it is briefed with exactly what was said. This is the whole of the // loop 6.5 closes: a review written in a browser tab is an input to the next turn, with no // daemon endpoint, no export step, and nothing between the two but the record stream. const bundle = buildTurnBundle(space().index, asked.primary.uri, asked.primary.cid) const mine = bundle.reviewFindings.filter( (entry) => entry.subject.uri === v1.artifact.uri && entry.verdict === 'request_changes', ) expect(mine).toHaveLength(1) expect(mine[0]?.findings).toEqual(findings) expect(bundle.basedOn.map((artifact) => artifact.uri)).toContain(v1.artifact.uri) }) it('answers the review request assigned to me, and takes it out of my queue', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const version = unitOf('implementation').current if (!version) throw new Error('fixture version missing') const owed = owedBy(version, FIXTURE_DIDS.tim) if (!owed) throw new Error('fixture review request missing') expect(reviewQueue(space().index, FIXTURE_DIDS.tim)).toHaveLength(1) await write(verdictArgs(version, { verdict: 'approve', findings: [], request: owed })) // Naming the request is what closes it: the fold marks a review request fulfilled when a trusted // review carries its ref and pins the same subject — that one, and only it. The daemon's // auto-review ask on the same version is untouched, which is the point of naming: answering // somebody else's request would cancel work they are doing. Nothing was deleted, nothing flagged. expect(reviewQueue(space().index, FIXTURE_DIDS.tim)).toHaveLength(0) const after = unitOf('implementation') expect(after.current?.reviewRequests.map((request) => request.value.assignee)).toEqual([ FIXTURE_DIDS.reviewer, ]) expect(after.verdict).toBe('approve') expect(space().index.ignored).toEqual([]) }) it('asks somebody else for a review, and it is not a unit of its own', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const version = unitOf('plan').current if (!version) throw new Error('fixture version missing') const before = unitsOf(space().index, ignored()).length await write( askReviewArgs({ target: ignored().target.uri, scope: 'goal', version, assignee: FIXTURE_DIDS.ana, brief: 'You wrote the goal — say whether this answers it.', }), ) // A review owed is a debt on one version, not a piece of work: it adds no row, it hangs off the // version it pins, and it shows up in exactly one person's queue. expect(unitsOf(space().index, ignored())).toHaveLength(before) const owed = unitOf('plan').current?.reviewRequests ?? [] expect(owed).toHaveLength(1) expect(owed[0]?.value.assignee).toBe(FIXTURE_DIDS.ana) expect(owed[0]?.value.subject).toEqual({ uri: version.artifact.uri, cid: version.artifact.cid }) expect(reviewQueue(space().index, FIXTURE_DIDS.ana)).toHaveLength(1) }) }) describe('retracting a request from the browser', () => { it('withdraws the author’s own request, and deletes nothing', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const goal = space().index.goals.find( (entry) => entry.target.value.title === 'Review queue in the web UI', ) if (!goal) throw new Error('fixture goal missing') const unit = unitsOf(space().index, goal).find((entry) => entry.type === 'implementation') const request = unit && retractableRequest(unit, space().directory, FIXTURE_DIDS.tim) if (!request) throw new Error('fixture request missing') await write(retractArgs(request)) const after = space().index.goals.find((entry) => entry.target.uri === goal.target.uri) const retracted = after && unitsOf(space().index, after).find((entry) => entry.type === 'implementation') expect(retracted?.retracted).toBe(true) expect(retracted?.state).toBe('retracted') // The row stays: a tombstone withdraws the request, it does not remove the record. expect(retracted?.requests.some((entry) => entry.uri === request.uri)).toBe(true) }) }) // The exit criterion of 6.7, and the four other records that govern a space. Every assertion pairs a // write with what the FOLD then says about it, because that is the whole claim: the browser is not // asking a privileged coordinator for permission, it is writing a record whose weight is decided by // who signed it. describe('governing the space from the browser', () => { const THREAT_MODEL = { name: 'threat-model', brief: 'Enumerate the ways in, and say which are mitigated and which are accepted.', description: 'Attack surface, ranked, with the mitigation that exists for each.', scope: 'goal' as const, durability: 'living' as const, } it('registers a type as an admin, and its button exists before anything navigates', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) expect(requestableTypes(space().index, 'goal').map((type) => type.name)).not.toContain('threat-model') const result = await write(typeArgs(space().uri, THREAT_MODEL)) // The record is in the admin's own repo — there is no registry service, and nothing was asked. expect(parseAtUri(result.primary.uri).did).toBe(FIXTURE_DIDS.tim) expect(parseAtUri(result.primary.uri).collection).toBe(COLLECTIONS.artifactType) // …and this is the Phase 5 walkthrough's "admin adds a type and its button appears", happening // entirely in a browser tab: the ⊕ menu on every goal is `requestableTypes`, generated from the // registry, so the menu item exists the instant the fold holds the record. expect(requestableTypes(space().index, 'goal').map((type) => type.name)).toEqual([ 'implementation', 'plan', 'security-review', 'threat-model', ]) expect(requestableTypes(space().index, 'project').map((type) => type.name)).not.toContain('threat-model') const row = registryRows(space().index).find((entry) => entry.name === 'threat-model') expect(row?.shadowed).toBe(false) expect(row?.durability).toBe('living') expect(space().index.ignored).toEqual([]) // And a request of that type is now writable, which is the only proof that matters. const goal = space().index.goals[0] if (!goal) throw new Error('fixture goal missing') const asked = await write( requestArgs( { kind: 'request', target: goal.target.uri, scope: 'goal', type: 'threat-model' }, { brief: '', assignee: '' }, ), ) expect( unitsOf(space().index, space().index.goals.find((entry) => entry.target.uri === goal.target.uri)!).find( (unit) => unit.openRequest?.uri === asked.primary.uri, )?.type, ).toBe('threat-model') }) it('refuses a duplicate registry name before anything is sent', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) harness.network.reset() // The guard is `runCli`'s, answered from the fold this tab is holding rather than from a lookup — // duplicate names resolve earliest-wins in core and latest-wins in the daemon, so a second one // would desync them. The form says so too, and this is what makes that more than a form rule. await expect(write(typeArgs(space().uri, { ...THREAT_MODEL, name: 'plan' }))).rejects.toThrow( /already exists/, ) expect(harness.network.counts('createRecord')).toBe(0) }) it('writes a non-admin’s registry record, which the fold then ignores', async () => { const harness = await open() // `ana` is an ordinary human member of the fixture space, which is exactly the case that has to // fail quietly at the fold rather than loudly at the PDS: her repo accepts this happily. signInAs(FIXTURE_DIDS.ana, harness.network) const result = await write(typeArgs(space().uri, THREAT_MODEL)) expect(parseAtUri(result.primary.uri).did).toBe(FIXTURE_DIDS.ana) expect(registryRows(space().index).map((row) => row.name)).not.toContain('threat-model') expect(requestableTypes(space().index, 'goal').map((type) => type.name)).not.toContain('threat-model') expect(space().index.ignored.map((entry) => entry.uri)).toContain(result.primary.uri) }) it('grants membership, and the DID’s records start counting', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const newcomer = 'did:plc:newcomer00001' expect(space().directory.get(newcomer).active).toBe(false) const result = await write(addMemberArgs(space().uri, { actor: newcomer, kind: 'human', role: 'member' })) const granted = space().index.members.find((member) => member.did === newcomer) expect(granted?.active).toBe(true) expect(granted?.role).toBe('member') // The grant is a strongref to the exact record, which is what a removal has to name. expect(granted?.membership).toEqual({ uri: result.primary.uri, cid: result.primary.cid }) expect(space().directory.get(newcomer).active).toBe(true) expect(space().index.ignored).toEqual([]) }) it('revokes the exact grant the index holds, and keeps what was written under it', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const grant = space().index.members.find((member) => member.did === FIXTURE_DIDS.ana)?.membership const wrote = space().index.goals.filter((goal) => goal.target.did === FIXTURE_DIDS.ana).length expect(grant).toBeDefined() expect(wrote).toBeGreaterThan(0) // `--actor` is all the browser passes; the grant comes from `write.ts`'s `resolveMembership`, // which is a lookup in the fold. A removal naming a stale grant is one the fold would drop. const result = await write(removeMemberArgs(space().uri, FIXTURE_DIDS.ana)) expect(space().index.members.find((member) => member.did === FIXTURE_DIDS.ana)?.active).toBe(false) expect(memberRows(space().index, space().directory).at(-1)?.actor.did).toBe(FIXTURE_DIDS.ana) expect(parseAtUri(result.primary.uri).collection).toBe(COLLECTIONS.removeMember) // Her goals are gone from the index, because a revoked grant un-counts everything — that is the // membership rule, and it is why the row stays on the page saying "removed" rather than vanishing. expect(space().index.goals.filter((goal) => goal.target.did === FIXTURE_DIDS.ana)).toHaveLength(0) }) it('lets an ordinary member create a project, which is not an admin act', async () => { const harness = await open() signInAs(FIXTURE_DIDS.ana, harness.network) const result = await write( projectArgs(space().uri, { name: 'radial-docs', gitUrl: 'git@github.com:disnet/radial-docs.git', defaultBranch: 'main', checks: [{ name: 'build', command: 'pnpm build' }], }), ) const created = space().index.projects.find((entry) => entry.target.uri === result.primary.uri) expect(created?.target.value.name).toBe('radial-docs') // The scp-style remote GitHub offers for copying is stored as the https URL it names, because // https is the only scheme a turn clones over. The record never carries the form that was typed. expect(created?.target.value.gitUrl).toBe('https://github.com/disnet/radial-docs.git') expect(created?.target.value.checks).toEqual([{ name: 'build', command: 'pnpm build' }]) expect(space().index.ignored).toEqual([]) }) it('overlays auto-review as an admin, without touching the project record', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) // The fixture project turns `plan` on in the project record itself, and says nothing about // `security-review`. Both cases matter: the switch has to be able to contradict a default that is // baked into an immutable record, not merely add to it. const pinned = project().target.cid expect(project().autoReview.plan).toBe(true) expect(project().autoReview['security-review']).toBeUndefined() await write(autoReviewArgs(project(), 'plan', false)) expect(project().autoReview.plan).toBe(false) await write(autoReviewArgs(project(), 'security-review', true)) expect(project().autoReview['security-review']).toBe(true) expect(project().autoReview.plan).toBe(false) // A standalone record each time. The project record is pinned by every goal under it and by the // overlay itself, so it has to be the same record it was — which is the whole reason this setting // was never a field somebody edits. expect(project().target.cid).toBe(pinned) expect(harness.network.counts('putRecord')).toBe(0) expect(space().index.ignored).toEqual([]) expect(space().index.edits).toEqual([]) }) it('ignores an auto-review setting from a member who does not govern', async () => { const harness = await open() signInAs(FIXTURE_DIDS.ana, harness.network) const result = await write(autoReviewArgs(project(), 'security-review', true)) expect(project().autoReview['security-review']).toBeUndefined() expect(space().index.ignored.map((entry) => entry.uri)).toContain(result.primary.uri) }) }) describe('editing and archiving a project, from the browser', () => { it('overlays name, remote and branch per field, without touching the project record', async () => { const harness = await open() // Tim wrote the fixture's projects, so this is the author editing their own — the only DID whose // `editProject` records the fold reads (an admin who did not write it is covered in the sidecar's // own tests, where the space has a second admin to try it with). signInAs(FIXTURE_DIDS.tim, harness.network) const pinned = project().target.cid const before = project() expect(before.name).toBe('radial-ng') expect(before.defaultBranch).toBe('main') await write(projectEditArgs(before, projectChanges(before, { name: 'radial', gitUrl: before.gitUrl, defaultBranch: before.defaultBranch, }))) // Renamed, and found under the new name — the route follows it, which is why the settings page // navigates after a rename. const renamed = projectByName(space().index, 'radial') if (!renamed) throw new Error('renamed project missing') expect(projectByName(space().index, 'radial-ng')).toBeUndefined() // The record itself is untouched: same CID, no putRecord, no edit annotation. That is the whole // reason this is a separate record — every goal beneath it pins the project by uri#cid. expect(renamed.target.cid).toBe(pinned) expect(renamed.target.value.name).toBe('radial-ng') expect(harness.network.counts('putRecord')).toBe(0) expect(space().index.edits).toEqual([]) // The goals under it are still there, still resolving through that pin. expect(goalsOf(space().index, renamed).length).toBe(goalsOf(space().index, before).length) // A second edit naming only the remote leaves the rename standing: the overlay is per field. await write(projectEditArgs(renamed, projectChanges(renamed, { name: renamed.name, gitUrl: 'git@github.com:disnet/moved.git', defaultBranch: 'trunk', }))) const moved = projectByName(space().index, 'radial') expect(moved?.name).toBe('radial') // The scp-style remote becomes its https URL, exactly as on create. expect(moved?.gitUrl).toBe('https://github.com/disnet/moved.git') expect(moved?.defaultBranch).toBe('trunk') expect(space().index.ignored).toEqual([]) // And a turn briefed on it clones where the project is now, not where it was written. const request = moved?.requests[0] if (!request) throw new Error('fixture project has no requests to brief a turn from') const bundle = buildTurnBundle(space().index, request.uri, request.cid) expect(bundle.project.gitUrl).toBe('https://github.com/disnet/moved.git') expect(bundle.project.defaultBranch).toBe('trunk') // The pin is still the record, so the turn's provenance says exactly what it was briefed from. expect(bundle.project.cid).toBe(pinned) }) it('writes nothing when the form changed nothing, and only the fields that did change', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const before = project() // Resubmitting the values already showing is not an edit. Sending it anyway would win those fields // latest-wins against somebody else's concurrent change — so the form computes the delta. expect(projectChanges(before, projectEditDraft(before))).toEqual({}) // A blank field is a field the form did not fill in, never an instruction to clear one. expect(projectChanges(before, { name: ' ', gitUrl: '', defaultBranch: 'trunk' })).toEqual({ defaultBranch: 'trunk', }) harness.network.reset() await write(projectEditArgs(before, { defaultBranch: 'trunk' })) expect(harness.network.counts('createRecord')).toBe(1) const after = projectByName(space().index, 'radial-ng') expect(after?.defaultBranch).toBe('trunk') expect(after?.name).toBe('radial-ng') expect(after?.gitUrl).toBe(before.gitUrl) }) it("ignores a member's edit and archive of a project they did not write", async () => { const harness = await open() signInAs(FIXTURE_DIDS.ana, harness.network) const target = project() expect(ownsProject(target, FIXTURE_DIDS.ana)).toBe(false) const edit = await write(projectEditArgs(target, { name: 'anas-project' })) const archive = await write(projectArchiveArgs(target, true)) expect(project().name).toBe('radial-ng') expect(project().archived).toBe(false) const ignored = space().index.ignored.map((entry) => entry.uri) expect(ignored).toContain(edit.primary.uri) expect(ignored).toContain(archive.primary.uri) }) it('archives a project, shelving every goal under it, and unarchives it the same way', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const before = project() const goalsUnder = goalsOf(space().index, before).length expect(goalsUnder).toBeGreaterThan(0) const liveBefore = liveUnits(space().index).length await write(projectArchiveArgs(before, true)) expect(project().archived).toBe(true) // Gone from the rail's list, and its work is out of every smart list — the same reach the daemon's // `activeGoals` has, seen from the reader's side. expect(liveProjects(space().index).map((entry) => entry.target.uri)).not.toContain( before.target.uri, ) expect(liveUnits(space().index).length).toBeLessThan(liveBefore) // Nothing was deleted or closed: the goals and their pages are all still there. expect(goalsOf(space().index, project()).length).toBe(goalsUnder) expect(projectByName(space().index, 'radial-ng')).toBeDefined() await write(projectArchiveArgs(project(), false)) expect(project().archived).toBe(false) expect(liveUnits(space().index).length).toBe(liveBefore) // A standalone record each way, so the project record was never rewritten. expect(harness.network.counts('putRecord')).toBe(0) expect(space().index.ignored).toEqual([]) }) }) describe('a goal’s ending, from the browser', () => { const mine = () => { const found = space().index.goals.find( (goal) => goal.target.did === FIXTURE_DIDS.tim && !isEnded(goal), ) if (!found) throw new Error('fixture goal missing') return found } it('ends and reopens a goal with standalone records', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const uri = mine().target.uri await write(closeArgs(mine(), true, 'completed')) const closed = space().index.goals.find((goal) => goal.target.uri === uri) expect(closed && isEnded(closed)).toBe(true) expect(closed?.disposition).toBe('completed') expect(harness.network.counts('createRecord')).toBe(1) expect(harness.network.counts('putRecord')).toBe(0) expect(space().index.edits).toEqual([]) expect(space().index.ignored).toEqual([]) // And it is reversible, in the fold as well as in the command (6.0). const reopened = space().index.goals.find((goal) => goal.target.uri === uri) if (!reopened) throw new Error('goal missing') await write(closeArgs(reopened, false)) expect(isEnded(space().index.goals.find((goal) => goal.target.uri === uri)!)).toBe(false) expect(space().index.edits).toEqual([]) }) it('lets another active member end the goal', async () => { const harness = await open() signInAs(FIXTURE_DIDS.ana, harness.network) const theirs = space().index.goals.find((goal) => goal.target.did === FIXTURE_DIDS.tim) if (!theirs) throw new Error('fixture goal missing') harness.network.reset() await write(closeArgs(theirs, true, 'dropped')) expect(harness.network.counts('putRecord')).toBe(0) expect(harness.network.counts('createRecord')).toBe(1) expect(space().index.goals.find((goal) => goal.target.uri === theirs.target.uri)?.disposition).toBe('dropped') }) it('parks a goal as any active member, and reopens it the same way', async () => { const harness = await open() signInAs(FIXTURE_DIDS.ana, harness.network) const theirs = space().index.goals.find((goal) => goal.target.did === FIXTURE_DIDS.tim && !goal.ended) if (!theirs) throw new Error('fixture goal missing') const uri = theirs.target.uri await write(closeArgs(theirs, true, 'parked')) expect(space().index.goals.find((goal) => goal.target.uri === uri)?.disposition).toBe('parked') const archived = space().index.goals.find((goal) => goal.target.uri === uri) if (!archived) throw new Error('goal missing') await write(closeArgs(archived, false)) expect(space().index.goals.find((goal) => goal.target.uri === uri)?.ended).toBe(false) // A standalone record each time, so the goal record itself was never touched — and `parked` is a // word on one ending rather than a second control with its own permission rule. expect(harness.network.counts('putRecord')).toBe(0) expect(space().index.ignored).toEqual([]) }) }) describe('creating a space from a browser that has never opened one', () => { it('writes the space and its seeded registry, then opens it over the wire', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) // The write that has no space behind it. Nothing is resolved and nothing is pinned, so it does not // need the space transport it happens to be able to borrow here. const result = await createSpace(spaceArgs({ name: 'Second', description: 'A space of its own.' })) expect(parseAtUri(result.primary.uri).collection).toBe(COLLECTIONS.space) expect(result.refs.seededArtifactTypes).toHaveLength(8) // Opened by URI, over `listRecords`, exactly as a space somebody handed you would be — nothing is // carried across from the write, so if this tab can read it so can everyone else. await point(harness, result.primary.uri) expect(session.status).toBe('ready') expect(space().index.space.value.name).toBe('Second') // Its author is its admin, with no grant to point at and nothing able to revoke it. expect(space().index.members).toEqual([ { did: FIXTURE_DIDS.tim, kind: 'human', role: 'admin', active: true }, ]) expect(registryRows(space().index).map((row) => row.name)).toEqual([ 'adr', 'architecture', 'architecture-inventory', 'conventions', 'fdr', 'glossary', 'implementation', 'plan', ]) // The first space's records are in the very same repo, and none of them are in this index: a space // is a fold over one space record, not over a repo. They are *read* — the poller lists every // radial collection — and then correctly not folded, which is what `ignored` is for. expect(space().index.projects).toEqual([]) expect(space().index.goals).toEqual([]) expect(space().index.ignored.map((entry) => entry.uri)).toContain(fixture.spaceUri) expect( space().index.ignored.every((entry) => entry.reason === 'record does not resolve into this space index'), ).toBe(true) }) it('will not create a space without a session', async () => { const harness = await open() await expect(createSpace(spaceArgs({ name: 'Nope', description: '' }))).rejects.toThrow(/Sign in/) expect(harness.network.counts('createRecord')).toBe(0) }) }) describe('what a write refuses to do', () => { it('will not write without a session', async () => { const harness = await open() await expect( write(['message', 'post', '--goal', space().index.goals[0]?.target.uri ?? '', '--body', 'nope']), ).rejects.toThrow(/Sign in/) expect(harness.network.counts('createRecord')).toBe(0) }) it('will not write against the read-only demo', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const goalUri = space().index.goals[0]?.target.uri ?? '' stop() await expect(write(['message', 'post', '--goal', goalUri, '--body', 'nope'])).rejects.toThrow( /demo is read-only/, ) expect(harness.network.counts('createRecord')).toBe(0) }) it('shows an acknowledged write even when the PDS goes away immediately afterwards', async () => { const harness = await open() // The PDS accepts the write and then goes away. There is no second read in the action path. adopt(FIXTURE_DIDS.tim, async (path, init) => { const response = await harness.network.fetch(new URL(path, FAKE_PDS), init) harness.network.offline = true return response }) const result = await write([ 'goal', 'create', '--project', project().target.uri, '--title', 'Written, not yet seen', '--body', 'The record exists whether or not this tab could re-read it.', ]) // The acknowledged record is real, public and already in this tab's fold. The next background // poll may report the repo stale, but the action itself needs no further network response. expect(parseAtUri(result.primary.uri).collection).toBe(COLLECTIONS.goal) expect(space().index.goals.some((goal) => goal.target.uri === result.primary.uri)).toBe(true) expect(session.error).toBe('') expect(session.status).toBe('ready') }) it('refuses a record the lexicon does not accept, before anything is sent', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) // A title past the lexicon's 300-character ceiling is caught by the same fold the daemon and // the CLI run, which is why it never reaches the PDS. await expect( write(['goal', 'create', '--project', project().target.uri, '--title', 'x'.repeat(301)]), ).rejects.toThrow(/title/) expect(harness.network.counts('createRecord')).toBe(0) }) it('writes a goal that is a title and nothing else', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) // A goal is often opened before its body is worth writing, so omitting `--body` is not an // error: the record carries the field the readers expect, empty. const result = await write([ 'goal', 'create', '--project', project().target.uri, '--title', 'Just the title, for now', ]) expect(harness.network.counts('createRecord')).toBe(1) const goal = goalsOf(space().index, project()).find( (entry) => entry.target.uri === result.primary.uri, ) expect(goal?.target.value.body).toBe('') expect(space().index.ignored).toEqual([]) }) }) describe('what this tab keeps', () => { it('persists no credential of its own', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) await write([ 'goal', 'create', '--project', project().target.uri, '--title', 'Nothing to steal', '--body', 'The only credential is a session this app cannot read.', ]) // Everything this app writes to storage is a cache of public data: which spaces were opened, // and which DID resolves to which PDS and handle. The bearer token exists in exactly one place // — the OAuth client's own store — and this app never copies it anywhere. (`radial:spaces` and // `radial:space` go to the ambient `localStorage`, which a node test does not have; the identity // cache is the one injected here. All three are caches, and none of them is a credential.) expect(harness.storage.keys()).toEqual(['radial:identities']) expect(harness.storage.dump()).not.toContain(TOKEN) expect(JSON.stringify(space())).not.toContain(TOKEN) }) it('forgets the session on sign-out', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) expect(account.status).toBe('signed-in') expect(account.did).toBe(FIXTURE_DIDS.tim) await signOut() expect(account.status).toBe('signed-out') expect(account.did).toBe('') await expect( write(['message', 'post', '--goal', space().index.goals[0]?.target.uri ?? '', '--body', 'nope']), ).rejects.toThrow(/Sign in/) }) }) // A record the fake PDS minted is a real `StoredRecord` to everything downstream; this asserts the // harness itself is not the thing making the tests pass. describe('the fake PDS', () => { it('serves a created record back through listRecords', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const result = await write([ 'goal', 'create', '--project', project().target.uri, '--title', 'Round trip', '--body', 'Written, then read back over the wire.', ]) const stored = (harness.network.repos.get(FIXTURE_DIDS.tim) ?? []).find( (record: StoredRecord) => record.uri === result.primary.uri, ) expect(stored?.collection).toBe(COLLECTIONS.goal) expect(stored?.value).toMatchObject({ title: 'Round trip' }) }) }) // ── images ────────────────────────────────────────────────────────────────────────────────────── /** A one-pixel PNG, byte for byte: the magic number is what `readImageFile` actually checks. */ const PNG_BYTES = new Uint8Array([ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, ]) const pickedFile = (name: string, bytes: Uint8Array, type: string) => ({ name, type, size: bytes.byteLength, arrayBuffer: async () => bytes.slice().buffer as ArrayBuffer, }) describe('adding an image to a body', () => { it('uploads the blob, anchors it in a record, and hands back markdown naming that record', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) harness.network.reset() const image = await uploadImage(pickedFile('claim ledger.png', PNG_BYTES, 'image/png')) // Two writes, in the order that makes the blob durable: a PDS reclaims a blob no record // references, so the upload alone is not storage. expect(harness.network.counts('uploadBlob')).toBe(1) expect(harness.network.counts('createRecord')).toBe(1) const uploaded = harness.network.calls.find((call) => call.method === 'uploadBlob') expect(uploaded?.detail).toBe(`image/png ${PNG_BYTES.byteLength}`) expect(uploaded?.authorization).toBe(`DPoP ${TOKEN}`) // The record is in the author's own repo, is an image record, and carries the blob. expect(parseAtUri(image.uri).did).toBe(FIXTURE_DIDS.tim) expect(parseAtUri(image.uri).collection).toBe(COLLECTIONS.image) const record = space().index.images.find((entry) => entry.uri === image.uri) expect(record?.value.blob.mimeType).toBe('image/png') expect(record?.value.alt).toBe('claim ledger') expect(record?.value.name).toBe('claim ledger.png') expect(harness.network.blobs.get(record?.value.blob.ref.$link ?? '')).toEqual(PNG_BYTES) // …and the markdown names the RECORD, not the PDS this tab happened to write to. expect(image.markdown).toBe(`![claim ledger](radial-image:${image.uri})`) expect(image.markdown).not.toContain(FAKE_PDS) expect(parseImageLocator(`radial-image:${image.uri}`)?.uri).toBe(image.uri) }) it('uploads what the bytes are, not what the filename claimed, and refuses an SVG outright', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) harness.network.reset() // Mislabelled but genuine: uploaded as the PNG it is, so every reader's decodes what the // record says it is decoding. await uploadImage(pickedFile('shot.jpg', PNG_BYTES, 'image/jpeg')) expect(harness.network.calls.find((call) => call.method === 'uploadBlob')?.detail).toMatch(/^image\/png /) // Not an image at all. Nothing crosses the network — the refusal is local, before the upload. harness.network.reset() const svg = new TextEncoder().encode('') await expect(uploadImage(pickedFile('logo.png', svg, 'image/png'))).rejects.toThrow(/not a PNG/) expect(harness.network.counts('uploadBlob')).toBe(0) expect(harness.network.counts('createRecord')).toBe(0) }) it('reports a record write that fails after the upload as nothing inserted', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) // The blob lands; the record does not. The upload is then an orphan the PDS may collect, so // calling it a partial success would be presenting something the reader does not have. const real = harness.network.fetch const failing = async (input: string | URL, init?: RequestInit): Promise => { if (String(input).includes('com.atproto.repo.createRecord')) throw new Error('repo is read-only') return real(input, init) } adopt(FIXTURE_DIDS.tim, (path, init) => failing(new URL(path, FAKE_PDS), { ...init, headers: { ...init.headers, authorization: `DPoP ${TOKEN}` } }), ) await expect(uploadImage(pickedFile('shot.png', PNG_BYTES, 'image/png'))).rejects.toThrow( /uploaded but not saved.*nothing was inserted/i, ) expect(space().index.images).toEqual([]) }) it('survives a reload and resolves off the author’s PDS rather than a stored URL', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const image = await uploadImage(pickedFile('shot.png', PNG_BYTES, 'image/png')) const goal = await write([ 'goal', 'create', '--project', project().target.uri, '--title', 'A goal with a picture in it', '--body', `Here is the state it was in:\n\n${image.markdown}\n`, ]) // Read the whole space back from the PDS, cold, the way another member's tab would. await point(harness, fixture.spaceUri) const reread = space().index.goals.find((view) => view.target.uri === goal.primary.uri) expect(reread?.target.value.body).toContain(image.markdown) // The body still names the record; the record is in the index; and the URL a reader builds from // it is getBlob on whatever PDS the author's DID resolves to NOW — which is what makes the prose // survive an author moving PDS. const held = space().index.images.find((entry) => entry.uri === image.uri) expect(held).toBeDefined() const url = imageBlobUrl(FAKE_PDS, FIXTURE_DIDS.tim, held?.value.blob.ref.$link ?? '') const fetched = await harness.network.fetch(url) expect(new Uint8Array(await fetched.arrayBuffer())).toEqual(PNG_BYTES) }) it('folds the same view whichever order the image and the body arrive in', async () => { const harness = await open() signInAs(FIXTURE_DIDS.tim, harness.network) const image = await uploadImage(pickedFile('shot.png', PNG_BYTES, 'image/png')) await write([ 'goal', 'create', '--project', project().target.uri, '--title', 'Order does not matter', '--body', image.markdown, ]) await point(harness, fixture.spaceUri) // Everything except observer-local claim timing: firstSeenAt is when THIS reader saw the claim, // and staleAt is the deadline derived from that stamp. const settled = (): string => JSON.stringify(space().index, (key, value: unknown) => key === 'firstSeenAt' || key === 'staleAt' ? undefined : value, ) const forwards = settled() // Same records, the image record ingested last. An image is presentation infrastructure and // nothing in the fold reads it, so the two indexes must be identical. const repo = harness.network.repos.get(FIXTURE_DIDS.tim) ?? [] harness.network.repos.set(FIXTURE_DIDS.tim, [ ...repo.filter((entry: StoredRecord) => entry.collection !== COLLECTIONS.image), ...repo.filter((entry: StoredRecord) => entry.collection === COLLECTIONS.image), ]) await point(harness, fixture.spaceUri) expect(settled()).toBe(forwards) }) it('refuses to upload for a reader with no session', async () => { await open() await expect(uploadImage(pickedFile('shot.png', PNG_BYTES, 'image/png'))).rejects.toThrow(/Sign in/) }) }) describe('a session that predates the permission it needs', () => { it('says what fixes a scope refusal, instead of repeating the PDS’s words', () => { // `RADIAL_SCOPE` is derived from `RADIAL_WRITES`, so shipping a collection — `closeGoal` — narrows // what an already-issued token authorizes relative to what the app now writes. The PDS refuses in // its own vocabulary, which names nothing a person can do; re-authorizing is what fixes it. const refused = writeFailure(new XrpcError(403, 'InvalidToken', 'Bad token scope')) expect(refused).toBeInstanceOf(Error) expect((refused as Error).message).toMatch(/sign out and sign in again/i) expect((refused as Error).message).toMatch(/Nothing was written/) expect(writeFailure(new XrpcError(401, 'InsufficientScope', 'nope'))).not.toBeInstanceOf(XrpcError) }) it('leaves every other failure exactly as it arrived', () => { // A message this app invented over a failure it did not understand is worse than the server's own. const swap = new XrpcError(400, 'InvalidSwap', 'Record was at a different cid') expect(writeFailure(swap)).toBe(swap) const rejected = new Error('title must be at most 300 characters') expect(writeFailure(rejected)).toBe(rejected) expect(writeFailure('not an error at all')).toBe('not an error at all') }) })