feat(review): 重构项目级单方案验收协作
This commit is contained in:
@@ -1,44 +1,67 @@
|
||||
process.env.NODE_ENV = 'test';
|
||||
delete process.env.DATABASE_URL;
|
||||
|
||||
const { db } = await import('../api/db.js');
|
||||
const { db, repairMultiSchemeRounds } = await import('../api/db.js');
|
||||
const { database, closeDatabase } = await import('../api/database.js');
|
||||
const { decideCandidateInTransaction, ReviewDecisionError } = await import('../api/services/reviewService.js');
|
||||
const { decideRoundInTransaction, ReviewDecisionError } = await import('../api/services/reviewService.js');
|
||||
const { addFeedbackReply, findFeedbackTarget, withdrawFeedback } = await import('../api/services/feedbackService.js');
|
||||
|
||||
function assert(condition: unknown, message: string): asserts condition {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
function assert(condition: unknown, message: string): asserts condition { if (!condition) throw new Error(message); }
|
||||
|
||||
db.exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
const project = await database.one<{ id: number }>('SELECT id FROM projects ORDER BY id LIMIT 1');
|
||||
assert(project, 'SQLite 测试需要至少一个项目');
|
||||
const collectionId = await database.insertId("INSERT INTO collections (project_id, name, status) VALUES (?, ?, 'reviewing')", [project.id, `候选稿测试 ${Date.now()}`]);
|
||||
const noteId = await database.insertId("INSERT INTO notes (collection_id, title, review_status) VALUES (?, ?, 'pending')", [collectionId, '候选稿 A']);
|
||||
const roundId = await database.insertId("INSERT INTO review_rounds (note_id, round_number, status) VALUES (?, 1, 'reviewing')", [noteId]);
|
||||
await database.execute('UPDATE notes SET active_round_id = ? WHERE id = ?', [roundId, noteId]);
|
||||
await database.execute("INSERT INTO work_versions (note_id, version_number, title, review_status, review_round_id, candidate_name, candidate_status) VALUES (?, 1, ?, 'pending', ?, ?, 'pending')", [noteId, '候选稿 A', roundId, '方案 A']);
|
||||
await database.execute("INSERT INTO work_versions (note_id, version_number, title, review_status, review_round_id, candidate_name, candidate_status) VALUES (?, 2, ?, 'pending', ?, ?, 'pending')", [noteId, '候选稿 B', roundId, '方案 B']);
|
||||
const group = await database.one<{ id: number }>('SELECT id FROM operation_groups ORDER BY id LIMIT 1');
|
||||
assert(group, 'SQLite 测试需要至少一个运营组');
|
||||
const project = { id: await database.insertId("INSERT INTO projects (group_id,name,slug) VALUES (?,?,?)", [group.id, '单轮验收测试', `round-test-${Date.now()}`]) };
|
||||
const collectionId = await database.insertId("INSERT INTO collections (project_id,name,status) VALUES (?,?,'reviewing')", [project.id, `轮次测试 ${Date.now()}`]);
|
||||
const noteId = await database.insertId("INSERT INTO notes (project_id,collection_id,title,review_status) VALUES (?,?,?,'pending')", [project.id, collectionId, '第 1 轮']);
|
||||
const round1 = await database.insertId("INSERT INTO review_rounds (note_id,round_number,status) VALUES (?,1,'reviewing')", [noteId]);
|
||||
await database.execute('UPDATE notes SET active_round_id=? WHERE id=?', [round1, noteId]);
|
||||
await database.execute("INSERT INTO work_versions (note_id,version_number,title,review_status,review_round_id,candidate_name,candidate_status) VALUES (?,1,?,'pending',?,'','pending')", [noteId, '第 1 轮', round1]);
|
||||
|
||||
await decideCandidateInTransaction(database, { noteId, versionNumber: 1, projectId: project.id, decision: 'changes_requested', reason: 'A 需调整', actorName: '测试客户', actorRole: 'client' });
|
||||
assert((await database.one<{ review_status: string }>('SELECT review_status FROM notes WHERE id = ?', [noteId]))?.review_status === 'pending', '仍有待验收候选稿时作品应保持待验收');
|
||||
db.exec('DROP INDEX idx_work_versions_one_per_round');
|
||||
await database.execute("INSERT INTO work_versions (note_id,version_number,title,review_status,review_round_id) VALUES (?,99,?,'pending',?)", [noteId, '旧多方案数据', round1]);
|
||||
repairMultiSchemeRounds();
|
||||
const migrated = await database.one<{ review_round_id: number; round_status: string }>('SELECT v.review_round_id,r.status AS round_status FROM work_versions v JOIN review_rounds r ON r.id=v.review_round_id WHERE v.note_id=? AND v.version_number=99', [noteId]);
|
||||
assert(migrated && Number(migrated.review_round_id) !== Number(round1) && migrated.round_status === 'completed', '旧多方案轮次未拆分为单方案历史轮次');
|
||||
await database.execute('DELETE FROM work_versions WHERE note_id=? AND version_number=99', [noteId]);
|
||||
await database.execute('DELETE FROM review_rounds WHERE id=?', [migrated.review_round_id]);
|
||||
db.exec('CREATE UNIQUE INDEX idx_work_versions_one_per_round ON work_versions(review_round_id) WHERE review_round_id IS NOT NULL');
|
||||
|
||||
await decideCandidateInTransaction(database, { noteId, versionNumber: 2, projectId: project.id, decision: 'approved', reason: '', actorName: '测试客户', actorRole: 'client' });
|
||||
const note = await database.one<{ version_number: number; approved_version_number: number; review_status: string }>('SELECT version_number, approved_version_number, review_status FROM notes WHERE id = ?', [noteId]);
|
||||
assert(note?.version_number === 2 && note.approved_version_number === 2 && note.review_status === 'approved', '作品未指向选中候选稿');
|
||||
const candidates = await database.all<Array<{ version_number: number; candidate_status: string }>[number]>('SELECT version_number, candidate_status FROM work_versions WHERE review_round_id = ? ORDER BY version_number', [roundId]);
|
||||
assert(candidates[0]?.candidate_status === 'not_selected' && candidates[1]?.candidate_status === 'selected', '同轮候选稿结果未正确收口');
|
||||
assert((await database.one<{ status: string }>('SELECT status FROM collections WHERE id = ?', [collectionId]))?.status === 'completed', '作品通过后作品交付集未自动完成');
|
||||
let duplicateRejected = false;
|
||||
try { await database.execute("INSERT INTO work_versions (note_id,version_number,title,review_status,review_round_id) VALUES (?,2,?,'pending',?)", [noteId, '非法第二方案', round1]); }
|
||||
catch { duplicateRejected = true; }
|
||||
assert(duplicateRejected, '同一验收轮次必须拒绝第二个方案');
|
||||
|
||||
await decideRoundInTransaction(database, { noteId, versionNumber: 1, projectId: project.id, decision: 'changes_requested', reason: '需要调整', actorName: '测试客户', actorRole: 'client' });
|
||||
assert((await database.one<{ review_status: string }>('SELECT review_status FROM notes WHERE id=?', [noteId]))?.review_status === 'changes_requested', '要求修改后作品状态错误');
|
||||
assert((await database.one<{ completion_reason: string }>('SELECT completion_reason FROM review_rounds WHERE id=?', [round1]))?.completion_reason === 'changes_requested', '退修轮次未完成');
|
||||
|
||||
const round2 = await database.insertId("INSERT INTO review_rounds (note_id,round_number,status) VALUES (?,2,'reviewing')", [noteId]);
|
||||
await database.execute("INSERT INTO work_versions (note_id,version_number,title,review_status,review_round_id,candidate_name,candidate_status) VALUES (?,2,?,'pending',?,'','pending')", [noteId, '第 2 轮', round2]);
|
||||
await database.execute("UPDATE notes SET active_round_id=?,version_number=2,title=?,review_status='pending' WHERE id=?", [round2, '第 2 轮', noteId]);
|
||||
const imageId = await database.insertId("INSERT INTO images (note_id,url,version_number) VALUES (?, '/test.jpg', 2)", [noteId]);
|
||||
const annotationId = await database.insertId("INSERT INTO annotations (image_id,x,y,content,author_name,author_role) VALUES (?,.5,.5,'待处理','测试运营','operator')", [imageId]);
|
||||
const commentId = await database.insertId("INSERT INTO work_comments (note_id,version_number,content) VALUES (?,2,'总体意见')", [noteId]);
|
||||
|
||||
const feedbackTarget = await findFeedbackTarget(noteId, 'image_annotation', annotationId);
|
||||
assert(feedbackTarget, '无法按作品找到图片批注');
|
||||
await addFeedbackReply(feedbackTarget, 'image_annotation', annotationId, '已收到,正在处理', '测试客户', 'client');
|
||||
assert((await database.one<{ count: number }>('SELECT COUNT(*) AS count FROM feedback_replies WHERE note_id=? AND feedback_id=?', [noteId, annotationId]))?.count === 1, '批注回复未保存');
|
||||
await withdrawFeedback('image_annotation', annotationId);
|
||||
assert(Boolean((await database.one<{ withdrawn_at: string | null }>('SELECT withdrawn_at FROM annotations WHERE id=?', [annotationId]))?.withdrawn_at), '撤回没有保留时间记录');
|
||||
|
||||
await decideRoundInTransaction(database, { noteId, versionNumber: 2, projectId: project.id, decision: 'approved', reason: '', actorName: '测试客户', actorRole: 'client' });
|
||||
const note = await database.one<{ version_number: number; review_status: string }>('SELECT version_number,review_status FROM notes WHERE id=?', [noteId]);
|
||||
assert(note?.version_number === 2 && note.review_status === 'approved', '第 2 轮通过后作品状态错误');
|
||||
assert((await database.one<{ review_status: string }>('SELECT review_status FROM projects WHERE id=?', [project.id]))?.review_status === 'completed', '全部作品通过后项目未完成');
|
||||
assert((await database.one<{ closure_reason: string }>('SELECT closure_reason FROM work_comments WHERE id=?', [commentId]))?.closure_reason === 'approved_with_round', '未处理反馈没有随轮关闭');
|
||||
|
||||
let historicalRejected = false;
|
||||
try {
|
||||
await decideCandidateInTransaction(database, { noteId, versionNumber: 1, projectId: project.id, decision: 'approved', reason: '', actorName: '测试客户', actorRole: 'client' });
|
||||
} catch (error) {
|
||||
historicalRejected = error instanceof ReviewDecisionError && error.statusCode === 409;
|
||||
}
|
||||
assert(historicalRejected, '已完成轮次的候选稿不应被重复验收');
|
||||
|
||||
process.stdout.write('SQLite 多候选稿验证通过:单稿退修、其他候选继续验收、选中收口与历史只读\n');
|
||||
try { await decideRoundInTransaction(database, { noteId, versionNumber: 1, projectId: project.id, decision: 'approved', reason: '', actorName: '测试客户', actorRole: 'client' }); }
|
||||
catch (error) { historicalRejected = error instanceof ReviewDecisionError && error.statusCode === 409; }
|
||||
assert(historicalRejected, '历史轮次不应被重复验收');
|
||||
process.stdout.write('SQLite 单方案轮次验证通过:一轮一稿、退修、回复、留痕撤回、通过、项目完成与历史只读\n');
|
||||
} finally {
|
||||
db.exec('ROLLBACK');
|
||||
await closeDatabase();
|
||||
|
||||
Reference in New Issue
Block a user