feat(版本): 完善计划覆盖与工作台待办

关键改动:

- 增加计划日志汇总、需求覆盖草稿校验与对应测试

- 抽取工作台工作项 Hook,补充待办计数能力

- 优化版本详情中计划、任务、测试用例和 Bug 的筛选与展示

Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
Script Generator
2026-06-30 13:43:18 +08:00
parent a6a4d7d3d9
commit 52a88626d4
23 changed files with 1082 additions and 281 deletions

View File

@@ -36,6 +36,54 @@ test('sortPlansNewestFirst places newly created plans before older plans', () =>
assert.deepEqual(plans.map((item) => item.id), ['plan-100', 'manual-old', 'plan-300']);
});
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) => {
@@ -116,3 +164,17 @@ test('updates requirement coverage, syncs legacy completed ids, and creates a pl
assert.equal(next.logs?.[0]?.actor, 'PM');
assert.equal(next.logs?.[0]?.requirementCode, 'QY0001');
});
test('validates requirement coverage record drafts by status', () => {
const canSaveRequirementCoverageDraft = (versionPlan as any).canSaveRequirementCoverageDraft as undefined | ((
status: string,
completedContent?: string,
remainingContent?: string,
) => boolean);
assert.equal(typeof canSaveRequirementCoverageDraft, 'function');
assert.equal(canSaveRequirementCoverageDraft!('completed'), true);
assert.equal(canSaveRequirementCoverageDraft!('partial', '完成主流程', ''), false);
assert.equal(canSaveRequirementCoverageDraft!('partial', '完成主流程', '补充异常状态'), true);
assert.equal(canSaveRequirementCoverageDraft!('not_started'), false);
});

View File

@@ -87,6 +87,11 @@ export type PlanLogDraft = Omit<VersionPlanLog, 'id' | 'createdAt'> & {
createdAt?: string;
};
export type VersionPlanLogView = VersionPlanLog & {
planId: string;
planTitle: string;
};
export const REQUIREMENT_COVERAGE_LABEL: Record<RequirementCoverageStatus, string> = {
not_started: '未开始',
partial: '部分完成',
@@ -177,6 +182,18 @@ export function getRequirementCoverageSummary(plan: VersionPlan): {
};
}
export function canSaveRequirementCoverageDraft(
status: RequirementCoverageStatus,
completedContent?: string,
remainingContent?: string,
): boolean {
if (status === 'completed') return true;
if (status === 'partial') {
return Boolean(completedContent?.trim()) && Boolean(remainingContent?.trim());
}
return false;
}
export function appendPlanLog(plan: VersionPlan, draft: PlanLogDraft): VersionPlanLog[] {
const createdAt = draft.createdAt ?? new Date().toISOString();
const log: VersionPlanLog = {
@@ -187,6 +204,21 @@ export function appendPlanLog(plan: VersionPlan, draft: PlanLogDraft): VersionPl
return [log, ...(plan.logs ?? [])];
}
function getPlanLogCreatedAtTime(log: VersionPlanLog): number {
const time = new Date(log.createdAt).getTime();
return Number.isFinite(time) ? time : 0;
}
export function getPlanLogsForPlans(plans: VersionPlan[]): VersionPlanLogView[] {
return plans
.flatMap((plan) => (plan.logs ?? []).map((log) => ({
...log,
planId: plan.id,
planTitle: plan.title,
})))
.sort((a, b) => getPlanLogCreatedAtTime(b) - getPlanLogCreatedAtTime(a));
}
export function updateRequirementCoverage(
plan: VersionPlan,
input: RequirementCoverageUpdateInput,

View File

@@ -0,0 +1,13 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { getWorkspacePendingCount } from './workspace-engine';
test('getWorkspacePendingCount counts unfinished work items only', () => {
const count = getWorkspacePendingCount([
{ completed: false },
{ completed: true },
{ completed: false },
]);
assert.equal(count, 2);
});

View File

@@ -123,6 +123,10 @@ export function aggregateWorkItems(
return items;
}
export function getWorkspacePendingCount(items: ReadonlyArray<Pick<WorkItem, 'completed'>>): number {
return items.reduce((sum, item) => sum + (item.completed ? 0 : 1), 0);
}
export const WORK_ITEM_TYPE_LABEL: Record<WorkItemType, string> = {
plan_research: '调研',
plan_product: '产品方案',