import test from 'node:test'; import assert from 'node:assert/strict'; import { TEST_CASE_STATUS_LABEL, aggregateTestCaseHours, calcTestProgress, canStartNextTestRound, copyTestCaseToRound, getNextTestRoundNo, getRequirementDeliveryStatus, getTestCaseEstimateHours, getTestCaseRoundNo, normalizeTestCase, } from './test-case'; import { DEFAULT_TEST_CATEGORY_ID } from './task-category'; test('normalizeTestCase backfills missing categoryId', () => { const tc = normalizeTestCase({ id: 'tc-1', caseNo: 'TC-001', versionId: 'v1', title: '登录正常', priority: 'P2', status: 'pending', createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25', } as any); assert.equal(tc.categoryId, DEFAULT_TEST_CATEGORY_ID); }); test('normalizeTestCase keeps existing categoryId', () => { const tc = normalizeTestCase({ id: 'tc-1', caseNo: 'TC-001', versionId: 'v1', title: '登录正常', priority: 'P2', status: 'pending', categoryId: 'cat-test-api', createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25', } as any); assert.equal(tc.categoryId, 'cat-test-api'); }); test('normalizeTestCase backfills missing roundNo to the first test round', () => { const tc = normalizeTestCase({ id: 'tc-1', caseNo: 'TC-001', versionId: 'v1', title: '第一轮用例', priority: 'P2', status: 'pending', categoryId: 'cat-test-api', createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25', } as any); assert.equal(tc.roundNo, 1); assert.equal(getTestCaseRoundNo(tc), 1); }); test('getNextTestRoundNo returns one more than the current max round', () => { const cases = [ normalizeTestCase({ id: 'tc-1', caseNo: 'TC-001', versionId: 'v1', title: 'R1', priority: 'P2', status: 'passed', categoryId: 'cat-test-api', roundNo: 1, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25' } as any), normalizeTestCase({ id: 'tc-2', caseNo: 'TC-002', versionId: 'v1', title: 'R3', priority: 'P2', status: 'passed', categoryId: 'cat-test-api', roundNo: 3, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25' } as any), ]; assert.equal(getNextTestRoundNo(cases), 4); }); test('canStartNextTestRound requires latest round cases all tested', () => { assert.equal(canStartNextTestRound([]), false); assert.equal(canStartNextTestRound([ normalizeTestCase({ id: 'tc-1', caseNo: 'TC-001', versionId: 'v1', title: '通过', priority: 'P2', status: 'passed', categoryId: 'cat-test-api', roundNo: 1, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25' } as any), normalizeTestCase({ id: 'tc-2', caseNo: 'TC-002', versionId: 'v1', title: '待测', priority: 'P2', status: 'pending', categoryId: 'cat-test-api', roundNo: 1, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25' } as any), ]), false); assert.equal(canStartNextTestRound([ normalizeTestCase({ id: 'tc-1', caseNo: 'TC-001', versionId: 'v1', title: '通过', priority: 'P2', status: 'passed', categoryId: 'cat-test-api', roundNo: 1, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25' } as any), normalizeTestCase({ id: 'tc-2', caseNo: 'TC-002', versionId: 'v1', title: '不通过', priority: 'P2', status: 'failed', categoryId: 'cat-test-api', roundNo: 1, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25' } as any), ]), true); assert.equal(canStartNextTestRound([ normalizeTestCase({ id: 'tc-1', caseNo: 'TC-001', versionId: 'v1', title: '首轮通过', priority: 'P2', status: 'passed', categoryId: 'cat-test-api', roundNo: 1, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25' } as any), normalizeTestCase({ id: 'tc-2', caseNo: 'TC-002', versionId: 'v1', title: '二轮待测', priority: 'P2', status: 'running', categoryId: 'cat-test-api', roundNo: 2, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25' } as any), ]), false); }); test('getRequirementDeliveryStatus marks a requirement submitted only when all its dev tasks are submitted', () => { const tasks = [ { requirementId: 'req-1', status: 'submitted' }, { requirementId: 'req-1', status: 'submitted' }, { requirementId: 'req-2', status: 'testing' }, ] as any; assert.equal(getRequirementDeliveryStatus('req-1', tasks), 'submitted'); assert.equal(getRequirementDeliveryStatus('req-2', tasks), 'pending'); assert.equal(getRequirementDeliveryStatus('req-empty', tasks), 'pending'); }); test('copyTestCaseToRound preserves estimates and references but clears execution state', () => { const source = normalizeTestCase({ id: 'tc-source', caseNo: 'TC-001', versionId: 'v1', requirementId: 'req-1', title: '登录正常', description: '步骤', priority: 'P1', status: 'passed', categoryId: 'cat-test-functional', estimateHours: 0.75, aiEstimateHours: 0.25, assigneeId: 'QA', startedAt: '2026-06-25T01:00:00.000Z', completedAt: '2026-06-25T02:00:00.000Z', executedAt: '2026-06-25T02:00:00.000Z', failReason: 'old fail', blockReason: 'old block', references: [{ type: 'requirement', id: 'req-1', label: 'REQ-001 登录' }], aiDraft: true, aiDraftAt: '2026-06-25T00:00:00.000Z', roundNo: 1, createdBy: 'AI', createdAt: '2026-06-25', updatedAt: '2026-06-25', } as any); const copied = copyTestCaseToRound(source, 2, 'tester'); assert.equal(copied.roundNo, 2); assert.equal(copied.sourceCaseId, 'tc-source'); assert.equal(copied.aiEstimateHours, 0.25); assert.equal(copied.estimateHours, 0.75); assert.deepEqual(copied.references, source.references); assert.equal(copied.startedAt, undefined); assert.equal(copied.completedAt, undefined); assert.equal(copied.executedAt, undefined); assert.equal(copied.failReason, undefined); assert.equal(copied.blockReason, undefined); assert.equal(copied.createdBy, 'tester'); }); test('normalizeTestCase keeps missing executor estimate empty', () => { const tc = normalizeTestCase({ id: 'tc-1', caseNo: 'TC-001', versionId: 'v1', title: '登录正常', priority: 'P2', status: 'pending', categoryId: 'cat-test-api', createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25', } as any); assert.equal(tc.estimateHours, undefined); assert.equal(getTestCaseEstimateHours(tc), 0); }); test('getTestCaseEstimateHours prefers executor estimate over AI estimate', () => { const tc = normalizeTestCase({ id: 'tc-1', caseNo: 'TC-001', versionId: 'v1', title: '登录正常', priority: 'P2', status: 'pending', categoryId: 'cat-test-api', estimateHours: 0.7, aiEstimateHours: 0.2, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25', } as any); assert.equal(getTestCaseEstimateHours(tc), 0.7); }); test('getTestCaseEstimateHours falls back to AI estimate', () => { const tc = normalizeTestCase({ id: 'tc-1', caseNo: 'TC-001', versionId: 'v1', title: '登录正常', priority: 'P2', status: 'pending', categoryId: 'cat-test-api', aiEstimateHours: 0.2, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25', } as any); assert.equal(getTestCaseEstimateHours(tc), 0.2); }); test('test case status labels use waiting and testing wording', () => { assert.equal(TEST_CASE_STATUS_LABEL.pending, '待测试'); assert.equal(TEST_CASE_STATUS_LABEL.running, '测试中'); assert.equal(TEST_CASE_STATUS_LABEL.failed, '不通过'); }); test('calcTestProgress uses estimate-weighted completion', () => { const progress = calcTestProgress([ normalizeTestCase({ id: 'tc-1', caseNo: 'TC-001', versionId: 'v1', title: '待测试', priority: 'P2', status: 'pending', categoryId: 'cat-test-functional', estimateHours: 1, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25', } as any), normalizeTestCase({ id: 'tc-2', caseNo: 'TC-002', versionId: 'v1', title: '已通过', priority: 'P2', status: 'passed', categoryId: 'cat-test-functional', estimateHours: 3, createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25', } as any), ]); assert.equal(progress.completionRate, 75); }); test('calcTestProgress calculates pass rate from passed cases that never had effective bugs', () => { const progress = calcTestProgress([ normalizeTestCase({ id: 'tc-clean', caseNo: 'TC-001', versionId: 'v1', title: '无 Bug 通过', priority: 'P2', status: 'passed', categoryId: 'cat-test-functional', createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25', } as any), normalizeTestCase({ id: 'tc-bug-fixed', caseNo: 'TC-002', versionId: 'v1', title: '提过 Bug 后通过', priority: 'P2', status: 'passed', categoryId: 'cat-test-functional', createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25', } as any), normalizeTestCase({ id: 'tc-failed', caseNo: 'TC-003', versionId: 'v1', title: '不通过', priority: 'P2', status: 'failed', categoryId: 'cat-test-functional', createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25', } as any), normalizeTestCase({ id: 'tc-running-with-bug', caseNo: 'TC-004', versionId: 'v1', title: '测试中已提 Bug', priority: 'P2', status: 'running', categoryId: 'cat-test-functional', createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25', } as any), normalizeTestCase({ id: 'tc-rejected-only', caseNo: 'TC-005', versionId: 'v1', title: '仅有驳回 Bug', priority: 'P2', status: 'passed', categoryId: 'cat-test-functional', createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25', } as any), normalizeTestCase({ id: 'tc-pending', caseNo: 'TC-006', versionId: 'v1', title: '未执行', priority: 'P2', status: 'pending', categoryId: 'cat-test-functional', createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25', } as any), ], [ { testCaseId: 'tc-bug-fixed', status: 'closed' }, { testCaseId: 'tc-running-with-bug', status: 'open' }, { testCaseId: 'tc-rejected-only', status: 'rejected' }, ]); assert.equal(progress.executed, 5); assert.equal(progress.passed, 3); assert.equal(progress.passRate, 40); }); test('aggregateTestCaseHours returns estimate and actual totals', () => { const hours = aggregateTestCaseHours([ normalizeTestCase({ id: 'tc-1', caseNo: 'TC-001', versionId: 'v1', title: '测试中', priority: 'P2', status: 'running', categoryId: 'cat-test-functional', estimateHours: 1, startedAt: '2026-06-25T01:00:00.000Z', createdBy: 'tester', createdAt: '2026-06-25', updatedAt: '2026-06-25', } as any), ], new Date('2026-06-25T02:00:00.000Z')); assert.equal(hours.estimate, 1); assert.equal(hours.actual, 1); });