Skip to content

Commit ef45c2f

Browse files
os-litantclaude
andauthored
test(rest): type the seven unchecked route.handler call sites in rest.test.ts (#15582)
`rest.test.ts` held five `const res = { json: vi.fn(), ... }` literals whose enclosing `rest` / `route` binding was declared `any`, so `route.handler(req, res)` checked neither argument. No repair to argument 1 could ever surface them and `test-typecheck-debt.json` could never see them: they were green because nothing looked, not because they conformed. Typing the enclosing binding at all five turns that into a measurement: - 3 of them (the `filterAppForUser` gate, the dashboard widget gate, and the `readMeta` helper of the default-config gate) already supplied all four members `IHttpResponse` requires. They CONFORM — tsc reports nothing at those three call sites once argument 2 is actually checked. - 2 of them supplied only `json` and `status`, and go red with exactly the TS2345 `missing the following properties from type 'IHttpResponse': send, header` that #13454 repaired and cleared from the ledger. Repaired the same way: `httpResponseTestDouble()`. Typing the shared `getPutRoute(rest, path)` helper also exposed 2 further unchecked calls in the same describe block, at 1930 / 1954, that the census pattern `const res = { json: vi.fn()` cannot see at all: their response is an anonymous literal written at the argument position rather than bound to a `const`. Same class, same two missing members, same repair. Argument 1 is repaired at all seven with the package's own `httpRequestForRoute(route!, …)` (#13377), because tsc reports at most one argument-assignability error per call expression — leaving a request literal missing `query` / `method` / `path` would simply mask argument 2 again and put the sites back where they started. `test-typecheck-debt.json` is untouched and stays empty: the shrink-only ledger is not the place for an error a fixture can just stop having. ⛔ Not touched, deliberately: the 21 `res as any` sites (#13377 declined that census for the request half and left 151 sites on purpose), and the mirrored `makeRes()` fixtures — the three named files plus the in-file `invoke()` helper at 4141 — where a mirror keeps only the LAST status while `mock.calls` keeps every one, so substitution would change the assertion rather than the fixture. Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N Co-authored-by: Claude <noreply@anthropic.com>
1 parent d6ae0d5 commit ef45c2f

1 file changed

Lines changed: 40 additions & 33 deletions

File tree

‎packages/rest/src/rest.test.ts‎

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1876,8 +1876,8 @@ describe('createRestApiPlugin', () => {
18761876
// ---------------------------------------------------------------------------
18771877

18781878
describe('PUT /meta/:type/:name handler — header → request plumbing (PR-10d.4)', () => {
1879-
function getPutRoute(rest: any, path: string) {
1880-
return rest.getRoutes().find((r: any) => r.method === 'PUT' && r.path === path);
1879+
function getPutRoute(rest: RestServer, path: string) {
1880+
return rest.getRoutes().find((r) => r.method === 'PUT' && r.path === path);
18811881
}
18821882

18831883
it('forwards If-Match header as parentVersion to protocol.saveMetaItem', async () => {
@@ -1892,14 +1892,14 @@ describe('PUT /meta/:type/:name handler — header → request plumbing (PR-10d.
18921892
const route = getPutRoute(rest, '/api/v1/meta/:type/:name');
18931893
expect(route).toBeDefined();
18941894

1895-
const req = {
1895+
const req = httpRequestForRoute(route!, {
18961896
params: { type: 'view', name: 'cases' },
18971897
headers: { 'if-match': 'sha256:abc', 'x-actor': 'user_42' },
18981898
body: { name: 'cases', type: 'grid', label: 'X', columns: ['id'] },
1899-
};
1900-
const res = { json: vi.fn(), status: vi.fn().mockReturnThis() };
1899+
});
1900+
const res = httpResponseTestDouble();
19011901

1902-
await route.handler(req, res);
1902+
await route!.handler(req, res);
19031903

19041904
// [#7941] `actor` is the authenticated identity, NOT the `x-actor` header
19051905
// this request also sends. The header is retained in the fixture on
@@ -1927,13 +1927,13 @@ describe('PUT /meta/:type/:name handler — header → request plumbing (PR-10d.
19271927
rest.registerRoutes();
19281928
const route = getPutRoute(rest, '/api/v1/meta/:type/:name');
19291929

1930-
await route.handler(
1931-
{
1930+
await route!.handler(
1931+
httpRequestForRoute(route!, {
19321932
params: { type: 'view', name: 'cases' },
19331933
headers: { 'If-Match': '"sha256:xyz"' },
19341934
body: {},
1935-
},
1936-
{ json: vi.fn(), status: vi.fn().mockReturnThis() },
1935+
}),
1936+
httpResponseTestDouble(),
19371937
);
19381938

19391939
expect(protocol.saveMetaItem).toHaveBeenCalledWith(
@@ -1951,13 +1951,13 @@ describe('PUT /meta/:type/:name handler — header → request plumbing (PR-10d.
19511951
rest.registerRoutes();
19521952
const route = getPutRoute(rest, '/api/v1/meta/:type/:name');
19531953

1954-
await route.handler(
1955-
{
1954+
await route!.handler(
1955+
httpRequestForRoute(route!, {
19561956
params: { type: 'view', name: 'cases' },
19571957
headers: {},
19581958
body: {},
1959-
},
1960-
{ json: vi.fn(), status: vi.fn().mockReturnThis() },
1959+
}),
1960+
httpResponseTestDouble(),
19611961
);
19621962

19631963
const arg = (protocol.saveMetaItem as any).mock.calls[0][0];
@@ -1985,9 +1985,13 @@ describe('PUT /meta/:type/:name handler — header → request plumbing (PR-10d.
19851985
rest.registerRoutes();
19861986
const route = getPutRoute(rest, '/api/v1/meta/:type/:name');
19871987

1988-
const res = { json: vi.fn(), status: vi.fn().mockReturnThis() };
1989-
await route.handler(
1990-
{ params: { type: 'view', name: 'cases' }, headers: { 'if-match': 'sha256:stale' }, body: {} },
1988+
const res = httpResponseTestDouble();
1989+
await route!.handler(
1990+
httpRequestForRoute(route!, {
1991+
params: { type: 'view', name: 'cases' },
1992+
headers: { 'if-match': 'sha256:stale' },
1993+
body: {},
1994+
}),
19911995
res,
19921996
);
19931997
expect(res.status).toHaveBeenCalledWith(409);
@@ -3891,15 +3895,15 @@ describe('filterAppForUser — ADR-0057 D10 requiresService gate', () => {
38913895
{ id: 'nav_organizations', type: 'object', requiresService: 'org-scoping' },
38923896
] },
38933897
});
3894-
const rest: any = new RestServer(createMockServer() as any, protocol, ANON_API as any);
3895-
rest.resolveExecCtx = async () => ({ userId: 'u1', systemPermissions: [] });
3896-
rest.serviceExistsProvider = (n: string) => n !== 'org-scoping';
3898+
const rest = new RestServer(createMockServer() as any, protocol, ANON_API as any);
3899+
(rest as any).resolveExecCtx = async () => ({ userId: 'u1', systemPermissions: [] });
3900+
(rest as any).serviceExistsProvider = (n: string) => n !== 'org-scoping';
38973901
rest.registerRoutes();
38983902
const route = rest.getRoutes().find(
3899-
(r: any) => r.method === 'GET' && r.path === '/api/v1/meta/:type/:name',
3903+
(r) => r.method === 'GET' && r.path === '/api/v1/meta/:type/:name',
39003904
);
39013905
const res = { json: vi.fn(), status: vi.fn().mockReturnThis(), header: vi.fn(), send: vi.fn() };
3902-
await route.handler({ params: { type: 'app', name: 'setup' }, query: {}, headers: {} }, res);
3906+
await route!.handler(httpRequestForRoute(route!, { params: { type: 'app', name: 'setup' } }), res);
39033907

39043908
const body = res.json.mock.calls.at(-1)![0];
39053909
expect(body).toMatchObject({ type: 'app', name: 'setup', lock: 'none' });
@@ -3955,7 +3959,7 @@ describe('filterDashboardForUser — ADR-0057 D10 widget requiresService gate',
39553959
protocol.getMetaItem = vi.fn().mockResolvedValue({
39563960
type: 'dashboard', name: 'system_overview', item: dash(),
39573961
});
3958-
const rest: any = new RestServer(
3962+
const rest = new RestServer(
39593963
createMockServer() as any,
39603964
protocol,
39613965
// [#5881] `enableCache: false` is no longer what makes this reachable —
@@ -3966,14 +3970,17 @@ describe('filterDashboardForUser — ADR-0057 D10 widget requiresService gate',
39663970
// green proof the gate worked at all while the default path skipped it.
39673971
{ api: { requireAuth: false }, metadata: { enableCache: false } } as any,
39683972
);
3969-
rest.resolveExecCtx = async () => ({ userId: 'u1', systemPermissions: [] });
3970-
rest.serviceExistsProvider = (n: string) => n !== 'org-scoping';
3973+
(rest as any).resolveExecCtx = async () => ({ userId: 'u1', systemPermissions: [] });
3974+
(rest as any).serviceExistsProvider = (n: string) => n !== 'org-scoping';
39713975
rest.registerRoutes();
39723976
const route = rest.getRoutes().find(
3973-
(r: any) => r.method === 'GET' && r.path === '/api/v1/meta/:type/:name',
3977+
(r) => r.method === 'GET' && r.path === '/api/v1/meta/:type/:name',
39743978
);
39753979
const res = { json: vi.fn(), status: vi.fn().mockReturnThis(), header: vi.fn(), send: vi.fn() };
3976-
await route.handler({ params: { type: 'dashboard', name: 'system_overview' }, query: {}, headers: {} }, res);
3980+
await route!.handler(
3981+
httpRequestForRoute(route!, { params: { type: 'dashboard', name: 'system_overview' } }),
3982+
res,
3983+
);
39773984

39783985
const body = res.json.mock.calls.at(-1)![0];
39793986
expect(body).toMatchObject({ type: 'dashboard', name: 'system_overview' });
@@ -4030,18 +4037,18 @@ describe('filterDashboardForUser — ADR-0057 D10 widget requiresService gate',
40304037
};
40314038
/** Default config — no `metadata` block at all, so `enableCache` is its default. */
40324039
const defaultServer = (protocol: any) => {
4033-
const rest: any = new RestServer(createMockServer() as any, protocol, ANON_API as any);
4034-
rest.resolveExecCtx = async () => ({ userId: 'u1', systemPermissions: [] });
4035-
rest.serviceExistsProvider = (n: string) => n !== 'org-scoping';
4040+
const rest = new RestServer(createMockServer() as any, protocol, ANON_API as any);
4041+
(rest as any).resolveExecCtx = async () => ({ userId: 'u1', systemPermissions: [] });
4042+
(rest as any).serviceExistsProvider = (n: string) => n !== 'org-scoping';
40364043
rest.registerRoutes();
40374044
return rest;
40384045
};
4039-
const readMeta = async (rest: any, type: string, name: string) => {
4046+
const readMeta = async (rest: RestServer, type: string, name: string) => {
40404047
const route = rest.getRoutes().find(
4041-
(r: any) => r.method === 'GET' && r.path === '/api/v1/meta/:type/:name',
4048+
(r) => r.method === 'GET' && r.path === '/api/v1/meta/:type/:name',
40424049
);
40434050
const res = { json: vi.fn(), status: vi.fn().mockReturnThis(), header: vi.fn(), send: vi.fn() };
4044-
await route.handler({ params: { type, name }, query: {}, headers: {} }, res);
4051+
await route!.handler(httpRequestForRoute(route!, { params: { type, name } }), res);
40454052
return res;
40464053
};
40474054

0 commit comments

Comments
 (0)