Skip to content

Commit a73b4e1

Browse files
committed
test(cli): measure the parse-check layer through a name that reaches it
The #16726 charset gate answers first for `foo.bar`, so #16541's pin now measures its own subject through `class` — inside the charset, refused by the compiler — and keeps every `foo.bar` assertion that is still about the command. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DapQyvYrFb1MxSYe7BL2nt
1 parent 39448d9 commit a73b4e1

3 files changed

Lines changed: 76 additions & 19 deletions

File tree

packages/cli/src/commands/generate.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,14 @@ async function runMetadataGeneration(type: string, name: string, flags: { dir?:
760760
' write and the name that lands in the file are always the same string.',
761761
));
762762
console.log(chalk.dim(
763-
` Nothing was written. Try \`${CLI_ALIAS} g ${type} order_line\` or \`${CLI_ALIAS} g ${type} customer\`.`,
763+
// ⛔ The examples are deliberately NOT built from what the author
764+
// typed. A suggestion derived from the refused name is option (B)
765+
// wearing a prompt: the author accepts it, and the divergence this
766+
// gate exists to prevent arrives one keystroke later.
767+
` Nothing was written. Names like \`${CLI_ALIAS} g ${type} customer\` or`,
768+
));
769+
console.log(chalk.dim(
770+
` \`${CLI_ALIAS} g ${type} sales_order\` are accepted.`,
764771
));
765772
console.log('');
766773
process.exit(1);

packages/cli/test/generate-refuses-name-outside-charset.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,13 @@ describe('[#16726] the gate and #16541`s parse check are DISTINCT layers', () =>
240240
expect(failures.length).toBeGreaterThan(0);
241241
expect(backstop.code).toBe(1);
242242
expect(backstop.stdout).toContain('does not parse');
243-
// ⛔ The gate must not have shadowed it: the author gets the compiler's
244-
// reason, which is the specific one.
245-
expect(backstop.stdout).toContain("',' expected.");
243+
// ⛔ The gate must not have shadowed it: the author gets the COMPILER's
244+
// reason, and the compiler's reason for this name is specific to it —
245+
// a hand-written "invalid name" line would satisfy every other
246+
// assertion in this block.
247+
expect(backstop.stdout).toContain("'class' is not allowed as a variable declaration name.");
248+
// ⛔ And it is NOT the charset refusal: `class` is inside the charset.
249+
expect(backstop.stdout).not.toContain('must match pattern');
246250
});
247251

248252
it('⚠️ records what the pair actually does with `os generate view class`', () => {

packages/cli/test/generate-refuses-unparseable-name.test.ts

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,31 @@
3131
* `integration` (behaviour)"), and ⛔ nothing is renamed to make the two cuts
3232
* agree.
3333
*
34+
* ## ⚠️ WHICH NAME REACHES THIS CHECK CHANGED (#16726)
35+
*
36+
* A charset gate now sits in FRONT of this one: a name outside the charset
37+
* `packages/spec` declares for an object `name` is refused before anything is
38+
* derived from it, so it never reaches the compiler at all. `foo.bar` — the
39+
* card's measured name — is one of those, and it still exits 1 having written
40+
* nothing, which is the defect #16541 was filed about. What it no longer
41+
* demonstrates is THIS check: the refusal it now meets is the gate's.
42+
*
43+
* So the parse check is measured through `class`, added here for that purpose.
44+
* It is inside the charset (every character is a lowercase letter), the gate
45+
* admits it, and `const class:` is still not a declaration — so it is the name
46+
* that proves this command consults the compiler before it writes, and that
47+
* the layer in front did not swallow the layer behind. ⛔ Nothing was deleted
48+
* to make room for it: every `foo.bar` assertion that is still about the
49+
* COMMAND (exit code, no rewrite, nothing on disk) is asserted below unchanged.
50+
*
51+
* One property genuinely stopped being reachable from here: a name that breaks
52+
* the BARREL line as well as the scaffold. A reserved word is legal as an
53+
* `export { default as … }` alias, so no charset-legal name breaks both, and
54+
* every name that does is now stopped one layer earlier. That half stays
55+
* pinned where it still runs — `generate-emission-parses.test.ts`'s CANARY row
56+
* measures `foo.bar` against both emissions, with the instrument this command
57+
* calls.
58+
*
3459
* ## The control is load-bearing
3560
*
3661
* A refusal that fires on everything would satisfy every assertion about
@@ -115,28 +140,34 @@ function parseErrors(source: string): string[] {
115140
let refusedDir: string;
116141
let controlDir: string;
117142
let dryRunDir: string;
143+
let unparseableDir: string;
118144

119145
let refused: Run;
120146
let refusedAgain: Run;
121147
let control: Run;
122148
let dryRun: Run;
149+
let unparseable: Run;
123150

124151
beforeAll(async () => {
125152
refusedDir = mkdtempSync(join(tmpdir(), 'os-g-refuse-'));
126153
controlDir = mkdtempSync(join(tmpdir(), 'os-g-control-'));
127154
dryRunDir = mkdtempSync(join(tmpdir(), 'os-g-dryrun-'));
155+
unparseableDir = mkdtempSync(join(tmpdir(), 'os-g-unparseable-'));
128156

129157
// Sequential on purpose: cold tsx starts, each loading every command module,
130158
// in a container several agents share.
131159
refused = await runTsx([CLI, 'generate', 'object', 'foo.bar'], refusedDir);
132160
// A second generator, to show the refusal is not one patched call site.
133161
refusedAgain = await runTsx([CLI, 'generate', 'flow', 'foo.bar'], refusedDir);
134162
dryRun = await runTsx([CLI, 'generate', 'object', 'foo.bar', '--dry-run'], dryRunDir);
163+
// Inside the charset, outside the grammar — the name that reaches THIS
164+
// check now that a charset gate stands in front of it (#16726).
165+
unparseable = await runTsx([CLI, 'generate', 'object', 'class'], unparseableDir);
135166
control = await runTsx([CLI, 'generate', 'object', 'order_line'], controlDir);
136167
}, RUN_TIMEOUT_MS);
137168

138169
afterAll(() => {
139-
for (const dir of [refusedDir, controlDir, dryRunDir]) {
170+
for (const dir of [refusedDir, controlDir, dryRunDir, unparseableDir]) {
140171
rmSync(dir, { recursive: true, force: true });
141172
}
142173
});
@@ -147,21 +178,9 @@ describe('[#16541] `os generate object foo.bar` refuses instead of exiting 0', (
147178
expect(refused.code).toBe(1);
148179
});
149180

150-
it('says it is refusing, and says the emission does not parse', () => {
181+
it('says it is refusing, and names the value it refused', () => {
151182
expect(refused.stdout).toContain('Refusing to generate');
152-
expect(refused.stdout).toContain('does not parse');
153-
});
154-
155-
it('names BOTH files the name would have corrupted', () => {
156-
expect(refused.stdout).toContain('foo.bar.object.ts');
157-
expect(refused.stdout).toContain('index.ts');
158-
});
159-
160-
it('quotes the compiler`s own diagnostic rather than a restatement of it', () => {
161-
// The message TypeScript emits for a property access in a binding
162-
// position. Asserted because a hand-written "invalid name" line would pass
163-
// every other assertion in this block.
164-
expect(refused.stdout).toContain("',' expected.");
183+
expect(refused.stdout).toContain('foo.bar');
165184
});
166185

167186
it('⛔ does not rewrite the name into a legal-looking identifier', () => {
@@ -178,6 +197,33 @@ describe('[#16541] `os generate object foo.bar` refuses instead of exiting 0', (
178197
});
179198
});
180199

200+
describe('[#16541] the parse check still refuses, with the compiler`s own words', () => {
201+
// `class`, not `foo.bar` — see the header. Everything asserted here is what
202+
// #16541 asserted about `foo.bar` before the #16726 gate started answering
203+
// for that spelling first.
204+
it('exits non-zero and says the emission does not parse', () => {
205+
expect(unparseable.code).toBe(1);
206+
expect(unparseable.stdout).toContain('Refusing to generate');
207+
expect(unparseable.stdout).toContain('does not parse');
208+
});
209+
210+
it('names the file the name would have corrupted', () => {
211+
expect(unparseable.stdout).toContain('class.object.ts');
212+
});
213+
214+
it('quotes the compiler`s own diagnostic rather than a restatement of it', () => {
215+
// The message TypeScript emits for a reserved word in a binding position.
216+
// Asserted because a hand-written "invalid name" line would pass every
217+
// other assertion in this block — and because it proves the gate in front
218+
// did not answer for this name.
219+
expect(unparseable.stdout).toContain("'class' is not allowed as a variable declaration name.");
220+
});
221+
222+
it('writes nothing', () => {
223+
expect(existsSync(join(unparseableDir, 'src'))).toBe(false);
224+
});
225+
});
226+
181227
describe('[#16541] the refusal is one chokepoint, not one patched generator', () => {
182228
it('`os generate flow foo.bar` is refused the same way', () => {
183229
expect(refusedAgain.code).toBe(1);

0 commit comments

Comments
 (0)