import { COLLECTIONS, materialize, MemoryRecordStore, type MaterializedIndex } from '@radial/core' import { FIXTURE_DIDS, fixtureSpace } from '@radial/core/fixture' import { describe, expect, it } from 'vitest' import { buildFixtureSpace } from './fixture.js' import { unitsOf } from './space.js' import { findVersion, liveUnits, unitsWithContext } from './units.js' import { askedForReview, askReviewArgs, blankFinding, findingsOf, inlineFinding, judgeable, mayJudge, owedBy, possibleReviewers, reviewQueue, unjudged, verdictArgs, wroteVerdict, } from './verdicts.js' // A review pins one exact version, and a request for one is a debt on that same version. Everything // here is that single rule seen from a different side: what the buttons produce, which request a // review closes, and what the queue is a list of. const space = buildFixtureSpace() const goal = (title: string) => { const found = space.index.goals.find((entry) => entry.target.value.title === title) if (!found) throw new Error(`fixture goal missing: ${title}`) return found } const unit = (title: string, type: string) => { const found = unitsOf(space.index, goal(title)).find((entry) => entry.type === type) if (!found) throw new Error(`fixture unit missing: ${type} in ${title}`) return found } const IGNORED = 'Surface the records the fold ignored' const QUEUE = 'Review queue in the web UI' describe('what the editor produces', () => { it('drops a row nobody filled in, rather than writing a severity with nothing behind it', () => { expect(findingsOf([blankFinding(), blankFinding()])).toEqual([]) expect(findingsOf([{ ...blankFinding(), path: 'src/a.ts', line: '12' }])).toEqual([]) }) it('carries a line only next to the path it points into', () => { expect( findingsOf([ { severity: 'error', path: 'src/store.ts', line: '41', body: 'Resolves by uri.' }, { severity: 'info', path: '', line: '9', body: 'A note about the whole document.' }, { severity: 'warning', path: 'src/a.ts', line: 'not a number', body: 'Somewhere in here.' }, { severity: 'warning', path: 'src/b.ts', line: '0', body: 'Lines start at one.' }, ]), ).toEqual([ { severity: 'error', path: 'src/store.ts', line: 41, body: 'Resolves by uri.' }, { severity: 'info', body: 'A note about the whole document.' }, { severity: 'warning', path: 'src/a.ts', body: 'Somewhere in here.' }, { severity: 'warning', path: 'src/b.ts', body: 'Lines start at one.' }, ]) }) it('trims, because a trailing newline is not part of what somebody said', () => { expect(findingsOf([{ severity: 'info', path: ' src/a.ts ', line: '3', body: ' Fix.\n' }])).toEqual([ { severity: 'info', path: 'src/a.ts', line: 3, body: 'Fix.' }, ]) }) it('keeps a selected passage and its suggestion together as portable markdown', () => { expect(inlineFinding('First line\nSecond line', 'Make this measurable.', 'info')).toEqual({ severity: 'info', path: '', line: '', body: '> First line\n> Second line\n\nMake this measurable.', }) }) }) describe('what a review pins', () => { it('names the version being read, by cid, and not the unit', () => { const plan = unit(QUEUE, 'plan') const [v1, v2] = plan.versions if (!v1 || !v2) throw new Error('fixture chain missing') const args = verdictArgs(v1, { verdict: 'request_changes', findings: [] }) expect(args).toEqual([ 'review', 'post', '--subject', `${v1.artifact.uri}#${v1.artifact.cid}`, '--verdict', 'request_changes', ]) // Browsing back to v1 and judging it judges v1 — the subject is never the tip. expect(args).not.toContain(`${v2.artifact.uri}#${v2.artifact.cid}`) }) it('sends findings inline, because a browser has no file to point --findings-file at', () => { const version = unit(IGNORED, 'plan').versions[0] if (!version) throw new Error('fixture version missing') const findings = findingsOf([ { severity: 'error', path: 'src/a.ts', line: '4', body: 'One.' }, { severity: 'warning', path: '', line: '', body: 'Two.' }, ]) const args = verdictArgs(version, { verdict: 'request_changes', findings }) expect(args).not.toContain('--findings-file') const json = args[args.indexOf('--findings-json') + 1] as string expect(JSON.parse(json)).toEqual(findings) }) it('names the request it answers, which is what closes it', () => { const implementation = unit(IGNORED, 'implementation') const version = implementation.current if (!version) throw new Error('fixture version missing') const request = owedBy(version, FIXTURE_DIDS.tim) if (!request) throw new Error('fixture review request missing') const args = verdictArgs(version, { verdict: 'approve', findings: [], request }) expect(args).toContain('--request') expect(args[args.indexOf('--request') + 1]).toBe(`${request.uri}#${request.cid}`) }) it('is offered on anything that landed, and on nothing that has not', () => { const plan = unit(IGNORED, 'plan') expect(judgeable(plan, plan.current)).toBe(true) // An open request has no version to pin a review to. const security = unit(QUEUE, 'security-review') expect(security.versions).toHaveLength(0) expect(judgeable(security, security.current)).toBe(false) }) it('counts only an active member, because the fold drops everybody else', () => { expect(mayJudge(space.directory, FIXTURE_DIDS.tim)).toBe(true) expect(mayJudge(space.directory, 'did:plc:nobodyatall00')).toBe(false) expect(mayJudge(space.directory, '')).toBe(false) }) it('says what it did, and whether it answered anyone', () => { const version = unit(IGNORED, 'implementation').current if (!version) throw new Error('fixture version missing') expect(wroteVerdict('approve', version, true)).toBe( 'Approved v1 — the review request is answered', ) expect(wroteVerdict('request_changes', version, false)).toBe('Changes requested on v1') }) }) describe('the request a review answers', () => { const version = () => { const found = unit(IGNORED, 'implementation').current if (!found) throw new Error('fixture version missing') return found } it('is the one naming me', () => { expect(owedBy(version(), FIXTURE_DIDS.tim)?.value.assignee).toBe(FIXTURE_DIDS.tim) }) it('is never one naming somebody else', () => { // The fold marks a review request fulfilled by any trusted review that pins the same subject — // it does not check who wrote it. Naming another member's request would cancel their work. // Nobody asked ana for anything here, so she has nothing to answer even though there are two // open asks on this exact version. expect(version().reviewRequests).toHaveLength(2) expect(owedBy(version(), FIXTURE_DIDS.ana)).toBeUndefined() // …and the daemon's own auto-review ask is the reviewer's to answer, never tim's and never hers. expect(owedBy(version(), FIXTURE_DIDS.reviewer)?.value.assignee).toBe(FIXTURE_DIDS.reviewer) expect(owedBy(version(), FIXTURE_DIDS.tim)?.value.assignee).toBe(FIXTURE_DIDS.tim) }) it('is nothing at all where nobody asked', () => { expect(owedBy(unit(IGNORED, 'plan').current, FIXTURE_DIDS.tim)).toBeUndefined() expect(owedBy(undefined, FIXTURE_DIDS.tim)).toBeUndefined() }) }) describe('asking somebody for one', () => { it('carries a subject where every other request carries a basedOn', () => { const version = unit(IGNORED, 'plan').current if (!version) throw new Error('fixture version missing') expect( askReviewArgs({ target: goal(IGNORED).target.uri, scope: 'goal', version, assignee: FIXTURE_DIDS.ana, brief: ' Look at the grouping. ', }), ).toEqual([ 'request', 'create', '--goal', goal(IGNORED).target.uri, '--type', 'review', '--subject', `${version.artifact.uri}#${version.artifact.cid}`, '--assignee', FIXTURE_DIDS.ana, '--brief', 'Look at the grouping.', ]) }) it('offers people and review-publishing agents, never the asker, never a producer of something else', () => { const reviewers = possibleReviewers(space.directory, FIXTURE_DIDS.tim).map((actor) => actor.name) // Humans first — a review is a human judgement, and an agent one is a convenience. `jun` is on the // list despite never having opened the space: asking for a review is membership, not attendance, // and being asked for one is a perfectly good reason to turn up. expect(reviewers).toEqual(['ana.dev', 'jun.example.com', 'reviewer']) // `builder` publishes implementations and `planner` publishes plans; neither judges. expect(reviewers).not.toContain('builder') expect(reviewers).not.toContain('planner') expect(possibleReviewers(space.directory, FIXTURE_DIDS.ana).map((actor) => actor.name)).not.toContain( 'ana.dev', ) }) it('says which version was asked about, and whether anyone in particular was asked', () => { const version = unit(IGNORED, 'plan').current if (!version) throw new Error('fixture version missing') expect(askedForReview(space.directory.get(FIXTURE_DIDS.ana), version)).toBe( 'Review of v1 requested from ana.dev', ) expect(askedForReview(undefined, version)).toBe('Review of v1 requested — open to any member') }) }) describe('the queue', () => { it('lists the reviews asked of one person, on the exact versions they were asked about', () => { const rows = reviewQueue(space.index, FIXTURE_DIDS.tim) expect(rows).toHaveLength(1) const [row] = rows expect(row?.unit.type).toBe('implementation') expect(row?.version?.artifact.uri).toBe(unit(IGNORED, 'implementation').current?.artifact.uri) // Nobody asked ana for anything. expect(reviewQueue(space.index, FIXTURE_DIDS.ana)).toHaveLength(0) }) it('falls back to every review owed by anyone when nobody is signed in', () => { expect(reviewQueue(space.index)).toHaveLength(1) }) it('separates the artifacts nobody was asked about from the ones somebody was', () => { const landed = unjudged(space.index) // The implementation is in the queue, so it is NOT also in the nobody-asked list; the plan // beside it landed with nothing pinned to it and nobody looking, which is the larger case. expect(landed.map(({ unit: entry }) => entry.type)).toEqual(['plan']) expect(landed[0]?.unit.needsVerdict).toBe(true) expect(landed[0]?.version).toBeUndefined() }) it('never lists something retracted, or something that has not landed', () => { for (const { unit: entry } of unjudged(space.index)) { expect(entry.retracted).toBe(false) expect(entry.versions.length).toBeGreaterThan(0) } }) }) describe('ending a goal empties what it was holding in the queue', () => { // Both flavours of "For me" are drawn from the ignored-records goal, so ending that one goal is the // whole queue going quiet. This is the shape of the bug it fixes: every goal in a space set aside, // its project page empty because `goalsOf()` honours the record, and "For me" still holding rows // nothing could ever clear. The ending here is a legacy `archiveGoal`, which is also the compat // reading under test: an old client's record still shelves the goal for a reader. const shelvedUri = fixtureSpace().goals.ignored const archivedIndex = (): MaterializedIndex => { const fixture = fixtureSpace() const goal = fixture.records.find((record) => record.uri === fixture.goals.ignored) if (!goal) throw new Error('fixture goal missing') const store = new MemoryRecordStore() for (const record of fixture.records) store.put(record) store.put({ did: FIXTURE_DIDS.tim, collection: COLLECTIONS.archiveGoal, rkey: '3lba0archive001', uri: `at://${FIXTURE_DIDS.tim}/${COLLECTIONS.archiveGoal}/3lba0archive001`, cid: 'cid-3lba0archive001', rev: '9999999999999', value: { $type: COLLECTIONS.archiveGoal, goal: { uri: goal.uri, cid: goal.cid }, archived: true, createdAt: fixture.asOf, }, }) return materialize(store, { spaceUri: fixture.spaceUri, asOf: fixture.asOf }) } it('drops both a review asked of somebody and one nobody asked for', () => { const index = archivedIndex() expect(index.goals.find((goal) => goal.target.uri === shelvedUri)?.ended).toBe(true) expect(reviewQueue(index, FIXTURE_DIDS.tim)).toEqual([]) expect(reviewQueue(index)).toEqual([]) expect(unjudged(index)).toEqual([]) }) it('still resolves a reference into the shelved goal, because a chip elsewhere still names it', () => { const index = archivedIndex() const shelved = unitsWithContext(index).filter(({ target }) => target.target.uri === shelvedUri) expect(shelved.length).toBeGreaterThan(0) expect(liveUnits(index).some(({ target }) => target.target.uri === shelvedUri)).toBe(false) // The exact count, so a unit dropped by some other rule is a failure here rather than a silent // narrowing: what leaves the smart lists is everything under an ended goal, no more and no less. // (`liveTargets` delegates to `activeGoals()`, so an ended goal's units go — including a // `completed` one's, which is the reader-side change 6.8 made deliberately; see `units.ts`.) const ended = unitsWithContext(index).filter( ({ target }) => 'ended' in target && target.ended, ) expect(ended.length).toBeGreaterThan(shelved.length) expect(liveUnits(index)).toHaveLength(unitsWithContext(index).length - ended.length) const version = shelved[0]?.unit.versions[0] if (!version) throw new Error('fixture version missing') expect(findVersion(index, version.artifact)?.unit.key).toBe(shelved[0]?.unit.key) }) })