Skip to content

Commit 247a088

Browse files
test(dogfood): pin the route-generation tombstones on every door, tsc included (#26887)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8VsCYkn9rw8Z1i5vq5MHxL4)_ Fixes #26572 (slice 3, the last, item: rest-route-generation-tombstones; slices 1 and 2 landed as #26787 and #26829) Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:给 api-backend 清单最后一个 PARTIAL 条目 rest-route-generation-tombstones 补钉子:四个已退役的 routes.* 键在真实 RestServer 构造器和插件启动路径上都被拒绝,退役提示指向的替代接口确实存在,TypeScript 编写这些键会在 tsc 报错,空的 routes 块仍可构造;并更新该条目的 automated.ref、revision 和 history。 - 为什么改:18.2.0 Tier 1 扫描把这条判为 PARTIAL(每个键只测一个值、提示的替代接口和 tsc 报错都无钉子);补上后 #26572 的 11 个条目全部在判据层有钉子。 - 风险与回滚:只加一个测试文件和清单文本,不改任何产品代码;回滚就是 revert 本 PR。 - 先看:`packages/qa/dogfood/test/rest-route-generation-tombstones.test.ts`(构造器、插件路径、tsc 子进程),`docs/qa/platform-checklist/areas/api-backend.json`。 - 两分钟验证:`pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/rest-route-generation-tombstones.test.ts`,预期 10 个用例全绿。 - 需要你做的:无,席位落地。 ## What changes `api-backend.rest-route-generation-tombstones` was the last of the 11 items the release-18.2.0 Tier 1 sweep (#26468) scored PARTIAL on this card. It now carries a pin at each clause's oracle layer. The new `rest-route-generation-tombstones.test.ts` boots no stack: every clause's oracle is `test` or `build`, and the item prescribes a constructed `RestServer`. | Clause | What the pin asserts | |---|---| | acceptance[0] | `includeObjects`, `excludeObjects`, `nameTransform` at both former values (`'none'` and `'plural'`), and `overrides` each refuse at construction on a real `RestServer`. Each refusal names `RouteGenerationConfigSchema` and "`` `routes.KEY` was removed in @objectstack/spec 17 ``". | | acceptance[1] / negative[0] | The same five refusals happen through `createRestApiPlugin(...).start()`, beside a control that boots. | | acceptance[2] | Each prescription names a surface that exists. `enable.apiEnabled` / `enable.apiMethods` are declared on the strict `ObjectCapabilities`, and an undeclared key there is refused. `crud.dataPrefix` moves a real server's data routes. The data routes take `:object` as the path segment. `object_operation` parses as a declarative endpoint type. The 404/405 enforcement itself is `second-object-exposure.dogfood.test.ts`'s, over HTTP. | | acceptance[3] | Each key, authored as a `RestServerConfig` in a scratch TypeScript file, fails `tsc` with an error on the key's own line naming `[REMOVED] Key retired`. An empty block beside it type-checks clean. | | acceptance[4] | `routes: {}` constructs and normalizes to `{}`, and an unknown key inside `routes` is stripped rather than refused. | The item's `automated.ref` names the pin and what it asserts, `automated.kind` is `dogfood`, `revision` is bumped and a `history` row cites #26572. No clause text changed. No product code changed. ## Why The card's ruled scope (triage 6437722483) is to pin the 11 api-backend Tier 1 clauses the 18.2.0 sweep scored PARTIAL at the layer their oracle names. The pins go in `packages/qa/dogfood/**`, with the checklist item revisions bumped, in slices of about five items, and with no new CI lane. Slices 1 (#26787) and 2 (#26829) pinned ten items, and this slice pins the eleventh, so no item is left and this PR closes the card. What stays unpinned is what the items themselves already record as having no stock door: the activation door's metadata-plane 503 and its 501/503 durability arms (`api-backend.action-activation-door-contract` knownGaps), and the procedural scoring clauses the card excludes. ## Tests The touched test file on head bbe0f12, after merging `main` (1a8b7eb) and rebuilding the dogfood closure, under the verify lock: ``` pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/rest-route-generation-tombstones.test.ts Test Files 1 passed (1) Tests 10 passed (10) ``` Every expectation was first measured against the live answer (the refusal texts, a `tsc` run over an authored key), then checked against the clause text before it was written down. No ablation was run: the diff changes no tested behaviour. ## Acceptance notes - The tsc leg writes its scratch files to a temporary directory with `@objectstack/spec` linked into a local `node_modules`, the pattern `declarative-endpoint-authoring-refusal.dogfood.test.ts` uses for the CLI. It runs the dogfood package's own `typescript` bin, so nothing is written into the repository tree. - `RestServer` and `createRestApiPlugin` come through `@objectstack/runtime`'s re-export, as in slice 2. `@objectstack/rest` is not a dogfood dependency, and adding one would move `pnpm-lock.yaml`. - The file is named `*.test.ts`, not `*.dogfood.test.ts`, because it boots no app. It runs in the `isolated` project like the package's other non-boot conformance tests. ## Verification <details><summary>Gates (dispatch-gates --commands, reconciled with --ran)</summary> - `node scripts/check-platform-checklist.mjs`: exit 0, `OK — 16 areas, 329 items (324 active, 1 planned)`. - 55 derived commands on merge head a4e98b2 (main at 95f818d merged) after a closure rebuild: all 55 exit 0. On the first pass, `pnpm --filter @objectstack/dogfood run typecheck` caught a TS2339 in the new pin (`.then` on `start()`'s `void | Promise<void>`). It was fixed in a4e98b2, and the second pass was all green. - `pnpm --filter @objectstack/dogfood run typecheck`: exit 0. eslint over the touched file: exit 0. - `dispatch-gates --ran`: 55 derived families accounted for, 55 run, 0 NOT-MEASURED, 0 unrun. - After merging the next `main` (1a8b7eb), the derived list was unchanged. The closure was rebuilt, and `check-platform-checklist`, the dogfood typecheck and the test file were re-run on bbe0f12, all exit 0. </details> No changeset: `@objectstack/dogfood` is private and `docs/qa/**` ships in no package, so this PR carries `skip-changeset`. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (eslint over the touched file) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset. (nothing publishes) - [x] 6. `packages/spec` touched: n/a. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JQwxbWUpGqdpDRiD79LqXV --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbWUpGqdpDRiD79LqXV)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent cea0871 commit 247a088

2 files changed

Lines changed: 247 additions & 3 deletions

File tree

‎docs/qa/platform-checklist/areas/api-backend.json‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2789,7 +2789,7 @@
27892789
"title": "RestServerConfig.routes is an all-tombstone sub-object: every one of the four retired selectors refuses at construction with the prescription that names where the contract moved, while the empty block still constructs",
27902790
"since": "v17",
27912791
"status": "active",
2792-
"revision": 3,
2792+
"revision": 4,
27932793
"priority": "P2",
27942794
"surface": "build",
27952795
"personas": [
@@ -2861,8 +2861,8 @@
28612861
"stale-dist"
28622862
],
28632863
"automated": {
2864-
"kind": "unit",
2865-
"ref": "packages/rest/src/rest-sub-config-parse-not-cast.test.ts — per clause. acceptance[1] in full, by §B's case 'rejects `createRestApiPlugin({ api: { routes: { nameTransform: \"none\" } } }).start()` — a #14691 tombstone, through the plugin path' (§E's own plugin-path case drives batch.defaultAtomic, not a routes key). acceptance[0] by §E's 'refuses every `routes.*` key — the whole sub-object is tombstones', for one authored value per key: nameTransform is driven with 'none' only, so its former 'plural' value is driven by hand. That case matches `routes.KEY`, RouteGenerationConfigSchema and /was removed/, never the clause's full 'was removed in @objectstack/spec 17', so the release-naming half is read off the harness's refusal texts and the pin scores acceptance[0] partial. acceptance[4]'s empty-block half by §E's 'an empty `routes` sub-object still constructs — the tombstones refuse keys, not the block'; its other half, an unknown key inside `routes` stripped, is pinned for batch and metadata.endpoints only (§C's 'STRIPS an unknown key inside a sub-object rather than refusing it — the schemas are non-strict'), never for routes. No pin covers acceptance[2] (each prescription names a replacement that exists today) or acceptance[3] (the tsc error): drive them by hand"
2864+
"kind": "dogfood",
2865+
"ref": "packages/rest/src/rest-sub-config-parse-not-cast.test.ts — per clause. acceptance[1] in full, by §B's case 'rejects `createRestApiPlugin({ api: { routes: { nameTransform: \"none\" } } }).start()` — a #14691 tombstone, through the plugin path' (§E's own plugin-path case drives batch.defaultAtomic, not a routes key). acceptance[0] by §E's 'refuses every `routes.*` key — the whole sub-object is tombstones', for one authored value per key: nameTransform is driven with 'none' only, so its former 'plural' value is driven by hand. That case matches `routes.KEY`, RouteGenerationConfigSchema and /was removed/, never the clause's full 'was removed in @objectstack/spec 17', so the release-naming half is read off the harness's refusal texts and the pin scores acceptance[0] partial. acceptance[4]'s empty-block half by §E's 'an empty `routes` sub-object still constructs — the tombstones refuse keys, not the block'; its other half, an unknown key inside `routes` stripped, is pinned for batch and metadata.endpoints only (§C's 'STRIPS an unknown key inside a sub-object rather than refusing it — the schemas are non-strict'), never for routes. packages/qa/dogfood/test/rest-route-generation-tombstones.test.ts (#26572) carries what that file leaves open: acceptance[0] on a real RestServer for every key, nameTransform at 'none' AND 'plural', each refusal naming RouteGenerationConfigSchema and '`routes.KEY` was removed in @objectstack/spec 17'; acceptance[1] the same five through createRestApiPlugin(...).start(), beside a control that boots; acceptance[2] each prescription's surface — enable.apiEnabled / enable.apiMethods declared on the strict ObjectCapabilities, crud.dataPrefix moving a real RestServer's data routes, the :object path segment, and object_operation parsed by ApiEndpointSchema (the 404/405 enforcement itself is second-object-exposure.dogfood.test.ts's, over HTTP); acceptance[3] each key authored as a RestServerConfig in a scratch TypeScript file fails tsc with an error on the key's own line naming '[REMOVED] Key retired', beside a clean control; acceptance[4] routes: {} normalizing to {} and an unknown key inside routes stripped"
28662866
},
28672867
"source": [
28682868
"packages/spec/liveness/route_generation.json (the ADR-0049 ledger this item answers: all four properties dead, removed 2026-09-03 — the sub-object ships nothing but its refusals)",
@@ -2890,6 +2890,12 @@
28902890
"date": "2026-10-26",
28912891
"change": "automated.ref says which half of acceptance[0]'s message the §E case asserts, from QA run #25815 (release-18.2.0-tier1, at 6679644a), finding a0. Re-read at base f6c61932: 'refuses every `routes.*` key' asserts the key, RouteGenerationConfigSchema and /was removed/, not 'was removed in @objectstack/spec 17'; nameTransform 'none' only, as the ref already said. No clause changed",
28922892
"ref": "#25837"
2893+
},
2894+
{
2895+
"revision": 4,
2896+
"date": "2026-10-27",
2897+
"change": "acceptance[0]–[4] and negative[0] are pinned at the layers their oracles name (the release-18.2.0 Tier 1 sweep #26468 scored a0, a4 and n0 half-asserted and a2, a3 unpinned). The new rest-route-generation-tombstones.test.ts drives every retired key at every former value through the constructor and the plugin path, checks each prescription against the shipped spec and a real RestServer, runs tsc over each authored key, and holds the empty-block and unknown-key controls. automated.kind is dogfood; automated.ref names what it asserts. Measured at base 4c29f384. No clause changed",
2898+
"ref": "#26572"
28932899
}
28942900
]
28952901
},
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// [#26572] `api-backend.rest-route-generation-tombstones`, pinned at the layers
4+
// its oracles name. Every key of `RestServerConfig.routes` is a `retiredKey()`
5+
// tombstone, so the item asks whether the SERVER refuses each one (a real
6+
// RestServer, directly and through `createRestApiPlugin(...).start()`), whether
7+
// each prescription points at a surface that exists, whether authoring the key
8+
// is a `tsc` error too, and whether the empty block still constructs. The
9+
// 18.2.0 Tier 1 sweep (#26468) scored the item PARTIAL: one authored value
10+
// per key and no release-naming half (acceptance[0]), no prescription check
11+
// (acceptance[2]), no typecheck (acceptance[3]) and no unknown-key strip
12+
// inside `routes` (acceptance[4]).
13+
//
14+
// ## What each case pins
15+
//
16+
// - acceptance[0]: includeObjects, excludeObjects, nameTransform at BOTH
17+
// former values and overrides each refuse at construction, naming
18+
// `routes.<key>`, RouteGenerationConfigSchema and "was removed in
19+
// @objectstack/spec 17";
20+
// - acceptance[1] / negative[0]: the same five refusals through the plugin
21+
// path, beside a control that boots;
22+
// - acceptance[2]: each prescription's replacement is in the shipped surface
23+
// — `enable.apiEnabled` / `enable.apiMethods` declared on the object's
24+
// capabilities, `crud.dataPrefix` moving the data routes of a real
25+
// RestServer, and `object_operation` an executable declarative endpoint
26+
// type;
27+
// - acceptance[3]: authoring each key in TypeScript fails `tsc` with a
28+
// diagnostic on the key's own line, beside a clean control;
29+
// - acceptance[4]: `routes: {}` constructs and normalizes to `{}`, and an
30+
// unknown key inside it is stripped rather than refused.
31+
//
32+
// The enforcement half of acceptance[2] (apiEnabled: false → 404,
33+
// apiMethods → 405) is driven over HTTP by second-object-exposure.dogfood.test.ts;
34+
// this file does not boot a stack.
35+
36+
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
37+
import { spawnSync } from 'node:child_process';
38+
import { createRequire } from 'node:module';
39+
import { mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
40+
import { tmpdir } from 'node:os';
41+
import { dirname, join } from 'node:path';
42+
import { RestServer, createRestApiPlugin } from '@objectstack/runtime';
43+
import { ObjectCapabilities } from '@objectstack/spec/data';
44+
import { ApiEndpointSchema, CrudEndpointsConfigSchema } from '@objectstack/spec/api';
45+
46+
const require = createRequire(import.meta.url);
47+
const SPEC_ROOT = dirname(require.resolve('@objectstack/spec/package.json'));
48+
const TSC = join(dirname(require.resolve('typescript/package.json')), 'bin', 'tsc');
49+
50+
/** One authored value per retired key — nameTransform at both former enum values. */
51+
const RETIRED: Array<[key: string, value: unknown]> = [
52+
['includeObjects', ['showcase_account']],
53+
['excludeObjects', ['sys_log']],
54+
['nameTransform', 'none'],
55+
['nameTransform', 'plural'],
56+
['overrides', { showcase_account: { enabled: false } }],
57+
];
58+
59+
function mockServer() {
60+
return {
61+
get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn(), patch: vi.fn(), use: vi.fn(),
62+
listen: vi.fn().mockResolvedValue(undefined), close: vi.fn().mockResolvedValue(undefined),
63+
};
64+
}
65+
66+
function mockProtocol() {
67+
return {
68+
getDiscovery: vi.fn().mockResolvedValue({ version: 'v0', routes: { data: '', metadata: '', ui: '', auth: '/auth' } }),
69+
getMetaTypes: vi.fn().mockResolvedValue([]),
70+
getMetaItems: vi.fn().mockResolvedValue([]),
71+
getMetaItem: vi.fn().mockResolvedValue({}),
72+
findData: vi.fn().mockResolvedValue([]),
73+
};
74+
}
75+
76+
function construct(config: Record<string, unknown>): RestServer {
77+
const rest = new RestServer(mockServer() as any, mockProtocol() as any, { api: { requireAuth: false }, ...config } as any);
78+
rest.registerRoutes();
79+
return rest;
80+
}
81+
82+
function refusalOf(config: Record<string, unknown>): string {
83+
try {
84+
construct(config);
85+
} catch (err) {
86+
return (err as Error).message;
87+
}
88+
throw new Error(`constructed cleanly with ${JSON.stringify(config)} — a tombstone was stripped instead of refused`);
89+
}
90+
91+
/** The plugin's start context: just enough for `start()` to build a REST server. */
92+
function bootCtx() {
93+
const services: Record<string, unknown> = { 'http.server': mockServer(), protocol: mockProtocol() };
94+
return {
95+
registerService: vi.fn(),
96+
getService: vi.fn((name: string) => {
97+
if (name in services) return services[name];
98+
throw new Error(`Service '${name}' not found`);
99+
}),
100+
getServices: vi.fn(() => new Map(Object.entries(services))),
101+
hook: vi.fn(),
102+
trigger: vi.fn().mockResolvedValue(undefined),
103+
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
104+
getKernel: vi.fn(),
105+
} as any;
106+
}
107+
108+
function expectRefusal(message: string, key: string) {
109+
expect(message).toContain('`routes` does not satisfy `RouteGenerationConfigSchema`');
110+
expect(message).toContain(`\`routes.${key}\` was removed in @objectstack/spec 17`);
111+
}
112+
113+
describe('[#26572] rest-route-generation-tombstones — every retired selector refuses, on every door', () => {
114+
it('acceptance[0]: each key refuses at construction, whatever value is authored', () => {
115+
for (const [key, value] of RETIRED) {
116+
expectRefusal(refusalOf({ routes: { [key]: value } }), key);
117+
}
118+
});
119+
120+
it('acceptance[1]: the same refusals survive the plugin path, beside a control that boots', async () => {
121+
const control = createRestApiPlugin({ api: { routes: {} } } as any);
122+
await expect(control.start!(bootCtx())).resolves.toBeUndefined();
123+
124+
for (const [key, value] of RETIRED) {
125+
const plugin = createRestApiPlugin({ api: { routes: { [key]: value } } } as any);
126+
let message: string | undefined;
127+
try {
128+
await plugin.start!(bootCtx());
129+
} catch (err) {
130+
message = (err as Error).message;
131+
}
132+
expect(message, `plugin path started with routes.${key} — the tombstone was stripped`).toBeDefined();
133+
expectRefusal(message!, key);
134+
}
135+
});
136+
137+
it('acceptance[2]: each prescription names a surface that exists in the shipped spec and server', () => {
138+
// Exposure moved onto the object: both keys are DECLARED (the capabilities
139+
// object is strict, so an undeclared key would be refused, not kept).
140+
const caps = ObjectCapabilities.parse({ apiEnabled: false, apiMethods: ['get', 'list'] });
141+
expect(caps).toMatchObject({ apiEnabled: false, apiMethods: ['get', 'list'] });
142+
expect(() => ObjectCapabilities.parse({ apiExcluded: true })).toThrow();
143+
for (const key of ['includeObjects', 'excludeObjects', 'overrides']) {
144+
const message = refusalOf({ routes: { [key]: key === 'overrides' ? {} : [] } });
145+
expect(message).toContain('`enable.apiEnabled');
146+
expect(message).toContain('`enable.apiMethods`');
147+
}
148+
149+
// The base path is `crud.dataPrefix`, and a real server moves its data routes by it.
150+
expect(refusalOf({ routes: { overrides: {} } })).toContain('`crud.dataPrefix`');
151+
expect(CrudEndpointsConfigSchema.parse({ dataPrefix: '/records' }).dataPrefix).toBe('/records');
152+
const moved = construct({ crud: { dataPrefix: '/records' } }).getRoutes().map((r: any) => r.path);
153+
expect(moved).toContain('/api/v1/records/:object');
154+
expect(moved.filter((p: string) => p.startsWith('/api/v1/data/'))).toEqual([]);
155+
156+
// The object name is the path segment: the data routes take `:object`, no transform.
157+
expect(refusalOf({ routes: { nameTransform: 'none' } })).toContain('the object `name` is the canonical id on every surface');
158+
expect(construct({}).getRoutes().map((r: any) => r.path)).toContain('/api/v1/data/:object/:id');
159+
160+
// A custom path is a declarative `api` endpoint, and object_operation is an executable type.
161+
expect(refusalOf({ routes: { overrides: {} } })).toContain('a declarative `api` endpoint');
162+
const endpoint = ApiEndpointSchema.parse({
163+
name: 'custom_accounts',
164+
path: '/custom/accounts',
165+
method: 'GET',
166+
type: 'object_operation',
167+
objectParams: { object: 'showcase_account', operation: 'find' },
168+
});
169+
expect(endpoint.type).toBe('object_operation');
170+
});
171+
172+
it('acceptance[4]: `routes: {}` constructs and normalizes to {}, and an unknown key inside it is stripped', () => {
173+
const empty = construct({ routes: {} }) as unknown as { config: { routes: unknown } };
174+
expect(empty.config.routes).toEqual({});
175+
176+
const unknown = construct({ routes: { someUnknownKey: 1 } }) as unknown as { config: { routes: unknown } };
177+
expect(unknown.config.routes).toEqual({});
178+
});
179+
});
180+
181+
describe('[#26572] rest-route-generation-tombstones — acceptance[3]: authoring a retired key is a tsc error', () => {
182+
let dir: string;
183+
184+
beforeAll(() => {
185+
dir = mkdtempSync(join(tmpdir(), 'dogfood-26572-tsc-'));
186+
mkdirSync(join(dir, 'node_modules', '@objectstack'), { recursive: true });
187+
symlinkSync(SPEC_ROOT, join(dir, 'node_modules', '@objectstack', 'spec'), 'dir');
188+
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name: 'tsc-26572', type: 'module', private: true }));
189+
writeFileSync(join(dir, 'tsconfig.json'), JSON.stringify({
190+
compilerOptions: {
191+
strict: true, noEmit: true, skipLibCheck: true, types: [],
192+
module: 'nodenext', moduleResolution: 'nodenext', target: 'es2022',
193+
},
194+
include: ['*.ts'],
195+
}));
196+
});
197+
198+
afterAll(() => {
199+
if (dir) rmSync(dir, { recursive: true, force: true });
200+
});
201+
202+
/** Type-check one authored config; the key sits on line 3 of the file. */
203+
function typecheck(file: string, routes: string) {
204+
writeFileSync(join(dir, file), [
205+
"import type { RestServerConfig } from '@objectstack/spec/api';",
206+
'export const config: RestServerConfig = { routes: {',
207+
` ${routes}`,
208+
'} };',
209+
'',
210+
].join('\n'));
211+
const run = spawnSync(process.execPath, [TSC, '-p', 'tsconfig.json'], { cwd: dir, encoding: 'utf8' });
212+
return { status: run.status, out: `${run.stdout}${run.stderr}` };
213+
}
214+
215+
it('control: the empty block typechecks clean', () => {
216+
const empty = typecheck('control.ts', '');
217+
expect(empty.status, empty.out).toBe(0);
218+
});
219+
220+
it.each([
221+
['include-objects.ts', "includeObjects: ['showcase_account'],"],
222+
['exclude-objects.ts', "excludeObjects: ['sys_log'],"],
223+
['name-transform-none.ts', "nameTransform: 'none',"],
224+
['name-transform-plural.ts', "nameTransform: 'plural',"],
225+
['overrides.ts', 'overrides: { showcase_account: { enabled: false } },'],
226+
])('%s fails tsc on the key\'s own line', (file, routes) => {
227+
rmSync(join(dir, 'control.ts'), { force: true });
228+
const res = typecheck(file, routes);
229+
try {
230+
expect(res.status, res.out).not.toBe(0);
231+
expect(res.out).toMatch(new RegExp(`${file.replace('.', '\\.')}\\(3,\\d+\\): error TS\\d+`));
232+
// The retiredKey input type names the retirement in the diagnostic itself.
233+
expect(res.out).toContain('[REMOVED] Key retired');
234+
} finally {
235+
rmSync(join(dir, file), { force: true });
236+
}
237+
});
238+
});

0 commit comments

Comments
 (0)