import test from 'node:test'; import assert from 'node:assert/strict'; import { getPlanCompletionState, hasPlanResult } from './version-plan-workflow'; import type { VersionPlan } from './version-plan'; function plan(patch: Partial): VersionPlan { return { id: 'plan-1', versionId: 'version-1', type: 'product', title: '产品方案', owner: 'PM', startTime: '2026-06-25T09:00', endTime: '2026-06-25T18:00', status: 'in_progress', linkedRequirementIds: ['r1'], completedRequirementIds: [], createdAt: '2026-06-25', addedBy: 'PM', ...patch, }; } test('research requires direction completion instead of linked requirement coverage', () => { const state = getPlanCompletionState(plan({ type: 'research', tasks: [{ id: 'task-1', title: '调研方向', status: 'pending' }], linkedRequirementIds: ['r1'], })); assert.equal(state.canSubmitResult, false); assert.ok(state.missingReasons.includes('调研方向未全部完成')); assert.equal(state.missingReasons.includes('关联需求未全部覆盖'), false); }); test('requires product requirement coverage when linked requirements exist', () => { const state = getPlanCompletionState(plan({})); assert.equal(state.canSubmitResult, false); assert.ok(state.missingReasons.includes('关联需求未全部覆盖')); }); test('does not treat partial requirement coverage as complete', () => { const state = getPlanCompletionState(plan({ requirementCoverage: [{ requirementId: 'r1', status: 'partial', completedContent: '完成列表主路径', remainingContent: '剩余筛选联动和空状态', updatedAt: '2026-06-29T09:00:00.000Z', updatedBy: 'PM', }], } as Partial)); assert.equal(state.requirementCompleted, 0); assert.equal(state.canSubmitResult, false); assert.ok(state.missingReasons.includes('关联需求未全部覆盖')); }); test('uses requirement coverage before legacy completed ids when both exist', () => { const state = getPlanCompletionState(plan({ completedRequirementIds: ['r1'], requirementCoverage: [{ requirementId: 'r1', status: 'partial', completedContent: '完成移动端', remainingContent: 'PC 端未完成', updatedAt: '2026-06-29T09:00:00.000Z', updatedBy: 'PM', }], } as Partial)); assert.equal(state.requirementCompleted, 0); assert.equal(state.canSubmitResult, false); }); test('allows product result submission after coverage is complete without task checklist', () => { const state = getPlanCompletionState(plan({ completedRequirementIds: ['r1'], })); assert.equal(state.canSubmitResult, true); assert.equal(state.canComplete, false); }); test('allows completion only after result exists', () => { const state = getPlanCompletionState(plan({ completedRequirementIds: ['r1'], resultType: 'link', resultTitle: '原型', resultUrl: 'https://example.com/prototype', prototypeReviewConfirmed: true, })); assert.equal(state.canComplete, true); }); test('ui plan does not require task checklist', () => { const state = getPlanCompletionState(plan({ type: 'ui', completedRequirementIds: ['r1'], })); assert.equal(state.canSubmitResult, true); }); test('research requires completed directions and result but not linked requirement coverage', () => { const state = getPlanCompletionState(plan({ type: 'research', tasks: [{ id: 'task-1', title: '调研', status: 'completed' }], linkedRequirementIds: ['r1'], completedRequirementIds: [], resultType: 'file', resultTitle: '调研报告', resultFileName: 'report.pdf', resultFileData: 'data:application/pdf;base64,abc', })); assert.equal(state.canComplete, true); }); test('detects link and file result payloads', () => { assert.equal(hasPlanResult({ resultType: 'link', resultTitle: '原型', resultUrl: 'https://example.com' }), true); assert.equal(hasPlanResult({ resultType: 'file', resultTitle: '文件', resultFileName: 'a.pdf', resultFileData: 'data:pdf' }), true); assert.equal(hasPlanResult({ resultType: 'link', resultTitle: '原型' }), false); }); test('product design plans require a prototype link and review confirmation', () => { const withoutReviewConfirmation = getPlanCompletionState(plan({ productPlanKind: 'design', completedRequirementIds: ['r1'], resultType: 'link', resultTitle: 'Prototype', resultUrl: 'https://example.com/prototype', } as Partial)); assert.equal(withoutReviewConfirmation.canComplete, false); const withReviewConfirmation = getPlanCompletionState(plan({ productPlanKind: 'design', completedRequirementIds: ['r1'], resultType: 'link', resultTitle: 'Prototype', resultUrl: 'https://example.com/prototype', prototypeReviewConfirmed: true, } as Partial)); assert.equal(withReviewConfirmation.canComplete, true); }); test('product design plans do not accept file results', () => { assert.equal(hasPlanResult({ productPlanKind: 'design', resultType: 'file', resultTitle: 'Axure file', resultFileName: 'prototype.rp', resultFileData: 'data:application/octet-stream;base64,abc', } as any), false); }); test('product review plans can complete with a pass result without prototype link', () => { const state = getPlanCompletionState(plan({ productPlanKind: 'review', completedRequirementIds: ['r1'], reviewResult: 'passed', } as Partial)); assert.equal(state.canComplete, true); }); test('product review plans require failure type and detail when review fails', () => { const missingFailureDetail = getPlanCompletionState(plan({ productPlanKind: 'review', completedRequirementIds: ['r1'], reviewResult: 'failed', } as Partial)); assert.equal(missingFailureDetail.canComplete, false); const completeFailureDetail = getPlanCompletionState(plan({ productPlanKind: 'review', completedRequirementIds: ['r1'], reviewResult: 'failed', reviewFailureTypes: ['interaction_flow'], reviewFailureReason: 'Critical path is missing empty and rollback states.', } as Partial)); assert.equal(completeFailureDetail.canComplete, true); });