import test from 'node:test'; import assert from 'node:assert/strict'; import { calcBugSeverityRanking, calcPersonalEffortRanking, calcStageEffortMetrics, calcVersionOverviewEffortTotals, } from './version-overview'; import type { VersionPlan } from './version-plan'; import type { DevTask } from './dev-task'; import type { TestCase } from './test-case'; import type { Bug } from './bug'; import type { OvertimeRecord } from './overtime'; function plan(patch: Partial): VersionPlan { return { id: patch.id || 'plan-1', versionId: patch.versionId || 'v-1', type: patch.type || 'research', title: patch.title || '计划', owner: patch.owner || 'Alice', startTime: patch.startTime || '2026-06-22T09:00:00', endTime: patch.endTime || '2026-06-22T18:00:00', status: patch.status || 'completed', actualStartAt: patch.actualStartAt, completedAt: patch.completedAt, createdAt: patch.createdAt || '2026-06-22T08:00:00', addedBy: patch.addedBy || 'Alice', }; } function devTask(patch: Partial): DevTask { return { id: patch.id || 'dev-1', taskNo: patch.taskNo || 'DEV-001', requirementId: patch.requirementId || 'req-1', title: patch.title || '开发任务', categoryId: patch.categoryId || 'cat-1', assigneeId: patch.assigneeId || 'Alice', priority: patch.priority || 'P2', expectedStartAt: patch.expectedStartAt || '2026-06-22T09:00:00', expectedEndAt: patch.expectedEndAt || '2026-06-22T18:00:00', estimateHours: patch.estimateHours, aiEstimateHours: patch.aiEstimateHours, actualStartAt: patch.actualStartAt, actualEndAt: patch.actualEndAt, status: patch.status || 'submitted', isBlocked: patch.isBlocked ?? false, createdBy: patch.createdBy || 'Alice', createdAt: patch.createdAt || '2026-06-22T08:00:00', updatedAt: patch.updatedAt || '2026-06-22T18:00:00', }; } function testCase(patch: Partial): TestCase { return { id: patch.id || 'tc-1', caseNo: patch.caseNo || 'TC-001', versionId: patch.versionId || 'v-1', title: patch.title || '测试用例', categoryId: patch.categoryId || 'cat-1', priority: patch.priority || 'P2', assigneeId: patch.assigneeId || 'Bob', status: patch.status || 'passed', estimateHours: patch.estimateHours, aiEstimateHours: patch.aiEstimateHours, startedAt: patch.startedAt, completedAt: patch.completedAt, createdBy: patch.createdBy || 'Bob', createdAt: patch.createdAt || '2026-06-22T08:00:00', updatedAt: patch.updatedAt || '2026-06-22T18:00:00', }; } function bug(patch: Partial): Bug { return { id: patch.id || 'bug-1', bugNo: patch.bugNo || 'BUG-001', versionId: patch.versionId || 'v-1', testCaseId: patch.testCaseId || 'tc-1', title: patch.title || 'Bug', description: patch.description || '描述', severity: patch.severity || 'major', priority: patch.priority || 'P1', reportedBy: patch.reportedBy || 'QA', assigneeId: patch.assigneeId || 'Bob', status: patch.status || 'closed', createdAt: patch.createdAt || '2026-06-22T10:00:00', updatedAt: patch.updatedAt || '2026-06-22T12:00:00', closedAt: patch.closedAt, resolvedAt: patch.resolvedAt, }; } test('calcStageEffortMetrics returns actual hours and AI estimates per stage', () => { const metrics = calcStageEffortMetrics({ plans: [ plan({ type: 'research', actualStartAt: '2026-06-22T09:00:00', completedAt: '2026-06-22T18:00:00' }), plan({ type: 'product', actualStartAt: '2026-06-23T09:00:00', completedAt: '2026-06-23T12:00:00' }), ], devTasks: [ devTask({ actualStartAt: '2026-06-24T09:00:00', actualEndAt: '2026-06-24T14:00:00', estimateHours: 4.5, aiEstimateHours: 3.25, }), ], testCases: [ testCase({ startedAt: '2026-06-25T09:00:00', completedAt: '2026-06-25T11:00:00', estimateHours: 2.25, aiEstimateHours: 1.5, }), ], bugs: [ bug({ createdAt: '2026-06-26T10:00:00', closedAt: '2026-06-26T10:10:00' }), ], }); assert.equal(metrics.requirement.actualHours, 9); assert.equal(metrics.product_design.actualHours, 3); assert.equal(metrics.dev.actualHours, 5); assert.equal(metrics.dev.estimateHours, 4.5); assert.equal(metrics.dev.aiEstimateHours, 3.25); assert.equal(metrics.testing.actualHours, 2); assert.equal(metrics.testing.estimateHours, 2.25); assert.equal(metrics.testing.aiEstimateHours, 1.5); assert.equal(metrics.bug.actualHours, 0.5); }); test('calcStageEffortMetrics uses updatedAt for terminal test cases missing completedAt', () => { const metrics = calcStageEffortMetrics({ plans: [], devTasks: [], testCases: [ testCase({ status: 'passed', startedAt: '2026-06-25T09:00:00', completedAt: undefined, updatedAt: '2026-06-25T09:20:00', }), ], bugs: [], now: new Date('2026-06-26T18:00:00'), }); assert.equal(metrics.testing.actualHours, 0.5); }); test('calcVersionOverviewEffortTotals sums actual hours and overtime records separately', () => { const overtimeRecords: OvertimeRecord[] = [ { id: 'ot-1', projectId: 'project-1', versionId: 'v-1', person: 'Alice', startTime: '2026-06-22T19:00:00', endTime: '2026-06-22T20:15:00', duration: 1.25, reasonId: 'reason-3', createdAt: '2026-06-22T20:20:00', }, { id: 'ot-2', projectId: 'project-1', versionId: 'v-1', person: 'Bob', startTime: '2026-06-22T20:00:00', endTime: '2026-06-22T22:03:00', duration: 2.05, reasonId: 'reason-4', createdAt: '2026-06-22T22:10:00', }, ]; const totals = calcVersionOverviewEffortTotals({ plans: [plan({ actualStartAt: '2026-06-22T09:00:00', completedAt: '2026-06-22T18:00:00' })], devTasks: [devTask({ actualStartAt: '2026-06-22T10:00:00', actualEndAt: '2026-06-22T12:00:00' })], testCases: [], bugs: [], overtimeRecords, }); assert.equal(totals.actualHours, 11); assert.equal(totals.overtimeHours, 3.3); }); test('calcPersonalEffortRanking includes bug work and sorts by total hours', () => { const overtimeRecords: OvertimeRecord[] = [ { id: 'ot-1', projectId: 'project-1', versionId: 'v-1', person: 'Alice', startTime: '2026-06-23T19:00:00', endTime: '2026-06-23T20:30:00', duration: 1.5, reasonId: 'reason-3', createdAt: '2026-06-23T20:35:00', }, { id: 'ot-2', projectId: 'project-1', versionId: 'v-1', person: 'Bob', startTime: '2026-06-25T19:00:00', endTime: '2026-06-25T21:00:00', duration: 2, reasonId: 'reason-4', createdAt: '2026-06-25T21:10:00', }, ]; const ranking = calcPersonalEffortRanking({ plans: [plan({ owner: 'Alice', actualStartAt: '2026-06-22T09:00:00', completedAt: '2026-06-22T18:00:00' })], devTasks: [devTask({ assigneeId: 'Alice', actualStartAt: '2026-06-23T09:00:00', actualEndAt: '2026-06-23T11:00:00' })], testCases: [testCase({ assigneeId: 'Bob', startedAt: '2026-06-24T09:00:00', completedAt: '2026-06-24T11:00:00' })], bugs: [bug({ assigneeId: 'Bob', createdAt: '2026-06-25T10:00:00', closedAt: '2026-06-25T12:00:00' })], overtimeRecords, }); assert.equal(ranking[0].name, 'Alice'); assert.equal(ranking[0].actualHours, 11); assert.equal(ranking[0].overtimeHours, 1.5); assert.equal(ranking[0].total, 12.5); assert.equal(ranking[1].name, 'Bob'); assert.equal(ranking[1].actualHours, 4); assert.equal(ranking[1].overtimeHours, 2); assert.equal(ranking[1].total, 6); }); test('calcBugSeverityRanking groups bugs by assignee and severity', () => { const ranking = calcBugSeverityRanking([ bug({ id: 'bug-1', assigneeId: 'Alice', severity: 'critical' }), bug({ id: 'bug-2', assigneeId: 'Alice', severity: 'major' }), bug({ id: 'bug-3', assigneeId: 'Alice', severity: 'minor' }), bug({ id: 'bug-4', assigneeId: 'Bob', severity: 'critical' }), bug({ id: 'bug-5', assigneeId: 'Bob', severity: 'trivial' }), bug({ id: 'bug-6', assigneeId: 'Alice', severity: 'trivial' }), ]); assert.deepEqual(ranking, [ { assigneeId: 'Alice', critical: 1, major: 1, minor: 1, trivial: 1, total: 4, }, { assigneeId: 'Bob', critical: 1, major: 0, minor: 0, trivial: 1, total: 2, }, ]); });