From 89f756549de8712fbae61f7123ffec18d2fec386 Mon Sep 17 00:00:00 2001 From: "kubestellar-hive[bot]" Date: Fri, 18 Sep 2026 04:38:23 -0400 Subject: [PATCH] test: cover the adr/ decision-record contract Adds tests/adr-contract.test.mjs, asserting that adr/README.md's index table and the adr/NNNN-*.md files agree: indexed links resolve and are named for their number, index numbers are unique, index Status and Title match each record's header and heading, every record declares a parseable Date and an allowed Status, each record heads itself with its own number, and relative links between ADR files resolve. Two assertions are marked todo because main already violates them: the supporting outreach draft takes the 0001- prefix without being ADR 0001, so two files claim that number and one is absent from the index. Closes #280 Signed-off-by: kubestellar-hive[bot] --- tests/adr-contract.test.mjs | 217 ++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 tests/adr-contract.test.mjs diff --git a/tests/adr-contract.test.mjs b/tests/adr-contract.test.mjs new file mode 100644 index 00000000..3a0a2000 --- /dev/null +++ b/tests/adr-contract.test.mjs @@ -0,0 +1,217 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { existsSync, readFileSync, readdirSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { join } from 'node:path'; + +const adrDir = fileURLToPath(new URL('../adr', import.meta.url)); +const readme = readFileSync(join(adrDir, 'README.md'), 'utf8'); + +const ALLOWED_STATUSES = new Set([ + 'Proposed', + 'Accepted', + 'Rejected', + 'Superseded', + 'Deprecated', +]); + +const markdownFiles = readdirSync(adrDir) + .filter((name) => name.endsWith('.md') && name !== 'README.md') + .sort(); + +// Files whose name begins with a four-digit sequence number claim an ADR +// number; supporting material that does not is exempt from the number and +// index contracts. +const numberPrefixed = markdownFiles.filter((name) => /^\d{4}-/.test(name)); + +// | [0001](./0001-slug.md) | Title | Status | +const INDEX_ROW = + /^\|\s*\[(\d{4})\]\((\.\/[^)]+)\)\s*\|\s*(.+?)\s*\|\s*(\S+)\s*\|\s*$/; + +const indexRows = readme + .split('\n') + .map((line) => line.match(INDEX_ROW)) + .filter(Boolean) + .map(([, number, href, title, status]) => ({ + number, + href, + title, + status, + })); + +function readAdr(name) { + return readFileSync(join(adrDir, name), 'utf8'); +} + +function headerField(body, field) { + const match = body.match( + new RegExp(`^- \\*\\*${field}\\*\\*:\\s*(.+)$`, 'm'), + ); + return match ? match[1].trim() : null; +} + +function heading(body) { + const match = body.match(/^#\s+(.+)$/m); + return match ? match[1].trim() : null; +} + +test('the index table lists at least one ADR', () => { + assert.ok( + indexRows.length > 0, + 'adr/README.md must contain a table of ADR rows matching | [NNNN](./file.md) | Title | Status |', + ); +}); + +test('every indexed ADR links to a file that exists', () => { + for (const row of indexRows) { + const target = row.href.replace(/^\.\//, ''); + assert.ok( + existsSync(join(adrDir, target)), + `adr/README.md row ${row.number} links to missing file ${row.href}`, + ); + } +}); + +test('every indexed ADR links to a file named for its number', () => { + for (const row of indexRows) { + const target = row.href.replace(/^\.\//, ''); + assert.ok( + target.startsWith(`${row.number}-`), + `adr/README.md row ${row.number} links to ${target}, which is not named for ADR ${row.number}`, + ); + } +}); + +test('index numbers are unique', () => { + const seen = new Set(); + for (const row of indexRows) { + assert.ok( + !seen.has(row.number), + `adr/README.md lists ADR ${row.number} more than once`, + ); + seen.add(row.number); + } +}); + +// A decision record is a number-prefixed file that either heads itself as an +// ADR or is linked from the index; supporting drafts filed alongside them are +// not held to the ADR header contract. +const adrFiles = numberPrefixed.filter( + (name) => + /^ADR\s+\d{4}:/.test(heading(readAdr(name)) ?? '') || + indexRows.some((row) => row.href.replace(/^\.\//, '') === name), +); + +test('at least one decision record is present', () => { + assert.ok(adrFiles.length > 0, 'adr/ contains no ADR files'); +}); + +test('every ADR file carries a parseable Date header', () => { + for (const name of adrFiles) { + const date = headerField(readAdr(name), 'Date'); + assert.ok(date, `adr/${name} is missing a "- **Date**:" header field`); + assert.ok( + !Number.isNaN(Date.parse(date)), + `adr/${name} has an unparseable Date header: ${date}`, + ); + } +}); + +test('every ADR file declares an allowed Status', () => { + for (const name of adrFiles) { + const status = headerField(readAdr(name), 'Status'); + assert.ok(status, `adr/${name} is missing a "- **Status**:" header field`); + assert.ok( + ALLOWED_STATUSES.has(status), + `adr/${name} has status "${status}"; expected one of ${[...ALLOWED_STATUSES].join(', ')}`, + ); + } +}); + +test('index status matches the status recorded in each ADR file', () => { + for (const row of indexRows) { + const target = row.href.replace(/^\.\//, ''); + if (!existsSync(join(adrDir, target))) continue; + const status = headerField(readAdr(target), 'Status'); + assert.equal( + row.status, + status, + `adr/README.md lists ADR ${row.number} as "${row.status}" but adr/${target} records "${status}"`, + ); + } +}); + +test('index title matches the ADR heading', () => { + for (const row of indexRows) { + const target = row.href.replace(/^\.\//, ''); + if (!existsSync(join(adrDir, target))) continue; + const title = heading(readAdr(target)); + assert.ok(title, `adr/${target} is missing an H1 heading`); + const expected = title.replace(/^ADR\s+\d{4}:\s*/, ''); + assert.equal( + row.title, + expected, + `adr/README.md titles ADR ${row.number} "${row.title}" but adr/${target} heads "${expected}"`, + ); + } +}); + +test('every ADR heading names its own file number', () => { + for (const name of adrFiles) { + const title = heading(readAdr(name)); + const declared = title?.match(/^ADR\s+(\d{4}):/)?.[1]; + if (!declared) continue; + assert.equal( + declared, + name.slice(0, 4), + `adr/${name} heads itself "ADR ${declared}" but is filed under ${name.slice(0, 4)}`, + ); + } +}); + +test('relative links between ADR files resolve', () => { + for (const name of [...markdownFiles, 'README.md']) { + const body = readFileSync(join(adrDir, name), 'utf8'); + for (const [, href] of body.matchAll(/]\((\.\/[^)#]+)[^)]*\)/g)) { + const target = href.replace(/^\.\//, ''); + assert.ok( + existsSync(join(adrDir, target)), + `adr/${name} links to ${href}, which does not exist`, + ); + } + } +}); + +// Known deviation, tracked separately: adr/0001-stakeholder-outreach-draft.md +// takes the 0001 prefix without being ADR 0001, so the directory holds two +// files claiming that number and one of them is absent from the index. +test('each ADR number is claimed by exactly one file', { todo: true }, () => { + const byNumber = new Map(); + for (const name of numberPrefixed) { + const number = name.slice(0, 4); + byNumber.set(number, [...(byNumber.get(number) ?? []), name]); + } + for (const [number, names] of byNumber) { + assert.equal( + names.length, + 1, + `ADR number ${number} is claimed by ${names.length} files: ${names.join(', ')}`, + ); + } +}); + +test( + 'every number-prefixed ADR file is listed in the index', + { todo: true }, + () => { + const indexed = new Set( + indexRows.map((row) => row.href.replace(/^\.\//, '')), + ); + for (const name of numberPrefixed) { + assert.ok( + indexed.has(name), + `adr/${name} is not listed in adr/README.md`, + ); + } + }, +);