chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:12 +08:00
commit d8dcd5f6d1
8604 changed files with 2479390 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
import test from "node:test";
import assert from "node:assert/strict";
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
import { reportStaleEntries, assertNoStale } from "../../../scripts/check/lib/allowlist.mjs";
test("reportStaleEntries: entrada da allowlist não mais violada => stale", () => {
const stale = reportStaleEntries(["/api/dead", "/api/live"], ["/api/live"], "fetch-targets");
assert.deepEqual(stale, ["/api/dead"]);
});
test("reportStaleEntries: todas as entradas ainda violadas => vazio", () => {
assert.deepEqual(reportStaleEntries(["a", "b"], ["a", "b"], "x"), []);
});
test("reportStaleEntries: Set como liveViolations funciona igual a array", () => {
const live = new Set(["/api/live"]);
const stale = reportStaleEntries(["/api/dead", "/api/live"], live, "fetch-targets");
assert.deepEqual(stale, ["/api/dead"]);
});
test("reportStaleEntries: allowlist vazia => sempre vazio", () => {
assert.deepEqual(reportStaleEntries([], ["/api/anything"], "x"), []);
});
test("assertNoStale: seta process.exitCode=1 quando há entradas stale", () => {
const original = process.exitCode;
process.exitCode = 0;
const stale = assertNoStale(["/api/dead", "/api/live"], ["/api/live"], "fetch-targets");
assert.equal(process.exitCode, 1);
assert.deepEqual(stale, ["/api/dead"]);
process.exitCode = original;
});
test("assertNoStale: NÃO seta process.exitCode quando não há stale", () => {
const original = process.exitCode;
process.exitCode = 0;
const stale = assertNoStale(["a", "b"], ["a", "b"], "x");
assert.equal(process.exitCode, 0);
assert.deepEqual(stale, []);
process.exitCode = original;
});
@@ -0,0 +1,161 @@
import { test } 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 {
assembleStandalone,
syncStandaloneNativeAssets,
syncStandaloneExtraModules,
} from "../../../scripts/build/assembleStandalone.mjs";
/** Recursively list relative file paths under dir (forward-slash normalised). */
function listFiles(dir: string, rootDir: string = dir, out: string[] = []): string[] {
let entries: string[] = [];
try {
entries = fs.readdirSync(dir);
} catch {
return out;
}
for (const entry of entries) {
const full = path.join(dir, entry);
if (fs.statSync(full).isDirectory()) {
listFiles(full, rootDir, out);
} else {
out.push(path.relative(rootDir, full).replace(/\\/g, "/"));
}
}
return out.sort();
}
/** Build a synthetic projectRoot containing every sidecar source the assembler copies. */
function seedSidecarSources(root: string) {
const files = [
"node_modules/wreq-js/rust/lib.so",
"node_modules/better-sqlite3/build/Release/better_sqlite3.node",
"src/mitm/tproxy/native/build/Release/transparent.node",
"node_modules/@swc/helpers/package.json",
"node_modules/pino-abstract-transport/index.js",
"node_modules/pino-pretty/index.js",
"node_modules/split2/index.js",
"node_modules/playwright-core/index.js",
"node_modules/sqlite-vec/index.js",
"node_modules/sqlite-vec-linux-x64/vec0.so",
"src/lib/db/migrations/001_init.sql",
"src/mitm/server.cjs",
"scripts/dev/run-standalone.mjs",
"scripts/dev/standalone-server-ws.mjs",
"scripts/dev/peer-stamp.mjs",
"scripts/dev/responses-ws-proxy.mjs",
"scripts/build/runtime-env.mjs",
"scripts/build/bootstrap-env.mjs",
"scripts/dev/healthcheck.mjs",
"public/logo.svg",
];
for (const rel of files) {
const full = path.join(root, rel);
fs.mkdirSync(path.dirname(full), { recursive: true });
fs.writeFileSync(full, `// ${rel}`);
}
}
test("assembleStandalone copies standalone + static + public + sidecars into outDir", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "assemble-"));
const distDir = path.join(tmp, ".build/next");
const outDir = path.join(tmp, "dist");
// minimal fake standalone tree
fs.mkdirSync(path.join(distDir, "standalone"), { recursive: true });
fs.writeFileSync(path.join(distDir, "standalone", "server.js"), "// server");
fs.mkdirSync(path.join(distDir, "static"), { recursive: true });
fs.writeFileSync(path.join(distDir, "static", "x.js"), "x");
fs.mkdirSync(path.join(tmp, "public"), { recursive: true });
fs.writeFileSync(path.join(tmp, "public", "logo.svg"), "<svg/>");
assembleStandalone({
distDir,
outDir,
projectRoot: tmp,
sanitizePaths: false,
copyNatives: false,
});
assert.ok(fs.existsSync(path.join(outDir, "server.js")), "server.js copied");
// Static lands under the distDir path (.build/next/static), where the standalone
// server.js — built with distDir baked into its config — serves /_next/static from.
assert.ok(
fs.existsSync(path.join(outDir, ".build/next/static/x.js")),
"static copied under distDir"
);
assert.ok(
!fs.existsSync(path.join(outDir, ".next/static/x.js")),
"static is NOT placed under a literal .next (would 404 against distDir server)"
);
assert.ok(fs.existsSync(path.join(outDir, "public/logo.svg")), "public copied");
fs.rmSync(tmp, { recursive: true, force: true });
});
// Drift guard: the async path (syncStandaloneNativeAssets / syncStandaloneExtraModules,
// used by build-next-isolated) and the sync path (assembleStandalone copyNatives, used by
// prepublish/electron) must copy the SAME sidecar tree. After the single-source refactor
// both derive from NATIVE_ASSET_ENTRIES/EXTRA_MODULE_ENTRIES — this test fails if a future
// edit reintroduces two divergent lists.
test("async and sync sidecar copy paths produce identical bundle trees", async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "assemble-parity-"));
const projectRoot = path.join(tmp, "src-root");
seedSidecarSources(projectRoot);
// Async path: copy into outAsync via the exported sync* helpers.
const outAsync = path.join(tmp, "out-async");
fs.mkdirSync(outAsync, { recursive: true });
const silent = { log() {} };
// The exported helpers derive the standalone root from NEXT_DIST_DIR; point them at outAsync.
await syncStandaloneNativeAssets(projectRoot, fs.promises, silent, outAsync);
await syncStandaloneExtraModules(projectRoot, fs.promises, silent, outAsync);
// Sync path: assembleStandalone(copyNatives) needs a standalone dir to copy first.
const distDir = path.join(projectRoot, ".build/next");
fs.mkdirSync(path.join(distDir, "standalone"), { recursive: true });
fs.writeFileSync(path.join(distDir, "standalone", "server.js"), "// server");
const outSync = path.join(tmp, "out-sync");
assembleStandalone({
distDir,
outDir: outSync,
projectRoot,
sanitizePaths: false,
copyNatives: true,
});
const asyncTree = listFiles(outAsync);
// The sync path also copies the standalone server.js + patches package.json; compare only
// the sidecar files the two paths share (drop server.js which is unique to assembleStandalone).
const syncTree = listFiles(outSync).filter((f) => f !== "server.js");
assert.deepEqual(
syncTree,
asyncTree,
"sync (assembleStandalone) and async (sync*ToDir) must copy the same sidecar tree"
);
// The TPROXY native addon must land at the cwd-relative path the runtime loader
// (transparentSocket.ts) resolves in the standalone bundle.
assert.ok(
asyncTree.includes("src/mitm/tproxy/native/build/Release/transparent.node"),
"TPROXY transparent.node copied into the standalone bundle"
);
fs.rmSync(tmp, { recursive: true, force: true });
});
test("the TPROXY addon source is skipped gracefully when it was not built (non-Linux)", async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "assemble-skip-"));
const projectRoot = path.join(tmp, "src-root");
// Seed everything EXCEPT the tproxy addon (simulating a non-Linux build).
fs.mkdirSync(projectRoot, { recursive: true });
const out = path.join(tmp, "out");
fs.mkdirSync(out, { recursive: true });
// No throw even though src/mitm/tproxy/native/... is absent.
await syncStandaloneNativeAssets(projectRoot, fs.promises, { log() {} }, out);
assert.ok(
!fs.existsSync(path.join(out, "src/mitm/tproxy/native/build/Release/transparent.node")),
"absent addon is simply not copied (graceful skip)"
);
fs.rmSync(tmp, { recursive: true, force: true });
});
+229
View File
@@ -0,0 +1,229 @@
import test 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 {
parseSizeLimitResults,
measureViaFileStat,
runSizeLimit,
evaluateBundleSizeRatchet,
readBaselineBundleSizeValue,
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
} from "../../../scripts/check/check-bundle-size.mjs";
type RatchetVerdict = { regressed: boolean; improved: boolean };
const evaluateSize = evaluateBundleSizeRatchet as (
current: number,
baseline: number
) => RatchetVerdict;
const readSizeBaseline = readBaselineBundleSizeValue as (p?: string) => number | null;
// ---------------------------------------------------------------------------
// parseSizeLimitResults
// ---------------------------------------------------------------------------
test("parseSizeLimitResults: soma os campos size de cada entrada", () => {
const results = [
{ name: "entry-a", size: 1024, passed: true },
{ name: "entry-b", size: 2048, passed: true },
];
assert.equal(parseSizeLimitResults(results), 3072);
});
test("parseSizeLimitResults: ignora entradas sem campo size", () => {
const results = [
{ name: "entry-a", size: 500 },
{ name: "entry-b" }, // sem size
];
assert.equal(parseSizeLimitResults(results), 500);
});
test("parseSizeLimitResults: lança TypeError para argumento não-array", () => {
assert.throws(
() => parseSizeLimitResults({ name: "x", size: 100 } as unknown as never[]),
TypeError
);
});
test("parseSizeLimitResults: lança Error quando nenhuma entrada tem size", () => {
const results = [{ name: "entry-a" }, { name: "entry-b" }];
assert.throws(() => parseSizeLimitResults(results), Error);
});
test("parseSizeLimitResults: array vazio lança Error (sem medições)", () => {
assert.throws(() => parseSizeLimitResults([]), Error);
});
test("parseSizeLimitResults: aceita size=0 como medição válida", () => {
const results = [{ name: "empty-entry", size: 0 }];
assert.equal(parseSizeLimitResults(results), 0);
});
// ---------------------------------------------------------------------------
// measureViaFileStat (fallback de leitura direta de arquivo)
// ---------------------------------------------------------------------------
function makeTmpConfig(dir: string, entries: { name: string; path: string }[]): string {
const configPath = path.join(dir, ".size-limit.json");
fs.writeFileSync(configPath, JSON.stringify(entries));
return configPath;
}
function writeTmpFile(dir: string, name: string, content: string): string {
const p = path.join(dir, name);
fs.writeFileSync(p, content);
return p;
}
test("measureViaFileStat: soma bytes de arquivos existentes", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "bundle-size-test-"));
const content = "hello world!"; // 12 bytes
writeTmpFile(dir, "entry.mjs", content);
const configPath = makeTmpConfig(dir, [{ name: "Entry", path: "entry.mjs" }]);
const { total, entries, allMissing } = measureViaFileStat(configPath, dir);
assert.equal(total, Buffer.byteLength(content));
assert.equal(allMissing, false);
assert.equal(entries.length, 1);
assert.equal(entries[0]!.size, Buffer.byteLength(content));
});
test("measureViaFileStat: arquivo ausente é registrado com size null sem lançar", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "bundle-size-test-"));
const configPath = makeTmpConfig(dir, [{ name: "Missing", path: "does-not-exist.mjs" }]);
const { total, entries, allMissing } = measureViaFileStat(configPath, dir);
assert.equal(total, 0);
assert.equal(allMissing, true);
assert.equal(entries[0]!.size, null);
});
test("measureViaFileStat: allMissing=false quando pelo menos um arquivo existe", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "bundle-size-test-"));
writeTmpFile(dir, "present.mjs", "data");
const configPath = makeTmpConfig(dir, [
{ name: "Present", path: "present.mjs" },
{ name: "Missing", path: "missing.mjs" },
]);
const { allMissing, entries } = measureViaFileStat(configPath, dir);
assert.equal(allMissing, false);
assert.equal(entries.filter((e) => e.size !== null).length, 1);
});
test("measureViaFileStat: soma múltiplos arquivos existentes", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "bundle-size-test-"));
const a = "AAA"; // 3 bytes
const b = "BBBBB"; // 5 bytes
writeTmpFile(dir, "a.mjs", a);
writeTmpFile(dir, "b.mjs", b);
const configPath = makeTmpConfig(dir, [
{ name: "A", path: "a.mjs" },
{ name: "B", path: "b.mjs" },
]);
const { total } = measureViaFileStat(configPath, dir);
assert.equal(total, Buffer.byteLength(a) + Buffer.byteLength(b));
});
test("measureViaFileStat: config ausente retorna allMissing=true e total=0", () => {
const { total, entries, allMissing } = measureViaFileStat(
"/tmp/nonexistent/.size-limit.json",
"/tmp"
);
assert.equal(total, 0);
assert.equal(allMissing, true);
assert.deepEqual(entries, []);
});
// ---------------------------------------------------------------------------
// runSizeLimit — comportamento quando o binário não existe
// ---------------------------------------------------------------------------
test("runSizeLimit: lança com code SL_NO_BIN quando binário não existe", () => {
assert.throws(
() => runSizeLimit("/tmp", "/nonexistent/path/size-limit"),
(err: unknown) => {
assert.ok(err instanceof Error);
assert.equal((err as NodeJS.ErrnoException & { code?: string }).code, "SL_NO_BIN");
return true;
}
);
});
// ---------------------------------------------------------------------------
// evaluateBundleSizeRatchet — ratchet direction:down (Etapa 2: flip to blocking)
// Regression when measured > baseline; baseline=5601 (gzip via @size-limit/file).
// ---------------------------------------------------------------------------
test("evaluateBundleSizeRatchet: medido == baseline passa (5601 vs 5601)", () => {
const r = evaluateSize(5601, 5601);
assert.equal(r.regressed, false);
assert.equal(r.improved, false);
});
test("evaluateBundleSizeRatchet: um byte a mais que o baseline é regressão", () => {
const r = evaluateSize(5602, 5601);
assert.equal(r.regressed, true, "any size increase must block");
assert.equal(r.improved, false);
});
test("evaluateBundleSizeRatchet: menor que o baseline é melhoria", () => {
const r = evaluateSize(5000, 5601);
assert.equal(r.regressed, false);
assert.equal(r.improved, true);
});
test("evaluateBundleSizeRatchet: comparação inteira estrita — qualquer aumento regride", () => {
assert.equal(evaluateSize(5602, 5601).regressed, true);
assert.equal(evaluateSize(5601, 5601).regressed, false);
assert.equal(evaluateSize(5600, 5601).regressed, false);
});
// ---------------------------------------------------------------------------
// readBaselineBundleSizeValue — leitura tolerante do quality-baseline.json
// ---------------------------------------------------------------------------
function withTmpBundleBaseline(content: string | null, fn: (p: string) => void) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "bundle-baseline-"));
const p = path.join(dir, "quality-baseline.json");
if (content !== null) fs.writeFileSync(p, content);
try {
fn(p);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}
test("readBaselineBundleSizeValue: lê metrics.bundleSize.value", () => {
withTmpBundleBaseline(JSON.stringify({ metrics: { bundleSize: { value: 5601 } } }), (p) => {
assert.equal(readSizeBaseline(p), 5601);
});
});
test("readBaselineBundleSizeValue: arquivo ausente retorna null (SKIP gracioso)", () => {
assert.equal(readSizeBaseline("/tmp/does-not-exist-77777/quality-baseline.json"), null);
});
test("readBaselineBundleSizeValue: métrica ausente retorna null", () => {
withTmpBundleBaseline(JSON.stringify({ metrics: {} }), (p) => {
assert.equal(readSizeBaseline(p), null);
});
});
test("readBaselineBundleSizeValue: value não-numérico retorna null", () => {
withTmpBundleBaseline(JSON.stringify({ metrics: { bundleSize: { value: "5601" } } }), (p) => {
assert.equal(readSizeBaseline(p), null);
});
});
test("readBaselineBundleSizeValue: JSON inválido retorna null (não lança)", () => {
withTmpBundleBaseline("not json at all", (p) => {
assert.equal(readSizeBaseline(p), null);
});
});
@@ -0,0 +1,64 @@
// tests/unit/build/check-circular-deps.test.ts
// TDD test for the parseDpdmOutput function in check-circular-deps.mjs.
// Validates JSON parsing logic without executing dpdm (which is slow and
// requires network-resolved node_modules at test time).
import test from "node:test";
import assert from "node:assert/strict";
import { parseDpdmOutput } from "../../../scripts/check/check-circular-deps.mjs";
test("parseDpdmOutput: empty circulars array returns count 0", () => {
const result = parseDpdmOutput(JSON.stringify({ circulars: [], tree: {}, entries: [] }));
assert.equal(result.count, 0);
assert.deepEqual(result.circulars, []);
});
test("parseDpdmOutput: counts each circular path as one entry", () => {
const synthetic = {
entries: ["src/a.ts", "src/b.ts"],
tree: {},
circulars: [
["src/a.ts", "src/b.ts"],
["src/b.ts", "src/a.ts"],
["src/c.ts", "src/d.ts", "src/c.ts"],
],
};
const result = parseDpdmOutput(JSON.stringify(synthetic));
assert.equal(result.count, 3);
assert.equal(result.circulars.length, 3);
});
test("parseDpdmOutput: preserves circular path arrays", () => {
const path1 = ["src/lib/db/core.ts", "src/lib/db/settings.ts"];
const path2 = ["open-sse/handlers/chatCore.ts", "open-sse/services/combo.ts"];
const result = parseDpdmOutput(JSON.stringify({ circulars: [path1, path2] }));
assert.equal(result.count, 2);
assert.deepEqual(result.circulars[0], path1);
assert.deepEqual(result.circulars[1], path2);
});
test("parseDpdmOutput: missing circulars key returns count 0", () => {
// dpdm omits circulars when there are none in some versions
const result = parseDpdmOutput(JSON.stringify({ entries: [], tree: {} }));
assert.equal(result.count, 0);
assert.deepEqual(result.circulars, []);
});
test("parseDpdmOutput: null circulars treated as empty", () => {
// defensive: dpdm could theoretically return null
const result = parseDpdmOutput(JSON.stringify({ circulars: null }));
assert.equal(result.count, 0);
assert.deepEqual(result.circulars, []);
});
test("parseDpdmOutput: throws on invalid JSON", () => {
assert.throws(() => parseDpdmOutput("not-json{{{"), /dpdm JSON parse failed/);
});
test("parseDpdmOutput: large synthetic output counts correctly", () => {
const circulars = Array.from({ length: 89 }, (_, i) => [
`src/lib/module${i}.ts`,
`src/lib/dep${i}.ts`,
]);
const result = parseDpdmOutput(JSON.stringify({ circulars }));
assert.equal(result.count, 89);
});
@@ -0,0 +1,327 @@
// tests/unit/build/check-codeql-ratchet.test.ts
// TDD unit tests for scripts/check/check-codeql-ratchet.mjs — Task 7.3 CodeQL ratchet.
//
// Strategy: test the exported pure function without calling the GitHub API.
// All fixtures are synthetic GitHub API responses.
// - parseCodeQLAlerts() — filters + counts open, non-dismissed CodeQL alerts
// (Hard Rule #14: dismissed alerts do not count)
import test from "node:test";
import assert from "node:assert/strict";
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
import {
parseCodeQLAlerts,
evaluateCodeqlRatchet,
} from "../../../scripts/check/check-codeql-ratchet.mjs";
type RatchetVerdict = { regressed: boolean; improved: boolean };
const evaluate = evaluateCodeqlRatchet as (current: number, baseline: number) => RatchetVerdict;
// ---------------------------------------------------------------------------
// Fixtures — synthetic GitHub code-scanning/alerts API responses
// ---------------------------------------------------------------------------
/** Helper to build a minimal alert object. */
function makeAlert(
overrides: {
number?: number;
state?: "open" | "dismissed" | "fixed";
tool?: string;
ruleId?: string;
severity?: string;
securitySeverity?: string;
dismissedReason?: string | null;
} = {}
) {
return {
number: overrides.number ?? 1,
state: overrides.state ?? "open",
dismissed_reason: overrides.dismissedReason ?? null,
dismissed_at: overrides.dismissedReason ? "2026-01-01T00:00:00Z" : null,
tool: {
name: overrides.tool ?? "CodeQL",
guid: null,
version: "2.16.0",
},
rule: {
id: overrides.ruleId ?? "js/sql-injection",
name: overrides.ruleId ?? "SQL Injection",
severity: overrides.severity ?? "error",
security_severity_level: overrides.securitySeverity ?? "high",
description: "Vulnerability description",
},
most_recent_instance: {
ref: "refs/heads/main",
state: overrides.state ?? "open",
},
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-02T00:00:00Z",
url: "https://api.github.com/repos/owner/repo/code-scanning/alerts/1",
html_url: "https://github.com/owner/repo/security/code-scanning/1",
};
}
// ---------------------------------------------------------------------------
// parseCodeQLAlerts — input inválido
// ---------------------------------------------------------------------------
test("parseCodeQLAlerts: null retorna alertCount=0", () => {
const result = parseCodeQLAlerts(null);
assert.equal(result.alertCount, 0);
assert.deepEqual(result.bySeverity, {});
assert.deepEqual(result.byRule, {});
});
test("parseCodeQLAlerts: undefined retorna alertCount=0", () => {
const result = parseCodeQLAlerts(undefined as unknown as null);
assert.equal(result.alertCount, 0);
});
test("parseCodeQLAlerts: objeto (não-array) retorna alertCount=0", () => {
const result = parseCodeQLAlerts({ number: 1, state: "open" } as unknown as null);
assert.equal(result.alertCount, 0);
});
test("parseCodeQLAlerts: string retorna alertCount=0", () => {
const result = parseCodeQLAlerts("open" as unknown as null);
assert.equal(result.alertCount, 0);
});
// ---------------------------------------------------------------------------
// parseCodeQLAlerts — array vazio
// ---------------------------------------------------------------------------
test("parseCodeQLAlerts: array vazio retorna alertCount=0", () => {
const result = parseCodeQLAlerts([]);
assert.equal(result.alertCount, 0);
});
// ---------------------------------------------------------------------------
// parseCodeQLAlerts — Hard Rule #14: dismissed alerts don't count
// ---------------------------------------------------------------------------
test("parseCodeQLAlerts: alerta dismissed NÃO conta (Hard Rule #14)", () => {
const alerts = [makeAlert({ state: "dismissed", dismissedReason: "false positive" })];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 0, "dismissed alert must not be counted");
});
test("parseCodeQLAlerts: alerta dismissed com razão 'used in tests' NÃO conta", () => {
const alerts = [makeAlert({ state: "dismissed", dismissedReason: "used in tests" })];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 0);
});
test("parseCodeQLAlerts: alerta dismissed independente da razão NÃO conta", () => {
const alerts = [
makeAlert({ state: "dismissed", dismissedReason: "wont fix" }),
makeAlert({ number: 2, state: "dismissed", dismissedReason: "false positive" }),
makeAlert({ number: 3, state: "dismissed", dismissedReason: "used in tests" }),
];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 0, "all dismissed states must be excluded");
});
// ---------------------------------------------------------------------------
// parseCodeQLAlerts — estado fixed não conta
// ---------------------------------------------------------------------------
test("parseCodeQLAlerts: alerta fixed NÃO conta", () => {
const alerts = [makeAlert({ state: "fixed" as "open" | "dismissed" | "fixed" })];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 0, "fixed alerts are not open, must not count");
});
// ---------------------------------------------------------------------------
// parseCodeQLAlerts — somente alertas CodeQL (filtra outras ferramentas)
// ---------------------------------------------------------------------------
test("parseCodeQLAlerts: alertas de outras ferramentas são ignorados", () => {
const alerts = [
makeAlert({ tool: "Semgrep", ruleId: "semgrep-rule-1" }),
makeAlert({ number: 2, tool: "ESLint", ruleId: "eslint-rule-1" }),
];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 0, "only CodeQL alerts should be counted");
});
test("parseCodeQLAlerts: tool 'CodeQL' (maiúsculas) conta", () => {
const alerts = [makeAlert({ tool: "CodeQL" })];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 1);
});
test("parseCodeQLAlerts: tool 'codeql' (minúsculas) conta (case-insensitive)", () => {
const alerts = [makeAlert({ tool: "codeql" })];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 1);
});
test("parseCodeQLAlerts: tool 'CodeQL Community' também conta (contém 'codeql')", () => {
const alerts = [makeAlert({ tool: "CodeQL Community" })];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 1);
});
// ---------------------------------------------------------------------------
// parseCodeQLAlerts — contagem de alertas open
// ---------------------------------------------------------------------------
test("parseCodeQLAlerts: 1 alerta open CodeQL retorna alertCount=1", () => {
const alerts = [makeAlert({ state: "open" })];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 1);
});
test("parseCodeQLAlerts: 3 alertas open retorna alertCount=3", () => {
const alerts = [
makeAlert({ number: 1, state: "open" }),
makeAlert({ number: 2, state: "open", ruleId: "js/xss" }),
makeAlert({ number: 3, state: "open", ruleId: "js/path-injection" }),
];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 3);
});
test("parseCodeQLAlerts: mix de open, dismissed, fixed — conta só open", () => {
const alerts = [
makeAlert({ number: 1, state: "open" }),
makeAlert({ number: 2, state: "dismissed", dismissedReason: "false positive" }),
makeAlert({ number: 3, state: "fixed" as "open" | "dismissed" | "fixed" }),
makeAlert({ number: 4, state: "open", ruleId: "js/xss" }),
];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 2, "only 2 open, non-dismissed alerts");
});
test("parseCodeQLAlerts: mix de CodeQL e outras ferramentas — conta só CodeQL", () => {
const alerts = [
makeAlert({ number: 1, tool: "CodeQL", state: "open" }),
makeAlert({ number: 2, tool: "Semgrep", state: "open" }),
makeAlert({ number: 3, tool: "CodeQL", state: "open", ruleId: "js/xss" }),
];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 2, "only CodeQL open alerts");
});
// ---------------------------------------------------------------------------
// parseCodeQLAlerts — severidade
// ---------------------------------------------------------------------------
test("parseCodeQLAlerts: coleta bySeverity de security_severity_level", () => {
const alerts = [
makeAlert({ number: 1, securitySeverity: "critical" }),
makeAlert({ number: 2, securitySeverity: "high" }),
makeAlert({ number: 3, securitySeverity: "medium" }),
makeAlert({ number: 4, securitySeverity: "high" }), // segundo high
];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 4);
assert.equal(result.bySeverity["critical"], 1);
assert.equal(result.bySeverity["high"], 2);
assert.equal(result.bySeverity["medium"], 1);
});
test("parseCodeQLAlerts: alerta sem security_severity_level usa rule.severity", () => {
const alerts = [
{
...makeAlert({ number: 1 }),
rule: {
id: "js/unused-local-variable",
name: "Unused variable",
severity: "warning",
// sem security_severity_level
description: "Local var not used",
},
},
];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 1);
assert.ok(
"warning" in result.bySeverity || "unknown" in result.bySeverity,
"severity should be captured"
);
});
// ---------------------------------------------------------------------------
// parseCodeQLAlerts — byRule
// ---------------------------------------------------------------------------
test("parseCodeQLAlerts: agrupa por rule.id em byRule", () => {
const alerts = [
makeAlert({ number: 1, ruleId: "js/sql-injection" }),
makeAlert({ number: 2, ruleId: "js/xss" }),
makeAlert({ number: 3, ruleId: "js/sql-injection" }), // segundo do mesmo rule
];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.byRule["js/sql-injection"], 2);
assert.equal(result.byRule["js/xss"], 1);
});
test("parseCodeQLAlerts: alerta sem rule retorna byRule com 'unknown'", () => {
const alert = {
number: 1,
state: "open",
dismissed_reason: null,
dismissed_at: null,
tool: { name: "CodeQL", guid: null, version: "2.16.0" },
// sem rule
};
const result = parseCodeQLAlerts([alert]);
assert.equal(result.alertCount, 1);
assert.ok("unknown" in result.byRule);
});
// ---------------------------------------------------------------------------
// parseCodeQLAlerts — dismissed não contamina contagens
// ---------------------------------------------------------------------------
test("parseCodeQLAlerts: dismissed com mesmo ruleId que open — dismissed não aparece em byRule", () => {
const alerts = [
makeAlert({ number: 1, state: "open", ruleId: "js/sql-injection" }),
makeAlert({
number: 2,
state: "dismissed",
ruleId: "js/sql-injection",
dismissedReason: "false positive",
}),
];
const result = parseCodeQLAlerts(alerts);
assert.equal(result.alertCount, 1);
assert.equal(result.byRule["js/sql-injection"], 1, "only the open alert should appear in byRule");
});
// ---------------------------------------------------------------------------
// evaluateCodeqlRatchet — ratchet direction:down (Task 7.3 promote to blocking)
// Mirror of evaluateDeadCode: regression when measured > baseline; the baseline
// is 0 (clean), so ANY open CodeQL alert is a regression that blocks.
// ---------------------------------------------------------------------------
test("evaluateCodeqlRatchet: equal to baseline passes (0 vs 0 — clean)", () => {
const r = evaluate(0, 0);
assert.equal(r.regressed, false);
assert.equal(r.improved, false);
});
test("evaluateCodeqlRatchet: one more alert than baseline 0 is a regression", () => {
const r = evaluate(1, 0);
assert.equal(r.regressed, true, "a single new open CodeQL alert must block");
assert.equal(r.improved, false);
});
test("evaluateCodeqlRatchet: fewer alerts than a non-zero baseline is an improvement", () => {
const r = evaluate(2, 5);
assert.equal(r.regressed, false);
assert.equal(r.improved, true);
});
test("evaluateCodeqlRatchet: zero alerts against a non-zero baseline is a maximum improvement", () => {
const r = evaluate(0, 5);
assert.equal(r.regressed, false);
assert.equal(r.improved, true);
});
test("evaluateCodeqlRatchet: strict integer comparison — any increase regresses", () => {
assert.equal(evaluate(6, 5).regressed, true);
assert.equal(evaluate(5, 5).regressed, false);
assert.equal(evaluate(4, 5).regressed, false);
});
@@ -0,0 +1,97 @@
// tests/unit/build/check-cognitive-complexity.test.ts
// Unit tests for the JSON-parsing helper in check-cognitive-complexity.mjs.
// These tests validate countCognitiveViolations() against synthetic ESLint
// JSON output — no filesystem access, no ESLint subprocess.
import test from "node:test";
import assert from "node:assert/strict";
import { countCognitiveViolations } from "../../../scripts/check/check-cognitive-complexity.mjs";
// Minimal ESLint JSON message shape used in fixtures.
type EslintMessage = {
ruleId: string;
severity: number;
message: string;
line: number;
column: number;
};
type EslintFileResult = {
filePath: string;
messages: EslintMessage[];
errorCount: number;
warningCount: number;
};
function makeResult(filePath: string, messages: EslintMessage[]): EslintFileResult {
return {
filePath,
messages,
errorCount: messages.filter((m) => m.severity === 2).length,
warningCount: messages.filter((m) => m.severity === 1).length,
};
}
function makeMsg(ruleId: string): EslintMessage {
return { ruleId, severity: 2, message: "violation", line: 1, column: 1 };
}
test("countCognitiveViolations: empty report returns 0", () => {
assert.equal(countCognitiveViolations([]), 0);
});
test("countCognitiveViolations: no cognitive-complexity messages returns 0", () => {
const report = [
makeResult("src/foo.ts", [makeMsg("complexity"), makeMsg("max-lines-per-function")]),
];
assert.equal(countCognitiveViolations(report), 0);
});
test("countCognitiveViolations: single cognitive-complexity violation in one file", () => {
const report = [
makeResult("src/foo.ts", [makeMsg("sonarjs/cognitive-complexity")]),
];
assert.equal(countCognitiveViolations(report), 1);
});
test("countCognitiveViolations: multiple cognitive-complexity violations across files", () => {
const report = [
makeResult("src/foo.ts", [
makeMsg("sonarjs/cognitive-complexity"),
makeMsg("sonarjs/cognitive-complexity"),
]),
makeResult("open-sse/bar.ts", [
makeMsg("sonarjs/cognitive-complexity"),
]),
];
assert.equal(countCognitiveViolations(report), 3);
});
test("countCognitiveViolations: ignores unrelated sonarjs rules", () => {
const report = [
makeResult("src/baz.ts", [
makeMsg("sonarjs/no-duplicate-string"),
makeMsg("sonarjs/cognitive-complexity"),
makeMsg("sonarjs/no-identical-functions"),
]),
];
assert.equal(countCognitiveViolations(report), 1);
});
test("countCognitiveViolations: file with no messages contributes 0", () => {
const report = [
makeResult("src/clean.ts", []),
makeResult("src/complex.ts", [makeMsg("sonarjs/cognitive-complexity")]),
];
assert.equal(countCognitiveViolations(report), 1);
});
test("countCognitiveViolations: mixes of rule IDs only counts cognitive-complexity", () => {
const report = [
makeResult("src/mixed.ts", [
makeMsg("complexity"),
makeMsg("sonarjs/cognitive-complexity"),
makeMsg("max-lines-per-function"),
makeMsg("sonarjs/cognitive-complexity"),
]),
];
assert.equal(countCognitiveViolations(report), 2);
});
+22
View File
@@ -0,0 +1,22 @@
// tests/unit/build/check-complexity.test.ts
// TDD test for check-complexity.mjs: the complexity ratchet must scan the SAME first-party
// scope documented in eslint.complexity.config.mjs `files` and in complexity-baseline.json
// (src + open-sse + electron + bin). Task 6A.11 re-baselined the count claiming bin/electron
// coverage, but ESLINT_ARGS only passed src+open-sse — a fake-green gap (a god-function added
// under bin/ would pass the gate unseen). This test locks the scan scope to the documented one.
import test from "node:test";
import assert from "node:assert/strict";
import { ESLINT_ARGS } from "../../../scripts/check/check-complexity.mjs";
test("check-complexity scans the full documented scope (src, open-sse, electron, bin)", () => {
assert.ok(
ESLINT_ARGS.includes("bin"),
"ESLINT_ARGS must include 'bin' — a new god-function under bin/ must not pass the gate green",
);
assert.ok(
ESLINT_ARGS.includes("electron"),
"ESLINT_ARGS must include 'electron' to match eslint.complexity.config.mjs `files` and the baseline scope",
);
assert.ok(ESLINT_ARGS.includes("src"), "ESLINT_ARGS must include 'src'");
assert.ok(ESLINT_ARGS.includes("open-sse"), "ESLINT_ARGS must include 'open-sse'");
});
+164
View File
@@ -0,0 +1,164 @@
import test from "node:test";
import assert from "node:assert/strict";
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
import { parseKnipMetrics } from "../../../scripts/check/check-dead-code.mjs";
// ---------------------------------------------------------------------------
// Fixtures — JSON sintético com formato do knip --reporter json
// O reporter emite: { issues: Array<{ file, exports?, files?, types?, ... }> }
// ---------------------------------------------------------------------------
/** Retorna um JSON vazio válido (nenhum problema encontrado). */
function makeEmptyReport() {
return { issues: [] };
}
/** Arquivo com 2 exports mortos e 1 tipo morto. */
function makeReportWithExports() {
return {
issues: [
{
file: "src/lib/utils.ts",
exports: [
{ name: "unusedHelper", line: 10, col: 0 },
{ name: "deadFn", line: 20, col: 0 },
],
types: [
{ name: "DeadType", line: 5, col: 0 },
],
},
],
};
}
/** Arquivo morto (inteiro não importado em lugar nenhum). */
function makeReportWithDeadFile() {
return {
issues: [
{
file: "src/lib/orphan.ts",
files: [{ name: "src/lib/orphan.ts" }],
},
],
};
}
/** Misto: 1 arquivo morto + 3 exports mortos em outro arquivo. */
function makeReportMixed() {
return {
issues: [
{
file: "src/lib/orphan.ts",
files: [{ name: "src/lib/orphan.ts" }],
},
{
file: "src/lib/active.ts",
exports: [
{ name: "deadExport1", line: 1, col: 0 },
{ name: "deadExport2", line: 2, col: 0 },
],
nsExports: [
{ name: "deadNsExport", line: 3, col: 0 },
],
},
],
};
}
/** Múltiplos tipos de dead exports: types, nsExports, nsTypes, enumMembers. */
function makeReportAllExportTypes() {
return {
issues: [
{
file: "src/lib/all-types.ts",
exports: [{ name: "e1", line: 1, col: 0 }],
types: [{ name: "t1", line: 2, col: 0 }],
nsExports: [{ name: "ns1", line: 3, col: 0 }],
nsTypes: [{ name: "nst1", line: 4, col: 0 }],
enumMembers: [{ name: "em1", line: 5, col: 0 }],
namespaceMembers: [{ name: "nm1", line: 6, col: 0 }],
},
],
};
}
// ---------------------------------------------------------------------------
// Testes
// ---------------------------------------------------------------------------
test("parseKnipMetrics: report vazio retorna tudo zero", () => {
const result = parseKnipMetrics(makeEmptyReport());
assert.deepEqual(result, { deadExports: 0, deadFiles: 0, deadTotal: 0 });
});
test("parseKnipMetrics: conta exports mortos e tipos mortos corretamente", () => {
const result = parseKnipMetrics(makeReportWithExports());
// 2 exports + 1 type = 3 deadExports, 0 deadFiles
assert.equal(result.deadExports, 3);
assert.equal(result.deadFiles, 0);
assert.equal(result.deadTotal, 3);
});
test("parseKnipMetrics: conta arquivos mortos corretamente", () => {
const result = parseKnipMetrics(makeReportWithDeadFile());
assert.equal(result.deadExports, 0);
assert.equal(result.deadFiles, 1);
assert.equal(result.deadTotal, 1);
});
test("parseKnipMetrics: relatório misto — arquivos mortos + exports mortos", () => {
const result = parseKnipMetrics(makeReportMixed());
// 1 arquivo morto + (2 exports + 1 nsExport) = 4 total
assert.equal(result.deadExports, 3);
assert.equal(result.deadFiles, 1);
assert.equal(result.deadTotal, 4);
});
test("parseKnipMetrics: soma todos os tipos de dead export (exports/types/nsExports/nsTypes/enumMembers/namespaceMembers)", () => {
const result = parseKnipMetrics(makeReportAllExportTypes());
// 1 de cada tipo × 6 tipos = 6 deadExports
assert.equal(result.deadExports, 6);
assert.equal(result.deadFiles, 0);
assert.equal(result.deadTotal, 6);
});
test("parseKnipMetrics: null retorna zeros (input inválido)", () => {
const result = parseKnipMetrics(null);
assert.deepEqual(result, { deadExports: 0, deadFiles: 0, deadTotal: 0 });
});
test("parseKnipMetrics: input sem campo issues retorna zeros", () => {
const result = parseKnipMetrics({ otherField: 123 });
assert.deepEqual(result, { deadExports: 0, deadFiles: 0, deadTotal: 0 });
});
test("parseKnipMetrics: entry sem campos de export não incrementa contador", () => {
// Arquivo que aparece na lista mas sem exports mortos e sem files
const report = {
issues: [
{ file: "src/lib/clean.ts" },
],
};
const result = parseKnipMetrics(report);
assert.deepEqual(result, { deadExports: 0, deadFiles: 0, deadTotal: 0 });
});
test("parseKnipMetrics: deadTotal == deadExports + deadFiles sempre", () => {
const report = makeReportMixed();
const result = parseKnipMetrics(report);
assert.equal(result.deadTotal, result.deadExports + result.deadFiles);
});
test("parseKnipMetrics: múltiplos arquivos mortos no mesmo relatório", () => {
const report = {
issues: [
{ file: "src/lib/orphan1.ts", files: [{ name: "src/lib/orphan1.ts" }] },
{ file: "src/lib/orphan2.ts", files: [{ name: "src/lib/orphan2.ts" }] },
{ file: "src/lib/orphan3.ts", files: [{ name: "src/lib/orphan3.ts" }] },
],
};
const result = parseKnipMetrics(report);
assert.equal(result.deadFiles, 3);
assert.equal(result.deadExports, 0);
assert.equal(result.deadTotal, 3);
});
+325
View File
@@ -0,0 +1,325 @@
// tests/unit/build/check-licenses.test.ts
// TDD unit tests for scripts/check/check-licenses.mjs — Task 7.20 license compliance.
//
// Strategy: test the three exported pure functions without spawning license-checker
// or reading the real .license-allowlist.json. All fixtures are synthetic.
// - loadAllowlist() — parses + validates the allowlist JSON shape
// - classifyLicense() — core policy decision (allowed / exception / denied)
// - stripVersion() — strips @version suffix from package keys
import test from "node:test";
import assert from "node:assert/strict";
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
import {
classifyLicense,
stripVersion,
loadAllowlist,
} from "../../../scripts/check/check-licenses.mjs";
// ---------------------------------------------------------------------------
// Helpers — synthetic allowlists for testing classifyLicense in isolation
// ---------------------------------------------------------------------------
function makeAllowlist(overrides: Partial<{
allowed: string[];
allowedExpressions: string[];
exceptions: Record<string, { license: string; justification: string; risk: string }>;
}> = {}) {
return {
allowed: ["MIT", "Apache-2.0", "BSD-3-Clause", "ISC", "0BSD"],
allowedExpressions: ["(MIT OR Apache-2.0)", "MIT AND ISC", "MIT*"],
exceptions: {},
...overrides,
};
}
// ---------------------------------------------------------------------------
// stripVersion
// ---------------------------------------------------------------------------
test("stripVersion: strips @version from a regular package", () => {
assert.equal(stripVersion("lodash@4.17.21"), "lodash");
});
test("stripVersion: strips @version from a scoped package", () => {
assert.equal(stripVersion("@img/sharp-libvips-linux-x64@1.2.4"), "@img/sharp-libvips-linux-x64");
});
test("stripVersion: returns bare name unchanged (no version)", () => {
assert.equal(stripVersion("lodash"), "lodash");
});
test("stripVersion: handles scoped package without version", () => {
assert.equal(stripVersion("@scope/pkg"), "@scope/pkg");
});
test("stripVersion: handles nested scope-like name with version", () => {
assert.equal(stripVersion("@aws-sdk/client-bedrock-runtime@3.1063.0"), "@aws-sdk/client-bedrock-runtime");
});
// ---------------------------------------------------------------------------
// classifyLicense — allowed
// ---------------------------------------------------------------------------
test("classifyLicense: MIT is allowed", () => {
const result = classifyLicense("some-pkg@1.0.0", "MIT", makeAllowlist());
assert.equal(result.status, "allowed");
});
test("classifyLicense: Apache-2.0 is allowed", () => {
const result = classifyLicense("some-pkg@1.0.0", "Apache-2.0", makeAllowlist());
assert.equal(result.status, "allowed");
});
test("classifyLicense: ISC is allowed", () => {
const result = classifyLicense("some-pkg@1.0.0", "ISC", makeAllowlist());
assert.equal(result.status, "allowed");
});
test("classifyLicense: 0BSD is allowed", () => {
const result = classifyLicense("some-pkg@1.0.0", "0BSD", makeAllowlist());
assert.equal(result.status, "allowed");
});
// ---------------------------------------------------------------------------
// classifyLicense — allowed expressions
// ---------------------------------------------------------------------------
test("classifyLicense: (MIT OR Apache-2.0) expression is allowed", () => {
const result = classifyLicense("some-pkg@1.0.0", "(MIT OR Apache-2.0)", makeAllowlist());
assert.equal(result.status, "allowed");
});
test("classifyLicense: MIT AND ISC expression is allowed", () => {
const result = classifyLicense("some-pkg@1.0.0", "MIT AND ISC", makeAllowlist());
assert.equal(result.status, "allowed");
});
test("classifyLicense: MIT* expression is allowed (e.g. khroma)", () => {
const result = classifyLicense("khroma@2.1.0", "MIT*", makeAllowlist());
assert.equal(result.status, "allowed");
});
// ---------------------------------------------------------------------------
// classifyLicense — denied
// ---------------------------------------------------------------------------
test("classifyLicense: GPL-3.0 is denied", () => {
const result = classifyLicense("gpl-pkg@1.0.0", "GPL-3.0", makeAllowlist());
assert.equal(result.status, "denied");
assert.ok(result.reason.includes("GPL-3.0"), `reason should mention license: ${result.reason}`);
});
test("classifyLicense: AGPL-3.0 is denied (strong copyleft)", () => {
const result = classifyLicense("agpl-pkg@1.0.0", "AGPL-3.0", makeAllowlist());
assert.equal(result.status, "denied");
});
test("classifyLicense: LGPL-3.0-or-later is denied without exception", () => {
const result = classifyLicense("lgpl-pkg@1.0.0", "LGPL-3.0-or-later", makeAllowlist());
assert.equal(result.status, "denied");
});
test("classifyLicense: MPL-2.0 is denied without exception or expression", () => {
const result = classifyLicense("mpl-pkg@1.0.0", "MPL-2.0", makeAllowlist());
assert.equal(result.status, "denied");
});
test("classifyLicense: unknown/UNKNOWN license is denied", () => {
const result = classifyLicense("mystery-pkg@1.0.0", "UNKNOWN", makeAllowlist());
assert.equal(result.status, "denied");
});
test("classifyLicense: Custom license is denied", () => {
const result = classifyLicense("custom-pkg@1.0.0", "Custom: LICENSE", makeAllowlist());
assert.equal(result.status, "denied");
});
// ---------------------------------------------------------------------------
// classifyLicense — exceptions
// ---------------------------------------------------------------------------
test("classifyLicense: LGPL package with registered exception returns 'exception'", () => {
const allowlist = makeAllowlist({
exceptions: {
"lgpl-native-pkg": {
license: "LGPL-3.0-or-later",
justification: "Dynamically linked native binary; user can replace.",
risk: "low",
},
},
});
const result = classifyLicense("lgpl-native-pkg@1.2.3", "LGPL-3.0-or-later", allowlist);
assert.equal(result.status, "exception");
assert.ok(result.reason.includes("exception"), `reason should mention exception: ${result.reason}`);
});
test("classifyLicense: scoped package with exception: version is stripped for lookup", () => {
const allowlist = makeAllowlist({
exceptions: {
"@img/sharp-libvips-linux-x64": {
license: "LGPL-3.0-or-later",
justification: "Prebuilt shared lib.",
risk: "low",
},
},
});
const result = classifyLicense(
"@img/sharp-libvips-linux-x64@1.2.4",
"LGPL-3.0-or-later",
allowlist
);
assert.equal(result.status, "exception", "scoped exception should be found after version strip");
});
test("classifyLicense: exception does not apply to different package", () => {
const allowlist = makeAllowlist({
exceptions: {
"only-this-pkg": {
license: "GPL-3.0",
justification: "Special case.",
risk: "high",
},
},
});
const result = classifyLicense("other-gpl-pkg@1.0.0", "GPL-3.0", allowlist);
assert.equal(result.status, "denied", "exception must be per-package, not per-license");
});
test("classifyLicense: exception with risk=medium still returns 'exception' (not denied)", () => {
const allowlist = makeAllowlist({
exceptions: {
"tls-client-node": {
license: "Custom: LICENSE",
justification: "Commons Clause + Apache-2.0. TODO: revisar.",
risk: "medium",
},
},
});
const result = classifyLicense("tls-client-node@0.2.0", "Custom: LICENSE", allowlist);
assert.equal(result.status, "exception");
});
// ---------------------------------------------------------------------------
// classifyLicense — reason field content
// ---------------------------------------------------------------------------
test("classifyLicense: denied result includes package name in reason", () => {
const result = classifyLicense("bad-pkg@1.0.0", "GPL-3.0", makeAllowlist());
assert.ok(
result.reason.includes("bad-pkg"),
`reason should include package name; got: ${result.reason}`
);
});
test("classifyLicense: allowed result mentions the matched license", () => {
const result = classifyLicense("ok-pkg@1.0.0", "MIT", makeAllowlist());
assert.ok(result.reason.includes("MIT"), `reason should include license; got: ${result.reason}`);
});
// ---------------------------------------------------------------------------
// loadAllowlist — shape validation (reads the real .license-allowlist.json)
// ---------------------------------------------------------------------------
test("loadAllowlist: returns an object with allowed, allowedExpressions, and exceptions keys", () => {
const allowlist = loadAllowlist();
assert.ok(typeof allowlist === "object" && allowlist !== null, "should be an object");
assert.ok(Array.isArray(allowlist.allowed), "allowed should be an array");
assert.ok(Array.isArray(allowlist.allowedExpressions), "allowedExpressions should be an array");
assert.ok(typeof allowlist.exceptions === "object", "exceptions should be an object");
});
test("loadAllowlist: allowed includes MIT", () => {
const allowlist = loadAllowlist();
assert.ok(allowlist.allowed.includes("MIT"), "MIT must be in allowed");
});
test("loadAllowlist: allowed includes Apache-2.0", () => {
const allowlist = loadAllowlist();
assert.ok(allowlist.allowed.includes("Apache-2.0"), "Apache-2.0 must be in allowed");
});
test("loadAllowlist: allowed includes ISC", () => {
const allowlist = loadAllowlist();
assert.ok(allowlist.allowed.includes("ISC"), "ISC must be in allowed");
});
test("loadAllowlist: exceptions entries have required fields", () => {
const allowlist = loadAllowlist();
for (const [pkgName, exc] of Object.entries(allowlist.exceptions)) {
assert.ok(
typeof (exc as any).license === "string",
`exceptions.${pkgName}.license should be a string`
);
assert.ok(
typeof (exc as any).justification === "string",
`exceptions.${pkgName}.justification should be a string`
);
assert.ok(
typeof (exc as any).risk === "string",
`exceptions.${pkgName}.risk should be a string`
);
assert.ok(
(exc as any).justification.length > 10,
`exceptions.${pkgName}.justification must be non-trivial (> 10 chars)`
);
}
});
test("loadAllowlist: tls-client-node exception has risk=medium (Commons Clause)", () => {
const allowlist = loadAllowlist();
const exc = allowlist.exceptions["tls-client-node"] as any;
assert.ok(exc, "tls-client-node exception must be registered");
assert.equal(exc.risk, "medium", "tls-client-node is a medium-risk exception (Commons Clause)");
});
test("loadAllowlist: LGPL packages have registered exceptions", () => {
const allowlist = loadAllowlist();
const lgplPkgs = ["@img/sharp-libvips-linux-x64", "@img/sharp-libvips-linuxmusl-x64"];
for (const pkg of lgplPkgs) {
assert.ok(
allowlist.exceptions[pkg],
`${pkg} (LGPL-3.0-or-later) must have a registered exception`
);
}
});
test("loadAllowlist: MPL-2.0 packages have registered exceptions or allowed expressions", () => {
const allowlist = loadAllowlist();
const mplPkgs = ["lightningcss", "lightningcss-linux-x64-gnu", "lightningcss-linux-x64-musl"];
for (const pkg of mplPkgs) {
const hasException = Boolean(allowlist.exceptions[pkg]);
const mplExpr = allowlist.allowedExpressions.some((e: string) => e.includes("MPL"));
assert.ok(
hasException || mplExpr,
`${pkg} (MPL-2.0) must be in exceptions or have an allowed expression`
);
}
});
// ---------------------------------------------------------------------------
// Integration: real allowlist correctly classifies known packages
// ---------------------------------------------------------------------------
test("integration: classifyLicense passes MIT packages against real allowlist", () => {
const allowlist = loadAllowlist();
const result = classifyLicense("lodash@4.17.21", "MIT", allowlist);
assert.equal(result.status, "allowed");
});
test("integration: classifyLicense passes tls-client-node as exception against real allowlist", () => {
const allowlist = loadAllowlist();
const result = classifyLicense("tls-client-node@0.2.0", "Custom: LICENSE", allowlist);
assert.equal(result.status, "exception");
});
test("integration: classifyLicense denies GPL-3.0 against real allowlist", () => {
const allowlist = loadAllowlist();
const result = classifyLicense("hypothetical-gpl@1.0.0", "GPL-3.0", allowlist);
assert.equal(result.status, "denied");
});
test("integration: classifyLicense denies AGPL-3.0 against real allowlist", () => {
const allowlist = loadAllowlist();
const result = classifyLicense("hypothetical-agpl@1.0.0", "AGPL-3.0", allowlist);
assert.equal(result.status, "denied");
});
+181
View File
@@ -0,0 +1,181 @@
// tests/unit/build/check-lockfile.test.ts
// TDD tests for check-lockfile.mjs — lockfile policy gate (Task 7.7).
//
// Strategy: the lockfile-lint binary is an external CLI tool; we do not spawn it
// in unit tests. Instead, we test the two exported pure functions:
// - getLockfileLintConfig() — returns the policy configuration object
// - buildLockfileLintArgs() — maps a config object to the argv array
//
// This validates the policy settings and the arg-assembly logic without requiring
// a real package-lock.json or a network call.
import test from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
import {
getLockfileLintConfig,
buildLockfileLintArgs,
} from "../../../scripts/check/check-lockfile.mjs";
// ---------------------------------------------------------------------------
// getLockfileLintConfig
// ---------------------------------------------------------------------------
test("getLockfileLintConfig: returns an object with required keys", () => {
const cfg = getLockfileLintConfig();
assert.ok(typeof cfg === "object" && cfg !== null, "config should be an object");
assert.ok("lockfilePath" in cfg, "should have lockfilePath");
assert.ok("type" in cfg, "should have type");
assert.ok("validateHttps" in cfg, "should have validateHttps");
assert.ok("validateIntegrity" in cfg, "should have validateIntegrity");
assert.ok("allowedHosts" in cfg, "should have allowedHosts");
});
test("getLockfileLintConfig: lockfilePath points to package-lock.json", () => {
const cfg = getLockfileLintConfig();
assert.ok(
cfg.lockfilePath.endsWith("package-lock.json"),
`lockfilePath should end with package-lock.json, got: ${cfg.lockfilePath}`
);
});
test("getLockfileLintConfig: type is npm", () => {
const cfg = getLockfileLintConfig();
assert.equal(cfg.type, "npm");
});
test("getLockfileLintConfig: validateHttps is true (HTTPS enforcement)", () => {
const cfg = getLockfileLintConfig();
assert.equal(cfg.validateHttps, true, "HTTPS enforcement must be enabled");
});
test("getLockfileLintConfig: validateIntegrity is true (integrity enforcement)", () => {
const cfg = getLockfileLintConfig();
assert.equal(cfg.validateIntegrity, true, "integrity validation must be enabled");
});
test("getLockfileLintConfig: allowedHosts includes npm (official registry)", () => {
const cfg = getLockfileLintConfig();
assert.ok(Array.isArray(cfg.allowedHosts), "allowedHosts should be an array");
assert.ok(
cfg.allowedHosts.includes("npm"),
"npm must be in allowedHosts (covers registry.npmjs.org)"
);
});
test("getLockfileLintConfig: no http:// hosts in allowedHosts", () => {
const cfg = getLockfileLintConfig();
for (const host of cfg.allowedHosts) {
assert.ok(
!host.startsWith("http://"),
`allowedHosts must not contain http:// URLs, found: ${host}`
);
}
});
// ---------------------------------------------------------------------------
// buildLockfileLintArgs
// ---------------------------------------------------------------------------
test("buildLockfileLintArgs: includes --path and --type", () => {
const cfg = getLockfileLintConfig();
const args = buildLockfileLintArgs(cfg);
assert.ok(args.includes("--path"), "args should include --path");
assert.ok(args.includes("--type"), "args should include --type");
const pathIdx = args.indexOf("--path");
assert.equal(args[pathIdx + 1], cfg.lockfilePath);
const typeIdx = args.indexOf("--type");
assert.equal(args[typeIdx + 1], cfg.type);
});
test("buildLockfileLintArgs: includes --validate-https when validateHttps=true", () => {
const args = buildLockfileLintArgs({
lockfilePath: "/tmp/package-lock.json",
type: "npm",
validateHttps: true,
validateIntegrity: false,
allowedHosts: [],
});
assert.ok(args.includes("--validate-https"), "should include --validate-https");
});
test("buildLockfileLintArgs: omits --validate-https when validateHttps=false", () => {
const args = buildLockfileLintArgs({
lockfilePath: "/tmp/package-lock.json",
type: "npm",
validateHttps: false,
validateIntegrity: false,
allowedHosts: [],
});
assert.ok(!args.includes("--validate-https"), "should not include --validate-https");
});
test("buildLockfileLintArgs: includes --validate-integrity when validateIntegrity=true", () => {
const args = buildLockfileLintArgs({
lockfilePath: "/tmp/package-lock.json",
type: "npm",
validateHttps: false,
validateIntegrity: true,
allowedHosts: [],
});
assert.ok(args.includes("--validate-integrity"), "should include --validate-integrity");
});
test("buildLockfileLintArgs: omits --validate-integrity when validateIntegrity=false", () => {
const args = buildLockfileLintArgs({
lockfilePath: "/tmp/package-lock.json",
type: "npm",
validateHttps: false,
validateIntegrity: false,
allowedHosts: [],
});
assert.ok(!args.includes("--validate-integrity"), "should not include --validate-integrity");
});
test("buildLockfileLintArgs: includes --allowed-hosts and its values", () => {
const args = buildLockfileLintArgs({
lockfilePath: "/tmp/package-lock.json",
type: "npm",
validateHttps: false,
validateIntegrity: false,
allowedHosts: ["npm", "myprivatescope"],
});
assert.ok(args.includes("--allowed-hosts"), "should include --allowed-hosts");
assert.ok(args.includes("npm"), "should include npm host");
assert.ok(args.includes("myprivatescope"), "should include additional host");
});
test("buildLockfileLintArgs: omits --allowed-hosts when array is empty", () => {
const args = buildLockfileLintArgs({
lockfilePath: "/tmp/package-lock.json",
type: "npm",
validateHttps: false,
validateIntegrity: false,
allowedHosts: [],
});
assert.ok(!args.includes("--allowed-hosts"), "should not include --allowed-hosts when empty");
});
test("buildLockfileLintArgs: full config produces expected canonical args", () => {
const cfg = getLockfileLintConfig();
const args = buildLockfileLintArgs(cfg);
// Must include all four enforcement flags
assert.ok(args.includes("--validate-https"), "must enforce HTTPS");
assert.ok(args.includes("--validate-integrity"), "must enforce integrity");
assert.ok(args.includes("--allowed-hosts"), "must restrict hosts");
assert.ok(args.includes("npm"), "npm must be an allowed host");
});
test("buildLockfileLintArgs: --allowed-hosts values follow immediately after the flag", () => {
const args = buildLockfileLintArgs({
lockfilePath: "/tmp/package-lock.json",
type: "npm",
validateHttps: false,
validateIntegrity: false,
allowedHosts: ["npm", "verdaccio"],
});
const hostIdx = args.indexOf("--allowed-hosts");
assert.ok(hostIdx !== -1, "--allowed-hosts should be present");
assert.equal(args[hostIdx + 1], "npm");
assert.equal(args[hostIdx + 2], "verdaccio");
});
@@ -0,0 +1,124 @@
import { test } from "node:test";
import assert from "node:assert";
import os from "node:os";
import fs from "node:fs";
import path from "node:path";
import {
evaluateMutationRatchet,
mutationScoreForFile,
measureMutationScores,
readBaselineMutationScores,
MUTATION_RATCHET_EPS,
} from "../../../scripts/check/check-mutation-ratchet.mjs";
// ── evaluateMutationRatchet: direction UP (score can only improve) ───────────
test("mutation ratchet flags a drop (direction up)", () => {
assert.equal(evaluateMutationRatchet(72.0, 75.0).regressed, true);
assert.equal(evaluateMutationRatchet(76.0, 75.0).regressed, false);
assert.equal(evaluateMutationRatchet(76.0, 75.0).improved, true);
assert.equal(evaluateMutationRatchet(75.0, 75.0).regressed, false); // equal holds
assert.equal(evaluateMutationRatchet(75.0, 75.0).improved, false);
});
// ── eps anti-flake: a drop WITHIN eps holds; a drop BEYOND eps regresses ──────
test("mutation ratchet tolerates sub-eps jitter but catches real drops", () => {
assert.ok(MUTATION_RATCHET_EPS > 0);
// drop of 0.5pt with default eps 1.0 -> within tolerance, not a regression
assert.equal(evaluateMutationRatchet(74.5, 75.0).regressed, false);
// drop just past eps -> regression
assert.equal(evaluateMutationRatchet(75.0 - MUTATION_RATCHET_EPS - 0.01, 75.0).regressed, true);
// explicit eps argument is honored (0 eps = strict)
assert.equal(evaluateMutationRatchet(74.99, 75.0, 0).regressed, true);
});
// ── mutationScoreForFile: covered score = detected/(detected+survived),
// NoCoverage EXCLUDED (it is a coverage gap, not a test-quality signal). ─────
test("mutationScoreForFile computes the covered score and excludes NoCoverage", () => {
const fileData = {
mutants: [
{ status: "Killed" },
{ status: "Killed" },
{ status: "Killed" },
{ status: "Timeout" }, // Timeout counts as detected
{ status: "Survived" },
{ status: "Survived" },
{ status: "NoCoverage" }, // excluded from denominator
{ status: "NoCoverage" },
{ status: "Ignored" }, // excluded (not a valid mutant)
],
};
// detected = 4 (3 Killed + 1 Timeout); denom = 6 (+2 Survived); NoCoverage/Ignored out.
assert.ok(Math.abs(mutationScoreForFile(fileData) - (4 / 6) * 100) < 1e-9);
});
test("mutationScoreForFile returns null when there are no covered mutants", () => {
assert.equal(mutationScoreForFile({ mutants: [{ status: "NoCoverage" }] }), null);
assert.equal(mutationScoreForFile({ mutants: [] }), null);
});
// ── measureMutationScores: per-file scores from a report (and merges batches) ─
test("measureMutationScores maps each mutated file to its score", () => {
const report = {
files: {
"src/a.ts": { mutants: [{ status: "Killed" }, { status: "Survived" }] }, // 50
"src/b.ts": { mutants: [{ status: "Killed" }, { status: "Killed" }] }, // 100
"src/empty.ts": { mutants: [{ status: "NoCoverage" }] }, // null -> omitted
},
};
const scores = measureMutationScores(report);
assert.equal(scores["src/a.ts"], 50);
assert.equal(scores["src/b.ts"], 100);
assert.equal("src/empty.ts" in scores, false);
});
test("measureMutationScores accepts several reports (per-batch) and unions them", () => {
const c = { files: { "src/a.ts": { mutants: [{ status: "Killed" }, { status: "Survived" }] } } };
const g = { files: { "src/b.ts": { mutants: [{ status: "Killed" }] } } };
const scores = measureMutationScores([c, g]);
assert.equal(scores["src/a.ts"], 50);
assert.equal(scores["src/b.ts"], 100);
});
// ── SAME file split across sibling batches (auth.ts -> a1:1-1109 + a2:1110-2218 by
// mutation range): the file's mutants are DISJOINT per batch and must be UNIONED, not
// overwritten — else the last batch wins and the score reflects only half the file. ──
test("measureMutationScores unions same-file mutants across split batches (not overwrite)", () => {
// a1 slice: 1 killed, 1 survived (would score 50 alone)
const a1 = {
files: { "src/sse/services/auth.ts": { mutants: [{ status: "Killed" }, { status: "Survived" }] } },
};
// a2 slice: 3 killed (would score 100 alone)
const a2 = {
files: {
"src/sse/services/auth.ts": {
mutants: [{ status: "Killed" }, { status: "Killed" }, { status: "Killed" }],
},
},
};
const scores = measureMutationScores([a1, a2]);
// Combined: detected = 4 (1+3), survived = 1 -> 4/5 = 80. NOT 50 (a1) nor 100 (a2).
assert.ok(Math.abs(scores["src/sse/services/auth.ts"] - 80) < 1e-9);
});
// ── readBaselineMutationScores: graceful skip when the file/keys are absent ──
test("readBaselineMutationScores returns {} when the baseline file is missing", () => {
assert.deepEqual(readBaselineMutationScores("/no/such/baseline.json"), {});
});
test("readBaselineMutationScores extracts mutationScore.<path> metric values", () => {
// Write a tiny baseline to a temp file and read it back.
const tmp = path.join(os.tmpdir(), `mut-baseline-${process.pid}.json`);
fs.writeFileSync(
tmp,
JSON.stringify({
metrics: {
eslintWarnings: { value: 10, direction: "down" },
"mutationScore.src/a.ts": { value: 70, direction: "up", dedicatedGate: true },
"mutationScore.src/b.ts": { value: 80, direction: "up", dedicatedGate: true },
},
})
);
const base = readBaselineMutationScores(tmp);
assert.deepEqual(base, { "src/a.ts": 70, "src/b.ts": 80 });
fs.rmSync(tmp, { force: true });
});
@@ -0,0 +1,74 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import {
moduleFragment,
testImportsModule,
findCoverageDrift,
} from "../../../scripts/check/check-mutation-test-coverage.mjs";
test("moduleFragment returns the 3-segment suffix without extension", () => {
assert.equal(
moduleFragment("open-sse/handlers/chatCore/headers.ts"),
"handlers/chatCore/headers"
);
assert.equal(moduleFragment("src/sse/services/auth.ts"), "sse/services/auth");
// shallow paths just use what is available
assert.equal(moduleFragment("a/b.ts"), "a/b");
});
test("testImportsModule matches static, dynamic and require imports of the module path", () => {
const frag = "handlers/chatCore/headers";
// static import-from
assert.equal(
testImportsModule(`import { x } from "@omniroute/open-sse/handlers/chatCore/headers";`, frag),
true
);
// dynamic await import, even split across lines
assert.equal(
testImportsModule(`const { y } = await import(\n "../../open-sse/handlers/chatCore/headers.ts"\n);`, frag),
true
);
// require()
assert.equal(
testImportsModule(`const z = require("../../open-sse/handlers/chatCore/headers.ts");`, frag),
true
);
// unrelated module is not matched
assert.equal(
testImportsModule(`import { a } from "@omniroute/open-sse/handlers/chatCore/idempotency";`, frag),
false
);
// the fragment appearing only in a comment (not an import string) is NOT a match
assert.equal(
testImportsModule(`// see handlers/chatCore/headers for details\nconst a = 1;`, frag),
false
);
});
test("findCoverageDrift flags covering unit tests absent from tap.testFiles", () => {
const mutate = [
"open-sse/handlers/chatCore/headers.ts",
"open-sse/handlers/chatCore/idempotency.ts",
"_a_comment_entry",
];
const tapTestFiles = ["tests/unit/chatcore-headers.test.ts"];
const unitTests = [
// covers headers, already in tap -> not drift
{ path: "tests/unit/chatcore-headers.test.ts", content: `await import("../../open-sse/handlers/chatCore/headers.ts");` },
// covers headers, NOT in tap -> drift
{ path: "tests/unit/no-memory-header.test.ts", content: `const { isNoMemoryRequested } = await import("../../open-sse/handlers/chatCore/headers.ts");` },
// covers idempotency, NOT in tap -> drift
{ path: "tests/unit/idempo.test.ts", content: `import { x } from "@omniroute/open-sse/handlers/chatCore/idempotency";` },
// covers nothing mutated -> ignored
{ path: "tests/unit/unrelated.test.ts", content: `import { z } from "@/lib/foo";` },
];
const drift = findCoverageDrift({ mutate, tapTestFiles, unitTests });
assert.deepEqual(drift["open-sse/handlers/chatCore/headers.ts"], [
"tests/unit/no-memory-header.test.ts",
]);
assert.deepEqual(drift["open-sse/handlers/chatCore/idempotency.ts"], [
"tests/unit/idempo.test.ts",
]);
// comment-only mutate entries are skipped
assert.equal("_a_comment_entry" in drift, false);
});
@@ -0,0 +1,174 @@
// tests/unit/build/check-openapi-breaking.test.ts
// TDD unit tests for scripts/check/check-openapi-breaking.mjs — Fase 8 B.4 oasdiff.
//
// Strategy:
// • parseOasdiffBreaking() — pure parser, tested with synthetic oasdiff JSON
// (the REAL shape emitted by `oasdiff breaking --format json`, verified at 1.19.1).
// • binary-absent SKIP — spawn the gate with a PATH stripped of oasdiff and assert
// it prints `openapiBreaking=SKIP reason=binary-absent` and exits 0 (advisory).
import test from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
import { parseOasdiffBreaking } from "../../../scripts/check/check-openapi-breaking.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, "../../..");
const GATE = path.join(REPO_ROOT, "scripts/check/check-openapi-breaking.mjs");
// ---------------------------------------------------------------------------
// Fixtures — REAL shape of `oasdiff breaking --format json` (oasdiff 1.19.1).
// e.g. removing a path emits:
// {"id":"api-path-removed-without-deprecation","text":"api path removed...",
// "level":3,"operation":"GET","path":"/api/bar","section":"paths",...}
// ---------------------------------------------------------------------------
function makeBreaking(
overrides: {
id?: string;
text?: string;
level?: number;
operation?: string;
path?: string;
} = {}
) {
return {
id: overrides.id ?? "api-path-removed-without-deprecation",
text: overrides.text ?? "api path removed without deprecation",
level: overrides.level ?? 3,
operation: overrides.operation ?? "GET",
path: overrides.path ?? "/api/foo",
section: "paths",
baseSource: { file: "/tmp/base.yaml", line: 12, column: 5 },
fingerprint: "33e8aeb41d4a",
};
}
// ---------------------------------------------------------------------------
// parseOasdiffBreaking — empty / invalid input
// ---------------------------------------------------------------------------
test("parseOasdiffBreaking: null retorna count=0", () => {
const r = parseOasdiffBreaking(null);
assert.equal(r.count, 0);
assert.deepEqual(r.byId, {});
assert.deepEqual(r.byPath, {});
assert.deepEqual(r.items, []);
});
test("parseOasdiffBreaking: undefined retorna count=0", () => {
const r = parseOasdiffBreaking(undefined as unknown as null);
assert.equal(r.count, 0);
});
test("parseOasdiffBreaking: array vazio (sem breaking change) retorna count=0", () => {
const r = parseOasdiffBreaking([]);
assert.equal(r.count, 0);
assert.deepEqual(r.byId, {});
assert.deepEqual(r.byPath, {});
});
test("parseOasdiffBreaking: objeto (não-array) retorna count=0", () => {
const r = parseOasdiffBreaking({ id: "x" } as unknown as null);
assert.equal(r.count, 0);
});
test("parseOasdiffBreaking: string retorna count=0", () => {
const r = parseOasdiffBreaking("breaking" as unknown as null);
assert.equal(r.count, 0);
});
test("parseOasdiffBreaking: número retorna count=0", () => {
const r = parseOasdiffBreaking(42 as unknown as null);
assert.equal(r.count, 0);
});
// ---------------------------------------------------------------------------
// parseOasdiffBreaking — counting
// ---------------------------------------------------------------------------
test("parseOasdiffBreaking: 1 breaking change retorna count=1", () => {
const r = parseOasdiffBreaking([makeBreaking()]);
assert.equal(r.count, 1);
assert.equal(r.items.length, 1);
});
test("parseOasdiffBreaking: 3 breaking changes retorna count=3", () => {
const r = parseOasdiffBreaking([
makeBreaking({ id: "api-path-removed-without-deprecation", path: "/api/a" }),
makeBreaking({ id: "request-parameter-became-required", path: "/api/b" }),
makeBreaking({ id: "response-property-removed", path: "/api/c" }),
]);
assert.equal(r.count, 3);
});
test("parseOasdiffBreaking: ignora entradas null/não-objeto no array", () => {
const r = parseOasdiffBreaking([makeBreaking(), null, 5, makeBreaking({ path: "/api/x" })]);
assert.equal(r.count, 2);
});
// ---------------------------------------------------------------------------
// parseOasdiffBreaking — grouping
// ---------------------------------------------------------------------------
test("parseOasdiffBreaking: agrupa por id em byId", () => {
const r = parseOasdiffBreaking([
makeBreaking({ id: "api-path-removed-without-deprecation", path: "/api/a" }),
makeBreaking({ id: "response-property-removed", path: "/api/b" }),
makeBreaking({ id: "api-path-removed-without-deprecation", path: "/api/c" }),
]);
assert.equal(r.byId["api-path-removed-without-deprecation"], 2);
assert.equal(r.byId["response-property-removed"], 1);
});
test("parseOasdiffBreaking: agrupa por path em byPath", () => {
const r = parseOasdiffBreaking([
makeBreaking({ path: "/api/chat", operation: "POST" }),
makeBreaking({ path: "/api/chat", operation: "GET" }),
makeBreaking({ path: "/api/models" }),
]);
assert.equal(r.byPath["/api/chat"], 2);
assert.equal(r.byPath["/api/models"], 1);
});
test("parseOasdiffBreaking: id ausente usa 'unknown'", () => {
const r = parseOasdiffBreaking([{ path: "/api/x", text: "weird" }]);
assert.equal(r.count, 1);
assert.equal(r.byId["unknown"], 1);
});
test("parseOasdiffBreaking: path ausente usa 'unknown'", () => {
const r = parseOasdiffBreaking([{ id: "some-rule", text: "weird" }]);
assert.equal(r.count, 1);
assert.equal(r.byPath["unknown"], 1);
});
// ---------------------------------------------------------------------------
// Integration — binary-absent SKIP (advisory, exit 0)
//
// Run the gate with a PATH that does NOT contain oasdiff. `findOasdiff()` then
// returns null and the gate must SKIP gracefully with exit 0. We point HOME to a
// temp dir too so a user-local ~/.local/bin/oasdiff cannot leak into the lookup.
// ---------------------------------------------------------------------------
test("gate: SKIP graceful (exit 0) quando oasdiff ausente do PATH", () => {
const res = spawnSync(process.execPath, [GATE, "--quiet"], {
cwd: REPO_ROOT,
encoding: "utf8",
timeout: 30_000,
env: {
// Minimal PATH with only the dir holding the node binary — no oasdiff.
PATH: path.dirname(process.execPath),
HOME: "/nonexistent-home-for-oasdiff-test",
BASE_REF: "origin/release/v3.8.26",
},
});
assert.equal(res.status, 0, `expected exit 0, got ${res.status}; stderr: ${res.stderr}`);
assert.match(
res.stdout,
/openapiBreaking=SKIP reason=binary-absent/,
`expected binary-absent SKIP, got stdout: ${res.stdout}`
);
});
+197
View File
@@ -0,0 +1,197 @@
// tests/unit/build/check-pr-evidence.test.ts
// TDD tests for the evaluatePrBody() pure function in check-pr-evidence.mjs.
// Validates that the gate correctly detects outcome claims and evidence blocks.
import test from "node:test";
import assert from "node:assert/strict";
import { evaluatePrBody } from "../../../scripts/check/check-pr-evidence.mjs";
// ---------------------------------------------------------------------------
// (a) Body with claim + evidence block => PASS
// ---------------------------------------------------------------------------
test("evaluatePrBody: strong claim + fenced code block with test output => pass", () => {
const body = `
## Summary
Fixed the null-pointer bug.
All tests pass.
## Output
\`\`\`
42 passing (3s)
0 failing
\`\`\`
`;
const { result } = evaluatePrBody(body);
assert.equal(result, "pass");
});
test("evaluatePrBody: strong claim + inline result code span => pass", () => {
const body = "Typecheck is clean. Result: `42 passing`";
const { result } = evaluatePrBody(body);
assert.equal(result, "pass");
});
test("evaluatePrBody: strong claim + Evidence section header with content => pass", () => {
const body = `
Fixed the regression.
Validated locally.
## Evidence
node --import tsx/esm --test tests/unit/foo.test.ts
14 tests passed, 0 failed.
`;
const { result } = evaluatePrBody(body);
assert.equal(result, "pass");
});
test("evaluatePrBody: 'all green' claim + fenced block with PASS token => pass", () => {
const body = `
All tests are green.
\`\`\`
PASS src/foo.test.ts
Test Suites: 1 passed, 1 total
Tests: 5 passed, 5 total
\`\`\`
`;
const { result } = evaluatePrBody(body);
assert.equal(result, "pass");
});
test("evaluatePrBody: two weak triggers + fenced code block with output => pass", () => {
const body = `
Added a new endpoint for health checks.
Resolves #123.
\`\`\`
npm run test
14 passing (1s)
\`\`\`
`;
const { result } = evaluatePrBody(body);
assert.equal(result, "pass");
});
// ---------------------------------------------------------------------------
// (b) Body with claim + NO evidence block => FAIL
// ---------------------------------------------------------------------------
test("evaluatePrBody: 'all tests pass' with no evidence => fail", () => {
const body = "Fixed the bug. All tests pass and I added a new endpoint.";
const { result, reason } = evaluatePrBody(body);
assert.equal(result, "fail");
assert.ok(reason.includes("Rule #18"), `reason should mention Rule #18, got: ${reason}`);
});
test("evaluatePrBody: 'typecheck is clean' with no evidence => fail", () => {
const body = "Refactored the module. Typecheck pass and lint is clean.";
const { result } = evaluatePrBody(body);
assert.equal(result, "fail");
});
test("evaluatePrBody: 'tests passing' + fenced block with no output-like content => fail", () => {
const body = `
Tests are passing!
\`\`\`ts
// just a code snippet, not output
export function foo() { return 42; }
\`\`\`
`;
const { result } = evaluatePrBody(body);
assert.equal(result, "fail");
});
test("evaluatePrBody: two weak triggers + no evidence => fail", () => {
const body = "Fixed the issue. Resolves #456. Looks good to me.";
const { result } = evaluatePrBody(body);
assert.equal(result, "fail");
});
test("evaluatePrBody: 'validated on VPS' with no evidence block => fail", () => {
const body = "Validated on VPS. Everything works correctly.";
const { result } = evaluatePrBody(body);
assert.equal(result, "fail");
});
// ---------------------------------------------------------------------------
// (c) Body with no claim terms => PASS
// ---------------------------------------------------------------------------
test("evaluatePrBody: body without any claim terms => pass", () => {
const body = `
## Summary
Update the README with new provider list.
Adds documentation for the new streaming format.
`;
const { result } = evaluatePrBody(body);
assert.equal(result, "pass");
});
test("evaluatePrBody: body with only one weak trigger => pass (not enough to fire)", () => {
const body = "Resolves #789 by updating the config file.";
const { result } = evaluatePrBody(body);
assert.equal(result, "pass");
});
test("evaluatePrBody: body describing a pure docs change with no claims => pass", () => {
const body = "Updates ARCHITECTURE.md to reflect the new combo routing design.";
const { result } = evaluatePrBody(body);
assert.equal(result, "pass");
});
// ---------------------------------------------------------------------------
// (d) Empty/missing body => SKIP (or pass — no blocking)
// ---------------------------------------------------------------------------
test("evaluatePrBody: empty string => skip", () => {
const { result } = evaluatePrBody("");
assert.equal(result, "skip");
});
test("evaluatePrBody: whitespace-only string => skip", () => {
const { result } = evaluatePrBody(" \n \t ");
assert.equal(result, "skip");
});
test("evaluatePrBody: undefined body => skip (treat as empty)", () => {
// @ts-expect-error — testing defensive path
const { result } = evaluatePrBody(undefined);
assert.equal(result, "skip");
});
// ---------------------------------------------------------------------------
// Edge cases
// ---------------------------------------------------------------------------
test("evaluatePrBody: fenced block with exit-code output => pass (strong trigger present)", () => {
const body = `
Fixes the build. Zero errors after this change.
\`\`\`
$ npm run typecheck
exit code 0
\`\`\`
`;
const { result } = evaluatePrBody(body);
assert.equal(result, "pass");
});
test("evaluatePrBody: evidence section header present but with minimal content => pass (strong trigger)", () => {
// "Validated locally" is a strong trigger. Evidence section has 25+ chars.
const body = `
Validated locally on the production VPS.
## Validation
Ran the full test suite and observed 0 failures from the terminal.
`;
const { result } = evaluatePrBody(body);
assert.equal(result, "pass");
});
test("evaluatePrBody: inline PASSED span counts as evidence", () => {
const body = "All tests pass. Result: `PASSED 14 tests`";
const { result } = evaluatePrBody(body);
assert.equal(result, "pass");
});
+331
View File
@@ -0,0 +1,331 @@
// tests/unit/build/check-secrets.test.ts
// TDD unit tests for scripts/check/check-secrets.mjs — Task 7.18 gitleaks.
//
// Strategy: test the exported pure function without spawning gitleaks.
// All fixtures are synthetic gitleaks --report-format json outputs.
// - parseGitleaksJson() — parses gitleaks findings array
import test 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 {
parseGitleaksJson,
evaluateSecretsRatchet,
readBaselineSecretsValue,
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
} from "../../../scripts/check/check-secrets.mjs";
type RatchetVerdict = { regressed: boolean; improved: boolean };
const evaluate = evaluateSecretsRatchet as (current: number, baseline: number) => RatchetVerdict;
const readBaseline = readBaselineSecretsValue as (p?: string) => number | null;
// ---------------------------------------------------------------------------
// Fixtures — synthetic gitleaks --report-format json output
// ---------------------------------------------------------------------------
/** Helper to build a minimal gitleaks finding. */
function makeFinding(
overrides: {
ruleId?: string;
file?: string;
description?: string;
startLine?: number;
secret?: string;
} = {}
) {
return {
Description: overrides.description ?? "GitHub Personal Access Token",
StartLine: overrides.startLine ?? 42,
EndLine: overrides.startLine ?? 42,
StartColumn: 15,
EndColumn: 50,
Match: "REDACTED",
Secret: overrides.secret ?? "ghp_REDACTED",
File: overrides.file ?? "src/config/secrets.ts",
SymlinkFile: "",
Commit: "",
Entropy: 4.5,
Author: "Developer",
Email: "dev@example.com",
Date: "2026-01-01T00:00:00Z",
Message: "add config",
Tags: [],
RuleID: overrides.ruleId ?? "github-pat",
Fingerprint: "abc123",
};
}
// ---------------------------------------------------------------------------
// parseGitleaksJson — input inválido / vazio
// ---------------------------------------------------------------------------
test("parseGitleaksJson: null retorna findingCount=0", () => {
const result = parseGitleaksJson(null);
assert.equal(result.findingCount, 0);
assert.deepEqual(result.byRule, {});
assert.deepEqual(result.byFile, {});
});
test("parseGitleaksJson: undefined retorna findingCount=0", () => {
const result = parseGitleaksJson(undefined as unknown as null);
assert.equal(result.findingCount, 0);
});
test("parseGitleaksJson: array vazio retorna findingCount=0", () => {
const result = parseGitleaksJson([]);
assert.equal(result.findingCount, 0);
assert.deepEqual(result.byRule, {});
assert.deepEqual(result.byFile, {});
});
test("parseGitleaksJson: objeto (não-array) retorna findingCount=0", () => {
const result = parseGitleaksJson({ RuleID: "github-pat" } as unknown as null);
assert.equal(result.findingCount, 0);
});
test("parseGitleaksJson: string retorna findingCount=0", () => {
const result = parseGitleaksJson("findings" as unknown as null);
assert.equal(result.findingCount, 0);
});
test("parseGitleaksJson: número retorna findingCount=0", () => {
const result = parseGitleaksJson(42 as unknown as null);
assert.equal(result.findingCount, 0);
});
// ---------------------------------------------------------------------------
// parseGitleaksJson — contagem básica
// ---------------------------------------------------------------------------
test("parseGitleaksJson: 1 finding retorna findingCount=1", () => {
const result = parseGitleaksJson([makeFinding()]);
assert.equal(result.findingCount, 1);
});
test("parseGitleaksJson: 3 findings retorna findingCount=3", () => {
const findings = [
makeFinding({ ruleId: "github-pat", file: "src/a.ts" }),
makeFinding({ ruleId: "aws-access-key", file: "src/b.ts" }),
makeFinding({ ruleId: "generic-api-key", file: "src/c.ts" }),
];
const result = parseGitleaksJson(findings);
assert.equal(result.findingCount, 3);
});
// ---------------------------------------------------------------------------
// parseGitleaksJson — agrupamento por RuleID
// ---------------------------------------------------------------------------
test("parseGitleaksJson: agrupa por RuleID em byRule", () => {
const findings = [
makeFinding({ ruleId: "github-pat" }),
makeFinding({ ruleId: "aws-access-key" }),
makeFinding({ ruleId: "github-pat" }), // segundo github-pat
];
const result = parseGitleaksJson(findings);
assert.equal(result.byRule["github-pat"], 2);
assert.equal(result.byRule["aws-access-key"], 1);
});
test("parseGitleaksJson: RuleID ausente usa 'unknown'", () => {
const finding = {
Description: "Some secret",
StartLine: 1,
File: "src/x.ts",
// sem RuleID
};
const result = parseGitleaksJson([finding]);
assert.equal(result.findingCount, 1);
assert.equal(result.byRule["unknown"], 1);
});
test("parseGitleaksJson: suporta campo ruleId (camelCase) como fallback", () => {
const finding = {
ruleId: "lowercase-rule",
File: "src/x.ts",
Description: "test",
};
const result = parseGitleaksJson([finding]);
assert.equal(result.byRule["lowercase-rule"], 1);
});
// ---------------------------------------------------------------------------
// parseGitleaksJson — agrupamento por arquivo
// ---------------------------------------------------------------------------
test("parseGitleaksJson: agrupa por File em byFile", () => {
const findings = [
makeFinding({ file: "src/config.ts", ruleId: "github-pat" }),
makeFinding({ file: "src/config.ts", ruleId: "aws-access-key" }), // mesmo arquivo
makeFinding({ file: "tests/fixtures/token.ts", ruleId: "github-pat" }),
];
const result = parseGitleaksJson(findings);
assert.equal(result.byFile["src/config.ts"], 2);
assert.equal(result.byFile["tests/fixtures/token.ts"], 1);
});
test("parseGitleaksJson: File ausente usa 'unknown' em byFile", () => {
const finding = {
RuleID: "github-pat",
Description: "Token",
StartLine: 1,
// sem File
};
const result = parseGitleaksJson([finding]);
assert.equal(result.byFile["unknown"], 1);
});
test("parseGitleaksJson: suporta campo file (camelCase) como fallback", () => {
const finding = {
RuleID: "generic-api-key",
file: "src/config.js",
Description: "test",
};
const result = parseGitleaksJson([finding]);
assert.equal(result.byFile["src/config.js"], 1);
});
// ---------------------------------------------------------------------------
// parseGitleaksJson — entradas inválidas dentro do array
// ---------------------------------------------------------------------------
test("parseGitleaksJson: entradas null dentro do array são ignoradas", () => {
const findings = [makeFinding(), null, makeFinding({ ruleId: "aws-access-key" })] as (ReturnType<
typeof makeFinding
> | null)[];
const result = parseGitleaksJson(findings as unknown as ReturnType<typeof makeFinding>[]);
assert.equal(result.findingCount, 2, "null entries should be skipped");
});
test("parseGitleaksJson: entradas primitivas dentro do array são ignoradas", () => {
const findings = [
makeFinding(),
"string-entry",
42,
makeFinding({ ruleId: "aws-access-key" }),
] as unknown[];
const result = parseGitleaksJson(findings as ReturnType<typeof makeFinding>[]);
assert.equal(result.findingCount, 2, "primitive entries should be skipped");
});
// ---------------------------------------------------------------------------
// parseGitleaksJson — casos de borda do RuleID PascalCase
// ---------------------------------------------------------------------------
test("parseGitleaksJson: RuleID PascalCase como emitido pelo gitleaks", () => {
// gitleaks emite RuleID com PascalCase nos campos
const finding = {
RuleID: "github-fine-grained-pat",
File: "config/auth.yaml",
Description: "Fine-grained PAT",
StartLine: 3,
};
const result = parseGitleaksJson([finding]);
assert.equal(result.byRule["github-fine-grained-pat"], 1);
});
// ---------------------------------------------------------------------------
// parseGitleaksJson — invariantes estruturais
// ---------------------------------------------------------------------------
test("parseGitleaksJson: findingCount == soma de todos os byRule values", () => {
const findings = [
makeFinding({ ruleId: "a" }),
makeFinding({ ruleId: "b" }),
makeFinding({ ruleId: "a" }),
makeFinding({ ruleId: "c" }),
];
const result = parseGitleaksJson(findings);
const sumByRule = Object.values(result.byRule).reduce((s, n) => s + n, 0);
assert.equal(result.findingCount, sumByRule, "findingCount must equal sum of byRule counts");
});
test("parseGitleaksJson: findingCount == soma de todos os byFile values", () => {
const findings = [
makeFinding({ file: "src/a.ts" }),
makeFinding({ file: "src/b.ts" }),
makeFinding({ file: "src/a.ts" }),
];
const result = parseGitleaksJson(findings);
const sumByFile = Object.values(result.byFile).reduce((s, n) => s + n, 0);
assert.equal(result.findingCount, sumByFile, "findingCount must equal sum of byFile counts");
});
// ---------------------------------------------------------------------------
// evaluateSecretsRatchet — ratchet direction:down (Etapa 2: flip to blocking)
// Regression when measured > baseline; baseline=3 → 4+ findings block, 3 passes.
// ---------------------------------------------------------------------------
test("evaluateSecretsRatchet: medida == baseline passa (3 vs 3)", () => {
const r = evaluate(3, 3);
assert.equal(r.regressed, false);
assert.equal(r.improved, false);
});
test("evaluateSecretsRatchet: uma a mais que o baseline é regressão (4 vs 3)", () => {
const r = evaluate(4, 3);
assert.equal(r.regressed, true, "a single new secret finding must block");
assert.equal(r.improved, false);
});
test("evaluateSecretsRatchet: menos que o baseline é melhoria", () => {
const r = evaluate(1, 3);
assert.equal(r.regressed, false);
assert.equal(r.improved, true);
});
test("evaluateSecretsRatchet: zero contra baseline não-zero é melhoria máxima", () => {
const r = evaluate(0, 5);
assert.equal(r.regressed, false);
assert.equal(r.improved, true);
});
test("evaluateSecretsRatchet: comparação inteira estrita — qualquer aumento regride", () => {
assert.equal(evaluate(6, 5).regressed, true);
assert.equal(evaluate(5, 5).regressed, false);
assert.equal(evaluate(4, 5).regressed, false);
});
// ---------------------------------------------------------------------------
// readBaselineSecretsValue — leitura tolerante do quality-baseline.json
// ---------------------------------------------------------------------------
function withTmpBaseline(content: string | null, fn: (p: string) => void) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "secrets-baseline-"));
const p = path.join(dir, "quality-baseline.json");
if (content !== null) fs.writeFileSync(p, content);
try {
fn(p);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}
test("readBaselineSecretsValue: lê metrics.secretFindings.value", () => {
withTmpBaseline(JSON.stringify({ metrics: { secretFindings: { value: 3 } } }), (p) => {
assert.equal(readBaseline(p), 3);
});
});
test("readBaselineSecretsValue: arquivo ausente retorna null (SKIP gracioso)", () => {
assert.equal(readBaseline("/tmp/does-not-exist-99999/quality-baseline.json"), null);
});
test("readBaselineSecretsValue: métrica ausente retorna null", () => {
withTmpBaseline(JSON.stringify({ metrics: {} }), (p) => {
assert.equal(readBaseline(p), null);
});
});
test("readBaselineSecretsValue: value não-numérico retorna null", () => {
withTmpBaseline(JSON.stringify({ metrics: { secretFindings: { value: "3" } } }), (p) => {
assert.equal(readBaseline(p), null);
});
});
test("readBaselineSecretsValue: JSON inválido retorna null (não lança)", () => {
withTmpBaseline("{ not valid json", (p) => {
assert.equal(readBaseline(p), null);
});
});
@@ -0,0 +1,35 @@
import test 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 { findRunnerMismatches } from "../../../scripts/check/check-test-runner-api.mjs";
function tmpRepo() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "runner-api-"));
fs.mkdirSync(path.join(root, "tests/unit/autoCombo"), { recursive: true });
return root;
}
test("flags a vitest-only-dir test that imports node:test", () => {
const root = tmpRepo();
fs.writeFileSync(
path.join(root, "tests/unit/autoCombo/bad.test.ts"),
`import { describe, it } from "node:test";\ndescribe("x", () => it("y", () => {}));\n`
);
const bad = findRunnerMismatches(root);
assert.equal(bad.length, 1);
assert.match(bad[0].file, /autoCombo\/bad\.test\.ts$/);
assert.match(bad[0].reason, /vitest-only/);
fs.rmSync(root, { recursive: true, force: true });
});
test("accepts a vitest-only-dir test that imports vitest", () => {
const root = tmpRepo();
fs.writeFileSync(
path.join(root, "tests/unit/autoCombo/good.test.ts"),
`import { describe, it } from "vitest";\ndescribe("x", () => it("y", () => {}));\n`
);
assert.equal(findRunnerMismatches(root).length, 0);
fs.rmSync(root, { recursive: true, force: true });
});
@@ -0,0 +1,62 @@
// tests/unit/build/check-tracked-artifacts.test.ts
// TDD test for check-tracked-artifacts.mjs gate.
import test from "node:test";
import assert from "node:assert/strict";
import { checkTrackedArtifacts } from "../../../scripts/check/check-tracked-artifacts.mjs";
test("checkTrackedArtifacts: empty list passes", () => {
const result = checkTrackedArtifacts([]);
assert.deepEqual(result, []);
});
test("checkTrackedArtifacts: node_modules/ prefix is flagged", () => {
const result = checkTrackedArtifacts(["node_modules/some-pkg/index.js"]);
assert.equal(result.length, 1);
assert.ok(result[0].includes("node_modules/some-pkg/index.js"));
});
test("checkTrackedArtifacts: .next/ prefix is flagged", () => {
const result = checkTrackedArtifacts([".next/static/chunks/main.js"]);
assert.equal(result.length, 1);
});
test("checkTrackedArtifacts: coverage/ prefix is flagged", () => {
const result = checkTrackedArtifacts(["coverage/lcov.info"]);
assert.equal(result.length, 1);
});
test("checkTrackedArtifacts: quality-metrics.json is flagged", () => {
const result = checkTrackedArtifacts(["quality-metrics.json"]);
assert.equal(result.length, 1);
});
test("checkTrackedArtifacts: config/quality/quality-metrics.json is flagged", () => {
const result = checkTrackedArtifacts(["config/quality/quality-metrics.json"]);
assert.equal(result.length, 1);
});
test("checkTrackedArtifacts: symlink mode (120000) is flagged", () => {
const result = checkTrackedArtifacts([], ["node_modules"]);
assert.equal(result.length, 1);
assert.ok(result[0].includes("node_modules"));
});
test("checkTrackedArtifacts: normal source files pass", () => {
const result = checkTrackedArtifacts([
"src/app/page.tsx",
"open-sse/handlers/chat.ts",
"package.json",
"tests/unit/some.test.ts",
]);
assert.deepEqual(result, []);
});
test("checkTrackedArtifacts: multiple violations reported", () => {
const result = checkTrackedArtifacts([
"src/ok.ts",
"node_modules/.bin/jscpd",
".next/server/app/page.js",
"coverage/lcov.info",
]);
assert.equal(result.length, 3);
});
@@ -0,0 +1,110 @@
// tests/unit/build/check-type-coverage.test.ts
// Unit tests for the parseTypeCoverageOutput() helper in check-type-coverage.mjs.
// Tests exercise the pure parsing logic against synthetic JSON strings — no child
// process is spawned, so the suite is fast and hermetic.
import test from "node:test";
import assert from "node:assert/strict";
import { parseTypeCoverageOutput } from "../../../scripts/check/check-type-coverage.mjs";
test("parseTypeCoverageOutput: parses standard type-coverage JSON output", () => {
const raw = JSON.stringify({
succeeded: true,
atLeastFailed: false,
correctCount: 246617,
percent: 91.66,
percentString: "91.66",
totalCount: 269047,
});
const pct = parseTypeCoverageOutput(raw);
assert.strictEqual(pct, 91.66);
});
test("parseTypeCoverageOutput: returns integer percent when exactly 100", () => {
const raw = JSON.stringify({
succeeded: true,
atLeastFailed: false,
correctCount: 1000,
percent: 100,
percentString: "100",
totalCount: 1000,
});
const pct = parseTypeCoverageOutput(raw);
assert.strictEqual(pct, 100);
});
test("parseTypeCoverageOutput: returns 0 when everything is untyped", () => {
const raw = JSON.stringify({
succeeded: true,
atLeastFailed: false,
correctCount: 0,
percent: 0,
percentString: "0",
totalCount: 500,
});
const pct = parseTypeCoverageOutput(raw);
assert.strictEqual(pct, 0);
});
test("parseTypeCoverageOutput: handles high-precision decimals", () => {
const raw = JSON.stringify({
succeeded: true,
atLeastFailed: false,
correctCount: 173155,
percent: 96.98,
percentString: "96.98",
totalCount: 178539,
});
const pct = parseTypeCoverageOutput(raw);
assert.strictEqual(pct, 96.98);
});
test("parseTypeCoverageOutput: throws on invalid JSON", () => {
assert.throws(
() => parseTypeCoverageOutput("not-valid-json"),
/Failed to parse JSON output/,
);
});
test("parseTypeCoverageOutput: throws when percent field is missing", () => {
const raw = JSON.stringify({ succeeded: true, correctCount: 100 });
assert.throws(
() => parseTypeCoverageOutput(raw),
/missing numeric 'percent' field/,
);
});
test("parseTypeCoverageOutput: throws when percent field is a string instead of number", () => {
const raw = JSON.stringify({
succeeded: true,
percent: "91.66",
percentString: "91.66",
totalCount: 100,
correctCount: 91,
});
assert.throws(
() => parseTypeCoverageOutput(raw),
/missing numeric 'percent' field/,
);
});
test("parseTypeCoverageOutput: throws on empty string", () => {
assert.throws(
() => parseTypeCoverageOutput(""),
/Failed to parse JSON output/,
);
});
test("parseTypeCoverageOutput: ignores extra unknown fields", () => {
const raw = JSON.stringify({
succeeded: false,
atLeastFailed: true,
correctCount: 50,
percent: 50.0,
percentString: "50.00",
totalCount: 100,
extra: "ignored",
});
const pct = parseTypeCoverageOutput(raw);
assert.strictEqual(pct, 50);
});
+407
View File
@@ -0,0 +1,407 @@
// tests/unit/build/check-vuln-ratchet.test.ts
// TDD unit tests for scripts/check/check-vuln-ratchet.mjs — Task 7.2 osv-scanner.
//
// Strategy: test the two exported pure functions without spawning osv-scanner
// or touching the filesystem. All fixtures are synthetic.
// - parseOsvJson() — parses osv-scanner --format json output
// - extractSeverity() — extracts severity from a vulnerability entry
import test 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 { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";
import {
parseOsvJson,
extractSeverity,
evaluateVulnRatchet,
readBaselineVulnValue,
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
} from "../../../scripts/check/check-vuln-ratchet.mjs";
type RatchetVerdict = { regressed: boolean; improved: boolean };
const evaluate = evaluateVulnRatchet as (current: number, baseline: number) => RatchetVerdict;
const readBaseline = readBaselineVulnValue as (p?: string) => number | null;
const SCRIPT_PATH = fileURLToPath(
new URL("../../../scripts/check/check-vuln-ratchet.mjs", import.meta.url)
);
// ---------------------------------------------------------------------------
// Fixtures — JSON sintético com formato do osv-scanner --format json
// ---------------------------------------------------------------------------
/** Resultado vazio (nenhuma vulnerabilidade encontrada). */
function makeEmptyResult() {
return { results: [] };
}
/** Um pacote com 1 vulnerabilidade, sem groups. */
function makeResultOnePkg(vulnCount: number) {
const vulnerabilities = Array.from({ length: vulnCount }, (_, i) => ({
id: `GHSA-fake-${i + 1}`,
aliases: [],
affected: [],
}));
return {
results: [
{
packages: [
{
package: { name: "lodash", version: "4.17.0", ecosystem: "npm" },
vulnerabilities,
},
],
},
],
};
}
/** Dois pacotes em dois resultados, sem groups. */
function makeResultTwoPkgs() {
return {
results: [
{
packages: [
{
package: { name: "pkg-a", version: "1.0.0", ecosystem: "npm" },
vulnerabilities: [
{ id: "GHSA-aaa-1", aliases: [], affected: [] },
{ id: "GHSA-aaa-2", aliases: [], affected: [] },
],
},
],
},
{
packages: [
{
package: { name: "pkg-b", version: "2.0.0", ecosystem: "npm" },
vulnerabilities: [
{ id: "GHSA-bbb-1", aliases: [], affected: [] },
],
},
],
},
],
};
}
/** Pacote com groups para deduplicação. */
function makeResultWithGroups() {
return {
results: [
{
packages: [
{
package: { name: "pkg-c", version: "3.0.0", ecosystem: "npm" },
// 3 vulnerabilidades mas apenas 2 grupos (1 foi deduplicado)
vulnerabilities: [
{ id: "GHSA-ccc-1", aliases: [], affected: [] },
{ id: "GHSA-ccc-2", aliases: [], affected: [] },
{ id: "GHSA-ccc-3", aliases: [], affected: [] },
],
groups: [
{ ids: ["GHSA-ccc-1"] },
{ ids: ["GHSA-ccc-2", "GHSA-ccc-3"] }, // grupo deduplicado
],
},
],
},
],
};
}
/** Pacote com severidade em database_specific.severity. */
function makeResultWithDbSeverity() {
return {
results: [
{
packages: [
{
package: { name: "pkg-sev", version: "1.0.0", ecosystem: "npm" },
vulnerabilities: [
{
id: "GHSA-sev-1",
database_specific: { severity: "HIGH" },
aliases: [],
affected: [],
},
{
id: "GHSA-sev-2",
database_specific: { severity: "CRITICAL" },
aliases: [],
affected: [],
},
{
id: "GHSA-sev-3",
database_specific: { severity: "low" }, // lowercase
aliases: [],
affected: [],
},
],
},
],
},
],
};
}
// ---------------------------------------------------------------------------
// parseOsvJson — input inválido
// ---------------------------------------------------------------------------
test("parseOsvJson: null retorna vulnCount=0", () => {
const result = parseOsvJson(null);
assert.equal(result.vulnCount, 0);
assert.deepEqual(result.bySeverity, {});
});
test("parseOsvJson: undefined retorna vulnCount=0", () => {
const result = parseOsvJson(undefined as unknown as null);
assert.equal(result.vulnCount, 0);
});
test("parseOsvJson: objeto sem results retorna vulnCount=0", () => {
const result = parseOsvJson({ other: [] } as unknown as { results: never[] });
assert.equal(result.vulnCount, 0);
});
test("parseOsvJson: results não-array retorna vulnCount=0", () => {
const result = parseOsvJson({ results: "invalid" } as unknown as { results: never[] });
assert.equal(result.vulnCount, 0);
});
// ---------------------------------------------------------------------------
// parseOsvJson — resultado vazio
// ---------------------------------------------------------------------------
test("parseOsvJson: results vazio retorna vulnCount=0", () => {
const result = parseOsvJson(makeEmptyResult());
assert.equal(result.vulnCount, 0);
assert.deepEqual(result.bySeverity, {});
});
test("parseOsvJson: result sem packages retorna vulnCount=0", () => {
const result = parseOsvJson({ results: [{ other: "data" }] } as unknown as { results: { packages: never[] }[] });
assert.equal(result.vulnCount, 0);
});
// ---------------------------------------------------------------------------
// parseOsvJson — contagem básica
// ---------------------------------------------------------------------------
test("parseOsvJson: 1 pacote com 1 vuln retorna vulnCount=1", () => {
const result = parseOsvJson(makeResultOnePkg(1));
assert.equal(result.vulnCount, 1);
});
test("parseOsvJson: 1 pacote com 3 vulns retorna vulnCount=3", () => {
const result = parseOsvJson(makeResultOnePkg(3));
assert.equal(result.vulnCount, 3);
});
test("parseOsvJson: 2 pacotes em 2 results retorna vulnCount=3 (2+1)", () => {
const result = parseOsvJson(makeResultTwoPkgs());
assert.equal(result.vulnCount, 3);
});
// ---------------------------------------------------------------------------
// parseOsvJson — deduplicação via groups
// ---------------------------------------------------------------------------
test("parseOsvJson: usa groups.length quando groups tem menos entradas que vulnerabilities", () => {
// 3 vulns, 2 grupos => vulnCount=2 (deduplicado)
const result = parseOsvJson(makeResultWithGroups());
assert.equal(result.vulnCount, 2);
});
test("parseOsvJson: usa vulnerabilities.length quando groups está ausente", () => {
// Sem groups => conta vulnerabilities diretamente
const result = parseOsvJson(makeResultOnePkg(5));
assert.equal(result.vulnCount, 5);
});
test("parseOsvJson: groups vazio não causa deduplicação (usa vulnerabilities)", () => {
const json = {
results: [
{
packages: [
{
package: { name: "pkg-d", version: "1.0.0", ecosystem: "npm" },
vulnerabilities: [
{ id: "GHSA-d-1", aliases: [], affected: [] },
{ id: "GHSA-d-2", aliases: [], affected: [] },
],
groups: [], // vazio — não deve deduplicar
},
],
},
],
};
const result = parseOsvJson(json);
assert.equal(result.vulnCount, 2);
});
// ---------------------------------------------------------------------------
// parseOsvJson — severidade
// ---------------------------------------------------------------------------
test("parseOsvJson: coleta severidade de database_specific.severity", () => {
const result = parseOsvJson(makeResultWithDbSeverity());
assert.equal(result.vulnCount, 3);
assert.ok("HIGH" in result.bySeverity, "deve ter HIGH");
assert.ok("CRITICAL" in result.bySeverity, "deve ter CRITICAL");
assert.ok("LOW" in result.bySeverity, "deve ter LOW (normalizado para uppercase)");
});
test("parseOsvJson: vuln sem severidade conta como UNKNOWN", () => {
const json = makeResultOnePkg(1);
// Vuln sem database_specific.severity e sem severity array
const result = parseOsvJson(json);
assert.ok("UNKNOWN" in result.bySeverity, "vuln sem severidade deve ser UNKNOWN");
});
// ---------------------------------------------------------------------------
// extractSeverity
// ---------------------------------------------------------------------------
test("extractSeverity: null retorna UNKNOWN", () => {
assert.equal(extractSeverity(null), "UNKNOWN");
});
test("extractSeverity: objeto vazio retorna UNKNOWN", () => {
assert.equal(extractSeverity({}), "UNKNOWN");
});
test("extractSeverity: usa database_specific.severity quando presente", () => {
const vuln = { database_specific: { severity: "HIGH" } };
assert.equal(extractSeverity(vuln), "HIGH");
});
test("extractSeverity: normaliza database_specific.severity para uppercase", () => {
const vuln = { database_specific: { severity: "critical" } };
assert.equal(extractSeverity(vuln), "CRITICAL");
});
test("extractSeverity: usa severity[0].type quando database_specific ausente", () => {
const vuln = { severity: [{ type: "CVSS_V3", score: "CVSS:3.1/AV:N/AC:L" }] };
assert.equal(extractSeverity(vuln), "CVSS_V3");
});
test("extractSeverity: database_specific.severity tem precedência sobre severity[]", () => {
const vuln = {
database_specific: { severity: "HIGH" },
severity: [{ type: "CVSS_V2", score: "AV:N/AC:L/Au:N/C:P/I:P/A:P" }],
};
assert.equal(extractSeverity(vuln), "HIGH");
});
test("extractSeverity: severity array vazio retorna UNKNOWN", () => {
const vuln = { severity: [] };
assert.equal(extractSeverity(vuln), "UNKNOWN");
});
test("extractSeverity: severity[0] sem type retorna UNKNOWN", () => {
const vuln = { severity: [{ score: "AV:N/AC:L" }] };
assert.equal(extractSeverity(vuln), "UNKNOWN");
});
test("extractSeverity: database_specific.severity vazia retorna UNKNOWN", () => {
const vuln = { database_specific: { severity: "" } };
assert.equal(extractSeverity(vuln), "UNKNOWN");
});
// ---------------------------------------------------------------------------
// evaluateVulnRatchet — ratchet direction:down (cycle-end: flip to blocking)
// Regression when measured > baseline; baseline=10 → 11+ blocks, 10 passes.
// ---------------------------------------------------------------------------
test("evaluateVulnRatchet: medida == baseline passa (10 vs 10)", () => {
const r = evaluate(10, 10);
assert.equal(r.regressed, false);
assert.equal(r.improved, false);
});
test("evaluateVulnRatchet: uma a mais que o baseline é regressão (11 vs 10)", () => {
const r = evaluate(11, 10);
assert.equal(r.regressed, true, "a single new vulnerability must block");
assert.equal(r.improved, false);
});
test("evaluateVulnRatchet: menos que o baseline é melhoria", () => {
const r = evaluate(5, 10);
assert.equal(r.regressed, false);
assert.equal(r.improved, true);
});
test("evaluateVulnRatchet: zero contra baseline não-zero é melhoria máxima", () => {
const r = evaluate(0, 13);
assert.equal(r.regressed, false);
assert.equal(r.improved, true);
});
test("evaluateVulnRatchet: comparação inteira estrita — qualquer aumento regride", () => {
assert.equal(evaluate(11, 10).regressed, true);
assert.equal(evaluate(10, 10).regressed, false);
assert.equal(evaluate(9, 10).regressed, false);
});
// ---------------------------------------------------------------------------
// readBaselineVulnValue — leitura tolerante do quality-baseline.json
// ---------------------------------------------------------------------------
function withTmpBaseline(content: string | null, fn: (p: string) => void) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "vuln-baseline-"));
const p = path.join(dir, "quality-baseline.json");
if (content !== null) fs.writeFileSync(p, content);
try {
fn(p);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}
test("readBaselineVulnValue: lê metrics.vulnCount.value", () => {
withTmpBaseline(JSON.stringify({ metrics: { vulnCount: { value: 10 } } }), (p) => {
assert.equal(readBaseline(p), 10);
});
});
test("readBaselineVulnValue: arquivo ausente retorna null (SKIP gracioso)", () => {
assert.equal(readBaseline("/tmp/does-not-exist-99999/quality-baseline.json"), null);
});
test("readBaselineVulnValue: métrica ausente retorna null", () => {
withTmpBaseline(JSON.stringify({ metrics: {} }), (p) => {
assert.equal(readBaseline(p), null);
});
});
test("readBaselineVulnValue: value não-numérico retorna null", () => {
withTmpBaseline(JSON.stringify({ metrics: { vulnCount: { value: "10" } } }), (p) => {
assert.equal(readBaseline(p), null);
});
});
test("readBaselineVulnValue: JSON inválido retorna null (não lança)", () => {
withTmpBaseline("{ not valid json", (p) => {
assert.equal(readBaseline(p), null);
});
});
// ---------------------------------------------------------------------------
// --ratchet end-to-end: binary-absent SKIP exits 0 (a missing measurement never
// blocks). Runs the script with an empty PATH so osv-scanner is unresolvable.
// ---------------------------------------------------------------------------
test("--ratchet com osv-scanner ausente (PATH vazio) faz SKIP e sai 0", () => {
const res = spawnSync(process.execPath, [SCRIPT_PATH, "--ratchet", "--quiet"], {
encoding: "utf8",
// Empty PATH → `which`/`osv-scanner` unresolvable → findOsvScanner() returns null.
env: { ...process.env, PATH: "/nonexistent-bin-dir" },
timeout: 30_000,
});
assert.equal(res.status, 0, "binary-absent SKIP must exit 0 even with --ratchet");
assert.match(res.stdout, /vulnCount=SKIP reason=binary-absent/);
});
+310
View File
@@ -0,0 +1,310 @@
// tests/unit/build/check-workflows.test.ts
// TDD unit tests for scripts/check/check-workflows.mjs — Task 7.19.
//
// Strategy: test the exported pure functions without spawning actionlint,
// zizmor, or touching the real .github/workflows directory.
// - parseActionlintOutput() — line-based finding counting
// - parseZizmorOutput() — JSON / text parsing + counting
// - collectWorkflowFiles() — directory listing helper
// - isBinaryAvailable() — PATH probe (tested structurally, not by
// spawning real processes)
//
// All tests are fast and hermetic (no network, no child processes except where
// explicitly exercising the PATH probe against a non-existent binary name).
import test 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 {
parseActionlintOutput,
parseZizmorOutput,
collectWorkflowFiles,
isBinaryAvailable,
evaluateZizmorRatchet,
readBaselineZizmorValue,
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
} from "../../../scripts/check/check-workflows.mjs";
type RatchetVerdict = { regressed: boolean; improved: boolean };
const evaluateZizmor = evaluateZizmorRatchet as (
current: number,
baseline: number
) => RatchetVerdict;
const readZizmorBaseline = readBaselineZizmorValue as (p?: string) => number | null;
// ─────────────────────────────────────────────────────────────────────────────
// parseActionlintOutput
// ─────────────────────────────────────────────────────────────────────────────
test("parseActionlintOutput: empty stdout returns count=0 and empty lines", () => {
const result = parseActionlintOutput("");
assert.equal(result.count, 0);
assert.deepEqual(result.lines, []);
});
test("parseActionlintOutput: whitespace-only stdout returns count=0", () => {
const result = parseActionlintOutput(" \n \t \n");
assert.equal(result.count, 0);
assert.deepEqual(result.lines, []);
});
test("parseActionlintOutput: one finding line returns count=1", () => {
const stdout =
".github/workflows/ci.yml:12:7: shellcheck reported issue in this script: SC2086:info:1:12: Double quote to prevent globbing and word splitting. [shellcheck]\n";
const result = parseActionlintOutput(stdout);
assert.equal(result.count, 1);
assert.equal(result.lines.length, 1);
assert.ok(result.lines[0].includes("shellcheck"));
});
test("parseActionlintOutput: multiple finding lines returns correct count", () => {
const stdout = [
'.github/workflows/ci.yml:5:1: "on" is the key of workflow trigger. Use quoted "on" [syntax-check]',
".github/workflows/ci.yml:42:9: event name 'pull_request' is not available for 'workflow_dispatch' [events]",
".github/workflows/deploy.yml:8:5: unknown key 'runs-ons' in step config [syntax-check]",
].join("\n");
const result = parseActionlintOutput(stdout);
assert.equal(result.count, 3);
assert.equal(result.lines.length, 3);
});
test("parseActionlintOutput: trailing newline does not add phantom finding", () => {
const stdout = ".github/workflows/ci.yml:10:3: some issue [rule]\n\n\n";
const result = parseActionlintOutput(stdout);
assert.equal(result.count, 1);
});
test("parseActionlintOutput: preserves finding text exactly (trimmed)", () => {
const finding = ".github/workflows/ci.yml:99:1: missing required key 'runs-on' [runner]";
const result = parseActionlintOutput(` ${finding} \n`);
assert.equal(result.lines[0], finding);
});
// ─────────────────────────────────────────────────────────────────────────────
// parseZizmorOutput
// ─────────────────────────────────────────────────────────────────────────────
test("parseZizmorOutput: empty string returns count=0", () => {
const result = parseZizmorOutput("");
assert.equal(result.count, 0);
assert.deepEqual(result.diagnostics, []);
});
test("parseZizmorOutput: JSON with empty diagnostics array returns count=0", () => {
const result = parseZizmorOutput(JSON.stringify({ diagnostics: [] }));
assert.equal(result.count, 0);
assert.deepEqual(result.diagnostics, []);
});
test("parseZizmorOutput: JSON { diagnostics: [...] } counts correctly", () => {
const diagnostics = [
{
id: "unpinned-uses",
severity: "medium",
message: "uses: actions/checkout@v4 is not pinned to a SHA",
},
{ id: "script-injection", severity: "high", message: "Untrusted input in run step" },
];
const result = parseZizmorOutput(JSON.stringify({ diagnostics }));
assert.equal(result.count, 2);
assert.equal(result.diagnostics.length, 2);
});
test("parseZizmorOutput: bare JSON array (older zizmor format) counts correctly", () => {
const findings = [
{ id: "unpinned-uses", workflow: "ci.yml" },
{ id: "excessive-permissions", workflow: "deploy.yml" },
{ id: "pull-request-target", workflow: "docker.yml" },
];
const result = parseZizmorOutput(JSON.stringify(findings));
assert.equal(result.count, 3);
assert.equal(result.diagnostics.length, 3);
});
test("parseZizmorOutput: invalid JSON falls back to line counting", () => {
// Non-JSON output (e.g. text format or error message) — each non-empty line = 1
const textOutput = "warning: unpinned action\nerror: script injection risk\n";
const result = parseZizmorOutput(textOutput);
assert.equal(result.count, 2);
// diagnostics is empty array in fallback mode
assert.deepEqual(result.diagnostics, []);
});
test("parseZizmorOutput: JSON with unknown shape returns count=0 (graceful)", () => {
// Unexpected but valid JSON — neither array nor { diagnostics }
const result = parseZizmorOutput(JSON.stringify({ errors: [], warnings: [] }));
assert.equal(result.count, 0);
});
test("parseZizmorOutput: whitespace-only returns count=0", () => {
const result = parseZizmorOutput(" \n\t\n ");
assert.equal(result.count, 0);
});
test("parseZizmorOutput: large diagnostics array counted correctly", () => {
const diagnostics = Array.from({ length: 47 }, (_, i) => ({
id: "unpinned-uses",
step: `step-${i}`,
}));
const result = parseZizmorOutput(JSON.stringify({ diagnostics }));
assert.equal(result.count, 47);
});
// ─────────────────────────────────────────────────────────────────────────────
// collectWorkflowFiles
// ─────────────────────────────────────────────────────────────────────────────
test("collectWorkflowFiles: returns empty array for non-existent directory", () => {
const result = collectWorkflowFiles("/this/path/does/not/exist/at/all-99999");
assert.deepEqual(result, []);
});
test("collectWorkflowFiles: returns .yml files from directory", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "check-workflows-test-"));
try {
fs.writeFileSync(path.join(dir, "ci.yml"), "name: CI\n");
fs.writeFileSync(path.join(dir, "deploy.yml"), "name: Deploy\n");
fs.writeFileSync(path.join(dir, "README.md"), "# docs\n"); // not a workflow
const files = collectWorkflowFiles(dir);
assert.equal(files.length, 2);
assert.ok(files.some((f) => f.endsWith("ci.yml")));
assert.ok(files.some((f) => f.endsWith("deploy.yml")));
assert.ok(!files.some((f) => f.endsWith("README.md")));
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test("collectWorkflowFiles: also collects .yaml extension", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "check-workflows-test-"));
try {
fs.writeFileSync(path.join(dir, "ci.yaml"), "name: CI\n");
fs.writeFileSync(path.join(dir, "deploy.yml"), "name: Deploy\n");
const files = collectWorkflowFiles(dir);
assert.equal(files.length, 2);
assert.ok(files.some((f) => f.endsWith(".yaml")));
assert.ok(files.some((f) => f.endsWith(".yml")));
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test("collectWorkflowFiles: returns absolute paths", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "check-workflows-test-"));
try {
fs.writeFileSync(path.join(dir, "ci.yml"), "name: CI\n");
const files = collectWorkflowFiles(dir);
assert.equal(files.length, 1);
assert.ok(path.isAbsolute(files[0]));
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test("collectWorkflowFiles: empty directory returns empty array", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "check-workflows-test-"));
try {
const files = collectWorkflowFiles(dir);
assert.deepEqual(files, []);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
// ─────────────────────────────────────────────────────────────────────────────
// isBinaryAvailable
// ─────────────────────────────────────────────────────────────────────────────
test("isBinaryAvailable: returns false for a nonsense binary name", () => {
// A binary named like this cannot exist in any real PATH.
const result = isBinaryAvailable("__this_binary_definitely_does_not_exist_zzz99999__");
assert.equal(result, false);
});
test("isBinaryAvailable: returns boolean (not null/undefined)", () => {
const result = isBinaryAvailable("node");
// node IS in PATH in this environment — but we only assert the type here
// to avoid environment coupling.
assert.equal(typeof result, "boolean");
});
test("isBinaryAvailable: node is available (sanity check for test environment)", () => {
// node must be in PATH for this test suite to even run.
assert.equal(isBinaryAvailable("node"), true);
});
// ─────────────────────────────────────────────────────────────────────────────
// evaluateZizmorRatchet — ratchet direction:down, zizmorFindings ONLY (Etapa 2)
// Regression when measured > baseline. actionlint is reported, not ratcheted.
// ─────────────────────────────────────────────────────────────────────────────
test("evaluateZizmorRatchet: measured == baseline passes (192 vs 192)", () => {
const r = evaluateZizmor(192, 192);
assert.equal(r.regressed, false);
assert.equal(r.improved, false);
});
test("evaluateZizmorRatchet: one more than baseline is a regression (193 vs 192)", () => {
const r = evaluateZizmor(193, 192);
assert.equal(r.regressed, true, "a single new zizmor finding must block");
assert.equal(r.improved, false);
});
test("evaluateZizmorRatchet: fewer than baseline is an improvement (190 vs 192)", () => {
const r = evaluateZizmor(190, 192);
assert.equal(r.regressed, false);
assert.equal(r.improved, true);
});
test("evaluateZizmorRatchet: strict integer comparison — any increase regresses", () => {
assert.equal(evaluateZizmor(193, 192).regressed, true);
assert.equal(evaluateZizmor(192, 192).regressed, false);
assert.equal(evaluateZizmor(191, 192).regressed, false);
});
// ─────────────────────────────────────────────────────────────────────────────
// readBaselineZizmorValue — tolerant read of quality-baseline.json
// ─────────────────────────────────────────────────────────────────────────────
function withTmpBaseline(content: string | null, fn: (p: string) => void) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "workflows-baseline-"));
const p = path.join(dir, "quality-baseline.json");
if (content !== null) fs.writeFileSync(p, content);
try {
fn(p);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}
test("readBaselineZizmorValue: reads metrics.zizmorFindings.value", () => {
withTmpBaseline(JSON.stringify({ metrics: { zizmorFindings: { value: 192 } } }), (p) => {
assert.equal(readZizmorBaseline(p), 192);
});
});
test("readBaselineZizmorValue: missing file returns null (graceful SKIP)", () => {
assert.equal(readZizmorBaseline("/tmp/does-not-exist-88888/quality-baseline.json"), null);
});
test("readBaselineZizmorValue: missing metric returns null", () => {
withTmpBaseline(JSON.stringify({ metrics: {} }), (p) => {
assert.equal(readZizmorBaseline(p), null);
});
});
test("readBaselineZizmorValue: non-numeric value returns null", () => {
withTmpBaseline(JSON.stringify({ metrics: { zizmorFindings: { value: "192" } } }), (p) => {
assert.equal(readZizmorBaseline(p), null);
});
});
test("readBaselineZizmorValue: invalid JSON returns null (does not throw)", () => {
withTmpBaseline("{ broken", (p) => {
assert.equal(readZizmorBaseline(p), null);
});
});
@@ -0,0 +1,147 @@
import { test } from "node:test";
import assert from "node:assert";
import {
classifyTestFiles,
aggregateRadiography,
classifyFromCounts,
redundancyCandidates,
} from "../../../scripts/quality/mutation-radiography.mjs";
// ── classifyTestFiles: the plan's canonical fixture ──────────────────────────
// 2 testFiles that kill mutants + 1 that kills nothing.
// m1 killed ONLY by A -> A gets a unique kill
// m2 killed by A AND B -> both get a shared kill
// m3 survived -> nobody
// Universe = [A, B, C]; C never appears in any killedBy -> empty.
test("classifies unique / redundant / empty test files from killedBy (file-name killedBy)", () => {
const report = {
files: {
"open-sse/utils/error.ts": {
mutants: [
{ id: "m1", status: "Killed", killedBy: ["A.test.ts"] },
{ id: "m2", status: "Killed", killedBy: ["A.test.ts", "B.test.ts"] },
{ id: "m3", status: "Survived", killedBy: [] },
],
},
},
};
const allTestFiles = ["A.test.ts", "B.test.ts", "C.test.ts"];
const r = classifyTestFiles(report, allTestFiles);
assert.equal(r["A.test.ts"].class, "unique"); // mata m1 sozinho
assert.equal(r["A.test.ts"].uniqueKills, 1);
assert.equal(r["A.test.ts"].sharedKills, 1);
assert.equal(r["B.test.ts"].class, "redundant"); // só m2 (compartilhado)
assert.equal(r["B.test.ts"].uniqueKills, 0);
assert.equal(r["C.test.ts"].class, "empty"); // não mata nada
});
// ── id resolution: real Stryker tap-runner reports use numeric test ids in
// killedBy and a testFiles{} section mapping id -> file name. ────────────────
test("resolves numeric killedBy ids to file names via the testFiles section", () => {
const report = {
testFiles: {
"tests/unit/x.test.ts": { tests: [{ id: "0", name: "tests/unit/x.test.ts" }] },
"tests/unit/y.test.ts": { tests: [{ id: "1", name: "tests/unit/y.test.ts" }] },
"tests/unit/z.test.ts": { tests: [{ id: "2", name: "tests/unit/z.test.ts" }] },
},
files: {
"src/m.ts": {
mutants: [
{ id: "m1", status: "Killed", killedBy: ["0"] }, // x alone -> unique
{ id: "m2", status: "Killed", killedBy: ["0", "1"] }, // x + y shared
],
},
},
};
// allTestFiles defaults to the testFiles keys when omitted.
const r = classifyTestFiles(report);
assert.equal(r["tests/unit/x.test.ts"].class, "unique");
assert.equal(r["tests/unit/y.test.ts"].class, "redundant");
assert.equal(r["tests/unit/z.test.ts"].class, "empty");
});
// ── overlapping (🟡): kills ≥1 unique but the majority of its kills are shared.
test("classifies overlapping when shared kills outnumber unique kills", () => {
const report = {
files: {
"src/m.ts": {
mutants: [
{ id: "m1", status: "Killed", killedBy: ["D"] }, // D unique
{ id: "m2", status: "Killed", killedBy: ["D", "E"] }, // D shared
{ id: "m3", status: "Killed", killedBy: ["D", "E", "F"] }, // D shared
],
},
},
};
const r = classifyTestFiles(report, ["D", "E", "F"]);
assert.equal(r["D"].uniqueKills, 1);
assert.equal(r["D"].sharedKills, 2);
assert.equal(r["D"].class, "overlapping"); // 2 shared > 1 unique
assert.equal(r["E"].class, "redundant");
assert.equal(r["F"].class, "redundant");
});
// ── classifyFromCounts: the pure threshold helper. ───────────────────────────
test("classifyFromCounts applies the threshold rules", () => {
assert.equal(classifyFromCounts(0, 0), "empty");
assert.equal(classifyFromCounts(0, 3), "redundant");
assert.equal(classifyFromCounts(2, 1), "unique"); // shared not > unique
assert.equal(classifyFromCounts(1, 1), "unique"); // tie -> unique
assert.equal(classifyFromCounts(1, 5), "overlapping"); // shared > unique
});
// ── redundancyCandidates (R1): the prune-candidate list = 🔴 empty 🟠 redundant.
// A file is a candidate iff it has ZERO unique kills (kills nothing, OR every mutant it
// kills is also killed by another file). Files with ≥1 unique kill (🟢/🟡) are NEVER
// candidates. Under a disableBail run killedBy is COMPLETE, so this list is accurate. ──
test("redundancyCandidates returns empty redundant files; never a file with a unique kill", () => {
const report = {
testFiles: {
"tests/unit/x.test.ts": { tests: [{ id: "0", name: "tests/unit/x.test.ts" }] },
"tests/unit/y.test.ts": { tests: [{ id: "1", name: "tests/unit/y.test.ts" }] },
"tests/unit/z.test.ts": { tests: [{ id: "2", name: "tests/unit/z.test.ts" }] },
},
files: {
"src/m.ts": {
mutants: [
{ id: "m1", status: "Killed", killedBy: ["0"] }, // x alone -> unique (KEEP)
{ id: "m2", status: "Killed", killedBy: ["0", "1"] }, // x+y; y only ever shared
],
},
},
};
const r = redundancyCandidates([report]);
assert.deepEqual(r.empty, ["tests/unit/z.test.ts"]); // kills nothing
assert.deepEqual(r.redundant, ["tests/unit/y.test.ts"]); // kills only shared mutants
assert.deepEqual(r.candidates, ["tests/unit/y.test.ts", "tests/unit/z.test.ts"]);
assert.ok(!r.candidates.includes("tests/unit/x.test.ts")); // has a unique kill -> safe
});
// ── aggregateRadiography: merge per-batch reports at the FILE level (ids are
// per-run, so each report is classified independently then summed). A file can
// be empty in one batch but unique in another -> unique overall. ─────────────
test("aggregateRadiography sums per-file kills across batches and reclassifies", () => {
const batchC = {
files: {
"src/routeGuard.ts": {
mutants: [{ id: "c1", status: "Killed", killedBy: ["A"] }], // A unique here
},
},
};
const batchG = {
files: {
"src/chatCore/x.ts": {
mutants: [
{ id: "g1", status: "Killed", killedBy: ["A", "B"] }, // A shared, B shared
],
},
},
};
// B only ever shares; A is unique in C and shared in G -> A unique overall.
const agg = aggregateRadiography([batchC, batchG], ["A", "B", "C"]);
assert.equal(agg["A"].uniqueKills, 1);
assert.equal(agg["A"].sharedKills, 1);
assert.equal(agg["A"].class, "unique");
assert.equal(agg["B"].class, "redundant");
assert.equal(agg["C"].class, "empty");
});
@@ -0,0 +1,65 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
// Regression guard for #3883: the @omniroute/opencode-plugin must ship an
// ESM-only bundle. OpenCode's Bun-based plugin loader resolves the package
// `main`/`exports` and applies CJS-to-ESM interop on a dual CJS bundle, which
// turns `mod.default` into the whole exports namespace, fails V1 plugin
// detection, and makes the loader throw "Plugin export is not a function".
// Shipping ESM-only (with a `./runtime` subpath) keeps `mod.default` the V1
// plugin object so the loader registers it. Do NOT re-introduce a CJS bundle.
const PKG_URL = new URL("../../../@omniroute/opencode-plugin/package.json", import.meta.url);
const TSUP_URL = new URL("../../../@omniroute/opencode-plugin/tsup.config.ts", import.meta.url);
function readJson(url: URL): Record<string, unknown> {
return JSON.parse(readFileSync(fileURLToPath(url), "utf8"));
}
test("opencode-plugin package.json ships ESM-only (no CJS bundle)", () => {
const pkg = readJson(PKG_URL);
// main must point at the ESM build, never the .cjs bundle.
assert.equal(pkg.main, "./dist/index.js", "main must be the ESM build");
assert.equal(
typeof pkg.main === "string" && pkg.main.endsWith(".cjs"),
false,
"main must not be a .cjs bundle"
);
// The legacy dual-bundle `module` field should be gone.
assert.equal("module" in pkg, false, "the dual-bundle `module` field must be removed");
const exports = pkg.exports as Record<string, unknown> | undefined;
assert.ok(exports && typeof exports === "object", "exports map must exist");
const root = exports["."] as Record<string, unknown>;
assert.ok(root && typeof root === "object", 'exports["."] must exist');
assert.equal(root.import, "./dist/index.js", 'exports["."].import must be the ESM build');
// A `require` condition would re-introduce the CJS path the loader chokes on.
assert.equal("require" in root, false, 'exports["."] must not declare a `require` condition');
// The `./runtime` subpath used by the OpenCode loader must resolve to ESM.
const runtime = exports["./runtime"] as Record<string, unknown>;
assert.ok(runtime && typeof runtime === "object", 'exports["./runtime"] must exist');
assert.equal(runtime.import, "./dist/index.js", 'exports["./runtime"].import must be ESM');
});
test("opencode-plugin tsup config builds ESM-only with cjsInterop disabled", () => {
const tsup = readFileSync(fileURLToPath(TSUP_URL), "utf8");
const formatMatch = tsup.match(/format:\s*\[([^\]]*)\]/);
assert.ok(formatMatch, "tsup config must declare a format array");
const formats = formatMatch[1]
.split(",")
.map((s) => s.trim().replace(/['"]/g, ""))
.filter(Boolean);
assert.deepEqual(formats, ["esm"], "tsup must build esm only (no cjs)");
assert.equal(
/cjsInterop:\s*true/.test(tsup),
false,
"cjsInterop must not be true for an ESM-only build"
);
});
@@ -0,0 +1,45 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { join } from "node:path";
const repoRoot = process.cwd();
function readJson<T = Record<string, unknown>>(relPath: string): T {
return JSON.parse(readFileSync(join(repoRoot, relPath), "utf8")) as T;
}
test("@huggingface/transformers is optional so onnxruntime CUDA install failures cannot abort OmniRoute install", () => {
const pkg = readJson<{
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
}>("package.json");
assert.equal(
pkg.dependencies?.["@huggingface/transformers"],
undefined,
"transformers must not be a regular dependency because it pulls onnxruntime-node install scripts"
);
assert.equal(pkg.optionalDependencies?.["@huggingface/transformers"], "3.5.2");
});
test("package-lock marks transformers and its onnxruntime runtime as optional", () => {
const lock = readJson<{
packages: Record<string, { optional?: boolean; dependencies?: Record<string, string>; optionalDependencies?: Record<string, string> }>;
}>("package-lock.json");
assert.equal(
lock.packages[""]?.dependencies?.["@huggingface/transformers"],
undefined,
"root lock dependencies must not keep transformers as mandatory"
);
assert.equal(lock.packages[""]?.optionalDependencies?.["@huggingface/transformers"], "3.5.2");
for (const packagePath of [
"node_modules/@huggingface/transformers",
"node_modules/onnxruntime-node",
"node_modules/onnxruntime-common",
]) {
assert.equal(lock.packages[packagePath]?.optional, true, `${packagePath} should be optional`);
}
});
@@ -0,0 +1,69 @@
// tests/unit/build/should-promote-latest-5301.test.ts
// Regression guard for #5301 — Docker Hub `:latest` tag not re-pointed on release.
//
// The docker-publish workflow decides whether the just-built VERSION should also
// move `:latest` by extracting the highest STABLE semver from the candidate set.
// The bug: on a `release: released` event the freshly-created git tag is often
// not yet visible to `git fetch --tags`, so a candidate set built purely from
// `git tag -l` resolved HIGHEST to the *previous* version and skipped promotion,
// leaving `latest` one release behind. The fix folds VERSION into the candidate
// set so the decision is independent of tag-sync timing.
//
// Strategy: spawn the real extracted helper (scripts/ci/should-promote-latest.sh)
// with fixture tag lists on stdin — this guards the actual shell code the
// workflow calls, not a parallel reimplementation. Hermetic: no git, no network.
import test from "node:test";
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";
const here = path.dirname(fileURLToPath(import.meta.url));
const SCRIPT = path.resolve(here, "../../../scripts/ci/should-promote-latest.sh");
/** Run the helper with `version` as argv[1] and `tags` (joined by \n) on stdin. */
function shouldPromote(version: string, tags: string[]): string {
return execFileSync("bash", [SCRIPT, version], {
input: tags.join("\n") + (tags.length ? "\n" : ""),
encoding: "utf8",
}).trim();
}
test("#5301 race: new tag not yet synced → still promotes latest", () => {
// VERSION just released, but `git tag -l` only shows the previous versions
// (the new tag hasn't propagated). The old logic returned false here.
assert.equal(shouldPromote("3.8.39", ["3.8.38", "3.8.37"]), "true");
});
test("new tag already visible → promotes", () => {
assert.equal(shouldPromote("3.8.39", ["3.8.39", "3.8.38"]), "true");
});
test("first release (no existing tags) → promotes", () => {
assert.equal(shouldPromote("3.8.39", []), "true");
});
test("patch on an older line while a higher minor exists → does NOT promote", () => {
// A 3.8.x patch published after 3.9.0 already shipped must not grab :latest.
assert.equal(shouldPromote("3.8.39", ["3.9.0", "3.8.39", "3.8.38"]), "false");
});
test("pre-release candidate tags are ignored when picking the highest", () => {
// 3.8.40-rc.1 must not count as higher than the stable 3.8.39.
assert.equal(shouldPromote("3.8.39", ["3.8.40-rc.1", "3.8.38"]), "true");
});
test("a pre-release VERSION never promotes latest", () => {
assert.equal(shouldPromote("3.8.40-rc.1", ["3.8.39"]), "false");
});
test("numeric (not lexical) semver ordering", () => {
// Lexical sort would rank 3.9.0 > 3.10.0; semver -V must rank 3.10.0 highest.
assert.equal(shouldPromote("3.10.0", ["3.9.0", "3.2.8"]), "true");
assert.equal(shouldPromote("3.9.0", ["3.10.0", "3.9.0"]), "false");
});
test("candidate tags with a leading `v` are normalized", () => {
assert.equal(shouldPromote("3.8.39", ["v3.8.38", "v3.8.37"]), "true");
});
@@ -0,0 +1,47 @@
import test 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 { syncChangelogSection } from "../../../scripts/release/sync-changelog-i18n.mjs";
function repo() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "cl-i18n-"));
fs.writeFileSync(
path.join(root, "CHANGELOG.md"),
"# Changelog\n\n## [Unreleased]\n\n---\n\n## [9.9.9] — 2026-01-01\n\n- **feat:** big new thing\n- **fix:** another\n\n---\n\n## [9.9.8] — 2025-12-01\n\n- old\n"
);
for (const loc of ["fr", "de"]) {
fs.mkdirSync(path.join(root, "docs/i18n", loc), { recursive: true });
fs.writeFileSync(
path.join(root, "docs/i18n", loc, "CHANGELOG.md"),
"# Changelog\n\n---\n\n## [9.9.9] — TBD\n\n_stub_\n\n---\n\n## [9.9.8] — 2025-12-01\n\n- old\n"
);
}
return root;
}
test("replaces the version section in every mirror with the root section", () => {
const root = repo();
const n = syncChangelogSection(root, "9.9.9", "9.9.8");
assert.equal(n, 2);
const fr = fs.readFileSync(path.join(root, "docs/i18n/fr/CHANGELOG.md"), "utf8");
assert.match(fr, /big new thing/);
assert.doesNotMatch(fr, /_stub_/);
fs.rmSync(root, { recursive: true, force: true });
});
test("inserts the section when a mirror lacks it", () => {
const root = repo();
// remove the 9.9.9 section from the fr mirror entirely
fs.writeFileSync(
path.join(root, "docs/i18n/fr/CHANGELOG.md"),
"# Changelog\n\n---\n\n## [9.9.8] — 2025-12-01\n\n- old\n"
);
const n = syncChangelogSection(root, "9.9.9", "9.9.8");
assert.equal(n, 2);
const fr = fs.readFileSync(path.join(root, "docs/i18n/fr/CHANGELOG.md"), "utf8");
assert.match(fr, /## \[9\.9\.9\]/);
assert.match(fr, /big new thing/);
fs.rmSync(root, { recursive: true, force: true });
});