// U6 页面接线契约测试(方案 r1 U6/KTD5):RDM 管理/项目页面的能力门控与命令 // 载荷构造是纯逻辑——先红后实现。门控语义与 rdmAuditReady 同源:命令清单不可用 //(未登录/请求失败/null)一律视为未就绪(fail-closed),普通基座不出现 RDM 页面。 import { describe, expect, it } from 'vitest' import { RDM_ROLES_COMMANDS, RDM_MEMBER_COMMANDS, RDM_DUTY_COMMANDS, ROLE_ACTIONS, rdmRolesReady, rdmMembersReady, rdmDutyReady, dutyMatrixPayload, dutyPreviewPayload, roleCreatePayload, roleActionPayload, roleAssignPayload, roleDisablePayload, memberAddPayload, memberRemovePayload, assignDocDutiesPayload, docSubmitPayload, docApprovePayload, docRejectPayload, docFinalPayload, docStatusPayload, RDM_PERM_MATRIX_COMMANDS, rdmPermMatrixReady, permCheckPayload, describeSource, parseReviewers, } from '../src/rdmManagement' const cmd = (id: string, enabled = true) => ({ id, enabled }) const all = (ids: readonly string[]) => ids.map(id => cmd(id)) describe('RDM 页面能力门控', () => { it('命令齐备且 enabled 才就绪', () => { expect(rdmRolesReady(all(RDM_ROLES_COMMANDS))).toBe(true) expect(rdmMembersReady(all(RDM_MEMBER_COMMANDS))).toBe(true) expect(rdmDutyReady(all(RDM_DUTY_COMMANDS))).toBe(true) }) it('缺一命令即未就绪', () => { const drop = (ids: readonly string[], skip: string) => ids.filter(i => i !== skip).map(id => cmd(id)) expect(rdmRolesReady(drop(RDM_ROLES_COMMANDS, 'rdm.role.list'))).toBe(false) expect(rdmMembersReady(drop(RDM_MEMBER_COMMANDS, 'rdm.project.member.add'))).toBe(false) expect(rdmDutyReady(drop(RDM_DUTY_COMMANDS, 'rdm.duty.matrix'))).toBe(false) }) it('disabled / null / 非数组 / 请求失败均未就绪(fail-closed)', () => { expect(rdmRolesReady(RDM_ROLES_COMMANDS.map(id => cmd(id, false)))).toBe(false) expect(rdmRolesReady(null)).toBe(false) expect(rdmMembersReady(undefined)).toBe(false) expect(rdmDutyReady('not-array' as unknown as never)).toBe(false) }) }) describe('RDM 动作清单(12 项,与 perms.AllActions 对齐)', () => { it('精确等于 RDM 动作枚举且不重复', () => { expect([...ROLE_ACTIONS].sort()).toEqual([ 'doc.publish', 'doc.read', 'doc.write', 'duty.assign', 'knowledge.publish', 'member.manage', 'project.archive', 'project.create', 'project.settings', 'review.participate', 'stage.advance', 'trace.edit', ]) expect(new Set(ROLE_ACTIONS).size).toBe(12) }) }) describe('命令载荷构造(身份只经认证会话——载荷不含任何身份字段)', () => { it('角色管理载荷', () => { expect(roleCreatePayload('工程师')).toEqual({ name: '工程师' }) expect(roleActionPayload('role_ab', 'doc.write', false, true)) .toEqual({ roleId: 'role_ab', action: 'doc.write', default: false, cap: true }) expect(roleAssignPayload('bob', 'role_ab')).toEqual({ owner: 'bob', roleId: 'role_ab' }) expect(roleDisablePayload('role_ab', true)).toEqual({ roleId: 'role_ab', disabled: true }) }) it('项目成员载荷(role 缺省 member)', () => { expect(memberAddPayload('p1', 'carol')).toEqual({ projectId: 'p1', member: 'carol', role: 'member' }) expect(memberAddPayload('p1', 'carol', 'owner')).toEqual({ projectId: 'p1', member: 'carol', role: 'owner' }) expect(memberRemovePayload('p1', 'carol')).toEqual({ projectId: 'p1', member: 'carol' }) }) it('文档职责与整文审签载荷', () => { expect(assignDocDutiesPayload('p1', 'd1', 'alice', ['bob', 'carol'], 'dave')) .toEqual({ projectId: 'p1', docId: 'd1', author: 'alice', reviewers: ['bob', 'carol'], approver: 'dave' }) expect(docSubmitPayload('p1', 'd1')).toEqual({ projectId: 'p1', docId: 'd1' }) expect(docApprovePayload('p1', 'd1', '通过')).toEqual({ projectId: 'p1', docId: 'd1', note: '通过' }) expect(docRejectPayload('p1', 'd1', '依据不足')).toEqual({ projectId: 'p1', docId: 'd1', reason: '依据不足' }) expect(docFinalPayload('p1', 'd1')).toEqual({ projectId: 'p1', docId: 'd1' }) expect(docFinalPayload('p1', 'd1', 3)).toEqual({ projectId: 'p1', docId: 'd1', round: 3 }) expect(docStatusPayload('p1', 'd1')).toEqual({ projectId: 'p1', docId: 'd1' }) expect(dutyMatrixPayload('p1', { view: 'matrix', docLimit: 10 })).toEqual({ projectId: 'p1', view: 'matrix', docLimit: 10 }) expect(dutyPreviewPayload('p1', [{ docId: 'd1', author: 'alice', reviewers: ['bob'], approver: 'dave', expectedDutyVersion: 2, confirmInvalidation: true }])) .toEqual({ projectId: 'p1', docs: [{ docId: 'd1', author: 'alice', reviewers: ['bob'], approver: 'dave', expectedDutyVersion: 2, confirmInvalidation: true }] }) }) it('审阅人 CSV 解析:去空白、去空项、保序去重', () => { expect(parseReviewers('bob, carol,,bob')).toEqual(['bob', 'carol']) expect(parseReviewers(' ')).toEqual([]) expect(parseReviewers('bob')).toEqual(['bob']) }) }) describe('项目权限矩阵页(rdm.perm.check 消费)', () => { it('命令面齐备才就绪(perm.check + 成员清单);缺一/disabled/清单不可用均未就绪', () => { const all2 = RDM_PERM_MATRIX_COMMANDS.map(id => ({ id, enabled: true })) expect(rdmPermMatrixReady(all2)).toBe(true) expect(rdmPermMatrixReady(RDM_PERM_MATRIX_COMMANDS.slice(1).map(id => ({ id, enabled: true })))).toBe(false) expect(rdmPermMatrixReady(RDM_PERM_MATRIX_COMMANDS.map(id => ({ id, enabled: false })))).toBe(false) expect(rdmPermMatrixReady(null)).toBe(false) }) it('矩阵查询载荷(身份只经认证会话)', () => { expect(permCheckPayload('p1', 'bob')).toEqual({ projectId: 'p1', member: 'bob' }) }) it('来源标签:解释读面的人话映射;未知来源回退原文', () => { expect(describeSource('admin')).toBe('系统管理员') expect(describeSource('prof-role-default')).toContain('角色') expect(describeSource('override')).toContain('覆盖') expect(describeSource('relation-default')).toContain('关系') expect(describeSource('non-member')).toContain('成员') expect(describeSource('weird')).toBe('weird') }) })