Skip to content

Commit bd3bbf6

Browse files
committed
test(client): pin the explicitly-undefined role as the same call as omission
A caller writing `invite({ email, role: maybeRole })` with an empty variable must reach the wire as `role: 'member'`, not as no role at all. This is the case that separates `{ ...req, role: req.role ?? 'member' }` from the `{ role: 'member', ...req }` spelling, which agrees with it on every status the driven cases can observe. Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2c90dad commit bd3bbf6

1 file changed

Lines changed: 27 additions & 4 deletions

File tree

‎packages/client/src/organization-invite-role-default.test.ts‎

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,16 @@
3939
* into the request, so a later "helpful" translation layer could add or rename
4040
* members without changing a type and without changing a status. ④ therefore
4141
* holds the request bytes to FULL-STRING equality — never `toContain`, which a
42-
* body carrying extra members would satisfy — and it is the guard that the
43-
* default is applied by SUBSTITUTION, not by clobbering: a spread written the
44-
* other way round (`{ role: 'member', ...req }`) passes ① and ③ and fails ②
45-
* and ④.
42+
* body carrying extra members would satisfy.
43+
*
44+
* It is also the guard that the default is applied by SUBSTITUTION rather than
45+
* by ordering. The tempting other spelling, `{ role: 'member', ...req }`, agrees
46+
* with the shipped one on every status cases ① – ③ can observe, and differs in
47+
* exactly two places ④ can: it re-orders the body, and — because a spread
48+
* copies an explicitly-`undefined` member over the default while `??` does not
49+
* — it puts a caller's `role: undefined` back on the wire as no `role` at all,
50+
* restoring the 400 for the caller who wrote `invite({ email, role: maybe })`.
51+
* ④c is that case.
4652
*
4753
* ## ⑤ pins the shape of the fix, not just its effect
4854
*
@@ -269,6 +275,23 @@ describe('#16582 the bytes organizations.invite puts on the wire', () => {
269275
JSON.stringify({ email: 'probe@example.com', role: 'admin', organizationId: 'org_probe' }),
270276
);
271277
});
278+
279+
it('④c an explicitly-undefined role is the same call as omitting it', async () => {
280+
const { client, fetchMock } = capturing();
281+
282+
// What `invite({ email, role: maybeRole })` compiles to when the variable
283+
// is empty — indistinguishable from omission to the caller, and it must be
284+
// indistinguishable on the wire too.
285+
await client.organizations.invite({
286+
email: 'probe@example.com',
287+
role: undefined,
288+
organizationId: 'org_probe',
289+
});
290+
291+
expect(soleBody(fetchMock)).toBe(
292+
JSON.stringify({ email: 'probe@example.com', role: 'member', organizationId: 'org_probe' }),
293+
);
294+
});
272295
});
273296

274297
// ─────────────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)