Files
Agent-System/web/src/lib/createProjectMembersPersist.test.ts
T
weichenandCursor c1bdfe3920 feat(web): persist exclusive reviewer duty on project create
Keep project_role as member/viewer, send document_duty=reviewer for USER_B, and clear the seed 李主管 reviewer so the assigned reviewer is the one that syncs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 21:57:53 +00:00

137 lines
4.0 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
buildInitialMemberUpsertPayload,
ensureCreatedProjectWithMembers,
persistInitialProjectMembers,
selectNonCreatorInitialMembers,
} from './createProjectMembersPersist';
test('selectNonCreatorInitialMembers skips creator only', () => {
const members = selectNonCreatorInitialMembers(
[
{ userId: 86, userName: 'A', role: 'creator' },
{ userId: 99, userName: 'B', role: 'reviewer' },
],
86,
);
assert.deepEqual(
members.map((m) => m.userId),
[99],
);
});
test('buildInitialMemberUpsertPayload maps reviewer UI role to member API role', () => {
assert.deepEqual(buildInitialMemberUpsertPayload({ userId: 99, userName: 'B', role: 'reviewer' }, 86), {
user_id: 99,
project_role: 'member',
document_duty: 'reviewer',
});
assert.deepEqual(buildInitialMemberUpsertPayload({ userId: 99, userName: 'B', role: 'viewer' }, 86), {
user_id: 99,
project_role: 'viewer',
document_duty: '',
});
});
test('persistInitialProjectMembers awaits each add and stops on failure', async () => {
const calls: number[] = [];
await assert.rejects(
async () =>
persistInitialProjectMembers({
projectId: 6071,
creatorUserId: 86,
initialMembers: [
{ userId: 86, userName: 'A', role: 'creator' },
{ userId: 2, userName: 'B', role: 'reviewer' },
{ userId: 3, userName: 'C', role: 'approver' },
],
addMember: async (_projectId, payload) => {
calls.push(payload.user_id);
if (payload.user_id === 3) {
throw new Error('add failed');
}
},
}),
/add failed/,
);
assert.deepEqual(calls, [2, 3]);
});
test('ensureCreatedProjectWithMembers skips recreate on bootstrap id and retries members', async () => {
let createCalls = 0;
const added: number[] = [];
const phases: string[] = [];
const first = await ensureCreatedProjectWithMembers({
bootstrapProjectId: null,
createProject: async () => {
createCalls += 1;
return { id: 6071 };
},
creatorUserId: 86,
initialMembers: [
{ userId: 86, userName: 'A', role: 'creator' },
{ userId: 99, userName: 'B', role: 'reviewer' },
],
addMember: async (_pid, payload) => {
added.push(payload.user_id);
},
onPhase: (phase) => {
phases.push(phase);
},
});
assert.equal(first.projectId, 6071);
assert.equal(first.createdNow, true);
assert.deepEqual(first.persistedUserIds, [99]);
assert.deepEqual(phases, ['creating', 'syncing_members']);
let failOnce = true;
await assert.rejects(
async () =>
ensureCreatedProjectWithMembers({
bootstrapProjectId: 6071,
createProject: async () => {
createCalls += 1;
return { id: 9999 };
},
creatorUserId: 86,
initialMembers: [
{ userId: 86, userName: 'A', role: 'creator' },
{ userId: 99, userName: 'B', role: 'reviewer' },
],
addMember: async () => {
if (failOnce) {
failOnce = false;
throw new Error('transient');
}
},
}),
/transient/,
);
assert.equal(createCalls, 1, 'must not recreate project on member retry');
const retry = await ensureCreatedProjectWithMembers({
bootstrapProjectId: 6071,
createProject: async () => {
createCalls += 1;
return { id: 9999 };
},
creatorUserId: 86,
initialMembers: [
{ userId: 86, userName: 'A', role: 'creator' },
{ userId: 99, userName: 'B', role: 'reviewer' },
],
addMember: async (_pid, payload) => {
added.push(payload.user_id);
},
onPhase: (phase) => {
phases.push(phase);
},
});
assert.equal(retry.createdNow, false);
assert.equal(retry.projectId, 6071);
assert.equal(createCalls, 1);
assert.ok(added.filter((id) => id === 99).length >= 2);
assert.deepEqual(phases, ['creating', 'syncing_members', 'syncing_members']);
});