179 lines
7.5 KiB
TypeScript
179 lines
7.5 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
// @ts-expect-error — .mjs gate module has no type declarations
|
|
import {
|
|
findUnapprovedDeps,
|
|
discoverManifests,
|
|
evaluateDepAge,
|
|
auditNewDepsRegistry,
|
|
} from "../../scripts/check/check-deps.mjs";
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
|
|
test("no unapproved deps when all are allowlisted", () => {
|
|
assert.deepEqual(findUnapprovedDeps(["react", "next"], new Set(["react", "next", "zod"])), []);
|
|
});
|
|
|
|
test("flags a dependency not on the allowlist (potential slopsquat)", () => {
|
|
assert.deepEqual(findUnapprovedDeps(["react", "reactt-router"], new Set(["react"])), [
|
|
"reactt-router",
|
|
]);
|
|
});
|
|
|
|
test("flags multiple new deps, preserves order, de-dupes", () => {
|
|
assert.deepEqual(findUnapprovedDeps(["a", "b", "a", "c"], new Set(["a"])), ["b", "c"]);
|
|
});
|
|
|
|
// --- 6A.8: automatic workspace discovery ---
|
|
|
|
test("6A.8: discoverManifests finds root and workspace package.json files", () => {
|
|
const manifests = discoverManifests(repoRoot);
|
|
// Must include the root
|
|
assert.ok(manifests.includes("package.json"), "root package.json must be included");
|
|
// Must include known workspaces
|
|
assert.ok(manifests.includes("electron/package.json"), "electron/package.json must be included");
|
|
assert.ok(manifests.includes("open-sse/package.json"), "open-sse/package.json must be included");
|
|
assert.ok(
|
|
manifests.includes("@omniroute/opencode-plugin/package.json"),
|
|
"@omniroute/opencode-plugin/package.json must be included"
|
|
);
|
|
assert.ok(
|
|
manifests.includes("@omniroute/opencode-provider/package.json"),
|
|
"@omniroute/opencode-provider/package.json must be included"
|
|
);
|
|
});
|
|
|
|
test("6A.8: discoverManifests does NOT include node_modules, .next, or deep reference dirs", () => {
|
|
const manifests = discoverManifests(repoRoot);
|
|
for (const m of manifests) {
|
|
assert.ok(!m.includes("node_modules"), `should not include node_modules: ${m}`);
|
|
assert.ok(!m.includes(".next"), `should not include .next: ${m}`);
|
|
assert.ok(!m.includes("_references"), `should not include _references: ${m}`);
|
|
assert.ok(!m.includes("_mono_repo"), `should not include _mono_repo: ${m}`);
|
|
assert.ok(!m.includes(".build"), `should not include .build: ${m}`);
|
|
assert.ok(!m.includes(".claude"), `should not include .claude: ${m}`);
|
|
assert.ok(!m.includes("dist-electron"), `should not include dist-electron: ${m}`);
|
|
}
|
|
});
|
|
|
|
test("6A.8: all workspace package deps are in the allowlist (gate exits 0 with expanded scope)", () => {
|
|
const allowlistPath = path.join(repoRoot, "config/quality/dependency-allowlist.json");
|
|
const allowlist = new Set(JSON.parse(fs.readFileSync(allowlistPath, "utf8")).allowed || []);
|
|
const manifests = discoverManifests(repoRoot);
|
|
const allDeps: string[] = [];
|
|
for (const rel of manifests) {
|
|
const abs = path.join(repoRoot, rel);
|
|
if (!fs.existsSync(abs)) continue;
|
|
const pkg = JSON.parse(fs.readFileSync(abs, "utf8"));
|
|
allDeps.push(
|
|
...Object.keys(pkg.dependencies || {}),
|
|
...Object.keys(pkg.devDependencies || {}),
|
|
...Object.keys(pkg.optionalDependencies || {}),
|
|
...Object.keys(pkg.peerDependencies || {})
|
|
);
|
|
}
|
|
const unapproved = findUnapprovedDeps(allDeps, allowlist);
|
|
assert.deepEqual(
|
|
unapproved,
|
|
[],
|
|
`expected all deps to be approved, got: ${unapproved.join(", ")}`
|
|
);
|
|
});
|
|
|
|
// --- 6A.8: stale-allowlist enforcement ---
|
|
// @ts-expect-error — reportStaleEntries from mjs
|
|
import { reportStaleEntries } from "../../scripts/check/lib/allowlist.mjs";
|
|
|
|
test("6A.8 stale: a dep removed from all manifests is detected as stale in allowlist", () => {
|
|
// Simulate: allowlist has "removed-lib" but no manifest uses it.
|
|
const stale = (reportStaleEntries as (a: string[], b: string[], c: string) => string[])(
|
|
["removed-lib", "react"],
|
|
["react"], // only "react" is live
|
|
"check-deps"
|
|
);
|
|
assert.deepEqual(stale, ["removed-lib"]);
|
|
});
|
|
|
|
// ─── Task 7.8: evaluateDepAge (pure function) ─────────────────────────────────
|
|
|
|
test("7.8 evaluateDepAge: package older than 72h is OK", () => {
|
|
const now = Date.now();
|
|
const createdMs = now - 73 * 60 * 60 * 1000; // 73 hours ago
|
|
const { ok, ageHours } = (
|
|
evaluateDepAge as (a: number, b: number, c?: number) => { ok: boolean; ageHours: number }
|
|
)(createdMs, now);
|
|
assert.ok(ok, "package older than 72h should be ok");
|
|
assert.ok(ageHours >= 73, "ageHours should reflect elapsed time");
|
|
});
|
|
|
|
test("7.8 evaluateDepAge: package exactly at 72h boundary is OK (boundary inclusive)", () => {
|
|
const now = Date.now();
|
|
const createdMs = now - 72 * 60 * 60 * 1000; // exactly 72 hours ago
|
|
const { ok } = (
|
|
evaluateDepAge as (a: number, b: number, c?: number) => { ok: boolean; ageHours: number }
|
|
)(createdMs, now);
|
|
assert.ok(ok, "package at exactly 72h should be ok (inclusive boundary)");
|
|
});
|
|
|
|
test("7.8 evaluateDepAge: package published 1h ago is NOT OK", () => {
|
|
const now = Date.now();
|
|
const createdMs = now - 1 * 60 * 60 * 1000; // 1 hour ago
|
|
const { ok, ageHours } = (
|
|
evaluateDepAge as (a: number, b: number, c?: number) => { ok: boolean; ageHours: number }
|
|
)(createdMs, now);
|
|
assert.ok(!ok, "package published 1h ago should NOT be ok");
|
|
assert.ok(ageHours < 72, "ageHours should reflect < 72h");
|
|
});
|
|
|
|
test("7.8 evaluateDepAge: respects custom minAgeHours", () => {
|
|
const now = Date.now();
|
|
const createdMs = now - 10 * 60 * 60 * 1000; // 10 hours ago
|
|
// With 24h minimum, 10h-old package should fail
|
|
const { ok: failWith24 } = (
|
|
evaluateDepAge as (a: number, b: number, c?: number) => { ok: boolean; ageHours: number }
|
|
)(createdMs, now, 24);
|
|
assert.ok(!failWith24, "10h-old package should fail with 24h minimum");
|
|
// With 6h minimum, 10h-old package should pass
|
|
const { ok: passWith6 } = (
|
|
evaluateDepAge as (a: number, b: number, c?: number) => { ok: boolean; ageHours: number }
|
|
)(createdMs, now, 6);
|
|
assert.ok(passWith6, "10h-old package should pass with 6h minimum");
|
|
});
|
|
|
|
test("7.8 evaluateDepAge: future timestamp (time.created in future) is NOT OK", () => {
|
|
const now = Date.now();
|
|
const createdMs = now + 1000; // 1s in future (clock skew edge case)
|
|
const { ok, ageHours } = (
|
|
evaluateDepAge as (a: number, b: number, c?: number) => { ok: boolean; ageHours: number }
|
|
)(createdMs, now);
|
|
assert.ok(!ok, "future timestamp should not be ok");
|
|
assert.ok(ageHours < 0, "ageHours should be negative for future timestamp");
|
|
});
|
|
|
|
// ─── Task 7.8: auditNewDepsRegistry (uses stubbed queryNpmRegistry) ───────────
|
|
// We test auditNewDepsRegistry by examining its logic with real deps that are
|
|
// known to exist and are old. The pure evaluateDepAge covers the age logic above.
|
|
// For auditNewDepsRegistry we test the aggregation/routing logic by noting that:
|
|
// - an empty dep list → all empty results
|
|
// - any real npm package like "react" is found and old → does NOT appear in any list
|
|
|
|
test("7.8 auditNewDepsRegistry: empty dep list returns all-empty results", () => {
|
|
const result = (
|
|
auditNewDepsRegistry as (
|
|
deps: string[],
|
|
minAge?: number,
|
|
now?: number
|
|
) => {
|
|
notFound: string[];
|
|
tooNew: Array<{ name: string; ageHours: number }>;
|
|
offline: string[];
|
|
}
|
|
)([], 72, Date.now());
|
|
assert.deepEqual(result.notFound, []);
|
|
assert.deepEqual(result.tooNew, []);
|
|
assert.deepEqual(result.offline, []);
|
|
});
|