Files
Agent-System-Frontend/test/rdm-management.test.ts
weichenandCursor 1b7ba779f1 Show document duties in three shared views
Why: the settings page was mixing duty assignment with whole-document review and chapter objects. The three views need one draft and one save check.
Evidence: SELF vitest run of the frontend suite, 278 tests passed, including duty-draft and design-guard.
Impact: the duties tab assigns existing documents only. Submit, approve, reject, and final stay out of this page. Commands that are not ready keep the tab hidden.
Refs: feature/rdm-duty-matrix
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-15 07:20:48 +00:00

102 lines
6.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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')
})
})