The strict schema rejected 38 of 39 approved traces in docs/library-decisions: lastRevalidated was required (most traces omit it), and the compliance frontmatter fields the _template.md documents (is-sub-processor, processes-pii, plus the sub-processor block: data-sent, region, dpa-signed, sccs-required, contact) were unrecognized keys. Make lastRevalidated optional, add the compliance fields (booleanish coercion for YAML string scalars), and require data-sent when is-sub-processor is true. New loop test asserts every committed trace parses so the schema can never drift away from the repo's own corpus again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
283 lines
8.1 KiB
JavaScript
283 lines
8.1 KiB
JavaScript
import { test, describe } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { validateTrace, parseTrace, parseFrontmatter } from "./schema.mjs";
|
|
|
|
function validRaw(overrides = {}) {
|
|
return {
|
|
package: "example-lib",
|
|
version: "^1.0.0",
|
|
tier: "feature",
|
|
decision: "approved",
|
|
date: "2026-05-14",
|
|
deciders: ["alice"],
|
|
adr: null,
|
|
lastRevalidated: null,
|
|
"filter-results": {
|
|
license: "MIT",
|
|
types: "native",
|
|
maintenance: "active",
|
|
"boundary-fit": "pass",
|
|
"shadow-check": "pass",
|
|
"eu-residency": "ok",
|
|
"cve-scan": "clean",
|
|
"named-consumer": "pass",
|
|
socketRisk: "clean",
|
|
},
|
|
"verification-commands": ["pnpm audit --audit-level=moderate"],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function writeTempTrace(frontmatter, body = "") {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "trace-"));
|
|
const file = path.join(dir, "trace.md");
|
|
fs.writeFileSync(file, `---\n${frontmatter}\n---\n\n${body}`);
|
|
return file;
|
|
}
|
|
|
|
const VALID_FM = `package: example-lib
|
|
version: "^1.0.0"
|
|
tier: feature
|
|
decision: approved
|
|
date: 2026-05-14
|
|
deciders: [alice, bob]
|
|
adr: null
|
|
lastRevalidated: null
|
|
filter-results:
|
|
license: MIT
|
|
types: native
|
|
maintenance: active
|
|
boundary-fit: pass
|
|
shadow-check: pass
|
|
eu-residency: ok
|
|
cve-scan: clean
|
|
named-consumer: pass
|
|
socketRisk: clean
|
|
verification-commands:
|
|
- pnpm audit --audit-level=moderate`;
|
|
|
|
describe("validateTrace > valid cases", () => {
|
|
test("valid trace round-trips without error", () => {
|
|
const result = validateTrace(validRaw());
|
|
assert.equal(result.package, "example-lib");
|
|
assert.equal(result["filter-results"].license, "MIT");
|
|
assert.deepEqual(result["verification-commands"], [
|
|
"pnpm audit --audit-level=moderate",
|
|
]);
|
|
});
|
|
|
|
test("accepted-cves absent is valid", () => {
|
|
const result = validateTrace(validRaw());
|
|
assert.equal(result["accepted-cves"], undefined);
|
|
});
|
|
|
|
test("accepted-cves present is valid", () => {
|
|
const result = validateTrace(
|
|
validRaw({ "accepted-cves": ["CVE-2024-0001"] }),
|
|
);
|
|
assert.deepEqual(result["accepted-cves"], ["CVE-2024-0001"]);
|
|
});
|
|
|
|
test("null adr is valid", () => {
|
|
assert.equal(validateTrace(validRaw({ adr: null })).adr, null);
|
|
});
|
|
|
|
test("string adr is valid", () => {
|
|
assert.equal(validateTrace(validRaw({ adr: "adr-022" })).adr, "adr-022");
|
|
});
|
|
});
|
|
|
|
describe("validateTrace > rejection cases", () => {
|
|
test("missing required field throws", () => {
|
|
const raw = validRaw();
|
|
delete raw.package;
|
|
assert.throws(() => validateTrace(raw), /invalid_type|Required/i);
|
|
});
|
|
|
|
test("invalid tier enum throws", () => {
|
|
assert.throws(
|
|
() => validateTrace(validRaw({ tier: "invalid" })),
|
|
/invalid_enum_value|Invalid enum value/i,
|
|
);
|
|
});
|
|
|
|
test("invalid maintenance enum throws", () => {
|
|
const raw = validRaw({
|
|
"filter-results": {
|
|
...validRaw()["filter-results"],
|
|
maintenance: "stale",
|
|
},
|
|
});
|
|
assert.throws(
|
|
() => validateTrace(raw),
|
|
/invalid_enum_value|Invalid enum value/i,
|
|
);
|
|
});
|
|
|
|
test("unknown key in filter-results rejected by strict schema", () => {
|
|
const raw = validRaw({
|
|
"filter-results": {
|
|
...validRaw()["filter-results"],
|
|
"unknown-filter": "x",
|
|
},
|
|
});
|
|
assert.throws(
|
|
() => validateTrace(raw),
|
|
/unrecognized_keys|Unrecognized key/i,
|
|
);
|
|
});
|
|
|
|
test("missing socketRisk in filter-results fails validation", () => {
|
|
const raw = validRaw();
|
|
delete raw["filter-results"].socketRisk;
|
|
assert.throws(() => validateTrace(raw), /invalid_type|Required/i);
|
|
});
|
|
});
|
|
|
|
describe("validateTrace > socketRisk", () => {
|
|
test('socketRisk "clean" round-trips', () => {
|
|
const result = validateTrace(validRaw());
|
|
assert.equal(result["filter-results"].socketRisk, "clean");
|
|
});
|
|
|
|
test('socketRisk "flagged" round-trips', () => {
|
|
const raw = validRaw({
|
|
"filter-results": {
|
|
...validRaw()["filter-results"],
|
|
socketRisk: "flagged",
|
|
},
|
|
});
|
|
assert.equal(validateTrace(raw)["filter-results"].socketRisk, "flagged");
|
|
});
|
|
|
|
test("socketRisk arbitrary string round-trips", () => {
|
|
const raw = validRaw({
|
|
"filter-results": {
|
|
...validRaw()["filter-results"],
|
|
socketRisk: "obfuscated-code",
|
|
},
|
|
});
|
|
assert.equal(
|
|
validateTrace(raw)["filter-results"].socketRisk,
|
|
"obfuscated-code",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("validateTrace > lastRevalidated", () => {
|
|
test("lastRevalidated null is valid", () => {
|
|
assert.equal(
|
|
validateTrace(validRaw({ lastRevalidated: null })).lastRevalidated,
|
|
null,
|
|
);
|
|
});
|
|
|
|
test("lastRevalidated ISO date string is valid", () => {
|
|
assert.equal(
|
|
validateTrace(validRaw({ lastRevalidated: "2026-05-14" }))
|
|
.lastRevalidated,
|
|
"2026-05-14",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("parseFrontmatter", () => {
|
|
test("parses scalar, inline array, nested object, block array", () => {
|
|
const text = `---\n${VALID_FM}\n---\n\n## Filter: license`;
|
|
const raw = parseFrontmatter(text);
|
|
assert.equal(raw.package, "example-lib");
|
|
assert.equal(raw.version, "^1.0.0");
|
|
assert.deepEqual(raw.deciders, ["alice", "bob"]);
|
|
assert.equal(raw.adr, null);
|
|
assert.equal(raw["filter-results"].license, "MIT");
|
|
assert.deepEqual(raw["verification-commands"], [
|
|
"pnpm audit --audit-level=moderate",
|
|
]);
|
|
});
|
|
|
|
test("throws when no frontmatter delimiters found", () => {
|
|
assert.throws(
|
|
() => parseFrontmatter("# No frontmatter"),
|
|
/No YAML frontmatter/,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("parseTrace", () => {
|
|
test("reads and validates a valid trace file", () => {
|
|
const file = writeTempTrace(VALID_FM, "## Filter: license\n\nok");
|
|
assert.equal(parseTrace(file).package, "example-lib");
|
|
});
|
|
|
|
test("throws on missing required field in file", () => {
|
|
const fm = VALID_FM.replace(/^package: example-lib\n/m, "");
|
|
const file = writeTempTrace(fm, "");
|
|
assert.throws(() => parseTrace(file), /invalid_type|Required/i);
|
|
});
|
|
});
|
|
|
|
describe("validateTrace > compliance frontmatter (ADR-022 amendment)", () => {
|
|
test("accepts is-sub-processor / processes-pii as YAML string booleans", () => {
|
|
const parsed = validateTrace(
|
|
validRaw({ "is-sub-processor": "false", "processes-pii": "true" }),
|
|
);
|
|
assert.equal(parsed["is-sub-processor"], false);
|
|
assert.equal(parsed["processes-pii"], true);
|
|
});
|
|
|
|
test("accepts a trace that omits lastRevalidated entirely", () => {
|
|
const raw = validRaw();
|
|
delete raw.lastRevalidated;
|
|
assert.equal(validateTrace(raw).lastRevalidated, undefined);
|
|
});
|
|
|
|
test("accepts a full sub-processor block", () => {
|
|
const parsed = validateTrace(
|
|
validRaw({
|
|
"is-sub-processor": "true",
|
|
"processes-pii": "true",
|
|
"data-sent": "error events, stack traces",
|
|
region: "eu",
|
|
"dpa-signed": "true",
|
|
"sccs-required": "false",
|
|
contact: "https://example.com/dpa",
|
|
}),
|
|
);
|
|
assert.equal(parsed["is-sub-processor"], true);
|
|
assert.equal(parsed["data-sent"], "error events, stack traces");
|
|
});
|
|
|
|
test("rejects is-sub-processor: true without data-sent", () => {
|
|
assert.throws(
|
|
() => validateTrace(validRaw({ "is-sub-processor": "true" })),
|
|
/data-sent/,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("parseTrace > every committed trace parses", () => {
|
|
// The schema exists to validate the traces actually in the repo — a schema
|
|
// that rejects committed, approved traces is broken (it silently rejected
|
|
// 38 of 39 before lastRevalidated became optional and the compliance
|
|
// fields were added).
|
|
test("all docs/library-decisions traces validate", () => {
|
|
const docsDir = path.join(
|
|
path.dirname(new URL(import.meta.url).pathname),
|
|
"..",
|
|
"..",
|
|
"docs",
|
|
"library-decisions",
|
|
);
|
|
const files = fs
|
|
.globSync(path.join(docsDir, "**", "*.md"))
|
|
.filter((f) => !f.endsWith("_template.md"));
|
|
assert.ok(files.length > 0, "expected committed traces to exist");
|
|
for (const f of files) {
|
|
assert.doesNotThrow(() => parseTrace(f), `trace failed to parse: ${f}`);
|
|
}
|
|
});
|
|
});
|