fix(web): align workbench user ids with auth seed
Unify MOCK_USERS with backend auth (admin id=1), merge session identity on login, and migrate demo seeds/tests to id 2/3/4 for 张工/李/王. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
weichen
co-authored by
Cursor
parent
a8c8cba447
commit
636eb0ccf1
@@ -10,7 +10,7 @@ test('isTemplateOnly 交付物应正确识别为初始模板展示态', () => {
|
||||
title: '用户初始需求收集表',
|
||||
stage: 'V0.1',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: true,
|
||||
isTemplateOnly: true,
|
||||
|
||||
@@ -139,7 +139,7 @@ export const AppShell: React.FC<AppShellProps> = ({
|
||||
onOpenUserSettings={onOpenUserSettings}
|
||||
onOpenAdminSettings={onOpenAdminSettings}
|
||||
onOpenProjectMembers={onOpenProjectMembers}
|
||||
isAdmin={currentUser.roleId === 'role-admin' || currentUser.name.includes('管理员')}
|
||||
isAdmin={currentUser.roleId === 'role-admin'}
|
||||
/>
|
||||
|
||||
{!leftCollapsed && (
|
||||
|
||||
@@ -33,7 +33,7 @@ export const ProjectMembersModal: React.FC<ProjectMembersModalProps> = ({
|
||||
if (!isOpen || !project) return null;
|
||||
|
||||
const isCreator = currentUser.id === project.creatorId;
|
||||
const isAdmin = currentUser.id === 4 || currentUser.roleId === 'role-admin';
|
||||
const isAdmin = currentUser.roleId === 'role-admin';
|
||||
const canManage = isCreator || isAdmin;
|
||||
|
||||
// 筛选尚未加入该项目的人员
|
||||
|
||||
@@ -36,7 +36,7 @@ export const MOCK_DOCUMENTS: Record<string, MockDocumentMeta> = {
|
||||
title: '4K超高清荧光内窥镜系统立项与可行性报告',
|
||||
stage: 'V0.5',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
version: 'V1.0',
|
||||
lifecycleState: 'drafting',
|
||||
@@ -97,7 +97,7 @@ $$
|
||||
title: '微创外科内窥镜全球竞品技术与临床深度调研报告',
|
||||
stage: 'V0.5',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
version: 'V1.0',
|
||||
lifecycleState: 'reviewing',
|
||||
@@ -151,7 +151,7 @@ flowchart TD
|
||||
title: '4K荧光内窥镜光学分光与电路架构初步设计规格书',
|
||||
stage: 'V0.5',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
version: 'V1.0',
|
||||
lifecycleState: 'approving',
|
||||
@@ -272,7 +272,7 @@ $$
|
||||
title: '4K双路CMOS核心器件供应链与成本灵敏度分析 (项目共享)',
|
||||
stage: 'V0.5',
|
||||
category: 'shared',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
sharedAt: '2026-09-08 15:20',
|
||||
version: 'V1.0',
|
||||
@@ -311,7 +311,7 @@ $$
|
||||
title: '微创医疗战略商务谈判底线与预付款让利推演备忘 (仅我可见)',
|
||||
stage: 'V1.0',
|
||||
category: 'private',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
version: 'V0.1',
|
||||
lifecycleState: 'drafting',
|
||||
@@ -377,7 +377,7 @@ $$
|
||||
title: '数字化高频超声手术吸引系统立项需求与产品定义',
|
||||
stage: 'V0.1',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
version: 'V1.0',
|
||||
lifecycleState: 'approved',
|
||||
@@ -408,7 +408,7 @@ $$
|
||||
title: '数字化高频超声手术吸引系统立项与可行性法规论证报告',
|
||||
stage: 'V0.5',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
version: 'V1.0',
|
||||
lifecycleState: 'approved',
|
||||
@@ -445,7 +445,7 @@ $$
|
||||
title: '24kHz高频空化钛合金刀头微裂纹与疲劳剥落风险专项分析报告',
|
||||
stage: 'V0.5',
|
||||
category: 'shared',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
version: 'V1.0',
|
||||
lifecycleState: 'approved',
|
||||
@@ -478,7 +478,7 @@ $$
|
||||
title: 'CDO超声手术系统工程样机研发与型检预算投资核算单',
|
||||
stage: 'V1.0',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
version: 'V1.0',
|
||||
lifecycleState: 'drafting',
|
||||
|
||||
@@ -14,12 +14,12 @@ export const DEFAULT_PROJECTS: ProjectInfo[] = [
|
||||
title: '超声吸引系统 (NeuroPulse-CUSA)',
|
||||
deviceType: '数字化高频超声手术吸引系统',
|
||||
description: '神经外科颅内微创高频超声吸引与切割系统研发项目',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
createdAt: '2026-09-08 09:00',
|
||||
members: [
|
||||
{
|
||||
userId: 1,
|
||||
userId: 2,
|
||||
userName: '张工',
|
||||
department: '机器人与微创器械研发中心',
|
||||
position: '方案工程师',
|
||||
@@ -28,7 +28,7 @@ export const DEFAULT_PROJECTS: ProjectInfo[] = [
|
||||
joinedAt: '2026-09-08 09:00',
|
||||
},
|
||||
{
|
||||
userId: 2,
|
||||
userId: 3,
|
||||
userName: '李主管',
|
||||
department: '机器人与微创内窥镜研发中心',
|
||||
position: '研发项目技术主管',
|
||||
@@ -37,7 +37,7 @@ export const DEFAULT_PROJECTS: ProjectInfo[] = [
|
||||
joinedAt: '2026-09-08 09:10',
|
||||
},
|
||||
{
|
||||
userId: 3,
|
||||
userId: 4,
|
||||
userName: '王总监',
|
||||
department: '医疗器械研发事业部',
|
||||
position: '微创器械研发总监',
|
||||
@@ -52,12 +52,12 @@ export const DEFAULT_PROJECTS: ProjectInfo[] = [
|
||||
title: '数字化超声手术吸引系统 (CUSA-Ultra)',
|
||||
deviceType: '高频超声手术吸引切割系统',
|
||||
description: '多频段钛合金压电换能手术吸引切割设备研发',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
createdAt: '2026-09-07 14:00',
|
||||
members: [
|
||||
{
|
||||
userId: 1,
|
||||
userId: 2,
|
||||
userName: '张工',
|
||||
department: '机器人与微创器械研发中心',
|
||||
position: '方案工程师',
|
||||
@@ -66,7 +66,7 @@ export const DEFAULT_PROJECTS: ProjectInfo[] = [
|
||||
joinedAt: '2026-09-07 14:00',
|
||||
},
|
||||
{
|
||||
userId: 2,
|
||||
userId: 3,
|
||||
userName: '李主管',
|
||||
department: '机器人与微创内窥镜研发中心',
|
||||
position: '研发项目技术主管',
|
||||
@@ -75,7 +75,7 @@ export const DEFAULT_PROJECTS: ProjectInfo[] = [
|
||||
joinedAt: '2026-09-07 14:10',
|
||||
},
|
||||
{
|
||||
userId: 3,
|
||||
userId: 4,
|
||||
userName: '王总监',
|
||||
department: '医疗器械研发事业部',
|
||||
position: '微创器械研发总监',
|
||||
@@ -90,12 +90,12 @@ export const DEFAULT_PROJECTS: ProjectInfo[] = [
|
||||
title: `${p.name} (${p.shortName})`,
|
||||
deviceType: p.deviceType,
|
||||
description: `对标临床 ${p.customer},面向 ${p.targetMarket} 准入。${p.intendedUse}`,
|
||||
creatorId: p.creatorUserId || 1,
|
||||
creatorId: p.creatorUserId || 2,
|
||||
creatorName: '张工',
|
||||
createdAt: '2026-09-11 08:30',
|
||||
members: [
|
||||
{
|
||||
userId: 1,
|
||||
userId: 2,
|
||||
userName: '张工',
|
||||
department: '机器人与微创器械研发中心',
|
||||
position: '方案工程师',
|
||||
@@ -104,7 +104,7 @@ export const DEFAULT_PROJECTS: ProjectInfo[] = [
|
||||
joinedAt: '2026-09-11 08:30',
|
||||
},
|
||||
{
|
||||
userId: 2,
|
||||
userId: 3,
|
||||
userName: '李主管',
|
||||
department: '机器人与微创内窥镜研发中心',
|
||||
position: '研发项目技术主管',
|
||||
@@ -113,7 +113,7 @@ export const DEFAULT_PROJECTS: ProjectInfo[] = [
|
||||
joinedAt: '2026-09-11 08:40',
|
||||
},
|
||||
{
|
||||
userId: 3,
|
||||
userId: 4,
|
||||
userName: '王总监',
|
||||
department: '医疗器械研发事业部',
|
||||
position: '微创器械研发总监',
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -187,8 +187,8 @@ export function assertNoCrossUserChatLeak(
|
||||
}
|
||||
|
||||
function guessUserNameById(userId?: number): string | null {
|
||||
if (userId === 1) return '张工';
|
||||
if (userId === 2) return '李主管';
|
||||
if (userId === 3) return '王总监';
|
||||
if (userId === 2) return '张工';
|
||||
if (userId === 3) return '李主管';
|
||||
if (userId === 4) return '王总监';
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -14,33 +14,33 @@ import {
|
||||
import { ChatMessage } from '../types/workbench';
|
||||
|
||||
test('每人独立私聊:同一项目不同用户拥有不同 sessionId', () => {
|
||||
const zhang = DEFAULT_SESSIONS.filter((s) => s.userId === 1 && s.projectId === 3);
|
||||
const li = DEFAULT_SESSIONS.filter((s) => s.userId === 2 && s.projectId === 3);
|
||||
const wang = DEFAULT_SESSIONS.filter((s) => s.userId === 3 && s.projectId === 3);
|
||||
const zhang = DEFAULT_SESSIONS.filter((s) => s.userId === 2 && s.projectId === 3);
|
||||
const li = DEFAULT_SESSIONS.filter((s) => s.userId === 3 && s.projectId === 3);
|
||||
const wang = DEFAULT_SESSIONS.filter((s) => s.userId === 4 && s.projectId === 3);
|
||||
|
||||
assert.equal(zhang.length, 1);
|
||||
assert.equal(li.length, 1);
|
||||
assert.equal(wang.length, 1);
|
||||
assert.equal(zhang[0].sessionId, buildPrivateSessionId('cusa', 1));
|
||||
assert.equal(li[0].sessionId, buildPrivateSessionId('cusa', 2));
|
||||
assert.equal(wang[0].sessionId, buildPrivateSessionId('cusa', 3));
|
||||
assert.equal(zhang[0].sessionId, buildPrivateSessionId('cusa', 2));
|
||||
assert.equal(li[0].sessionId, buildPrivateSessionId('cusa', 3));
|
||||
assert.equal(wang[0].sessionId, buildPrivateSessionId('cusa', 4));
|
||||
assert.notEqual(zhang[0].sessionId, li[0].sessionId);
|
||||
});
|
||||
|
||||
test('左侧会话列表按当前用户过滤,不可见他人私聊', () => {
|
||||
const zhangView = filterSessionsForUser(DEFAULT_SESSIONS, 1);
|
||||
const liView = filterSessionsForUser(DEFAULT_SESSIONS, 2);
|
||||
const zhangView = filterSessionsForUser(DEFAULT_SESSIONS, 2);
|
||||
const liView = filterSessionsForUser(DEFAULT_SESSIONS, 3);
|
||||
|
||||
assert.ok(zhangView.every((s) => s.userId === 1));
|
||||
assert.ok(liView.every((s) => s.userId === 2));
|
||||
assert.ok(!zhangView.some((s) => s.sessionId === buildPrivateSessionId('cusa', 2)));
|
||||
assert.ok(!liView.some((s) => s.sessionId === buildPrivateSessionId('cusa', 1)));
|
||||
assert.ok(zhangView.every((s) => s.userId === 2));
|
||||
assert.ok(liView.every((s) => s.userId === 3));
|
||||
assert.ok(!zhangView.some((s) => s.sessionId === buildPrivateSessionId('cusa', 3)));
|
||||
assert.ok(!liView.some((s) => s.sessionId === buildPrivateSessionId('cusa', 2)));
|
||||
});
|
||||
|
||||
test('消息严格归属各自私聊 session,无跨用户混聊', () => {
|
||||
const zhangSess = buildPrivateSessionId('cusa', 1);
|
||||
const liSess = buildPrivateSessionId('cusa', 2);
|
||||
const wangSess = buildPrivateSessionId('cusa', 3);
|
||||
const zhangSess = buildPrivateSessionId('cusa', 2);
|
||||
const liSess = buildPrivateSessionId('cusa', 3);
|
||||
const wangSess = buildPrivateSessionId('cusa', 4);
|
||||
|
||||
const zhangMsgs = DEFAULT_MESSAGES.filter((m) => m.sessionId === zhangSess);
|
||||
const liMsgs = DEFAULT_MESSAGES.filter((m) => m.sessionId === liSess);
|
||||
@@ -50,13 +50,10 @@ test('消息严格归属各自私聊 session,无跨用户混聊', () => {
|
||||
assert.ok(liMsgs.length >= 4, '李主管私聊应有实质对话');
|
||||
assert.ok(wangMsgs.length >= 2, '王总监私聊应有实质对话');
|
||||
|
||||
// 张工窗口不得出现李主管/王总监作为 user 发言人
|
||||
assert.ok(!zhangMsgs.some((m) => m.role === 'user' && m.authorName === '李主管'));
|
||||
assert.ok(!zhangMsgs.some((m) => m.role === 'user' && m.authorName === '王总监'));
|
||||
// 李主管窗口不得出现张工/王总监 user 发言
|
||||
assert.ok(!liMsgs.some((m) => m.role === 'user' && m.authorName === '张工'));
|
||||
assert.ok(!liMsgs.some((m) => m.role === 'user' && m.authorName === '王总监'));
|
||||
// 王总监窗口不得出现张工/李主管 user 发言
|
||||
assert.ok(!wangMsgs.some((m) => m.role === 'user' && m.authorName === '张工'));
|
||||
assert.ok(!wangMsgs.some((m) => m.role === 'user' && m.authorName === '李主管'));
|
||||
|
||||
@@ -67,17 +64,17 @@ test('消息严格归属各自私聊 session,无跨用户混聊', () => {
|
||||
test('切换用户时 resolve 到该用户同项目私聊会话', () => {
|
||||
const next = resolveActiveSessionForUser(
|
||||
DEFAULT_SESSIONS,
|
||||
2,
|
||||
3,
|
||||
buildPrivateSessionId('cusa', 1)
|
||||
3,
|
||||
buildPrivateSessionId('cusa', 2)
|
||||
);
|
||||
assert.equal(next, buildPrivateSessionId('cusa', 2));
|
||||
assert.equal(next, buildPrivateSessionId('cusa', 3));
|
||||
});
|
||||
|
||||
test('resolve 优先有消息的私聊,跳过空壳 sess-p{N}-u{id}', () => {
|
||||
const shell = {
|
||||
sessionId: 'sess-p3-u1',
|
||||
userId: 1,
|
||||
sessionId: 'sess-p3-u2',
|
||||
userId: 2,
|
||||
projectId: 3,
|
||||
title: '数字化超声手术吸引系统 (CUSA-Ultra)',
|
||||
deviceType: '高频超声手术吸引切割系统',
|
||||
@@ -91,21 +88,21 @@ test('resolve 优先有消息的私聊,跳过空壳 sess-p{N}-u{id}', () => {
|
||||
for (const m of DEFAULT_MESSAGES) {
|
||||
counts[m.sessionId] = (counts[m.sessionId] || 0) + 1;
|
||||
}
|
||||
const next = resolveActiveSessionForUser(mixed, 1, 3, 'sess-p3-u1', counts);
|
||||
assert.equal(next, buildPrivateSessionId('cusa', 1));
|
||||
const next = resolveActiveSessionForUser(mixed, 2, 3, 'sess-p3-u2', counts);
|
||||
assert.equal(next, buildPrivateSessionId('cusa', 2));
|
||||
});
|
||||
|
||||
test('ensurePrivateSessionsForUser 为缺失用户补齐空私聊', () => {
|
||||
const onlyZhang = DEFAULT_SESSIONS.filter((s) => s.userId === 1);
|
||||
const ensured = ensurePrivateSessionsForUser(onlyZhang, 2);
|
||||
const liCusa = ensured.find((s) => s.userId === 2 && s.projectId === 3);
|
||||
const onlyZhang = DEFAULT_SESSIONS.filter((s) => s.userId === 2);
|
||||
const ensured = ensurePrivateSessionsForUser(onlyZhang, 3);
|
||||
const liCusa = ensured.find((s) => s.userId === 3 && s.projectId === 3);
|
||||
assert.ok(liCusa, '应为李主管补齐 CUSA 私聊会话');
|
||||
assert.equal(liCusa!.messageCount, 0);
|
||||
});
|
||||
|
||||
test('多项目会话消息隔离测试仍成立(endo vs ultrasound)', () => {
|
||||
const endo = buildPrivateSessionId('endo', 1);
|
||||
const ultra = buildPrivateSessionId('ultrasound', 1);
|
||||
const endo = buildPrivateSessionId('endo', 2);
|
||||
const ultra = buildPrivateSessionId('ultrasound', 2);
|
||||
const sess1Msgs = DEFAULT_MESSAGES.filter((m) => m.sessionId === endo);
|
||||
const sess2Msgs = DEFAULT_MESSAGES.filter((m) => m.sessionId === ultra);
|
||||
|
||||
|
||||
+47
-47
@@ -16,7 +16,7 @@ export const STORAGE_KEYS = {
|
||||
DELIVERABLES: `${STORAGE_PREFIX}deliverables`,
|
||||
DOC_CACHE: `${STORAGE_PREFIX}doc_cache`,
|
||||
ROLES: `${STORAGE_PREFIX}roles`,
|
||||
USERS: `${STORAGE_PREFIX}users`,
|
||||
USERS: `${STORAGE_PREFIX}users_v7`,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -43,17 +43,6 @@ export const DEFAULT_SESSIONS: ProjectSession[] = [
|
||||
...NEUROPULSE_SESSIONS,
|
||||
...TEN_PROJECTS_SESSIONS,
|
||||
// CUSA-Ultra:张工 / 李主管 / 王总监 各一条私聊
|
||||
{
|
||||
sessionId: buildPrivateSessionId('cusa', 1),
|
||||
userId: 1,
|
||||
projectId: 3,
|
||||
title: '数字化超声手术吸引系统 (CUSA-Ultra)',
|
||||
deviceType: '高频超声手术吸引切割系统',
|
||||
stage: 'V0.5',
|
||||
stageLabel: 'V0.5 方案成型与法规对齐',
|
||||
messageCount: 12,
|
||||
updatedAt: '刚刚',
|
||||
},
|
||||
{
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
userId: 2,
|
||||
@@ -62,7 +51,7 @@ export const DEFAULT_SESSIONS: ProjectSession[] = [
|
||||
deviceType: '高频超声手术吸引切割系统',
|
||||
stage: 'V0.5',
|
||||
stageLabel: 'V0.5 方案成型与法规对齐',
|
||||
messageCount: 8,
|
||||
messageCount: 12,
|
||||
updatedAt: '刚刚',
|
||||
},
|
||||
{
|
||||
@@ -73,13 +62,24 @@ export const DEFAULT_SESSIONS: ProjectSession[] = [
|
||||
deviceType: '高频超声手术吸引切割系统',
|
||||
stage: 'V0.5',
|
||||
stageLabel: 'V0.5 方案成型与法规对齐',
|
||||
messageCount: 8,
|
||||
updatedAt: '刚刚',
|
||||
},
|
||||
{
|
||||
sessionId: buildPrivateSessionId('cusa', 4),
|
||||
userId: 4,
|
||||
projectId: 3,
|
||||
title: '数字化超声手术吸引系统 (CUSA-Ultra)',
|
||||
deviceType: '高频超声手术吸引切割系统',
|
||||
stage: 'V0.5',
|
||||
stageLabel: 'V0.5 方案成型与法规对齐',
|
||||
messageCount: 4,
|
||||
updatedAt: '刚刚',
|
||||
},
|
||||
// 其他项目默认归属张工私聊
|
||||
{
|
||||
sessionId: buildPrivateSessionId('endo', 1),
|
||||
userId: 1,
|
||||
sessionId: buildPrivateSessionId('endo', 2),
|
||||
userId: 2,
|
||||
projectId: 1,
|
||||
title: '4K超高清荧光内窥镜系统',
|
||||
deviceType: '硬管荧光腹腔内窥镜',
|
||||
@@ -89,8 +89,8 @@ export const DEFAULT_SESSIONS: ProjectSession[] = [
|
||||
updatedAt: '14:20',
|
||||
},
|
||||
{
|
||||
sessionId: buildPrivateSessionId('ultrasound', 1),
|
||||
userId: 1,
|
||||
sessionId: buildPrivateSessionId('ultrasound', 2),
|
||||
userId: 2,
|
||||
projectId: 2,
|
||||
title: '便携式掌上无线超声诊断仪',
|
||||
deviceType: '便携式超声探头',
|
||||
@@ -112,14 +112,14 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
// ==================================================================================
|
||||
{
|
||||
id: 'm-cusa-0',
|
||||
sessionId: buildPrivateSessionId('cusa', 1),
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
role: 'system',
|
||||
content: '项目助手已就位,当前项目【数字化高频超声手术吸引系统 (CUSA-Ultra)】进入【V0.5 方案成型与法规对齐】阶段。全生命周期法定责任人已指定:编制人 张工、审核人 李主管、批准人 王总监。',
|
||||
timestamp: '16:00',
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-1',
|
||||
sessionId: buildPrivateSessionId('cusa', 1),
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
role: 'assistant',
|
||||
authorName: '项目助手',
|
||||
intentTag: '🎯 项目初始画像与责任人就绪',
|
||||
@@ -135,7 +135,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-2',
|
||||
sessionId: buildPrivateSessionId('cusa', 1),
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
role: 'user',
|
||||
authorName: '张工',
|
||||
content:
|
||||
@@ -144,7 +144,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-3',
|
||||
sessionId: buildPrivateSessionId('cusa', 1),
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
role: 'assistant',
|
||||
authorName: '项目助手',
|
||||
intentTag: '📚 监管法规与核心参数咨询',
|
||||
@@ -154,7 +154,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-4',
|
||||
sessionId: buildPrivateSessionId('cusa', 1),
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
role: 'user',
|
||||
authorName: '张工',
|
||||
content:
|
||||
@@ -163,7 +163,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-5',
|
||||
sessionId: buildPrivateSessionId('cusa', 1),
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
role: 'assistant',
|
||||
authorName: '项目助手',
|
||||
intentTag: '☕ 生活调剂与健康关怀',
|
||||
@@ -173,7 +173,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-6',
|
||||
sessionId: buildPrivateSessionId('cusa', 1),
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
role: 'user',
|
||||
authorName: '张工',
|
||||
content:
|
||||
@@ -182,7 +182,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-7',
|
||||
sessionId: buildPrivateSessionId('cusa', 1),
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
role: 'assistant',
|
||||
authorName: '项目助手',
|
||||
intentTag: '📝 编制人补充关键工程事实',
|
||||
@@ -199,14 +199,14 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
// --- 李主管私聊会话开场 ---
|
||||
{
|
||||
id: 'm-cusa-li-0',
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
sessionId: buildPrivateSessionId('cusa', 3),
|
||||
role: 'system',
|
||||
content: '项目助手已就位。您以【审核人·李主管】身份进入 CUSA-Ultra 项目私聊工作区;此处仅您与智能体可见,编制人/批准人对话不会出现在本窗口。',
|
||||
timestamp: '16:14',
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-li-1',
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
sessionId: buildPrivateSessionId('cusa', 3),
|
||||
role: 'assistant',
|
||||
authorName: '项目助手',
|
||||
intentTag: '🎯 审核人私聊就绪',
|
||||
@@ -215,7 +215,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-8',
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
sessionId: buildPrivateSessionId('cusa', 3),
|
||||
role: 'user',
|
||||
authorName: '李主管',
|
||||
content:
|
||||
@@ -224,7 +224,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-9',
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
sessionId: buildPrivateSessionId('cusa', 3),
|
||||
role: 'assistant',
|
||||
authorName: '项目助手',
|
||||
intentTag: '⚖️ 审核人合规与临床评价咨询',
|
||||
@@ -234,7 +234,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-10',
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
sessionId: buildPrivateSessionId('cusa', 3),
|
||||
role: 'user',
|
||||
authorName: '李主管',
|
||||
content:
|
||||
@@ -243,7 +243,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-11',
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
sessionId: buildPrivateSessionId('cusa', 3),
|
||||
role: 'assistant',
|
||||
authorName: '项目助手',
|
||||
intentTag: '🔬 流体力学物理方程学术推演',
|
||||
@@ -253,7 +253,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-12',
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
sessionId: buildPrivateSessionId('cusa', 3),
|
||||
role: 'user',
|
||||
authorName: '李主管',
|
||||
content:
|
||||
@@ -262,7 +262,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-13',
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
sessionId: buildPrivateSessionId('cusa', 3),
|
||||
role: 'assistant',
|
||||
authorName: '项目助手',
|
||||
intentTag: '🛡️ 审核人补充法规强标约束',
|
||||
@@ -272,7 +272,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-14',
|
||||
sessionId: buildPrivateSessionId('cusa', 1),
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
role: 'user',
|
||||
authorName: '张工',
|
||||
content:
|
||||
@@ -281,7 +281,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-15',
|
||||
sessionId: buildPrivateSessionId('cusa', 1),
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
role: 'assistant',
|
||||
authorName: '项目助手',
|
||||
intentTag: '⚡ 自由专项推演任务完成',
|
||||
@@ -297,7 +297,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-16',
|
||||
sessionId: buildPrivateSessionId('cusa', 1),
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
role: 'user',
|
||||
authorName: '张工',
|
||||
content:
|
||||
@@ -306,7 +306,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-17',
|
||||
sessionId: buildPrivateSessionId('cusa', 1),
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
role: 'assistant',
|
||||
authorName: '项目助手',
|
||||
intentTag: '🔒 文档锁定并提请合规审核',
|
||||
@@ -316,7 +316,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-18',
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
sessionId: buildPrivateSessionId('cusa', 3),
|
||||
role: 'user',
|
||||
authorName: '李主管',
|
||||
content:
|
||||
@@ -325,7 +325,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-19',
|
||||
sessionId: buildPrivateSessionId('cusa', 2),
|
||||
sessionId: buildPrivateSessionId('cusa', 3),
|
||||
role: 'assistant',
|
||||
authorName: '项目助手',
|
||||
intentTag: '📋 审核通过流转至终审',
|
||||
@@ -337,14 +337,14 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
// --- 王总监私聊会话开场 ---
|
||||
{
|
||||
id: 'm-cusa-wang-0',
|
||||
sessionId: buildPrivateSessionId('cusa', 3),
|
||||
sessionId: buildPrivateSessionId('cusa', 4),
|
||||
role: 'system',
|
||||
content: '项目助手已就位。您以【批准人·王总监】身份进入 CUSA-Ultra 项目私聊工作区;此处仅您与智能体可见。',
|
||||
timestamp: '16:44',
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-wang-1',
|
||||
sessionId: buildPrivateSessionId('cusa', 3),
|
||||
sessionId: buildPrivateSessionId('cusa', 4),
|
||||
role: 'assistant',
|
||||
authorName: '项目助手',
|
||||
intentTag: '🎯 批准人私聊就绪',
|
||||
@@ -353,7 +353,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-20',
|
||||
sessionId: buildPrivateSessionId('cusa', 3),
|
||||
sessionId: buildPrivateSessionId('cusa', 4),
|
||||
role: 'user',
|
||||
authorName: '王总监',
|
||||
content:
|
||||
@@ -362,7 +362,7 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
},
|
||||
{
|
||||
id: 'm-cusa-21',
|
||||
sessionId: buildPrivateSessionId('cusa', 3),
|
||||
sessionId: buildPrivateSessionId('cusa', 4),
|
||||
role: 'assistant',
|
||||
authorName: '项目助手',
|
||||
intentTag: '🎉 项目立项阶段终审批准发布',
|
||||
@@ -380,14 +380,14 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
// 张工私聊:4K超高清荧光内窥镜 (sess-endo-u1)
|
||||
{
|
||||
id: 'm-001-0',
|
||||
sessionId: buildPrivateSessionId('endo', 1),
|
||||
sessionId: buildPrivateSessionId('endo', 2),
|
||||
role: 'system',
|
||||
content: '项目助手已就位,当前项目进入【V0.5 方案成型与法规对齐】阶段。',
|
||||
timestamp: '14:00',
|
||||
},
|
||||
{
|
||||
id: 'm-001-1',
|
||||
sessionId: buildPrivateSessionId('endo', 1),
|
||||
sessionId: buildPrivateSessionId('endo', 2),
|
||||
role: 'assistant',
|
||||
authorName: '项目助手',
|
||||
intentTag: '🎯 推进立项阶段',
|
||||
@@ -417,14 +417,14 @@ export const DEFAULT_MESSAGES: ChatMessage[] = [
|
||||
// 张工私聊:便携式掌上无线超声诊断仪 (sess-ultrasound-u1)
|
||||
{
|
||||
id: 'm-002-0',
|
||||
sessionId: buildPrivateSessionId('ultrasound', 1),
|
||||
sessionId: buildPrivateSessionId('ultrasound', 2),
|
||||
role: 'system',
|
||||
content: '项目助手已就位,当前项目进入【V0.1 需求摸底与产品定义】阶段。',
|
||||
timestamp: '09:30',
|
||||
},
|
||||
{
|
||||
id: 'm-002-1',
|
||||
sessionId: buildPrivateSessionId('ultrasound', 1),
|
||||
sessionId: buildPrivateSessionId('ultrasound', 2),
|
||||
role: 'assistant',
|
||||
authorName: '项目助手',
|
||||
intentTag: '💡 掌超项目摸底',
|
||||
|
||||
@@ -53,7 +53,7 @@ const SAMPLE_DELIVERABLES: DeliverableItem[] = [
|
||||
title: '4K超高清荧光内窥镜立项报告',
|
||||
stage: 'V0.1',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: true,
|
||||
isTemplateOnly: false,
|
||||
@@ -89,7 +89,7 @@ const SAMPLE_DELIVERABLES: DeliverableItem[] = [
|
||||
title: 'Sony vs 长光辰芯 CMOS 传感器成本分析',
|
||||
stage: 'V0.5',
|
||||
category: 'shared',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
sharedAt: '2026-09-08 15:00',
|
||||
isFixedWorkflow: false,
|
||||
@@ -108,7 +108,7 @@ const SAMPLE_DELIVERABLES: DeliverableItem[] = [
|
||||
title: '张工个人商务底线与让利谈判备忘',
|
||||
stage: 'V1.0',
|
||||
category: 'private',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: false,
|
||||
isTemplateOnly: false,
|
||||
@@ -126,7 +126,7 @@ const SAMPLE_DELIVERABLES: DeliverableItem[] = [
|
||||
title: '李主管专属:团队工时饱和度与绩效评估',
|
||||
stage: 'V0.5',
|
||||
category: 'private',
|
||||
creatorId: 2,
|
||||
creatorId: 3,
|
||||
creatorName: '李主管',
|
||||
isFixedWorkflow: false,
|
||||
isTemplateOnly: false,
|
||||
@@ -140,9 +140,9 @@ const SAMPLE_DELIVERABLES: DeliverableItem[] = [
|
||||
];
|
||||
|
||||
test('三层交付物多用户隐私绝对隔离测试 (Multi-user Strict Privacy Isolation)', () => {
|
||||
// 1. 张工 (ID: 1) 登录:必须看到 2个工作流文档 + 1个共享文档 + 张工自己的私密文档 (共4个)
|
||||
// 1. 张工 (ID: 2) 登录:必须看到 2个工作流文档 + 1个共享文档 + 张工自己的私密文档 (共4个)
|
||||
// 严禁看到李主管的私密文档!
|
||||
const zhangView = filterVisibleDeliverables(SAMPLE_DELIVERABLES, 1);
|
||||
const zhangView = filterVisibleDeliverables(SAMPLE_DELIVERABLES, 2);
|
||||
assert.equal(zhangView.length, 4);
|
||||
assert.ok(zhangView.some((d) => d.docId === 'doc-wf-1'));
|
||||
assert.ok(zhangView.some((d) => d.docId === 'doc-wf-2'));
|
||||
@@ -150,9 +150,9 @@ test('三层交付物多用户隐私绝对隔离测试 (Multi-user Strict Privac
|
||||
assert.ok(zhangView.some((d) => d.docId === 'doc-priv-zhang'));
|
||||
assert.ok(!zhangView.some((d) => d.docId === 'doc-priv-li'), '张工界面中绝不可泄漏李主管的私密备忘');
|
||||
|
||||
// 2. 李主管 (ID: 2) 登录:必须看到 2个工作流文档 + 1个共享文档 + 李主管自己的私密文档 (共4个)
|
||||
// 2. 李主管 (ID: 3) 登录:必须看到 2个工作流文档 + 1个共享文档 + 李主管自己的私密文档 (共4个)
|
||||
// 严禁看到张工的私密文档!
|
||||
const liView = filterVisibleDeliverables(SAMPLE_DELIVERABLES, 2);
|
||||
const liView = filterVisibleDeliverables(SAMPLE_DELIVERABLES, 3);
|
||||
assert.equal(liView.length, 4);
|
||||
assert.ok(liView.some((d) => d.docId === 'doc-wf-1'));
|
||||
assert.ok(liView.some((d) => d.docId === 'doc-wf-2'));
|
||||
@@ -160,8 +160,8 @@ test('三层交付物多用户隐私绝对隔离测试 (Multi-user Strict Privac
|
||||
assert.ok(liView.some((d) => d.docId === 'doc-priv-li'));
|
||||
assert.ok(!liView.some((d) => d.docId === 'doc-priv-zhang'), '李主管界面中绝不可泄漏张工的私密谈判备忘');
|
||||
|
||||
// 3. 王总监 (ID: 3) 登录:王总监未生成任何私密文档,只应看到 2个工作流文档 + 1个共享文档 (共3个)
|
||||
const wangView = filterVisibleDeliverables(SAMPLE_DELIVERABLES, 3);
|
||||
// 3. 王总监 (ID: 4) 登录:王总监未生成任何私密文档,只应看到 2个工作流文档 + 1个共享文档 (共3个)
|
||||
const wangView = filterVisibleDeliverables(SAMPLE_DELIVERABLES, 4);
|
||||
assert.equal(wangView.length, 3);
|
||||
assert.ok(wangView.every((d) => d.category !== 'private'), '无私密文档的用户只看到公共受控与共享内容');
|
||||
});
|
||||
@@ -183,8 +183,8 @@ test('一键共享至项目参考库流转测试 (Promote to Shared Reference Te
|
||||
d.docId === sharedDoc.docId ? sharedDoc : d
|
||||
);
|
||||
|
||||
// 李主管 (ID: 2) 重新刷新视图:现在李主管可以查阅这份被共享出来的文档!
|
||||
const liViewAfterShare = filterVisibleDeliverables(updatedDeliverables, 2);
|
||||
// 李主管 (ID: 3) 重新刷新视图:现在李主管可以查阅这份被共享出来的文档!
|
||||
const liViewAfterShare = filterVisibleDeliverables(updatedDeliverables, 3);
|
||||
assert.ok(
|
||||
liViewAfterShare.some((d) => d.docId === 'doc-priv-zhang'),
|
||||
'一旦创建者一键共享,项目组成员李主管立即具有查阅权限'
|
||||
@@ -194,24 +194,24 @@ test('一键共享至项目参考库流转测试 (Promote to Shared Reference Te
|
||||
test('撤回共享恢复私有草稿测试 (Revoke Shared Reference Test)', () => {
|
||||
const sharedDoc = SAMPLE_DELIVERABLES.find((d) => d.docId === 'doc-sh-1')!;
|
||||
assert.equal(sharedDoc.category, 'shared');
|
||||
assert.equal(sharedDoc.creatorId, 1);
|
||||
assert.equal(sharedDoc.creatorId, 2);
|
||||
|
||||
// 1. 非创建者 (李主管 ID: 2) 尝试撤回:必须抛出权限越权异常
|
||||
// 1. 非创建者 (李主管 ID: 3) 尝试撤回:必须抛出权限越权异常
|
||||
assert.throws(
|
||||
() => revokeShareToPrivate(sharedDoc, 2),
|
||||
() => revokeShareToPrivate(sharedDoc, 3),
|
||||
/只有该共享文档的创建者本人方可撤回为私有/
|
||||
);
|
||||
|
||||
// 2. 创建者 (张工 ID: 1) 正确执行撤回
|
||||
const revokedDoc = revokeShareToPrivate(sharedDoc, 1);
|
||||
// 2. 创建者 (张工 ID: 2) 正确执行撤回
|
||||
const revokedDoc = revokeShareToPrivate(sharedDoc, 2);
|
||||
assert.equal(revokedDoc.category, 'private');
|
||||
assert.equal(revokedDoc.sharedAt, undefined);
|
||||
|
||||
// 3. 验证撤回后李主管 (ID: 2) 的视图已彻底不可见
|
||||
// 3. 验证撤回后李主管 (ID: 3) 的视图已彻底不可见
|
||||
const updatedDeliverables = SAMPLE_DELIVERABLES.map((d) =>
|
||||
d.docId === revokedDoc.docId ? revokedDoc : d
|
||||
);
|
||||
const liViewAfterRevoke = filterVisibleDeliverables(updatedDeliverables, 2);
|
||||
const liViewAfterRevoke = filterVisibleDeliverables(updatedDeliverables, 3);
|
||||
assert.ok(
|
||||
!liViewAfterRevoke.some((d) => d.docId === 'doc-sh-1'),
|
||||
'撤回私有后,其他成员立刻无法访问该文档'
|
||||
|
||||
@@ -9,7 +9,7 @@ test('集成验证:项目切换时交付物严格隔离,新建项目即刻
|
||||
projectId: 1,
|
||||
projectName: '4K超高清荧光内窥镜系统',
|
||||
deviceType: '硬管荧光腹腔内窥镜',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ test('集成验证:项目切换时交付物严格隔离,新建项目即刻
|
||||
projectId: 2,
|
||||
projectName: '便携式掌上无线超声诊断仪',
|
||||
deviceType: '便携式超声探头',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
});
|
||||
|
||||
@@ -64,7 +64,7 @@ test('集成验证:三层隐私与项目物理隔离双重约束', () => {
|
||||
projectId: 1,
|
||||
projectName: '项目A',
|
||||
deviceType: '器械A',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ test('集成验证:三层隐私与项目物理隔离双重约束', () => {
|
||||
projectId: 2,
|
||||
projectName: '项目B',
|
||||
deviceType: '器械B',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
});
|
||||
|
||||
@@ -84,7 +84,7 @@ test('集成验证:三层隐私与项目物理隔离双重约束', () => {
|
||||
title: '张工专属推演备忘',
|
||||
stage: 'V0.5',
|
||||
category: 'private',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: false,
|
||||
isTemplateOnly: false,
|
||||
@@ -103,7 +103,7 @@ test('集成验证:三层隐私与项目物理隔离双重约束', () => {
|
||||
title: '李主管专属预算底稿',
|
||||
stage: 'V0.5',
|
||||
category: 'private',
|
||||
creatorId: 2,
|
||||
creatorId: 3,
|
||||
creatorName: '李主管',
|
||||
isFixedWorkflow: false,
|
||||
isTemplateOnly: false,
|
||||
@@ -120,14 +120,14 @@ test('集成验证:三层隐私与项目物理隔离双重约束', () => {
|
||||
// 1. 张工在项目 1:可看 7 份工作流模板 + 自己的私密文档 = 8 份 (李主管私密文档绝对不可见)
|
||||
const zhangP1Visible = allDocs
|
||||
.filter((d) => d.projectId === 1)
|
||||
.filter((d) => d.category === 'workflow' || d.category === 'shared' || d.creatorId === 1);
|
||||
.filter((d) => d.category === 'workflow' || d.category === 'shared' || d.creatorId === 2);
|
||||
assert.strictEqual(zhangP1Visible.length, 8);
|
||||
assert.ok(!zhangP1Visible.some((d) => d.docId === 'doc-p1-priv-li'));
|
||||
|
||||
// 2. 张工在项目 2:仅可看项目 2 的 7 份模板,项目 1 的任何交付物(包括张工自己的私密文档)均不穿透
|
||||
const zhangP2Visible = allDocs
|
||||
.filter((d) => d.projectId === 2)
|
||||
.filter((d) => d.category === 'workflow' || d.category === 'shared' || d.creatorId === 1);
|
||||
.filter((d) => d.category === 'workflow' || d.category === 'shared' || d.creatorId === 2);
|
||||
assert.strictEqual(zhangP2Visible.length, 7);
|
||||
assert.ok(zhangP2Visible.every((d) => d.projectId === 2));
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ const sampleUsers: Record<string, UserProfile> = {
|
||||
roleId: 'role-admin',
|
||||
},
|
||||
director: {
|
||||
id: 3,
|
||||
id: 4,
|
||||
name: '王总监',
|
||||
employeeId: 'SMDF-0018',
|
||||
department: '医疗器械研发事业部',
|
||||
@@ -28,7 +28,7 @@ const sampleUsers: Record<string, UserProfile> = {
|
||||
roleId: 'role-director',
|
||||
},
|
||||
lead: {
|
||||
id: 2,
|
||||
id: 3,
|
||||
name: '李主管',
|
||||
employeeId: 'SMDF-0205',
|
||||
department: '机器人与微创内窥镜研发中心',
|
||||
@@ -36,7 +36,7 @@ const sampleUsers: Record<string, UserProfile> = {
|
||||
roleId: 'role-tech-lead',
|
||||
},
|
||||
engineer: {
|
||||
id: 1,
|
||||
id: 2,
|
||||
name: '张工',
|
||||
employeeId: 'SMDF-0821',
|
||||
department: '机器人与微创内窥镜研发中心',
|
||||
@@ -60,7 +60,7 @@ const createMockDoc = (overrides: Partial<DeliverableItem> = {}): DeliverableIte
|
||||
title: '测试受控交付物',
|
||||
stage: 'V0.1',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: true,
|
||||
isTemplateOnly: false,
|
||||
@@ -177,7 +177,7 @@ test('用例 6: 单文档覆盖 - public (放宽可见性,无 workflow:view_al
|
||||
test('用例 7: 私密文档隔离强红线 (category === private 无论任何管理员或单文档覆盖,非创建者绝对不可见)', () => {
|
||||
const privateDraft = createMockDoc({
|
||||
category: 'private',
|
||||
creatorId: 1, // 张工创建
|
||||
creatorId: 2, // 张工创建
|
||||
creatorName: '张工',
|
||||
visibilityRule: {
|
||||
mode: 'public', // 即使误设 public 也绝不能穿透 private 隔离
|
||||
@@ -232,7 +232,7 @@ test('用例 9: filterVisibleDeliverablesWithRBAC 能够批处理过滤大盘交
|
||||
docId: 'doc-3',
|
||||
title: '张工个人草案',
|
||||
category: 'private',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
}),
|
||||
];
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ test('集成场景 1: 动态新增合规审计员角色,并配置其仅能查
|
||||
title: '常规内部立项可行性报告',
|
||||
stage: 'V0.1',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: true,
|
||||
isTemplateOnly: false,
|
||||
@@ -63,7 +63,7 @@ test('集成场景 1: 动态新增合规审计员角色,并配置其仅能查
|
||||
title: '器械注册与临床评价白皮书 (开放审计)',
|
||||
stage: 'V0.5',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: true,
|
||||
isTemplateOnly: false,
|
||||
@@ -86,7 +86,7 @@ test('集成场景 1: 动态新增合规审计员角色,并配置其仅能查
|
||||
title: '张工敏感商业测算草案',
|
||||
stage: 'V0.1',
|
||||
category: 'private',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: false,
|
||||
isTemplateOnly: false,
|
||||
@@ -120,7 +120,7 @@ test('集成场景 2: 动态修改角色权限矩阵,权限即刻全量生效'
|
||||
assert.equal(engineerRole.permissions.includes('project:create'), false);
|
||||
|
||||
const zhangUser: UserProfile = {
|
||||
id: 1,
|
||||
id: 2,
|
||||
name: '张工',
|
||||
employeeId: 'SMDF-0821',
|
||||
department: '微创中心',
|
||||
@@ -146,7 +146,7 @@ test('集成场景 3: 单文档覆盖 designated_only 严格收紧与撤回恢
|
||||
const roles = DEFAULT_ROLES;
|
||||
|
||||
const zhangAuthor: UserProfile = {
|
||||
id: 1,
|
||||
id: 2,
|
||||
name: '张工',
|
||||
employeeId: 'SMDF-0821',
|
||||
department: '微创中心',
|
||||
@@ -155,7 +155,7 @@ test('集成场景 3: 单文档覆盖 designated_only 严格收紧与撤回恢
|
||||
};
|
||||
|
||||
const liReviewer: UserProfile = {
|
||||
id: 2,
|
||||
id: 3,
|
||||
name: '李主管',
|
||||
employeeId: 'SMDF-0205',
|
||||
department: '微创中心',
|
||||
@@ -179,7 +179,7 @@ test('集成场景 3: 单文档覆盖 designated_only 严格收紧与撤回恢
|
||||
title: '涉密荧光滤光片设计图',
|
||||
stage: 'V0.5',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: true,
|
||||
isTemplateOnly: false,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mergeSessionUser } from './mergeSessionUser';
|
||||
import { UserProfile } from '../types/docgov';
|
||||
|
||||
const catalogEngineer: UserProfile = {
|
||||
id: 1,
|
||||
name: '张工',
|
||||
employeeId: 'RD-001',
|
||||
department: '研发组',
|
||||
position: '工程师',
|
||||
roleId: 'role-engineer',
|
||||
projectPermissions: { 'doc-001': 'author' },
|
||||
};
|
||||
|
||||
test('mergeSessionUser prefers session identity over stale catalog entry', () => {
|
||||
const sessionAdmin: UserProfile = {
|
||||
id: 1,
|
||||
name: '系统管理员',
|
||||
employeeId: 'SYS-000',
|
||||
department: '系统运维部',
|
||||
position: '系统管理员',
|
||||
roleId: 'role-admin',
|
||||
};
|
||||
|
||||
const merged = mergeSessionUser(sessionAdmin, catalogEngineer);
|
||||
assert.equal(merged.id, 1);
|
||||
assert.equal(merged.name, '系统管理员');
|
||||
assert.equal(merged.roleId, 'role-admin');
|
||||
assert.deepEqual(merged.projectPermissions, { 'doc-001': 'author' });
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { UserProfile } from '../types/docgov';
|
||||
|
||||
/**
|
||||
* 登录/session 用户身份优先,演示目录仅补充 projectPermissions 等扩展字段。
|
||||
*/
|
||||
export function mergeSessionUser(sessionUser: UserProfile, catalog?: UserProfile): UserProfile {
|
||||
if (!catalog) {
|
||||
return sessionUser;
|
||||
}
|
||||
|
||||
return {
|
||||
...catalog,
|
||||
...sessionUser,
|
||||
projectPermissions: catalog.projectPermissions ?? sessionUser.projectPermissions,
|
||||
};
|
||||
}
|
||||
@@ -39,19 +39,19 @@ test('项目立项、成员职责分配与退出留痕治理测试套件', async
|
||||
id: 9001,
|
||||
title: '全磁悬浮人工心脏离心泵研发项目',
|
||||
deviceType: '植入式有源医疗器械',
|
||||
creatorId: 1, // 张工
|
||||
creatorId: 2, // 张工
|
||||
creatorName: '张工',
|
||||
createdAt: '2026-09-11 10:00',
|
||||
members: [
|
||||
{
|
||||
userId: 1,
|
||||
userId: 2,
|
||||
userName: '张工',
|
||||
role: 'creator',
|
||||
status: 'active',
|
||||
joinedAt: '2026-09-11 10:00',
|
||||
},
|
||||
{
|
||||
userId: 2,
|
||||
userId: 3,
|
||||
userName: '李主管',
|
||||
role: 'reviewer',
|
||||
status: 'active',
|
||||
@@ -63,7 +63,7 @@ test('项目立项、成员职责分配与退出留痕治理测试套件', async
|
||||
const allSessions: ProjectSession[] = [
|
||||
{
|
||||
sessionId: 'sess-p9001-u1',
|
||||
userId: 1,
|
||||
userId: 2,
|
||||
projectId: 9001,
|
||||
title: testProject.title,
|
||||
deviceType: testProject.deviceType,
|
||||
@@ -74,7 +74,7 @@ test('项目立项、成员职责分配与退出留痕治理测试套件', async
|
||||
},
|
||||
{
|
||||
sessionId: 'sess-p9001-u2',
|
||||
userId: 2,
|
||||
userId: 3,
|
||||
projectId: 9001,
|
||||
title: testProject.title,
|
||||
deviceType: testProject.deviceType,
|
||||
@@ -98,14 +98,14 @@ test('项目立项、成员职责分配与退出留痕治理测试套件', async
|
||||
|
||||
const currentProjects = [testProject];
|
||||
|
||||
// 张工 (userId: 1) 可见
|
||||
const zhangVisible = filterProjectSessionsForUser(allSessions, currentProjects, 1, false);
|
||||
// 张工 (userId: 2) 可见
|
||||
const zhangVisible = filterProjectSessionsForUser(allSessions, currentProjects, 2, false);
|
||||
assert.equal(zhangVisible.length, 1);
|
||||
assert.equal(zhangVisible[0].sessionId, 'sess-p9001-u1');
|
||||
assert.equal(zhangVisible[0].isExited, false);
|
||||
|
||||
// 李主管 (userId: 2) 可见
|
||||
const liVisible = filterProjectSessionsForUser(allSessions, currentProjects, 2, false);
|
||||
// 李主管 (userId: 3) 可见
|
||||
const liVisible = filterProjectSessionsForUser(allSessions, currentProjects, 3, false);
|
||||
assert.equal(liVisible.length, 1);
|
||||
assert.equal(liVisible[0].sessionId, 'sess-p9001-u2');
|
||||
assert.equal(liVisible[0].isExited, false);
|
||||
@@ -120,12 +120,12 @@ test('项目立项、成员职责分配与退出留痕治理测试套件', async
|
||||
id: 9002,
|
||||
title: '高精内窥镜荧光摄像系统',
|
||||
deviceType: '医用光学成像设备',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
createdAt: '2026-09-11 10:10',
|
||||
members: [
|
||||
{
|
||||
userId: 1,
|
||||
userId: 2,
|
||||
userName: '张工',
|
||||
role: 'creator',
|
||||
status: 'active',
|
||||
@@ -137,7 +137,7 @@ test('项目立项、成员职责分配与退出留痕治理测试套件', async
|
||||
const allSessions: ProjectSession[] = [
|
||||
{
|
||||
sessionId: 'sess-p9002-u1',
|
||||
userId: 1,
|
||||
userId: 2,
|
||||
projectId: 9002,
|
||||
title: project.title,
|
||||
deviceType: project.deviceType,
|
||||
@@ -148,7 +148,7 @@ test('项目立项、成员职责分配与退出留痕治理测试套件', async
|
||||
},
|
||||
{
|
||||
sessionId: 'sess-p9002-u3',
|
||||
userId: 3,
|
||||
userId: 4,
|
||||
projectId: 9002,
|
||||
title: project.title,
|
||||
deviceType: project.deviceType,
|
||||
@@ -159,21 +159,21 @@ test('项目立项、成员职责分配与退出留痕治理测试套件', async
|
||||
},
|
||||
];
|
||||
|
||||
// 添加王总监 (userId: 3) 为批准人
|
||||
// 添加王总监 (userId: 4) 为批准人
|
||||
project.members.push({
|
||||
userId: 3,
|
||||
userId: 4,
|
||||
userName: '王总监',
|
||||
role: 'approver',
|
||||
status: 'active',
|
||||
joinedAt: '2026-09-11 10:15',
|
||||
});
|
||||
|
||||
const wangVisible = filterProjectSessionsForUser(allSessions, [project], 3, false);
|
||||
const wangVisible = filterProjectSessionsForUser(allSessions, [project], 4, false);
|
||||
assert.equal(wangVisible.length, 1, '成为项目成员后会话即刻显现');
|
||||
assert.equal(wangVisible[0].sessionId, 'sess-p9002-u3');
|
||||
assert.equal(wangVisible[0].isExited, false);
|
||||
|
||||
const membership = getProjectMembership(project, 3);
|
||||
const membership = getProjectMembership(project, 4);
|
||||
assert.equal(membership?.role, 'approver', '职责分配必须正确记录');
|
||||
assert.equal(membership?.status, 'active', '状态必须为 active');
|
||||
});
|
||||
@@ -183,12 +183,12 @@ test('项目立项、成员职责分配与退出留痕治理测试套件', async
|
||||
id: 9003,
|
||||
title: '穿刺手术导航机器人系统',
|
||||
deviceType: '手术机器人与空间配准系统',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
createdAt: '2026-09-11 10:20',
|
||||
members: [
|
||||
{
|
||||
userId: 1,
|
||||
userId: 2,
|
||||
userName: '张工',
|
||||
role: 'creator',
|
||||
status: 'active',
|
||||
@@ -247,12 +247,12 @@ test('项目立项、成员职责分配与退出留痕治理测试套件', async
|
||||
id: 9004,
|
||||
title: '高度机密自研微创系统',
|
||||
deviceType: '核心技术研发',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
createdAt: '2026-09-11 10:35',
|
||||
members: [
|
||||
{
|
||||
userId: 1,
|
||||
userId: 2,
|
||||
userName: '张工',
|
||||
role: 'creator',
|
||||
status: 'active',
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('用户上传文档私密归档与共享流转测试', () => {
|
||||
assert.ok(deliv, `项目 ${pid} 必须存在 URS 交付物`);
|
||||
assert.equal(deliv.category, 'shared', '张工上传后已共享至项目协作参考库');
|
||||
assert.equal(deliv.space, 'project_shared', '必须在项目共享参考空间');
|
||||
assert.equal(deliv.creatorId, 1, '编制人必须为张工 (id=1)');
|
||||
assert.equal(deliv.creatorId, 2, '编制人必须为张工 (id=2)');
|
||||
|
||||
// 2. 检查 documents (AST)
|
||||
const doc = (seed.documents as any)[expectedDocId];
|
||||
@@ -50,7 +50,7 @@ describe('用户上传文档私密归档与共享流转测试', () => {
|
||||
// 6. 验证系统流程联动:提审待办派发与审核人处的连续两条助手消息 (第一条专业回复,第二条确认卡)
|
||||
seed.projects.forEach((proj: any) => {
|
||||
const pid = proj.id;
|
||||
const u2Sess = seed.sessions.find((s: any) => s.projectId === pid && s.userId === 2);
|
||||
const u2Sess = seed.sessions.find((s: any) => s.projectId === pid && s.userId === 3);
|
||||
assert.ok(u2Sess, `项目 ${pid} 必须存在李主管会话`);
|
||||
const u2Msgs = seed.messages.filter((m: any) => m.sessionId === u2Sess!.sessionId);
|
||||
assert.equal(u2Msgs.length, 7, `李主管会话必须包含 7 条完整流转消息`);
|
||||
@@ -59,7 +59,7 @@ describe('用户上传文档私密归档与共享流转测试', () => {
|
||||
assert.equal(u2Msgs[4].role, 'assistant', '第 5 条连续为助手消息');
|
||||
assert.ok(u2Msgs[4].interactiveCard, '连续第 2 条助手消息必须挂载初审确认卡');
|
||||
|
||||
const u3Sess = seed.sessions.find((s: any) => s.projectId === pid && s.userId === 3);
|
||||
const u3Sess = seed.sessions.find((s: any) => s.projectId === pid && s.userId === 4);
|
||||
assert.ok(u3Sess, `项目 ${pid} 必须存在王总监会话`);
|
||||
const u3Msgs = seed.messages.filter((m: any) => m.sessionId === u3Sess!.sessionId);
|
||||
assert.equal(u3Msgs.length, 7, `王总监会话必须包含 7 条完整流转消息`);
|
||||
|
||||
@@ -60,7 +60,7 @@ export const testUser: UserProfile = {
|
||||
|
||||
export const testSession: ProjectSession = {
|
||||
sessionId: 'sess-001',
|
||||
userId: 1,
|
||||
userId: 2,
|
||||
projectId: 1,
|
||||
title: '4K超高清荧光内窥镜系统',
|
||||
deviceType: '硬管荧光腹腔内窥镜',
|
||||
@@ -123,7 +123,7 @@ export const testDeliverable: DeliverableItem = {
|
||||
title: '注册可行性评估报告',
|
||||
stage: 'V0.5',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: true,
|
||||
isTemplateOnly: false,
|
||||
@@ -143,7 +143,7 @@ test('DeliverableItem 必须包含 projectId 与 isTemplateOnly 属性', () => {
|
||||
title: '用户初始需求收集表',
|
||||
stage: 'V0.1',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorId: 2,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: true,
|
||||
isTemplateOnly: true,
|
||||
|
||||
Reference in New Issue
Block a user