test(architecture): ratchet top-level layer direction and mutual imports (#10584)

* test(architecture): ratchet top-level layer direction and mutual imports

Declare a layer for every top-level src/ directory and root source file
(domain, contracts, foundation, providers, application, composition,
adapters) in scripts/lib/module-layers.mjs. verify:module-boundaries now also
builds the production import graph between those units and compares it with
a counted baseline: 45 unit pairs import a higher layer (248 imports) and 23
unit pairs import each other. A new upward pair, more imports on a recorded
pair, a new mutual import, or an undeclared unit fails; shrinkage is
reported so the baseline can be lowered.

* fix(architecture): enforce layers for isolated units
This commit is contained in:
morluto authored and GitHub committed 2026-11-15 08:37:04 +08:00
1 parent 351c2f4a03
commit 8c052bdec5
8 files changed
+472 -9

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
import { readdir } from "node:fs/promises";
import { describe, expect, it } from "vitest";
import {
UNIT_LAYERS,
compareLayerBaseline,
layerGraphFacts,
sourceUnit,
} from "../../scripts/lib/module-layers.mjs";
const edge = (file: string, target: string) => ({ file, target });
describe("top-level module layers", () => {
it("declares a layer for every top-level source unit", async () => {
const entries = await readdir("src", { withFileTypes: true });
const units = entries
.filter(
(entry) =>
entry.isDirectory() ||
(entry.name.endsWith(".ts") &&
sourceUnit(`src/${entry.name}`) !== undefined),
)
.map(({ name }) => name);
expect(units.filter((unit) => UNIT_LAYERS[unit] === undefined)).toEqual([]);
});
it("maps files, root files and built specifiers to units, ignoring tests", () => {
expect(sourceUnit("src/native/elf/Reader.ts")).toBe("native");
expect(sourceUnit("src/native/elf/Reader.js")).toBe("native");
expect(sourceUnit("src/cli.js")).toBe("cli.ts");
expect(sourceUnit("src/native/elf/Reader.test.ts")).toBeUndefined();
expect(sourceUnit("tests/fixtures/x.ts")).toBeUndefined();
});
it("counts upward imports per unit pair and reports mutual imports", () => {
const facts = layerGraphFacts([
edge("src/native/A.ts", "src/application/B.js"),
edge("src/native/C.ts", "src/application/D.js"),
edge("src/application/B.ts", "src/native/A.js"),
edge("src/application/B.ts", "src/domain/E.js"),
edge("src/application/B.ts", "src/application/F.js"),
edge("src/native/A.test.ts", "src/cli/G.js"),
]);
expect(facts).toEqual({
undeclared: [],
upward: { "native -> application": 2 },
cycles: ["application <-> native"],
});
});
it("refuses an undeclared unit instead of leaving it unconstrained", () => {
expect(
layerGraphFacts([edge("src/newProvider/A.ts", "src/domain/B.js")])
.undeclared,
).toEqual(["newProvider"]);
expect(layerGraphFacts([], ["src/isolated/A.ts"]).undeclared).toEqual([
"isolated",
]);
expect(
layerGraphFacts(
[edge("src/isolated/A.ts", "src/isolated/B.js")],
["src/isolated/A.ts"],
).undeclared,
).toEqual(["isolated"]);
});
it("fails on growth and only reports shrinkage", () => {
const baseline = {
upward: { "native -> application": 2, "gdb -> application": 1 },
cycles: ["application <-> native", "application <-> gdb"],
};
const grown = compareLayerBaseline(
{
undeclared: [],
upward: { "native -> application": 3, "rizin -> application": 1 },
cycles: ["application <-> native", "application <-> rizin"],
},
baseline,
);
expect(grown.failures).toEqual([
"upward import native -> application: 3 imports, baseline 2",
"upward import rizin -> application: 1 imports, baseline 0",
"mutual import not in baseline: application <-> rizin",
]);
expect(grown.lowerable).toEqual([
"upward import gdb -> application: 0, baseline 1",
"recorded mutual import no longer present: application <-> gdb",
]);
});
});