Skip to content

Commit 099d2d8

Browse files
dmealingclaude
andcommitted
test(cli): catalog-conformance asserted the duplicate-name refusal without exercising it
The test was named "composition refuses a duplicate stable name" and its body was `expect(() => composeCatalog()).not.toThrow()`. That says the three live slices do not collide today — a true and much weaker statement than its name. Deleting the guard from `composeCatalog` left the whole file green, which a mutation run confirms: with the throw disabled, 8 of 9 passed, including that assertion. `composeCatalog` closes over the module-level `SLICES`, so the refusal was unreachable from a test at all. The composition is now `composeSlices(slices)` with `composeCatalog()` calling it with the live three; the guard is unchanged. Three assertions replace the one: the live slices still compose (kept, under an honest name), two slices registering the same name throw with BOTH package names in the message — "there is a collision" is not actionable when three packages write into one table — and a name registered by exactly one slice is kept, so the guard refuses a collision rather than any repeat visit. Re-running the same mutation now fails exactly that test and nothing else. Note for the record: this file is not a source-text test. Its one file read is `registry.json`, `JSON.parse`d into a typed model, and every other assertion runs `composeCatalog` / `listCatalog` / `packageOf` / `stableNameIndex` / `factory()`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011cQxyVuTkdPmAduLiedNEg
1 parent 3adfa2d commit 099d2d8

2 files changed

Lines changed: 55 additions & 5 deletions

File tree

‎server/typescript/packages/cli/src/lib/catalog.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,23 @@ export function catalogPackages(): string[] {
5454
* another.
5555
*/
5656
export function composeCatalog(): Record<string, GeneratorRegistryEntry> {
57+
return composeSlices(SLICES);
58+
}
59+
60+
/**
61+
* The composition itself, over any slice list.
62+
*
63+
* Separate from {@link composeCatalog} so the REFUSAL is reachable from a test. With the
64+
* live `SLICES` closed over, the only thing a test could assert is that today's three
65+
* packages happen not to collide — which stays true if the check above is deleted, so
66+
* the guard that makes a collision a build failure was never covered by anything.
67+
*/
68+
export function composeSlices(
69+
slices: ReadonlyArray<readonly [string, Record<string, GeneratorRegistryEntry>]>,
70+
): Record<string, GeneratorRegistryEntry> {
5771
const out: Record<string, GeneratorRegistryEntry> = {};
5872
const from: Record<string, string> = {};
59-
for (const [pkg, slice] of SLICES) {
73+
for (const [pkg, slice] of slices) {
6074
for (const [name, entry] of Object.entries(slice)) {
6175
if (name in out) {
6276
throw new Error(

‎server/typescript/packages/cli/test/catalog-conformance.test.ts‎

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { describe, it, expect } from "bun:test";
1414
import { existsSync, readFileSync } from "node:fs";
1515
import { dirname, join } from "node:path";
1616
import { GENERATOR_LAYERS, stableNameIndex } from "@metaobjectsdev/codegen-ts";
17-
import { composeCatalog, listCatalog, packageOf, catalogPackages } from "../src/lib/catalog.js";
17+
import { composeCatalog, composeSlices, listCatalog, packageOf, catalogPackages } from "../src/lib/catalog.js";
1818

1919
const PORT = "typescript" as const;
2020

@@ -93,12 +93,48 @@ describe("the composed TS catalog conforms to the canonical manifest", () => {
9393
}
9494
});
9595

96-
it("composition refuses a duplicate stable name", () => {
97-
// The live slices must not collide; the throw path is what makes that a build
98-
// failure rather than last-slice-wins.
96+
it("the live slices do not collide", () => {
9997
expect(() => composeCatalog()).not.toThrow();
10098
});
10199

100+
it("composition REFUSES a duplicate stable name, naming both packages", () => {
101+
// Asserted over `composeSlices` rather than `composeCatalog`, because the latter
102+
// closes over the three live slices and they do not collide. Passing it two slices
103+
// that DO is the only way to reach the guard — without this, deleting the throw
104+
// would leave every test in this file green while `meta gen --list` described one
105+
// generator and `meta gen` ran another.
106+
const catalog = composeCatalog();
107+
const [name, entry] = Object.entries(catalog)[0]!;
108+
109+
let thrown: Error | undefined;
110+
try {
111+
composeSlices([
112+
["@acme/first", { [name]: entry }],
113+
["@acme/second", { [name]: entry }],
114+
]);
115+
} catch (e) {
116+
thrown = e as Error;
117+
}
118+
119+
expect(thrown, "a duplicate stable name was accepted").toBeDefined();
120+
// The message has to name BOTH packages — "there is a collision" is not actionable
121+
// when three packages register into one table.
122+
expect(thrown!.message).toContain(`"${name}"`);
123+
expect(thrown!.message).toContain("@acme/first");
124+
expect(thrown!.message).toContain("@acme/second");
125+
});
126+
127+
it("composition keeps a name registered by exactly one slice", () => {
128+
// The other half of the guard: it must refuse a COLLISION, not any repeat visit.
129+
const catalog = composeCatalog();
130+
const [name, entry] = Object.entries(catalog)[0]!;
131+
const composed = composeSlices([
132+
["@acme/first", { [name]: entry }],
133+
["@acme/second", {}],
134+
]);
135+
expect(Object.keys(composed)).toEqual([name]);
136+
});
137+
102138
it("listCatalog() is grouped by layer, in the declared layer order", () => {
103139
const seen: string[] = [];
104140
for (const e of listCatalog()) {

0 commit comments

Comments
 (0)