merge: enforce CI gates the contributor rules already required (D1, A3, A4, D2)
Add cargo fmt --check (strict) and cargo clippy (advisory) to CI, ratchet the traceability threshold 50 -> 82, add a dangling-ID gate, and fix the offlineCatalog flake (cold dynamic import, not a timer).
This commit is contained in:
@@ -14,7 +14,17 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { countDefinedRequirements, computeCoverage } from "./extract-traces";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import {
|
||||
countDefinedRequirements,
|
||||
computeCoverage,
|
||||
findDanglingIds,
|
||||
MIN_COVERAGE_PERCENT,
|
||||
} from "./extract-traces";
|
||||
|
||||
// import.meta.dir is Bun-only; derive from import.meta.url under vitest.
|
||||
const HERE = path.dirname(new URL(import.meta.url).pathname);
|
||||
|
||||
describe("countDefinedRequirements", () => {
|
||||
it("counts a well-formed table row as a defined requirement", () => {
|
||||
@@ -82,6 +92,80 @@ Some prose explaining that UR-005 relates to DR-001 and JA-002.
|
||||
expect(defined.ids.has("DR-050")).toBe(true);
|
||||
expect(defined.ids.has("UR-999")).toBe(false);
|
||||
});
|
||||
|
||||
it("collects UT/IT rows separately, out of the coverage denominator", () => {
|
||||
// §4 defines the test taxonomy. Those rows must be known (so a TRACES
|
||||
// comment may name them) without ever moving the coverage ratio.
|
||||
const md = `
|
||||
| UR-001 | A | High | Done |
|
||||
| UT-001 | Player state transitions | DR-001 | Pending |
|
||||
| IT-004 | Playback end-to-end | DR-002 | Pending |
|
||||
`;
|
||||
const defined = countDefinedRequirements(md);
|
||||
expect(defined.total).toBe(1);
|
||||
expect(defined.ids.has("UT-001")).toBe(false);
|
||||
expect(defined.testIds.has("UT-001")).toBe(true);
|
||||
expect(defined.testIds.has("IT-004")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findDanglingIds", () => {
|
||||
const defined = {
|
||||
UR: 1,
|
||||
IR: 0,
|
||||
DR: 1,
|
||||
JA: 0,
|
||||
total: 2,
|
||||
ids: new Set(["UR-001", "DR-001"]),
|
||||
testIds: new Set(["UT-001"]),
|
||||
};
|
||||
|
||||
it("flags a requirement ID that requirements.md does not define", () => {
|
||||
expect(findDanglingIds(["UR-001", "DR-189"], defined)).toEqual(["DR-189"]);
|
||||
});
|
||||
|
||||
it("flags an undefined UT/IT id, which the coverage orphan list cannot", () => {
|
||||
// The gap this closes: computeCoverage deliberately ignores UT/IT, so
|
||||
// UT-188 sat in three source files, defined nowhere, entirely unreported.
|
||||
expect(computeCoverage(["UT-188"], defined).orphaned).toEqual([]);
|
||||
expect(findDanglingIds(["UT-188"], defined)).toEqual(["UT-188"]);
|
||||
});
|
||||
|
||||
it("accepts every ID that is defined, requirement or test", () => {
|
||||
expect(findDanglingIds(["UR-001", "DR-001", "UT-001"], defined)).toEqual([]);
|
||||
});
|
||||
|
||||
it("deduplicates and sorts, so one typo is reported once", () => {
|
||||
expect(
|
||||
findDanglingIds(["DR-189", "DR-189", "UR-999", "DR-189"], defined)
|
||||
).toEqual(["DR-189", "UR-999"]);
|
||||
});
|
||||
|
||||
it("ignores IDs whose prefix is not a known trace type", () => {
|
||||
// e.g. an unrelated "AB-123" caught by the loose ID regex.
|
||||
expect(findDanglingIds(["AB-123"], defined)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("coverage threshold", () => {
|
||||
it("matches MIN_THRESHOLD in the Gitea traceability workflow", () => {
|
||||
// Two files must agree on the gate: the script (local `traces:coverage`)
|
||||
// and the workflow. Drift means the local gate and CI disagree about what
|
||||
// passes, which is how the 50%-while-actually-86% slack went unnoticed.
|
||||
const workflow = fs.readFileSync(
|
||||
path.resolve(HERE, "../.gitea/workflows/traceability-check.yml"),
|
||||
"utf-8"
|
||||
);
|
||||
const match = workflow.match(/^\s*MIN_THRESHOLD=(\d+)\s*$/m);
|
||||
expect(match).not.toBeNull();
|
||||
expect(Number(match![1])).toBe(MIN_COVERAGE_PERCENT);
|
||||
});
|
||||
|
||||
it("is a ratchet: never lower it to make a red build pass", () => {
|
||||
// Sanity bound. If coverage genuinely climbs, raise both numbers together.
|
||||
expect(MIN_COVERAGE_PERCENT).toBeGreaterThanOrEqual(82);
|
||||
expect(MIN_COVERAGE_PERCENT).toBeLessThanOrEqual(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeCoverage", () => {
|
||||
@@ -92,6 +176,7 @@ describe("computeCoverage", () => {
|
||||
JA: 0,
|
||||
total: 4,
|
||||
ids: new Set(["UR-001", "UR-002", "DR-001", "DR-002"]),
|
||||
testIds: new Set<string>(),
|
||||
};
|
||||
|
||||
it("computes coverage as traced ∩ defined over defined", () => {
|
||||
@@ -138,7 +223,15 @@ describe("computeCoverage", () => {
|
||||
});
|
||||
|
||||
it("reports 0% rather than NaN when nothing is defined", () => {
|
||||
const empty = { UR: 0, IR: 0, DR: 0, JA: 0, total: 0, ids: new Set<string>() };
|
||||
const empty = {
|
||||
UR: 0,
|
||||
IR: 0,
|
||||
DR: 0,
|
||||
JA: 0,
|
||||
total: 0,
|
||||
ids: new Set<string>(),
|
||||
testIds: new Set<string>(),
|
||||
};
|
||||
const cov = computeCoverage([], empty);
|
||||
expect(cov.percent).toBe(0);
|
||||
expect(Number.isNaN(cov.percent)).toBe(false);
|
||||
@@ -163,12 +256,8 @@ describe("live requirements.md", () => {
|
||||
// (total 114) while the real file had grown to 211. Update these numbers
|
||||
// deliberately when requirements are added — that edit is the signal the
|
||||
// denominator is live rather than frozen.
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
// import.meta.dir is Bun-only; derive from import.meta.url under vitest.
|
||||
const here = path.dirname(new URL(import.meta.url).pathname);
|
||||
const md = fs.readFileSync(
|
||||
path.resolve(here, "../docs/requirements.md"),
|
||||
path.resolve(HERE, "../docs/requirements.md"),
|
||||
"utf-8"
|
||||
);
|
||||
const defined = countDefinedRequirements(md);
|
||||
|
||||
Reference in New Issue
Block a user