145 lines
5.0 KiB
TypeScript
145 lines
5.0 KiB
TypeScript
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>): 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('does not allow research result submission when subtasks are incomplete', () => {
|
|
const state = getPlanCompletionState(plan({
|
|
type: 'research',
|
|
tasks: [{ id: 'task-1', title: '调研方向', status: 'pending' }],
|
|
linkedRequirementIds: [],
|
|
}));
|
|
assert.equal(state.canSubmitResult, false);
|
|
assert.ok(state.missingReasons.includes('子任务未全部完成'));
|
|
});
|
|
|
|
test('requires product requirement coverage when linked requirements exist', () => {
|
|
const state = getPlanCompletionState(plan({}));
|
|
assert.equal(state.canSubmitResult, false);
|
|
assert.ok(state.missingReasons.includes('关联需求未全部覆盖'));
|
|
});
|
|
|
|
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 tasks and result but not 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<VersionPlan>));
|
|
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<VersionPlan>));
|
|
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<VersionPlan>));
|
|
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<VersionPlan>));
|
|
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<VersionPlan>));
|
|
assert.equal(completeFailureDetail.canComplete, true);
|
|
});
|