Files
ftb-project-management/apps/web/lib/version-plan.test.ts
2e595c7e72
Some checks failed
Deploy Production / Build, push, deploy, verify (push) Has been cancelled
refactor(data): 收口关系表运行时数据源
- 移除已迁移业务 AppData 运行时 fallback,改走领域 API 和关系表快读
- 补齐需求产品负责人、版本计划任务 JSON 和成员 username 回填迁移
- 统一治理字典入口,并补充 AI provider、数据源契约和领域服务测试

Co-Authored-By: Codex GPT-5 <codex@openai.com>
2026-07-09 14:59:49 +08:00

377 lines
13 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import test from 'node:test';
import assert from 'node:assert/strict';
import * as versionPlan from './version-plan';
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: 'pending',
createdAt: '2026-06-25',
addedBy: 'PM',
...patch,
};
}
test('sortPlansNewestFirst places newly created plans before older plans', () => {
const sortPlansNewestFirst = (versionPlan as any).sortPlansNewestFirst as undefined | ((plans: VersionPlan[]) => VersionPlan[]);
assert.equal(typeof sortPlansNewestFirst, 'function');
const plans = [
plan({ id: 'plan-100', createdAt: '2026-06-26' }),
plan({ id: 'manual-old', createdAt: '2026-06-25' }),
plan({ id: 'plan-300', createdAt: '2026-06-26' }),
];
const sorted = (sortPlansNewestFirst as (items: VersionPlan[]) => VersionPlan[])(plans);
assert.deepEqual(sorted.map((item) => item.id), ['plan-300', 'plan-100', 'manual-old']);
assert.deepEqual(plans.map((item) => item.id), ['plan-100', 'manual-old', 'plan-300']);
});
test('calcTotalDuration follows overview actual-duration display for short plans', () => {
const calcTotalDuration = (versionPlan as any).calcTotalDuration as undefined | ((plans: VersionPlan[]) => string);
assert.equal(typeof calcTotalDuration, 'function');
assert.equal(calcTotalDuration!([
plan({
type: 'product',
startTime: '2026-06-22T09:00:00',
endTime: '2026-06-22T10:00:00',
status: 'completed',
actualStartAt: '2026-06-22T09:00:00',
completedAt: '2026-06-22T10:00:00',
}),
]), '1h0.04天)');
});
test('collects logs across plans with plan context and newest first', () => {
const getPlanLogsForPlans = (versionPlan as any).getPlanLogsForPlans as undefined | ((plans: VersionPlan[]) => Array<{
id: string;
planId: string;
planTitle: string;
createdAt: string;
}>);
assert.equal(typeof getPlanLogsForPlans, 'function');
const logs = getPlanLogsForPlans!([
plan({
id: 'plan-a',
title: '产品方案第一天',
logs: [
{
id: 'log-old',
type: 'system',
title: '开始计划',
actor: 'PM',
createdAt: '2026-06-29T09:00:00.000Z',
},
],
}),
plan({
id: 'plan-b',
title: '产品方案第二天',
logs: [
{
id: 'log-new',
type: 'requirement_progress',
title: '更新需求进度',
actor: 'PM',
createdAt: '2026-06-29T15:00:00.000Z',
},
],
}),
]);
assert.deepEqual(logs.map((log) => ({
id: log.id,
planId: log.planId,
planTitle: log.planTitle,
})), [
{ id: 'log-new', planId: 'plan-b', planTitle: '产品方案第二天' },
{ id: 'log-old', planId: 'plan-a', planTitle: '产品方案第一天' },
]);
});
test('derives requirement coverage from new records and legacy completed ids', () => {
const getRequirementCoverageStatus = (versionPlan as any).getRequirementCoverageStatus as undefined | ((item: VersionPlan, requirementId: string) => string);
const getRequirementCoverageSummary = (versionPlan as any).getRequirementCoverageSummary as undefined | ((item: VersionPlan) => {
total: number;
completed: number;
partial: number;
notStarted: number;
percent: number;
});
assert.equal(typeof getRequirementCoverageStatus, 'function');
assert.equal(typeof getRequirementCoverageSummary, 'function');
const item = plan({
linkedRequirementIds: ['r1', 'r2', 'r3', 'r4'],
completedRequirementIds: ['r2'],
requirementCoverage: [
{
requirementId: 'r1',
status: 'partial',
completedContent: '完成主流程原型',
remainingContent: '补充异常状态',
updatedAt: '2026-06-29T09:00:00.000Z',
updatedBy: 'PM',
},
{
requirementId: 'r3',
status: 'completed',
completedContent: '已覆盖列表和详情',
updatedAt: '2026-06-29T10:00:00.000Z',
updatedBy: 'PM',
},
],
} as Partial<VersionPlan>);
assert.equal(getRequirementCoverageStatus!(item, 'r1'), 'partial');
assert.equal(getRequirementCoverageStatus!(item, 'r2'), 'completed');
assert.equal(getRequirementCoverageStatus!(item, 'r4'), 'not_started');
assert.deepEqual(getRequirementCoverageSummary!(item), {
total: 4,
completed: 2,
partial: 1,
notStarted: 1,
percent: 50,
});
});
test('updates requirement coverage, syncs legacy completed ids, and creates a plan log', () => {
const updateRequirementCoverage = (versionPlan as any).updateRequirementCoverage as undefined | ((item: VersionPlan, input: {
requirementId: string;
status: string;
completedContent?: string;
remainingContent?: string;
updatedBy: string;
updatedAt: string;
requirementCode?: string;
requirementTitle?: string;
}) => any);
assert.equal(typeof updateRequirementCoverage, 'function');
const next = updateRequirementCoverage!(plan({
linkedRequirementIds: ['r1'],
completedRequirementIds: ['r1'],
requirementCoverage: [
{
requirementId: 'r1',
status: 'completed',
currentWorkStartedAt: '2026-06-29T09:30:00.000Z',
updatedAt: '2026-06-28T12:00:00.000Z',
updatedBy: 'PM',
},
],
}), {
requirementId: 'r1',
status: 'partial',
completedContent: '完成移动端主流程',
remainingContent: 'PC 端筛选规则未完成',
updatedBy: 'PM',
updatedAt: '2026-06-29T12:00:00.000Z',
requirementCode: 'QY0001',
requirementTitle: '需求池筛选',
});
assert.deepEqual(next.completedRequirementIds, []);
assert.equal(next.requirementCoverage?.[0]?.status, 'partial');
assert.equal(next.requirementCoverage?.[0]?.currentWorkStartedAt, undefined);
assert.equal(next.logs?.length, 1);
assert.equal(next.logs?.[0]?.type, 'requirement_progress');
assert.equal(next.logs?.[0]?.actor, 'PM');
assert.equal(next.logs?.[0]?.requirementCode, 'QY0001');
assert.equal(next.logs?.[0]?.completedContent, '完成移动端主流程');
assert.equal(next.logs?.[0]?.remainingContent, 'PC 端筛选规则未完成');
assert.equal(next.logs?.[0]?.workStartedAt, '2026-06-29T09:30:00.000Z');
});
test('starts requirement coverage work while preserving existing progress', () => {
const startRequirementCoverageWork = (versionPlan as any).startRequirementCoverageWork as undefined | ((item: VersionPlan, input: {
requirementId: string;
startedAt: string;
updatedBy: string;
}) => any);
assert.equal(typeof startRequirementCoverageWork, 'function');
const next = startRequirementCoverageWork!(plan({
requirementCoverage: [
{
requirementId: 'r1',
status: 'partial',
completedContent: '完成主流程',
remainingContent: '补充异常状态',
updatedAt: '2026-06-28T12:00:00.000Z',
updatedBy: 'PM',
},
],
}), {
requirementId: 'r1',
startedAt: '2026-06-29T09:30:00.000Z',
updatedBy: 'PM',
});
assert.equal(next.requirementCoverage?.[0]?.status, 'partial');
assert.equal(next.requirementCoverage?.[0]?.completedContent, '完成主流程');
assert.equal(next.requirementCoverage?.[0]?.remainingContent, '补充异常状态');
assert.equal(next.requirementCoverage?.[0]?.currentWorkStartedAt, '2026-06-29T09:30:00.000Z');
assert.equal(next.requirementCoverage?.[0]?.updatedBy, 'PM');
});
test('merges stale requirement coverage patches without dropping current sessions', () => {
const mergeVersionPlanPatch = (versionPlan as any).mergeVersionPlanPatch as undefined | ((
current: VersionPlan,
patch: Partial<VersionPlan>,
) => Partial<VersionPlan>);
assert.equal(typeof mergeVersionPlanPatch, 'function');
const current = plan({
requirementCoverage: [
{
requirementId: 'r1',
status: 'not_started',
currentWorkStartedAt: '2026-06-29T09:30:00.000Z',
updatedAt: '2026-06-29T09:30:00.000Z',
updatedBy: 'PM',
},
],
});
const merged = mergeVersionPlanPatch!(current, {
requirementCoverage: [
{
requirementId: 'r2',
status: 'not_started',
currentWorkStartedAt: '2026-06-29T10:00:00.000Z',
updatedAt: '2026-06-29T10:00:00.000Z',
updatedBy: 'PM',
},
],
});
assert.deepEqual(
merged.requirementCoverage?.map((item) => [item.requirementId, item.currentWorkStartedAt]),
[
['r2', '2026-06-29T10:00:00.000Z'],
['r1', '2026-06-29T09:30:00.000Z'],
],
);
});
test('merged requirement coverage patches preserve legacy completed ids outside the patch', () => {
const mergeVersionPlanPatch = (versionPlan as any).mergeVersionPlanPatch as undefined | ((
current: VersionPlan,
patch: Partial<VersionPlan>,
) => Partial<VersionPlan>);
assert.equal(typeof mergeVersionPlanPatch, 'function');
const merged = mergeVersionPlanPatch!(plan({
linkedRequirementIds: ['r1', 'r2'],
completedRequirementIds: ['r1'],
}), {
requirementCoverage: [
{
requirementId: 'r2',
status: 'not_started',
currentWorkStartedAt: '2026-06-29T10:00:00.000Z',
updatedAt: '2026-06-29T10:00:00.000Z',
updatedBy: 'PM',
},
],
});
assert.deepEqual(merged.completedRequirementIds?.sort(), ['r1']);
});
test('merges stale research direction patches without dropping current sessions', () => {
const mergeVersionPlanPatch = (versionPlan as any).mergeVersionPlanPatch as undefined | ((
current: VersionPlan,
patch: Partial<VersionPlan>,
) => Partial<VersionPlan>);
assert.equal(typeof mergeVersionPlanPatch, 'function');
const current = plan({
type: 'research',
tasks: [
{
id: 'task-1',
title: '竞品分析',
status: 'in_progress',
currentWorkStartedAt: '2026-06-29T09:30:00.000Z',
},
{ id: 'task-2', title: '用户访谈', status: 'pending' },
],
});
const merged = mergeVersionPlanPatch!(current, {
tasks: [
{ id: 'task-1', title: '竞品分析', status: 'pending' },
{
id: 'task-2',
title: '用户访谈',
status: 'in_progress',
currentWorkStartedAt: '2026-06-29T10:00:00.000Z',
},
],
});
assert.deepEqual(
merged.tasks?.map((item) => [item.id, item.status, item.currentWorkStartedAt]),
[
['task-1', 'in_progress', '2026-06-29T09:30:00.000Z'],
['task-2', 'in_progress', '2026-06-29T10:00:00.000Z'],
],
);
});
test('validates requirement coverage record drafts by started work session', () => {
const canSaveRequirementCoverageDraft = (versionPlan as any).canSaveRequirementCoverageDraft as undefined | ((
status: string,
completedContent?: string,
remainingContent?: string,
workStartedAt?: string,
) => boolean);
assert.equal(typeof canSaveRequirementCoverageDraft, 'function');
assert.equal(canSaveRequirementCoverageDraft!('completed'), false);
assert.equal(canSaveRequirementCoverageDraft!('completed', undefined, undefined, '2026-06-29T09:30:00.000Z'), true);
assert.equal(canSaveRequirementCoverageDraft!('partial', '完成主流程', '', '2026-06-29T09:30:00.000Z'), false);
assert.equal(canSaveRequirementCoverageDraft!('partial', '完成主流程', '补充异常状态'), false);
assert.equal(canSaveRequirementCoverageDraft!('partial', '完成主流程', '补充异常状态', '2026-06-29T09:30:00.000Z'), true);
assert.equal(canSaveRequirementCoverageDraft!('not_started', undefined, undefined, '2026-06-29T09:30:00.000Z'), false);
});
test('hides coverage record action after a requirement is completed', () => {
const canOpenRequirementCoverageRecord = (versionPlan as any).canOpenRequirementCoverageRecord as undefined | ((
status: string,
canEdit: boolean,
) => boolean);
assert.equal(typeof canOpenRequirementCoverageRecord, 'function');
assert.equal(canOpenRequirementCoverageRecord!('not_started', true), true);
assert.equal(canOpenRequirementCoverageRecord!('partial', true), true);
assert.equal(canOpenRequirementCoverageRecord!('completed', true), false);
assert.equal(canOpenRequirementCoverageRecord!('partial', false), false);
});
test('keeps research direction presets visible while disabling selected presets', () => {
const getResearchDirectionPresetOptions = (versionPlan as any).getResearchDirectionPresetOptions as undefined | ((
tasks: Array<{ title: string }>,
) => Array<{ title: string; disabled: boolean }>);
assert.equal(typeof getResearchDirectionPresetOptions, 'function');
const options = getResearchDirectionPresetOptions!([{ title: '竞品分析' }]);
assert.ok(options.length >= 6);
assert.equal(options.find((option) => option.title === '竞品分析')?.disabled, true);
assert.equal(options.find((option) => option.title === '用户访谈')?.disabled, false);
});