mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
9120f3dab1
Inviting an address that is already an ACTIVE member of the workspace
showed the generic "invitation failed, try again later" copy, so the user
had no way to learn that retrying could never succeed.
The dialog already branches on B's allowlisted invite-create codes; the
gap was in the allowlist itself. B rejects that case with HTTP 409
`{"error":"invite_existing_member"}`, which was absent from
WORKSPACE_INVITE_CREATE_ERROR_ALIASES, so it degraded to the
transport-shaped `create_409` and fell through to the retry-later
fallback. Add the alias, and split the two 409 duplicate variants apart:
"already a team member" and "already has a pending invitation" call for
different next steps and no longer share one merged sentence.
The code -> copy mapping moves out of the component into
`collab/invite-error-copy.ts` as a pure code -> i18n-key function, so a
new B code has exactly one place to be taught and the mapping is
testable on its own.
Verified red on the alias-less baseline (4 failures across both specs)
and green after.
Record: recvrovm9Bcyy0 (V0.19.1 acceptance)
70 lines
3.0 KiB
TypeScript
70 lines
3.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { workspaceInviteErrorMessageKey } from '../src/collab/invite-error-copy';
|
|
import { en } from '../src/i18n/locales/en';
|
|
import { zhCN } from '../src/i18n/locales/zh-CN';
|
|
|
|
// Red-spec for the V0.19.1 acceptance bug (record recvrovm9Bcyy0): inviting an
|
|
// address that is ALREADY an active member of the workspace showed the generic
|
|
// "invitation failed, try again later" copy, so the user could not tell that
|
|
// retrying would never help. B rejects that case with HTTP 409
|
|
// `{"error":"invite_existing_member"}`; before the fix that code was absent
|
|
// from the client's allowlist, degraded to the transport-shaped `create_409`,
|
|
// and fell through to the retry-later fallback.
|
|
|
|
describe('workspaceInviteErrorMessageKey', () => {
|
|
it("names the reason when the address is already an active member (B's invite_existing_member)", () => {
|
|
expect(workspaceInviteErrorMessageKey('invite_existing_member')).toBe(
|
|
'workspaceInvite.errorAlreadyMember',
|
|
);
|
|
});
|
|
|
|
it.each(['already_member', 'invite_existing_member'])(
|
|
'maps the already-a-member code %s to member-specific copy',
|
|
(code) => {
|
|
expect(workspaceInviteErrorMessageKey(code)).toBe('workspaceInvite.errorAlreadyMember');
|
|
},
|
|
);
|
|
|
|
it.each(['active_pending_invite', 'invite_duplicate'])(
|
|
'maps the pending-invite code %s to invite-specific copy',
|
|
(code) => {
|
|
expect(workspaceInviteErrorMessageKey(code)).toBe('workspaceInvite.errorPendingInvite');
|
|
},
|
|
);
|
|
|
|
it('keeps the two 409 duplicate variants distinguishable to the user', () => {
|
|
const member = workspaceInviteErrorMessageKey('invite_existing_member');
|
|
const pending = workspaceInviteErrorMessageKey('invite_duplicate');
|
|
expect(member).not.toBe(pending);
|
|
for (const dict of [en, zhCN]) {
|
|
expect(dict[member]).not.toBe(dict[pending]);
|
|
// Neither reason may reuse the retry-later sentence — retrying cannot
|
|
// resolve either conflict.
|
|
expect(dict[member]).not.toBe(dict['workspaceInvite.submitFailed']);
|
|
expect(dict[pending]).not.toBe(dict['workspaceInvite.submitFailed']);
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
'workspace_seat_limit_reached',
|
|
'workspace_subscription_seat_allocation_unavailable',
|
|
])('routes the seat conflict %s to the seats copy', (code) => {
|
|
expect(workspaceInviteErrorMessageKey(code)).toBe('workspaceInvite.seatsExhaustedBody');
|
|
});
|
|
|
|
it.each([
|
|
['no_session', 'workspaceInvite.errorNoSession'],
|
|
['no_workspace', 'workspaceInvite.errorNoWorkspace'],
|
|
['create_unreachable', 'workspaceInvite.errorUnreachable'],
|
|
])('keeps the transport/session outcome %s on its own copy', (code, key) => {
|
|
expect(workspaceInviteErrorMessageKey(code)).toBe(key);
|
|
});
|
|
|
|
it.each([undefined, 'create_409', 'create_500', 'some_future_b_code'])(
|
|
'falls back to the generic retry copy for the unrecognized code %s',
|
|
(code) => {
|
|
expect(workspaceInviteErrorMessageKey(code)).toBe('workspaceInvite.submitFailed');
|
|
},
|
|
);
|
|
});
|