From 531b683a92ed12daa196abfef2debaaec309ee4e Mon Sep 17 00:00:00 2001 From: weichen Date: Mon, 12 Oct 2026 10:23:24 +0000 Subject: [PATCH] Add red contract tests for RDM management gating and payloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: U6 页面接线的门控/载荷是纯逻辑,先行红证模块缺失。 Evidence: npx vitest run test/rdm-management.test.ts → 1 failed (module not found);收据 /tmp/fe-u6-red-receipt.txt。 Impact: 仅测试文件。 Refs: 方案 r1 U6/KTD5 --- test/rdm-management.test.ts | 76 +++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 test/rdm-management.test.ts diff --git a/test/rdm-management.test.ts b/test/rdm-management.test.ts new file mode 100644 index 0000000..4fe890e --- /dev/null +++ b/test/rdm-management.test.ts @@ -0,0 +1,76 @@ +// U6 页面接线契约测试(方案 r1 U6/KTD5):RDM 管理/项目页面的能力门控与命令 +// 载荷构造是纯逻辑——先红后实现。门控语义与 rdmAuditReady 同源:命令清单不可用 +//(未登录/请求失败/null)一律视为未就绪(fail-closed),普通基座不出现 RDM 页面。 +import { describe, expect, it } from 'vitest' +import { + RDM_ROLES_COMMANDS, RDM_MEMBER_COMMANDS, RDM_DOC_REVIEW_COMMANDS, ROLE_ACTIONS, + rdmRolesReady, rdmMembersReady, rdmDocReviewReady, + roleCreatePayload, roleActionPayload, roleAssignPayload, roleDisablePayload, + memberAddPayload, memberRemovePayload, assignDocDutiesPayload, + docSubmitPayload, docApprovePayload, docRejectPayload, docFinalPayload, docStatusPayload, + 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(rdmDocReviewReady(all(RDM_DOC_REVIEW_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(rdmDocReviewReady(drop(RDM_DOC_REVIEW_COMMANDS, 'rdm.review.doc.final'))).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(rdmDocReviewReady('not-array' as unknown as never)).toBe(false) + }) +}) + +describe('RDM 动作清单(12 项,与 perms.AllActions 对齐)', () => { + it('精确等于 RDM 动作枚举且不重复', () => { + expect([...ROLE_ACTIONS].sort()).toEqual([ + 'duty.assign', 'doc.publish', 'doc.read', 'doc.write', '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' }) + }) + it('审阅人 CSV 解析:去空白、去空项、保序去重', () => { + expect(parseReviewers('bob, carol,,bob')).toEqual(['bob', 'carol']) + expect(parseReviewers(' ')).toEqual([]) + expect(parseReviewers('bob')).toEqual(['bob']) + }) +})