Skip to content

Commit fb65988

Browse files
committed
test(plugin-auth): pin the import protocol members as unannotated, add changeset
The behavioural upsert tests already discriminate the CONSEQUENCE of the historical read — ablated to `args.query?.$filter ?? {}`, two of the 26 go red — but nothing in the repo can see the opt-out itself: re-annotating the parameter `any` leaves every test and every gate green, which is exactly how the hole this card closes stayed open. The new source pin is that guard. Claude-Session: https://claude.ai/code/session_01ToDPcx9AESFubJkDiFMtKW Co-authored-by: Claude <noreply@anthropic.com>
1 parent b28801c commit fb65988

2 files changed

Lines changed: 49 additions & 0 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/plugin-auth": patch
3+
---
4+
5+
fix(plugin-auth): let `ImportProtocolLike` type the admin import protocol's members (#17422)
6+
7+
`admin-import-users.ts` is the only hand-written in-repo implementor of the runner's `ImportProtocolLike`, and it annotated all three required members `args: any`. An explicit parameter annotation wins over the contextual type, so #16952's newly declared request dialect held every implementor except this one — the one with a demonstrated history: before #16950 this file read `args?.query?.$filter ?? {}`, the runner moved to the canonical spelling, the read went `undefined`, and the `?? {}` default degraded the import's duplicate probe into match-everything, so `POST /api/v1/auth/admin/import-users` updated the wrong users without a sound.
8+
9+
The three annotations are deleted, so `findData` / `createData` / `updateData` are typed by the contract they implement. Measured: with the annotations gone, reading a retired wire alias (`args.query?.$filter`) is `TS2339 Property '$filter' does not exist on type 'QueryInput'`; with `args: any` restored the identical probe type-checks at exit 0.
10+
11+
`FindDataRequest` declares `query` optional, so `findData` now states its refusal in code — a thrown `Error` carrying the already-registered `INVALID_REQUEST` code — instead of relying on an incidental `TypeError` from a property read on `undefined`. No `??` fallback and no optional chaining were added: both spell match-everything, which is the defect this closes.
12+
13+
No API, request body, response shape or exported signature changes. A caller that reaches `findData` through `runImport` always supplies `query`, so no supported call moves; only a protocol call that was already failing now fails with a code attached.

packages/plugins/plugin-auth/src/admin-import-users.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { describe, it, expect, vi } from 'vitest';
4+
import { readFileSync } from 'node:fs';
5+
import { dirname, resolve } from 'node:path';
6+
import { fileURLToPath } from 'node:url';
47
import { assertEngineUpdateDispatch } from '@objectstack/objectql';
58
import { runAdminImportUsers, IMPORT_USERS_MAX_ROWS, type IdentityImportDeps } from './admin-import-users.js';
69
import type { AdminActor } from './admin-user-endpoints.js';
710

811
const ACTOR: AdminActor = { id: 'admin-1', email: 'admin@example.com' };
912

13+
const HERE = dirname(fileURLToPath(import.meta.url));
14+
const IMPORT_USERS_SOURCE = readFileSync(resolve(HERE, 'admin-import-users.ts'), 'utf8');
15+
1016
function makeRequest(body: unknown): Request {
1117
return new Request('http://localhost/api/v1/auth/admin/import-users', {
1218
method: 'POST',
@@ -624,3 +630,33 @@ describe('runAdminImportUsers — CSV payloads', () => {
624630
expect(m.createUser.mock.calls.map((c) => c[0].body.email).sort()).toEqual(['c1@x.co', 'c2@x.co']);
625631
});
626632
});
633+
634+
/**
635+
* [#17422] The IMPLEMENTOR half of #16952's contract.
636+
*
637+
* `ImportProtocolLike` types the three required members, but an EXPLICIT
638+
* parameter annotation wins over a contextual type — so `findData(args: any)`
639+
* opts this file back out of the contract while `tsc --noEmit` stays green.
640+
* Measured on #17422 in this package: with the annotation restored, a probe
641+
* reading the retired wire alias (`args.query?.$filter` — the pre-#16950 read
642+
* whose `?? {}` default degraded the duplicate probe into match-everything)
643+
* type-checks at exit 0; with the annotation gone the same probe is
644+
* `TS2339 Property '$filter' does not exist on type 'QueryInput'`.
645+
*
646+
* ⇒ Nothing else in the repo can see that difference. The behavioural upsert
647+
* tests above discriminate the CONSEQUENCE (ablated to the historical read,
648+
* two of them go red) but not the opt-out itself: re-annotating the parameter
649+
* leaves every one of them green and every gate green. This is that guard.
650+
*/
651+
describe('[#17422] the import protocol literal is typed BY `ImportProtocolLike`', () => {
652+
it('binds the literal to the exported contract', () => {
653+
expect(IMPORT_USERS_SOURCE).toContain('const protocol: ImportProtocolLike = {');
654+
});
655+
656+
for (const member of ['findData', 'createData', 'updateData'] as const) {
657+
it(`leaves \`${member}\`'s parameter unannotated, so the contract types it`, () => {
658+
expect(IMPORT_USERS_SOURCE).toContain(`async ${member}(args) {`);
659+
expect(IMPORT_USERS_SOURCE).not.toMatch(new RegExp(`async\\s+${member}\\s*\\(\\s*args\\s*:`));
660+
});
661+
}
662+
});

0 commit comments

Comments
 (0)