Files
agentic-dev/scripts/library-decisions/check.test.mjs
Danijel Martinek 26bcbb7a91 feat(scripts): add --staged-against flag to library-decisions check
Adds `--staged-against <base>` CLI flag to `check.mjs` so the reviewer
agent can compare `git diff <base>...HEAD` instead of the git index.
This gives the sandcastle reviewer a CI-compatible code path that works
in its clean sandbox where `git diff --cached` may be empty.

Appends a "Library-trace check" section to `.sandcastle/reviewer.prompt.md`
instructing the reviewer to run the command before issuing a verdict.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 05:57:10 +00:00

184 lines
5.8 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 { execSync } from "node:child_process";
import { checkLibraryDecisions } from "./check.mjs";
/** Create a temp git repo with one initial commit so HEAD exists. */
function makeRepo() {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "libcheck-"));
const g = (cmd) => execSync(cmd, { cwd: dir, stdio: "pipe" });
g("git init");
g("git config user.email test@test.com");
g("git config user.name Test");
g("git config commit.gpgsign false");
fs.writeFileSync(path.join(dir, ".gitkeep"), "");
g("git add .gitkeep");
g("git commit -m init");
return { dir, g };
}
/** Write a package.json under relDir and commit it as the baseline. */
function commitPkg(dir, g, relDir, pkg) {
const pkgDir = path.join(dir, relDir);
fs.mkdirSync(pkgDir, { recursive: true });
fs.writeFileSync(
path.join(pkgDir, "package.json"),
JSON.stringify(pkg, null, 2),
);
g(`git add ${relDir}/package.json`);
g("git commit -m add-pkg");
}
/** Overwrite a package.json and stage the result (no new commit). */
function stagePkg(dir, g, relDir, pkg) {
fs.writeFileSync(
path.join(dir, relDir, "package.json"),
JSON.stringify(pkg, null, 2),
);
g(`git add ${relDir}/package.json`);
}
function traceFm(depName, decision) {
return `package: ${depName}
version: "^1.0.0"
tier: feature
decision: ${decision}
date: 2026-05-14
deciders: [alice]
adr: null
filter-results:
license: MIT
types: native
maintenance: active
boundary-fit: pass
shadow-check: pass
eu-residency: ok
cve-scan: clean
named-consumer: pass
verification-commands:
- pnpm audit --audit-level=moderate`;
}
/** Write a trace file and stage it. */
function stageTrace(dir, g, depName, decision = "approved") {
const traceDir = path.join(dir, "docs", "library-decisions");
fs.mkdirSync(traceDir, { recursive: true });
const file = `2026-05-14-${depName}.md`;
fs.writeFileSync(
path.join(traceDir, file),
`---\n${traceFm(depName, decision)}\n---\n\n`,
);
g(`git add docs/library-decisions/${file}`);
}
describe("checkLibraryDecisions", () => {
test("new feature-tier dep without trace → exit 1", () => {
const { dir, g } = makeRepo();
commitPkg(dir, g, "packages/feat-a", { dependencies: {} });
stagePkg(dir, g, "packages/feat-a", {
dependencies: { "new-lib": "^1.0.0" },
});
const errs = checkLibraryDecisions(dir);
assert.equal(errs.length, 1);
assert.equal(errs[0].dep, "new-lib");
assert.equal(errs[0].reason, "no-trace");
});
test("new feature-tier dep with approved trace staged → exit 0", () => {
const { dir, g } = makeRepo();
commitPkg(dir, g, "packages/feat-a", { dependencies: {} });
stagePkg(dir, g, "packages/feat-a", {
dependencies: { "new-lib": "^1.0.0" },
});
stageTrace(dir, g, "new-lib", "approved");
assert.deepEqual(checkLibraryDecisions(dir), []);
});
test("new feature-tier dep with rejected-decision trace staged → exit 1", () => {
const { dir, g } = makeRepo();
commitPkg(dir, g, "packages/feat-a", { dependencies: {} });
stagePkg(dir, g, "packages/feat-a", {
dependencies: { "new-lib": "^1.0.0" },
});
stageTrace(dir, g, "new-lib", "rejected");
const errs = checkLibraryDecisions(dir);
assert.equal(errs.length, 1);
assert.equal(errs[0].dep, "new-lib");
assert.equal(errs[0].reason, "not-approved");
assert.equal(errs[0].decision, "rejected");
});
test("new app-tier dep → exit 0", () => {
const { dir, g } = makeRepo();
commitPkg(dir, g, "apps/web", { dependencies: {} });
stagePkg(dir, g, "apps/web", { dependencies: { "new-lib": "^1.0.0" } });
assert.deepEqual(checkLibraryDecisions(dir), []);
});
test("new devDependency → exit 0", () => {
const { dir, g } = makeRepo();
commitPkg(dir, g, "packages/feat-a", {});
stagePkg(dir, g, "packages/feat-a", {
devDependencies: { "test-lib": "^1.0.0" },
});
assert.deepEqual(checkLibraryDecisions(dir), []);
});
test("multi-file diff with mixed pass/fail → exit 1 with per-package report", () => {
const { dir, g } = makeRepo();
commitPkg(dir, g, "packages/feat-a", { dependencies: {} });
commitPkg(dir, g, "packages/feat-b", { dependencies: {} });
stagePkg(dir, g, "packages/feat-a", {
dependencies: { "lib-a": "^1.0.0" },
});
stagePkg(dir, g, "packages/feat-b", {
dependencies: { "lib-b": "^1.0.0" },
});
stageTrace(dir, g, "lib-a", "approved"); // feat-a passes; no trace for lib-b
const errs = checkLibraryDecisions(dir);
assert.equal(errs.length, 1);
assert.equal(errs[0].pkgJson, "packages/feat-b/package.json");
assert.equal(errs[0].dep, "lib-b");
assert.equal(errs[0].reason, "no-trace");
});
test("peerDependencies-only change → exit 0", () => {
const { dir, g } = makeRepo();
commitPkg(dir, g, "packages/feat-a", {});
stagePkg(dir, g, "packages/feat-a", {
peerDependencies: { react: "^18.0.0" },
});
assert.deepEqual(checkLibraryDecisions(dir), []);
});
test("--staged-against mode: new feature-tier dep without trace → exit 1", () => {
const { dir, g } = makeRepo();
// Baseline commit: feature package with no deps
commitPkg(dir, g, "packages/feat-a", { dependencies: {} });
// Second commit: adds new-lib — no trace file committed alongside it
commitPkg(dir, g, "packages/feat-a", {
dependencies: { "new-lib": "^1.0.0" },
});
// HEAD has new-lib; HEAD~1 doesn't — no trace in the diff → exit 1
const errs = checkLibraryDecisions(dir, { stagedAgainst: "HEAD~1" });
assert.equal(errs.length, 1);
assert.equal(errs[0].dep, "new-lib");
assert.equal(errs[0].reason, "no-trace");
});
});