Files
clean-architecture-frontend…/tests/unit/registry-governance.test.ts
T

377 lines
12 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
diffRegistrySnapshots,
validateBreakingEvidence,
verifyRegistryBaselineApproval,
} from "../../scripts/lib/registry-compatibility.ts";
type RegistryDefinition = Readonly<{
registryId: string;
owner: string;
requiredFields: string[];
fieldTypes: Record<string, string>;
path: string;
exportName: string;
rowsPath?: string;
rowKeyFields?: string[];
uniqueFields?: string[];
uniqueFieldSets?: string[][];
snapshotProjection?: {
singletonRowKey: string;
canonicalArrayKeyFields: Record<string, string[]>;
};
consumers?: Array<{ path: string; token: string }>;
breakingFields?: string[];
}>;
describe("registry governance manifest", () => {
it("declares eleven typed, single-owner executable registries", async () => {
const governance = JSON.parse(
await readFile("config/contracts/registry-governance.json", "utf8"),
);
const registries = governance.registries as RegistryDefinition[];
expect(governance.registries).toHaveLength(11);
expect(new Set(registries.map((entry) => entry.registryId)).size).toBe(
11,
);
expect(registries.every((entry) => entry.owner)).toBe(true);
expect(
registries.every(
(entry) =>
Array.isArray(entry.requiredFields) &&
entry.requiredFields.length > 0 &&
entry.fieldTypes &&
Object.keys(entry.fieldTypes).length > 0,
),
).toBe(true);
});
it("governs the installed invalidation graph and wire versions at their real exports", async () => {
const governance = JSON.parse(
await readFile("config/contracts/registry-governance.json", "utf8"),
) as { registries: RegistryDefinition[] };
const byId = new Map(
governance.registries.map((registry) => [registry.registryId, registry]),
);
expect(byId.get("FE-REG-QUERY-INVALIDATION")).toMatchObject({
path: "src/features/installed-feature-contracts.ts",
exportName: "INVALIDATION_REGISTRY",
rowsPath: "edges",
rowKeyFields: [
"topicId",
"namespace.namespaceId",
"namespace.namespaceVersion",
],
uniqueFieldSets: [
[
"topicId",
"namespace.namespaceId",
"namespace.namespaceVersion",
],
],
snapshotProjection: {
singletonRowKey: "invalidation-graph",
canonicalArrayKeyFields: {
topics: ["$value"],
namespaces: ["namespaceId", "namespaceVersion"],
edges: [
"topicId",
"namespace.namespaceId",
"namespace.namespaceVersion",
],
},
},
consumers: [
{
path: "src/bootstrap/runtime-adapters.ts",
token: "indexInvalidationRegistry(INVALIDATION_REGISTRY)",
},
],
breakingFields: ["topics", "namespaces", "edges"],
});
expect(byId.get("FE-REG-QUERY-INVALIDATION-TOPIC-VERSION")).toMatchObject({
path: "src/features/installed-feature-contracts.ts",
exportName: "INVALIDATION_TOPIC_VERSIONS",
rowKeyFields: ["topicId"],
uniqueFields: ["topicId"],
consumers: [
{
path: "src/bootstrap/runtime-adapters.ts",
token: "indexInvalidationTopicVersions(",
},
],
breakingFields: ["topicId", "topicVersion"],
});
});
it("projects graph and array exports into deterministic governed rows", async () => {
const directory = await mkdtemp(
path.join(tmpdir(), "registry-governance-projection-"),
);
const artifact = path.join(directory, "registries.json");
try {
const result = spawnSync(
process.execPath,
[
"scripts/check-registries.ts",
"--governance",
"tests/fixtures/registry/invalidation/governance.json",
"--artifact",
artifact,
"--no-baseline",
],
{ encoding: "utf8" },
);
expect(result.status, result.stderr).toBe(0);
const report = JSON.parse(await readFile(artifact, "utf8")) as {
registries: Array<{
registryId: string;
rows: Record<string, Record<string, unknown>>;
}>;
};
expect(
report.registries.find(
(registry) => registry.registryId === "FIXTURE-INVALIDATION-EDGES",
)?.rows,
).toEqual({
"invalidation-graph": {
topics: ["orders", "qinv.fixture.changed"],
namespaces: [
{ namespaceId: "orders", namespaceVersion: 1 },
{ namespaceId: "qinv.fixture.changed", namespaceVersion: 1 },
],
edges: [
{
topicId: "orders",
namespace: {
namespaceId: "qinv.fixture.changed",
namespaceVersion: 1,
},
},
{
topicId: "qinv.fixture.changed",
namespace: { namespaceId: "orders", namespaceVersion: 1 },
},
],
},
});
expect(
report.registries.find(
(registry) => registry.registryId === "FIXTURE-INVALIDATION-VERSIONS",
)?.rows,
).toHaveProperty('["qinv.fixture.changed"]');
} finally {
await rm(directory, { recursive: true, force: true });
}
});
it("requires explicit evidence for post-baseline graph and version drift", async () => {
const directory = await mkdtemp(
path.join(tmpdir(), "registry-governance-tamper-"),
);
const artifact = path.join(directory, "registries.json");
try {
const result = spawnSync(
process.execPath,
[
"scripts/check-registries.ts",
"--governance",
"tests/fixtures/registry/invalidation/governance.json",
"--artifact",
artifact,
"--no-baseline",
],
{ encoding: "utf8" },
);
expect(result.status, result.stderr).toBe(0);
const report = JSON.parse(await readFile(artifact, "utf8")) as {
registries: Array<{
registryId: string;
contract: Record<string, unknown>;
rows: Record<string, Record<string, unknown>>;
}>;
};
const baseline = { schemaVersion: 2, registries: report.registries };
expect(
baseline.registries.find(
(registry) => registry.registryId === "FIXTURE-INVALIDATION-EDGES",
)?.rows["invalidation-graph"],
).toBeDefined();
const tamperCases = [
{
name: "topic",
registryId: "FIXTURE-INVALIDATION-EDGES",
field: "topics",
apply(snapshot: typeof baseline) {
const graph = snapshot.registries.find(
(registry) => registry.registryId === this.registryId,
)?.rows["invalidation-graph"];
(graph?.topics as string[])[0] = "tampered-topic";
},
},
{
name: "namespace",
registryId: "FIXTURE-INVALIDATION-EDGES",
field: "namespaces",
apply(snapshot: typeof baseline) {
const graph = snapshot.registries.find(
(registry) => registry.registryId === this.registryId,
)?.rows["invalidation-graph"];
const namespaces = graph?.namespaces as Array<{
namespaceId: string;
}>;
namespaces[0]!.namespaceId = "tampered-namespace";
},
},
{
name: "edge",
registryId: "FIXTURE-INVALIDATION-EDGES",
field: "edges",
apply(snapshot: typeof baseline) {
const graph = snapshot.registries.find(
(registry) => registry.registryId === this.registryId,
)?.rows["invalidation-graph"];
const edges = graph?.edges as Array<{ topicId: string }>;
edges[0]!.topicId = "tampered-edge";
},
},
{
name: "version",
registryId: "FIXTURE-INVALIDATION-VERSIONS",
field: "topicVersion",
apply(snapshot: typeof baseline) {
const versions = snapshot.registries.find(
(registry) => registry.registryId === this.registryId,
)?.rows;
const firstVersion = versions?.[Object.keys(versions)[0] ?? ""];
if (!firstVersion) throw new Error("version fixture row missing");
firstVersion.topicVersion = 2;
},
},
];
for (const tamperCase of tamperCases) {
const current = structuredClone(baseline);
tamperCase.apply(current);
const diff = diffRegistrySnapshots(baseline, current);
expect(diff.impact, tamperCase.name).toBe("breaking");
expect(
diff.changes.some((change) => change.kind === "registry-added"),
tamperCase.name,
).toBe(false);
const breakingChanges = diff.changes.filter(
(change) => change.impact === "breaking",
);
expect(breakingChanges, tamperCase.name).toEqual(
expect.arrayContaining([
expect.objectContaining({
registryId: tamperCase.registryId,
field: tamperCase.field,
}),
]),
);
expect(
validateBreakingEvidence(diff, { changes: [] }).passed,
tamperCase.name,
).toBe(false);
expect(
validateBreakingEvidence(diff, {
changes: breakingChanges.map((change) => ({
changeId: change.changeId,
versionBump: "2",
migration: `migrate ${tamperCase.name}`,
compatibilityWindow: "one release",
rollback: `restore ${tamperCase.name} baseline`,
owner: "fixture-owner",
})),
}).passed,
tamperCase.name,
).toBe(true);
}
} finally {
await rm(directory, { recursive: true, force: true });
}
});
it("pins the full invalidation graph and versions in the approved repository baseline", async () => {
const baseline = JSON.parse(
await readFile("config/contracts/registry-baseline.json", "utf8"),
) as {
schemaVersion: number;
registries: Array<{
registryId: string;
rows: Record<string, Record<string, unknown>>;
}>;
};
const approval = JSON.parse(
await readFile(
"config/contracts/registry-baseline.approval.json",
"utf8",
),
) as Record<string, unknown>;
expect(verifyRegistryBaselineApproval(baseline, approval).passed).toBe(true);
expect(baseline.registries).toHaveLength(11);
const graph = baseline.registries.find(
(registry) => registry.registryId === "FE-REG-QUERY-INVALIDATION",
);
const versions = baseline.registries.find(
(registry) =>
registry.registryId ===
"FE-REG-QUERY-INVALIDATION-TOPIC-VERSION",
);
expect(graph?.rows["invalidation-graph"]).toMatchObject({
topics: expect.any(Array),
namespaces: expect.any(Array),
edges: expect.any(Array),
});
expect(versions?.rows).toBeDefined();
expect(approval).toMatchObject({
owner: "frontend-platform",
reason:
"Baseline canonical invalidation graph and topic-version contracts after FE-REG-QUERY retirement",
});
});
it("rejects duplicate composite invalidation edges after projection", async () => {
const directory = await mkdtemp(
path.join(tmpdir(), "registry-governance-duplicate-"),
);
const artifact = path.join(directory, "registries.json");
try {
const result = spawnSync(
process.execPath,
[
"scripts/check-registries.ts",
"--governance",
"tests/fixtures/registry/invalidation/duplicate-governance.json",
"--artifact",
artifact,
"--no-baseline",
],
{ encoding: "utf8" },
);
expect(result.status).toBe(1);
const report = JSON.parse(await readFile(artifact, "utf8")) as {
failures: string[];
};
expect(report.failures).toEqual([
expect.stringContaining(
"duplicates topicId+namespace.namespaceId+namespace.namespaceVersion",
),
]);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
});