import { describe, expect, it } from 'vitest' import { absorbServerDocs, applyCell, cellDuty, completeness, editCell, replacementNote, saveIssues, savePayload, type DutyDocDraft, } from '../src/duties/draft' function doc(over: Partial = {}): DutyDocDraft { return { docId: 'd1', title: '需求', code: 'UR-1', typeKey: 'design', stage: 'BD', dutyVersion: 3, activeRound: 0, legacySectionOverrides: 0, dirty: false, author: 'alice', reviewers: ['bob'], approver: 'carol', ...over, } } describe('文档职责草稿', () => { it('单元格由三权反推,三人视图读到同一份角色', () => { const roles = { author: 'alice', reviewers: ['bob', 'dave'], approver: 'carol' } expect(cellDuty(roles, 'alice')).toBe('author') expect(cellDuty(roles, 'bob')).toBe('reviewer') expect(cellDuty(roles, 'dave')).toBe('reviewer') expect(cellDuty(roles, 'carol')).toBe('approver') expect(cellDuty(roles, 'erin')).toBe('none') expect(completeness(roles)).toEqual({ authors: 1, reviewers: 2, approvers: 1, conflicts: 0 }) }) it('替换编制前先记住原编制,并保证一人一责', () => { const next = applyCell({ author: 'alice', reviewers: ['bob'], approver: 'carol' }, 'bob', 'author') expect(next.replacedAuthor).toBe('alice') expect(next.roles).toEqual({ author: 'bob', reviewers: [], approver: 'carol' }) expect(replacementNote({ author: 'alice', reviewers: ['bob'], approver: 'carol' }, 'bob', 'author')).toBe('将替换alice') }) it('清掉唯一编制后允许留在草稿,但禁止保存', () => { const cleared = editCell([doc()], 'd1', 'alice', 'none') expect(cleared[0].author).toBe('') expect(cleared[0].dirty).toBe(true) expect(saveIssues(cleared[0])).toContain('需指定编制人') expect(savePayload(cleared, false).issues.join(' ')).toContain('需指定编制人') }) it('过期服务端页不覆盖未保存草稿', () => { const dirty = doc({ author: 'erin', dirty: true, dutyVersion: 3 }) const absorbed = absorbServerDocs([dirty], [{ docId: 'd1', title: '需求', typeKey: 'design', dutyVersion: 4, }], [{ docId: 'd1', owner: 'alice', duty: 'author' }, { docId: 'd1', owner: 'bob', duty: 'reviewer' }, { docId: 'd1', owner: 'carol', duty: 'approver' }]) expect(absorbed[0].author).toBe('erin') expect(absorbed[0].dutyVersion).toBe(3) }) it('写入结果未知时采用服务端版本,不再留着旧草稿重放', () => { const dirty = doc({ author: 'erin', dirty: true, dutyVersion: 3 }) const absorbed = absorbServerDocs([dirty], [{ docId: 'd1', title: '需求', typeKey: 'design', dutyVersion: 4, }], [ { docId: 'd1', owner: 'alice', duty: 'author' }, { docId: 'd1', owner: 'bob', duty: 'reviewer' }, { docId: 'd1', owner: 'carol', duty: 'approver' }, ], true) expect(absorbed[0]).toMatchObject({ author: 'alice', dutyVersion: 4, dirty: false }) }) it('保存载荷是每份脏文档的完整目标,超过 20 份被拒绝', () => { const many = Array.from({ length: 21 }, (_, index) => doc({ docId: `d${index}`, dirty: true })) const blocked = savePayload(many, true) expect(blocked.issues).toContain('一次保存最多 20 份文档') const one = savePayload([doc({ dirty: true, activeRound: 2 })], true) expect(one.docs[0]).toMatchObject({ docId: 'd1', author: 'alice', reviewers: ['bob'], approver: 'carol', expectedDutyVersion: 3, confirmInvalidation: true, }) expect(one.docs[0]).not.toHaveProperty('accountIds') }) it('候选检索选中的 userId 随预览/批量保存以 accountIds{owner:userId} 携带,只含本文档职责人', () => { const known = { bob: 'u-bob', erin: 'u-erin' } const out = savePayload([ doc({ dirty: true }), doc({ docId: 'd2', dirty: true, author: 'dave', reviewers: ['frank'], approver: 'gina' }), ], false, known) expect(out.docs[0].accountIds).toEqual({ bob: 'u-bob' }) expect(out.docs[1]).not.toHaveProperty('accountIds') }) })