Files
ftb-project-management/apps/web/lib/version-plan-workflow.test.ts
Script Generator 3d3d56697a feat(版本): 完善研发计划与预警已读
关键改动:

- 增加版本表单、发布校验和调研方向进度规则

- 扩展小宝预警已读状态、风险签名和今日证据

- 补充组长权限、加班查看范围、活动记录与相关测试

Co-Authored-By: Codex GPT-5 <codex@openai.com>
2026-06-30 16:48:58 +08:00

180 lines
6.2 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('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<VersionPlan>));
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<VersionPlan>));
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<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);
});