feat(测试用例): 状态流转接入单条工作流

This commit is contained in:
Script Generator
2026-06-25 14:13:32 +08:00
parent 52833d81ac
commit c617625a99
5 changed files with 357 additions and 37 deletions

View File

@@ -1,5 +1,8 @@
import type { Priority } from './derive';
import type { Reference } from './dev-task';
import { DEFAULT_TEST_CATEGORY_ID } from './task-category';
import { calcWorkHours, type TimeInterval } from './work-hours';
import { aggregateWorkEffort } from './work-effort-engine';
export type TestCaseStatus = 'pending' | 'running' | 'passed' | 'failed' | 'blocked';
@@ -10,25 +13,30 @@ export interface TestCase {
requirementId?: string;
title: string;
description?: string;
categoryId: string;
priority: Priority;
assigneeId?: string;
status: TestCaseStatus;
estimateHours?: number;
startedAt?: string;
completedAt?: string;
executedAt?: string;
executedBy?: string;
failReason?: string;
blockReason?: string;
references?: Reference[];
aiDraft?: boolean;
aiDraftAt?: string;
createdBy: string;
createdAt: string;
updatedAt: string;
}
export const TEST_CASE_STATUS_LABEL: Record<TestCaseStatus, string> = {
pending: '待执行',
running: '执行中',
pending: '待测试',
running: '测试中',
passed: '通过',
failed: '失败',
failed: '不通过',
blocked: '阻塞',
};
@@ -48,6 +56,14 @@ export const TC_ALLOWED_TRANSITIONS: Record<TestCaseStatus, TestCaseStatus[]> =
blocked: ['running'],
};
export const TEST_CASE_STATUS_PROGRESS: Record<TestCaseStatus, number> = {
pending: 0,
running: 50,
passed: 100,
failed: 100,
blocked: 100,
};
export function canTcTransition(from: TestCaseStatus, to: TestCaseStatus): boolean {
return TC_ALLOWED_TRANSITIONS[from].includes(to);
}
@@ -60,6 +76,38 @@ export function generateCaseNo(existingCases: TestCase[]): string {
return `TC-${String(maxNum + 1).padStart(3, '0')}`;
}
export function normalizeTestCase(testCase: Partial<TestCase>, index = 0): TestCase {
return {
id: testCase.id || `tc-${index + 1}`,
caseNo: testCase.caseNo || `TC-${String(index + 1).padStart(3, '0')}`,
versionId: testCase.versionId || '',
requirementId: testCase.requirementId,
title: testCase.title || `测试用例 ${index + 1}`,
description: testCase.description,
categoryId: testCase.categoryId || DEFAULT_TEST_CATEGORY_ID,
priority: testCase.priority || 'P2',
assigneeId: testCase.assigneeId,
status: testCase.status || 'pending',
estimateHours: typeof testCase.estimateHours === 'number' && testCase.estimateHours > 0 ? testCase.estimateHours : 0.5,
startedAt: testCase.startedAt,
completedAt: testCase.completedAt,
executedAt: testCase.executedAt,
executedBy: testCase.executedBy,
failReason: testCase.failReason,
blockReason: testCase.blockReason,
references: testCase.references,
aiDraft: testCase.aiDraft,
aiDraftAt: testCase.aiDraftAt,
createdBy: testCase.createdBy || '系统',
createdAt: testCase.createdAt || new Date().toISOString(),
updatedAt: testCase.updatedAt || new Date().toISOString(),
};
}
export function normalizeTestCases(cases: Partial<TestCase>[] = []): TestCase[] {
return cases.map((tc, index) => normalizeTestCase(tc, index));
}
export function calcTestProgress(cases: TestCase[]): { total: number; executed: number; passed: number; failed: number; blocked: number; passRate: number; completionRate: number } {
const total = cases.length;
if (total === 0) return { total: 0, executed: 0, passed: 0, failed: 0, blocked: 0, passRate: 0, completionRate: 0 };
@@ -68,16 +116,35 @@ export function calcTestProgress(cases: TestCase[]): { total: number; executed:
const blocked = cases.filter((c) => c.status === 'blocked').length;
const executed = passed + failed + blocked;
const passRate = (passed + failed) > 0 ? Math.round((passed / (passed + failed)) * 100) : 0;
const completionRate = Math.round((executed / total) * 100);
const completionRate = aggregateWorkEffort(cases.map((c) => ({
estimateHours: getTestCaseEstimateHours(c),
actualHours: getTestCaseActualHours(c),
progress: TEST_CASE_STATUS_PROGRESS[c.status],
}))).progress;
return { total, executed, passed, failed, blocked, passRate, completionRate };
}
export function getTestCaseEstimateHours(tc: TestCase): number {
return typeof tc.estimateHours === 'number' && tc.estimateHours > 0
? Math.round(tc.estimateHours * 2) / 2
: 0.5;
}
export function getTestCaseActualHours(tc: TestCase, now: Date = new Date()): number {
if (!tc.startedAt) return 0;
const end = tc.completedAt ?? now.toISOString();
return calcWorkHours(tc.startedAt, end);
}
export function aggregateTestCaseHours(cases: TestCase[], now: Date = new Date()): { estimate: number; actual: number } {
const summary = aggregateWorkEffort(cases.map((c) => ({
estimateHours: getTestCaseEstimateHours(c),
actualHours: getTestCaseActualHours(c, now),
progress: TEST_CASE_STATUS_PROGRESS[c.status],
})));
return { estimate: summary.estimateHours, actual: summary.actualHours };
}
export function aggregateTestCaseActualHours(cases: TestCase[], now: Date = new Date()): number {
let sum = 0;
for (const c of cases) sum += getTestCaseActualHours(c, now);