feat(版本): 完善计划覆盖与工作台待办
关键改动: - 增加计划日志汇总、需求覆盖草稿校验与对应测试 - 抽取工作台工作项 Hook,补充待办计数能力 - 优化版本详情中计划、任务、测试用例和 Bug 的筛选与展示 Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
13
apps/web/lib/workspace-engine.test.ts
Normal file
13
apps/web/lib/workspace-engine.test.ts
Normal 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);
|
||||
});
|
||||
@@ -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: '产品方案',
|
||||
|
||||
Reference in New Issue
Block a user