import { describe, expect, test } from "bun:test"; import { computeCoreMetrics } from "./b32_sama_v2_metrics.ts"; import { WORKING_SET_MAX_LOC, WORKING_SET_MIN_LOC, type ProfileSpec, type SamaV2Input, } from "./a31_sama_v2.ts"; // Flat fixture profile (one prefix per layer) so the metric tests // don't depend on the live profile. The Law-check sublayer ordering // isn't relevant here — these tests target the metrics computation, // not the conformance verdict. const FIXTURE_PROFILE: ProfileSpec = { samaVersion: "2.0", profile: "metrics-test", layers: { 0: { sublayers: [{ name: "default", prefix: "p0_", index: 0 }] }, 1: { sublayers: [{ name: "default", prefix: "p1_", index: 0 }] }, 2: { sublayers: [{ name: "default", prefix: "p2_", index: 0 }] }, 3: { sublayers: [{ name: "default", prefix: "p3_", index: 0 }] }, }, }; const mk = (entries: Array<[string, string]>): SamaV2Input => ({ profile: FIXTURE_PROFILE, files: new Map(entries), }); // Helper: produce a file with `n` lines of harmless code (so // split("\n").length === n). const linesOf = (n: number): string => Array.from({ length: n }, (_, i) => `const x${i} = ${i};`).join("\n"); // Helper: a minimal sibling test body for Layer-1/2 fixtures. const TEST_BODY = 'import { test, expect } from "bun:test"; test("ok", () => { expect(1).toBe(1); });\n'; describe("computeCoreMetrics — graphDepth", () => { test("empty repo → 0", () => { const m = computeCoreMetrics(mk([])); expect(m.graphDepth).toBe(0); }); test("single file with no imports → 1", () => { const m = computeCoreMetrics(mk([ ["src/p0_a.ts", "export const x = 1;\n"], ])); expect(m.graphDepth).toBe(1); }); test("chain p3 → p2 → p1 → p0 → 4", () => { const m = computeCoreMetrics(mk([ ["src/p0_a.ts", "export const x = 1;\n"], ["src/p1_a.ts", `import { x } from "./p0_a.ts";\nexport const y = x;\n`], ["src/p1_a.test.ts", TEST_BODY], ["src/p2_a.ts", `import { y } from "./p1_a.ts";\nexport const z = y;\n`], ["src/p2_a.test.ts", TEST_BODY], ["src/p3_a.ts", `import { z } from "./p2_a.ts";\nexport const w = z;\n`], ])); expect(m.graphDepth).toBe(4); }); test("a cycle is bounded (does not infinite-loop)", () => { // p1_a ↔ p1_b cycle (same-layer; Law would flag it, but graphDepth // must still terminate with a finite number). const m = computeCoreMetrics(mk([ ["src/p1_a.ts", `import { y } from "./p1_b.ts";\nexport const x = y;\n`], ["src/p1_a.test.ts", TEST_BODY], ["src/p1_b.ts", `import { x } from "./p1_a.ts";\nexport const y = x;\n`], ["src/p1_b.test.ts", TEST_BODY], ])); expect(Number.isFinite(m.graphDepth)).toBe(true); expect(m.graphDepth).toBeGreaterThanOrEqual(1); }); }); describe("computeCoreMetrics — fanByLayer", () => { test("empty repo → all-zero summaries", () => { const m = computeCoreMetrics(mk([])); for (const L of [0, 1, 2, 3] as const) { expect(m.fanByLayer[L].fanIn).toEqual({ mean: 0, p50: 0, p95: 0, max: 0 }); expect(m.fanByLayer[L].fanOut).toEqual({ mean: 0, p50: 0, p95: 0, max: 0 }); } }); test("single Layer-0 file with no edges → all zeros at L0", () => { const m = computeCoreMetrics(mk([ ["src/p0_a.ts", "export const x = 1;\n"], ])); expect(m.fanByLayer[0].fanIn.max).toBe(0); expect(m.fanByLayer[0].fanOut.max).toBe(0); }); test("two Layer-1 files importing same Layer-0 → L0.fanIn.max = 2, L1.fanOut.max = 1", () => { const m = computeCoreMetrics(mk([ ["src/p0_a.ts", "export const x = 1;\n"], ["src/p1_a.ts", `import { x } from "./p0_a.ts";\nexport const y = x;\n`], ["src/p1_a.test.ts", TEST_BODY], ["src/p1_b.ts", `import { x } from "./p0_a.ts";\nexport const z = x;\n`], ["src/p1_b.test.ts", TEST_BODY], ])); expect(m.fanByLayer[0].fanIn.max).toBe(2); expect(m.fanByLayer[1].fanOut.max).toBe(1); expect(m.fanByLayer[1].fanIn.max).toBe(0); }); }); describe("computeCoreMetrics — boundaryRatio", () => { test("no parse boundaries anywhere → 1.0 (vacuously)", () => { const m = computeCoreMetrics(mk([ ["src/p0_a.ts", "export const x = 1;\n"], ])); expect(m.boundaryRatio).toBe(1.0); }); test("JSON.parse only in Layer 2 → 1.0", () => { const m = computeCoreMetrics(mk([ ["src/p2_a.ts", "export const f = (s: string) => JSON.parse(s);\n"], ["src/p2_a.test.ts", TEST_BODY], ])); expect(m.boundaryRatio).toBe(1.0); }); test("JSON.parse in Layer 1 and Layer 2 → 0.5", () => { const m = computeCoreMetrics(mk([ ["src/p1_a.ts", "export const f = (s: string) => JSON.parse(s);\n"], ["src/p1_a.test.ts", TEST_BODY], ["src/p2_a.ts", "export const g = (s: string) => JSON.parse(s);\n"], ["src/p2_a.test.ts", TEST_BODY], ])); expect(m.boundaryRatio).toBe(0.5); }); test("string literal containing JSON.parse doesn't false-positive", () => { const m = computeCoreMetrics(mk([ ["src/p1_a.ts", `const explainer = "call JSON.parse here";\nexport const x = explainer.length;\n`], ["src/p1_a.test.ts", TEST_BODY], ])); expect(m.boundaryRatio).toBe(1.0); }); test("counts every call site, not just every file", () => { // Two JSON.parse in one Layer-2 file, one in Layer-1 → ratio = 2/3 const m = computeCoreMetrics(mk([ ["src/p1_a.ts", "export const f = (s: string) => JSON.parse(s);\n"], ["src/p1_a.test.ts", TEST_BODY], ["src/p2_a.ts", "export const g = (s: string) => JSON.parse(s);\nexport const h = (s: string) => JSON.parse(s);\n"], ["src/p2_a.test.ts", TEST_BODY], ])); expect(m.boundaryRatio).toBeCloseTo(2 / 3, 6); }); }); describe("computeCoreMetrics — workingSetFit", () => { test("empty repo → 1.0", () => { const m = computeCoreMetrics(mk([])); expect(m.workingSetFit).toBe(1.0); }); test("a single 100-line file → 1.0", () => { const m = computeCoreMetrics(mk([ ["src/p0_a.ts", linesOf(100)], ])); expect(m.workingSetFit).toBe(1.0); }); test("a 10-line file falls below the min → 0.0", () => { const m = computeCoreMetrics(mk([ ["src/p0_a.ts", linesOf(10)], ])); expect(m.workingSetFit).toBe(0.0); }); test("a 600-line file exceeds the max → 0.0", () => { const m = computeCoreMetrics(mk([ ["src/p0_a.ts", linesOf(600)], ])); expect(m.workingSetFit).toBe(0.0); }); test("two files: one 100-line (in), one 10-line (out) → 0.5", () => { const m = computeCoreMetrics(mk([ ["src/p0_a.ts", linesOf(100)], ["src/p0_b.ts", linesOf(10)], ])); expect(m.workingSetFit).toBe(0.5); }); test("exact bounds are inclusive (50 and 500 count as in the sweet spot)", () => { const m = computeCoreMetrics(mk([ ["src/p0_min.ts", linesOf(WORKING_SET_MIN_LOC)], ["src/p0_max.ts", linesOf(WORKING_SET_MAX_LOC)], ])); expect(m.workingSetFit).toBe(1.0); }); test("test files don't count toward the metric (only SAMA source files)", () => { // One 100-line Layer-1 source + a tiny sibling test. Sibling test // is 1 line, far below the min, but it's excluded. const m = computeCoreMetrics(mk([ ["src/p1_a.ts", linesOf(100)], ["src/p1_a.test.ts", TEST_BODY], ])); expect(m.workingSetFit).toBe(1.0); }); }); describe("computeCoreMetrics — violationCounts", () => { test("conforming fixture → all counts = 0", () => { const m = computeCoreMetrics(mk([ ["src/p0_a.ts", "export const x = 1;\n"], ])); expect(m.violationCounts).toEqual({ sorted: 0, architecture: 0, modeledTests: 0, modeledBoundary: 0, atomic: 0, law: 0, consistency: 0, }); }); test("Layer-1 file without sibling test → modeledTests = 1", () => { const m = computeCoreMetrics(mk([ ["src/p1_a.ts", "export const y = 1;\n"], ])); expect(m.violationCounts.modeledTests).toBe(1); }); test("counts are populated even when overall verdict is conforming (trailing signal shape)", () => { // Single Layer-0 file → all checks pass → all counts are 0 (not // missing). This is the §5 contract: keys exist regardless. const m = computeCoreMetrics(mk([ ["src/p0_a.ts", "export const x = 1;\n"], ])); const keys = Object.keys(m.violationCounts).sort(); expect(keys).toEqual([ "architecture", "atomic", "consistency", "law", "modeledBoundary", "modeledTests", "sorted", ]); }); }); describe("computeCoreMetrics — reproducibility", () => { test("same input → identical output across two runs (deep-equal)", () => { const input = mk([ ["src/p0_a.ts", "export const x = 1;\n"], ["src/p1_a.ts", `import { x } from "./p0_a.ts";\nexport const y = x;\n`], ["src/p1_a.test.ts", TEST_BODY], ["src/p2_a.ts", `import { y } from "./p1_a.ts";\nexport const f = (s: string) => JSON.parse(s);\n`], ["src/p2_a.test.ts", TEST_BODY], ]); const m1 = computeCoreMetrics(input); const m2 = computeCoreMetrics(input); expect(m1).toEqual(m2); }); });