Why: a lost write response was keeping the old duty version, so the next save replayed it, and a busy save could stay locked if another request advanced the sequence. Evidence: SELF vitest duty-draft 6 passed, tsc passed, knip 43, Playwright duty flows 3 passed. Impact: an unknown save reloads server roles, confirmation resets, and busy clears even if a later read is discarded. Refs: docs/proposals/2026-10-15-rdm-duty-matrix-rework.md Co-authored-by: Cursor <cursoragent@cursor.com>
74 lines
3.5 KiB
TypeScript
74 lines
3.5 KiB
TypeScript
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> = {}): 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,
|
|
})
|
|
})
|
|
})
|