Some checks failed
Deploy Production / Build, push, deploy, verify (push) Has been cancelled
- 移除已迁移业务 AppData 运行时 fallback,改走领域 API 和关系表快读 - 补齐需求产品负责人、版本计划任务 JSON 和成员 username 回填迁移 - 统一治理字典入口,并补充 AI provider、数据源契约和领域服务测试 Co-Authored-By: Codex GPT-5 <codex@openai.com>
377 lines
13 KiB
TypeScript
377 lines
13 KiB
TypeScript
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',
|
||
}),
|
||
]), '1h(0.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);
|
||
});
|