/** * Formatting/lint policy test — locks in the workspace formatting and lint * policy (E00-S05-T01, CI stage 3: formatting/lint). * * Policy (every file tracked by git, i.e. every committed text file): * - LF line endings: no carriage returns (no CRLF, no lone CR) * - no UTF-8 byte-order mark * - no trailing whitespace on any line * - no tab characters anywhere (indentation is spaces) * - exactly one final newline: the file must end with `\n`, with no blank * line left at the end of the file * JSON files additionally must: * - parse as strict JSON (no trailing commas, no comments) * - contain no duplicate object keys * - use 2-space indentation (every line's leading spaces are an even count) * * The scan is scoped to files tracked by git (`git ls-files`), so ignored and * generated files (node_modules/, dist/, .env, probe scratch files) never * enter the policy. Binary files (containing a NUL byte) are skipped. * * Mutation probes prove every rule is non-vacuous: each violation below is * injected into a temp file and must be reported. * * Run: `pnpm lint` (== `node --test tests/formatting-policy.test.mjs`) * (node:test — built into Node >= 18; no dependencies, lockfile untouched.) */ import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const read = (relPath) => readFileSync(path.join(REPO_ROOT, relPath), 'utf8'); /** True for binary files: the policy applies to text files only. */ const isBinary = (content) => content.includes('\0'); /** * Returns the list of tracked files (git ls-files, NUL-delimited) under the * given repo root, or fails the suite when git is unavailable. */ function trackedFiles(root = REPO_ROOT) { const result = spawnSync('git', ['-C', root, 'ls-files', '-z'], { encoding: 'utf8' }); assert.equal(result.status, 0, `git ls-files must succeed in ${root}`); return result.stdout.split('\0').filter((p) => p.length > 0); } /** * Collects the duplicate object keys of a JSON document. JSON.parse collapses * duplicate keys (last value wins) before any reviver or post-parse walker can * see them, so this scans the raw text: a string literal immediately followed * by `:` is an object key, and keys are tracked per enclosing `{…}` frame, so * same-named keys in different objects stay legal while a repeated key inside * one object is reported. */ function collectDuplicateKeys(text, out) { const frames = []; let i = 0; const n = text.length; while (i < n) { const ch = text[i]; if (ch === '"') { let j = i + 1; while (j < n && text[j] !== '"') { if (text[j] === '\\') j += 1; j += 1; } const key = text.slice(i + 1, j).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); let k = j + 1; while (k < n && (text[k] === ' ' || text[k] === '\t' || text[k] === '\n' || text[k] === '\r')) k += 1; if (text[k] === ':' && frames.length > 0) { const seen = frames[frames.length - 1]; if (seen.has(key)) out.push(`duplicate JSON key: ${key}`); seen.add(key); } i = k; continue; } if (ch === '{') { frames.push(new Set()); i += 1; continue; } if (ch === '}') { frames.pop(); i += 1; continue; } i += 1; } } /** * Returns the formatting/lint violations for one file relative to `root`, or * [] when the file conforms. Binary files are out of policy scope. */ function violationsForFile(root, relPath) { let content; try { content = readFileSync(path.join(root, relPath), 'utf8'); } catch (err) { return [`${relPath}: unreadable: ${err.message}`]; } if (isBinary(content)) return []; const label = (message) => `${relPath}: ${message}`; const out = []; if (content.includes('\r')) out.push(label('carriage return (use LF line endings)')); if (content.startsWith('\uFEFF')) out.push(label('UTF-8 byte-order mark')); if (content.includes('\t')) out.push(label('tab character (indentation must be spaces)')); const lines = content.split('\n'); lines.forEach((line, i) => { if (/[ \t]+$/.test(line)) out.push(label(`trailing whitespace on line ${i + 1}`)); }); if (content.length > 0) { if (lines[lines.length - 1] !== '') out.push(label('missing final newline')); else if (lines[lines.length - 2] === '') out.push(label('blank line at end of file')); } if (relPath.endsWith('.json')) { let parsed; try { parsed = JSON.parse(content); } catch (err) { out.push(label(`invalid JSON: ${err.message}`)); return out; } const duplicateKeys = []; collectDuplicateKeys(content, duplicateKeys); for (const message of duplicateKeys) out.push(label(message)); lines.forEach((line, i) => { const indent = /^([ ]*)\S/.exec(line); if (indent && indent[1].length % 2 !== 0) { out.push(label(`JSON indentation must be 2 spaces per level (line ${i + 1})`)); } }); } return out; } /** Writes a probe file into a fresh temp dir and returns its violations. */ function probeViolations(filename, content) { const dir = mkdtempSync(path.join(os.tmpdir(), 'eppp-format-policy-')); try { writeFileSync(path.join(dir, filename), content); return violationsForFile(dir, filename); } finally { rmSync(dir, { recursive: true, force: true }); } } // --------------------------------------------------------------------------- // Real-tree scan // --------------------------------------------------------------------------- test('every tracked text file conforms to the formatting/lint policy', () => { const files = trackedFiles(); assert.ok( files.length >= 25, `expected a meaningful tracked file set to scan (got ${files.length})`, ); const violations = []; for (const file of files) violations.push(...violationsForFile(REPO_ROOT, file)); assert.deepEqual(violations, [], `formatting/lint violations:\n${violations.join('\n')}`); }); test('the scan covers the workspace source, docs, manifests and workflow', () => { const files = trackedFiles(); for (const expected of [ 'apps/server/src/index.ts', 'packages/config/src/env.ts', 'docs/development/non-container.md', 'package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', 'compose.yaml', '.gitea/workflows/ci.yml', ]) { assert.ok(files.includes(expected), `tracked file set must include ${expected}`); } }); // --------------------------------------------------------------------------- // Mutation probes — every rule is non-vacuous // --------------------------------------------------------------------------- test('a trailing whitespace is reported (mutation probe)', () => { const violations = probeViolations('probe.txt', 'line with trailing space \n'); assert.ok(violations.some((v) => /trailing whitespace/.test(v)), `got: ${violations.join('; ')}`); }); test('a tab character is reported (mutation probe)', () => { const violations = probeViolations('probe.txt', 'line\twith tab\n'); assert.ok(violations.some((v) => /tab character/.test(v)), `got: ${violations.join('; ')}`); }); test('a CRLF line ending is reported (mutation probe)', () => { const violations = probeViolations('probe.txt', 'line\r\n'); assert.ok(violations.some((v) => /carriage return/.test(v)), `got: ${violations.join('; ')}`); }); test('a UTF-8 byte-order mark is reported (mutation probe)', () => { const violations = probeViolations('probe.txt', '\uFEFFline\n'); assert.ok(violations.some((v) => /byte-order mark/.test(v)), `got: ${violations.join('; ')}`); }); test('a missing final newline is reported (mutation probe)', () => { const violations = probeViolations('probe.txt', 'no final newline'); assert.ok(violations.some((v) => /missing final newline/.test(v)), `got: ${violations.join('; ')}`); }); test('a blank line at the end of the file is reported (mutation probe)', () => { const violations = probeViolations('probe.txt', 'line\n\n'); assert.ok(violations.some((v) => /blank line at end of file/.test(v)), `got: ${violations.join('; ')}`); }); test('invalid JSON is reported (mutation probe)', () => { const violations = probeViolations('probe.json', '{"a": 1,}\n'); assert.ok(violations.some((v) => /invalid JSON/.test(v)), `got: ${violations.join('; ')}`); }); test('a duplicate JSON key is reported (mutation probe)', () => { const violations = probeViolations('probe.json', '{"a": 1, "a": 2}\n'); assert.ok(violations.some((v) => /duplicate JSON key/.test(v)), `got: ${violations.join('; ')}`); }); test('odd JSON indentation is reported (mutation probe)', () => { const violations = probeViolations('probe.json', '{\n "a": 1,\n "b": 2\n}\n'); assert.ok(violations.some((v) => /2 spaces per level/.test(v)), `got: ${violations.join('; ')}`); }); test('binary files are out of policy scope (mutation probe)', () => { const violations = probeViolations('probe.bin', 'a\0b'); assert.deepEqual(violations, []); }); test('a conforming file yields no violations (mutation probe)', () => { const violations = probeViolations('probe.json', '{\n "a": 1\n}\n'); assert.deepEqual(violations, []); });