Adds `withAnalytics(analytics, factory)` to packages/core-analytics — mirrors the `withAudit` pattern: thin forwarding closure that attaches the `__analyzed` brand via `attachBrand` from `@repo/core-shared/conformance` without mutating the original factory. Exports `Analyzed<F>` type and `withAnalytics` from the `@repo/core-analytics` root barrel. Adds `with-analytics.test.ts` asserting brand is present after wrapping, absent on the original fn, output passes through unchanged, and errors propagate. Adds `@repo/core-shared` as a production dependency. Also fixes `scripts/library-decisions/check.mjs` to exempt workspace-protocol entries (`workspace:*`) from the library trace requirement — internal monorepo packages are not third-party libraries and were incorrectly gated. Adds a regression test in `check.test.mjs` covering the exemption. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
354 lines
10 KiB
JavaScript
354 lines
10 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, checkRenovatePr } 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("new workspace-protocol dep (internal monorepo package) → exit 0", () => {
|
|
const { dir, g } = makeRepo();
|
|
commitPkg(dir, g, "packages/feat-a", { dependencies: {} });
|
|
stagePkg(dir, g, "packages/feat-a", {
|
|
dependencies: { "@repo/core-shared": "workspace:*" },
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// checkRenovatePr — integration tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function makeTempDir() {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), "renprc-"));
|
|
}
|
|
|
|
function writeFixture(dir, relPath, content) {
|
|
const full = path.join(dir, relPath);
|
|
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
fs.writeFileSync(full, content);
|
|
}
|
|
|
|
function featurePkg(dir, name, deps) {
|
|
writeFixture(
|
|
dir,
|
|
`packages/${name}/package.json`,
|
|
JSON.stringify({ dependencies: deps }, null, 2),
|
|
);
|
|
}
|
|
|
|
function appPkg(dir, name, deps) {
|
|
writeFixture(
|
|
dir,
|
|
`apps/${name}/package.json`,
|
|
JSON.stringify({ dependencies: deps }, null, 2),
|
|
);
|
|
}
|
|
|
|
function traceFixture(dir, depName, lastRevalidated) {
|
|
const lr = lastRevalidated == null ? "null" : lastRevalidated;
|
|
const fm = `package: ${depName}
|
|
version: "^1.0.0"
|
|
tier: feature
|
|
decision: approved
|
|
date: 2026-05-14
|
|
lastRevalidated: ${lr}
|
|
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
|
|
socketRisk: clean
|
|
verification-commands:
|
|
- pnpm audit`;
|
|
writeFixture(
|
|
dir,
|
|
`docs/library-decisions/2026-05-14-${depName}.md`,
|
|
`---\n${fm}\n---\n\n`,
|
|
);
|
|
}
|
|
|
|
function buildLockfileDiff(pkg, fromVer, toVer) {
|
|
return [
|
|
"--- a/pnpm-lock.yaml",
|
|
"+++ b/pnpm-lock.yaml",
|
|
"@@ -10,7 +10,7 @@ packages:",
|
|
`- ${pkg}@${fromVer}: {}`,
|
|
`+ ${pkg}@${toVer}: {}`,
|
|
"",
|
|
].join("\n");
|
|
}
|
|
|
|
describe("checkRenovatePr", () => {
|
|
test("minor bump on feature-tier dep → pass", () => {
|
|
const dir = makeTempDir();
|
|
featurePkg(dir, "feat-a", { "some-lib": "^1.0.0" });
|
|
|
|
const errs = checkRenovatePr(dir, {
|
|
branch: "renovate/some-lib-1.1.0",
|
|
diff: buildLockfileDiff("some-lib", "1.0.0", "1.1.0"),
|
|
today: "2026-05-14",
|
|
});
|
|
|
|
assert.deepEqual(errs, []);
|
|
});
|
|
|
|
test("major bump + fresh lastRevalidated → pass", () => {
|
|
const dir = makeTempDir();
|
|
featurePkg(dir, "feat-a", { "some-lib": "^2.0.0" });
|
|
traceFixture(dir, "some-lib", "2026-05-14");
|
|
|
|
const errs = checkRenovatePr(dir, {
|
|
branch: "renovate/some-lib-2.0.0",
|
|
diff: buildLockfileDiff("some-lib", "1.2.3", "2.0.0"),
|
|
today: "2026-05-14",
|
|
});
|
|
|
|
assert.deepEqual(errs, []);
|
|
});
|
|
|
|
test("major bump + stale lastRevalidated → fail with pointer", () => {
|
|
const dir = makeTempDir();
|
|
featurePkg(dir, "feat-a", { "some-lib": "^1.0.0" });
|
|
traceFixture(dir, "some-lib", "2026-01-01");
|
|
|
|
const errs = checkRenovatePr(dir, {
|
|
branch: "renovate/some-lib-2.0.0",
|
|
diff: buildLockfileDiff("some-lib", "1.2.3", "2.0.0"),
|
|
today: "2026-05-14",
|
|
});
|
|
|
|
assert.equal(errs.length, 1);
|
|
assert.equal(errs[0].dep, "some-lib");
|
|
assert.equal(errs[0].from, "1.2.3");
|
|
assert.equal(errs[0].to, "2.0.0");
|
|
assert.equal(errs[0].reason, "stale");
|
|
assert.equal(errs[0].lastRevalidated, "2026-01-01");
|
|
assert.ok(typeof errs[0].tracePath === "string");
|
|
assert.ok(errs[0].tracePath.includes("some-lib"));
|
|
});
|
|
|
|
test("major bump on app-tier dep → pass", () => {
|
|
const dir = makeTempDir();
|
|
// dep only in apps/, not in packages/ → app-tier exemption
|
|
appPkg(dir, "web", { "some-lib": "^1.0.0" });
|
|
|
|
const errs = checkRenovatePr(dir, {
|
|
branch: "renovate/some-lib-2.0.0",
|
|
diff: buildLockfileDiff("some-lib", "1.2.3", "2.0.0"),
|
|
today: "2026-05-14",
|
|
});
|
|
|
|
assert.deepEqual(errs, []);
|
|
});
|
|
|
|
test("patch bump in Renovate branch → pass", () => {
|
|
const dir = makeTempDir();
|
|
featurePkg(dir, "feat-a", { "some-lib": "^1.0.0" });
|
|
|
|
const errs = checkRenovatePr(dir, {
|
|
branch: "renovate/some-lib-1.2.4",
|
|
diff: buildLockfileDiff("some-lib", "1.2.3", "1.2.4"),
|
|
today: "2026-05-14",
|
|
});
|
|
|
|
assert.deepEqual(errs, []);
|
|
});
|
|
|
|
test("non-Renovate branch with major bump → pass", () => {
|
|
const dir = makeTempDir();
|
|
featurePkg(dir, "feat-a", { "some-lib": "^1.0.0" });
|
|
|
|
const errs = checkRenovatePr(dir, {
|
|
branch: "main",
|
|
diff: buildLockfileDiff("some-lib", "1.2.3", "2.0.0"),
|
|
today: "2026-05-14",
|
|
});
|
|
|
|
assert.deepEqual(errs, []);
|
|
});
|
|
});
|