|
1 | 1 | // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. |
2 | 2 |
|
3 | 3 | 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'; |
4 | 7 | import { assertEngineUpdateDispatch } from '@objectstack/objectql'; |
5 | 8 | import { runAdminImportUsers, IMPORT_USERS_MAX_ROWS, type IdentityImportDeps } from './admin-import-users.js'; |
6 | 9 | import type { AdminActor } from './admin-user-endpoints.js'; |
7 | 10 |
|
8 | 11 | const ACTOR: AdminActor = { id: 'admin-1', email: 'admin@example.com' }; |
9 | 12 |
|
| 13 | +const HERE = dirname(fileURLToPath(import.meta.url)); |
| 14 | +const IMPORT_USERS_SOURCE = readFileSync(resolve(HERE, 'admin-import-users.ts'), 'utf8'); |
| 15 | + |
10 | 16 | function makeRequest(body: unknown): Request { |
11 | 17 | return new Request('http://localhost/api/v1/auth/admin/import-users', { |
12 | 18 | method: 'POST', |
@@ -624,3 +630,33 @@ describe('runAdminImportUsers — CSV payloads', () => { |
624 | 630 | expect(m.createUser.mock.calls.map((c) => c[0].body.email).sort()).toEqual(['c1@x.co', 'c2@x.co']); |
625 | 631 | }); |
626 | 632 | }); |
| 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