import { FIXTURE_DIDS } from '@radial/core/fixture' import { describe, expect, it } from 'vitest' import { buildFixtureSpace } from './fixture.js' import { DOC_TYPE_DRAFTS, ROADMAP_DOC_TYPE, USER_DOC_TYPE, docDrift, docGroups, docPostArgs, docTier, docTitle, docsHref, docsOf, driftBadges, editRequestArgs, hasDrifted, headMoved, isEditable, missingDocTypes, repinnedSources, resumableEdit, roadmapSections, tierFromParam, } from './docs.js' import { projectByName, unitsOf } from './space.js' import { artifactTypes, type UnitView } from '@radial/core' // The docs route's whole knowledge of what a tier is, tested against the fixture rather than against // invented records — the same rule the request tests follow, for the same reason: no answer here may // come from a type NAME being special, only from what the registry says. const space = buildFixtureSpace() const project = () => { const found = projectByName(space.index, 'radial-ng') if (!found) throw new Error('fixture project missing') return found } const doc = (title: string): UnitView => { const found = unitsOf(space.index, project()).find((unit) => docTitle(unit) === title) if (!found) throw new Error(`fixture doc missing: ${title}`) return found } describe('tiers', () => { it('is two registry entries and everything else', () => { // The design decision, as an assertion: the tiers are registry data, so the fixture's registry // is what puts a unit in one — nothing in the app carries a list of document names. const registry = artifactTypes(space.index) expect(registry.get(USER_DOC_TYPE)?.scope).toBe('project') expect(registry.get(ROADMAP_DOC_TYPE)?.durability).toBe('living') expect(missingDocTypes(space.index)).toEqual([]) }) it('splits a project’s units into dev, user and roadmap', () => { const tiers = docsOf(space.index, project()) expect(new Set(tiers.dev.map((unit) => unit.type))).toEqual( new Set(['adr', 'architecture', 'conventions']), ) expect(tiers.user.map(docTitle)).toEqual(['Artifact types', 'Review queue']) expect(tiers.roadmap.map(docTitle)).toEqual(['Ingestion']) }) it('puts an unknown project-scoped type in the dev tier, with no code change', () => { // A type an admin registers tomorrow. It is a dev doc because it is not one of the two, which is // the only rule there is — nothing has to learn its name. expect(docTier({ type: 'fdr' })).toBe('dev') expect(docTier({ type: USER_DOC_TYPE })).toBe('user') expect(isEditable('dev')).toBe(false) expect(isEditable('roadmap')).toBe(true) }) it('groups a tier by the kind of document, alphabetically and with nothing hard-coded', () => { const tiers = docsOf(space.index, project()) const dev = docGroups(tiers.dev) expect(dev.map((group) => group.type)).toEqual(['adr', 'architecture', 'conventions']) // The label is `typeLabel`'s, so an acronym is spoken as one and everything else is shown as the // registry spells it. Nothing here knows which names exist. expect(dev.map((group) => group.label)).toEqual(['ADR', 'architecture', 'conventions']) // Every page still appears exactly once, and a group keeps the reading order `docsOf` sorted. const byName = (left: string, right: string) => left.localeCompare(right) expect([...dev.flatMap((group) => group.units.map(docTitle))].sort(byName)).toEqual( [...tiers.dev.map(docTitle)].sort(byName), ) for (const group of dev) { const titles = group.units.map(docTitle) expect(titles).toEqual([...titles].sort(byName)) } // An editable tier IS one registry type, so it has exactly one group — which is why the page // draws the disclosures only where there is more than one. expect(docGroups(tiers.user).map((group) => group.type)).toEqual([USER_DOC_TYPE]) expect(docGroups([])).toEqual([]) }) it('reads the tier out of the URL and puts it back', () => { expect(tierFromParam(null)).toBe('dev') expect(tierFromParam('nonsense')).toBe('dev') expect(tierFromParam('roadmap')).toBe('roadmap') const href = docsHref(project(), 'user', doc('Review queue')) expect(href).toContain('tier=user') expect(href).toContain(`doc=${encodeURIComponent(doc('Review queue').key)}`) // The default tier is not spelled out: a docs link a person pastes should be the short one. expect(docsHref(project(), 'dev')).not.toContain('tier=') }) }) describe('the two drift signals', () => { it('reports a moved source, and reports it as a source rather than as a merge', () => { // "Review queue" documents the architecture at v1 and the architecture is at v3. Nothing has // merged under the project since the page landed, so the other signal is silent — which is the // point: source drift is a thing a quiet repository can still accumulate. const drift = docDrift(space.index, project(), doc('Review queue')) expect(drift.sources.map((entry) => entry.behind)).toEqual([2]) expect(drift.merges).toBe(0) expect(hasDrifted(drift)).toBe(true) expect(driftBadges(drift).map((badge) => badge.text)).toEqual(['1 source moved on']) }) it('badges the two signals separately and never adds them together', () => { // They answer different questions — "the sources moved" and "the code moved" — and a reader // acts on each differently, so a single summed number would be the app inventing a fact. const [sources] = docDrift(space.index, project(), doc('Review queue')).sources if (!sources) throw new Error('fixture doc is not drifted') const badges = driftBadges({ merges: 3, sources: [sources] }) expect(badges.map((badge) => badge.text)).toEqual(['1 source moved on', '3 merges since']) expect(badges.map((badge) => badge.text).join(' ')).not.toContain('4') }) it('counts merge drift on a dev doc, where nothing else can have drifted', () => { const architecture = docsOf(space.index, project()).dev.find( (unit) => unit.type === 'architecture', ) if (!architecture) throw new Error('fixture project has no architecture doc') const drift = docDrift(space.index, project(), architecture) expect(drift.merges).toBe(3) expect(driftBadges(drift).map((badge) => badge.text)).toEqual(['3 merges since']) }) it('says nothing about sources for a page pinned at every head', () => { const drift = docDrift(space.index, project(), doc('Artifact types')) expect(drift.sources).toEqual([]) expect(driftBadges(drift).some((badge) => badge.text.includes('source'))).toBe(false) }) it('never claims a dev doc has drifted from a source: it IS the source', () => { const architecture = docsOf(space.index, project()).dev.find( (unit) => unit.type === 'architecture', ) expect(architecture).toBeDefined() expect(docDrift(space.index, project(), architecture as UnitView).sources).toEqual([]) }) }) describe('the roadmap tier', () => { it('derives the road ahead and the road travelled from live goal state', () => { const sections = roadmapSections(space.index, doc('Ingestion')) expect(sections.ahead.map((entry) => entry.goal.target.value.title)).toEqual([ 'Jetstream subscription with polling fallback', ]) expect(sections.travelled.map((entry) => entry.goal.disposition)).toEqual([ 'completed', 'dropped', ]) // "here's where we got web push" — the merged implementations under an ended goal, derived // rather than authored, which is what the roadmap tier exists to show. expect(sections.travelled[0]?.landed.length).toBeGreaterThan(0) }) it('ignores refs that are not goals, so an artifact ref on a roadmap is provenance only', () => { const named = doc('Ingestion').current?.request?.value.basedOn.length ?? 0 const sections = roadmapSections(space.index, doc('Ingestion')) expect(named).toBe(4) expect(sections.ahead.length + sections.travelled.length).toBe(3) }) }) describe('writing a doc', () => { const did = FIXTURE_DIDS.tim it('always assigns an edit request to its own author', () => { // The load-bearing one. An unassigned open request is claimable by any operator's daemon, so a // half-written page must never look open — `dispatch.ts` refuses a request named to a person. const args = editRequestArgs({ project: project(), type: USER_DOC_TYPE, did, sources: [], }) expect(args.slice(0, 6)).toEqual([ 'request', 'create', '--project', project().target.uri, '--type', USER_DOC_TYPE, ]) expect(args).toContain('--assignee') expect(args[args.indexOf('--assignee') + 1]).toBe(did) }) it('pins the predecessor first, then every source, so the edit continues the chain', () => { const unit = doc('Review queue') const previous = unit.current if (!previous) throw new Error('fixture doc has no version') const sources = repinnedSources(space.index, unit) const args = editRequestArgs({ project: project(), type: USER_DOC_TYPE, did, prev: previous, sources, }) const based = args.flatMap((value, position) => value === '--based-on' ? [args[position + 1] as string] : [], ) expect(based[0]).toBe(`${previous.artifact.uri}#${previous.artifact.cid}`) expect(based).toHaveLength(1 + sources.length) }) it('re-pins a drifted source to the current head — that is what clears the badge', () => { const unit = doc('Review queue') const drifted = docDrift(space.index, project(), unit).sources[0] if (!drifted) throw new Error('fixture doc is not drifted') const repinned = repinnedSources(space.index, unit) expect(repinned).toContainEqual({ uri: drifted.current.artifact.uri, cid: drifted.current.artifact.cid, }) expect(repinned.map((reference) => reference.uri)).not.toContain(drifted.ref.uri) // A source that had not moved is carried through untouched, at the version it was read at. expect(repinned).toHaveLength(unit.current?.request?.value.basedOn.length ?? 0) // And the page's own predecessor is never in the list: it is pinned as `prev`, not as a source. for (const version of unit.versions) { expect(repinned.map((reference) => reference.uri)).not.toContain(version.artifact.uri) } }) it('posts the artifact with the title, the body and the version it supersedes', () => { const unit = doc('Review queue') const previous = unit.current if (!previous) throw new Error('fixture doc has no version') const args = docPostArgs({ request: { uri: 'at://did:plc:x/com.disnetdev.radial.artifactRequest/1', cid: 'cid-1' }, title: ' Review queue ', body: '# Body', prev: previous, }) expect(args).toEqual([ 'artifact', 'post', '--request', 'at://did:plc:x/com.disnetdev.radial.artifactRequest/1#cid-1', '--prev', `${previous.artifact.uri}#${previous.artifact.cid}`, '--title', 'Review queue', '--body', '# Body', ]) }) it('offers to resume an edit whose artifact never landed, and only to its own author', () => { // Nothing in the fixture is half-written, which is the state this predicate must not invent. expect(resumableEdit(space.index, project(), did)).toEqual([]) expect(resumableEdit(space.index, project(), '')).toEqual([]) }) it('notices the head moving under an open form without refusing the save', () => { const unit = doc('Review queue') expect(headMoved(unit, unit.current?.artifact.uri)).toBe(false) expect(headMoved(unit, 'at://did:plc:x/com.disnetdev.radial.artifact/older')).toBe(true) // Nothing to compare against — a new page — is not a fork. expect(headMoved(unit, undefined)).toBe(false) }) }) describe('setting the tiers up', () => { it('offers the two registry drafts a space needs, briefed for an agent', () => { expect(DOC_TYPE_DRAFTS.map((draft) => draft.name)).toEqual([USER_DOC_TYPE, ROADMAP_DOC_TYPE]) for (const draft of DOC_TYPE_DRAFTS) { expect(draft.scope).toBe('project') expect(draft.durability).toBe('living') expect(draft.brief.length).toBeGreaterThan(80) } }) })