syntaxai/tdd.md · main · src / b32_sama_v2_metrics.test.ts

b32_sama_v2_metrics.test.ts 254 lines · 9134 bytes raw
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);
  });
});