import test from 'node:test'; import assert from 'node:assert/strict'; import type { Bug } from './bug'; import type { DevTask } from './dev-task'; import type { TestCase } from './test-case'; import type { TaskWorklog } from './task-worklog'; import type { VersionPlan } from './version-plan'; import type { WorkActivity } from './work-activity'; import { buildVersionDailyEvidence, buildXiaobaoWorkItems } from './xiaobao-risk-evidence'; function plan(patch: Partial = {}): VersionPlan { return { id: 'plan-1', versionId: 'ver-1', type: 'product', title: 'Product plan', owner: 'pm-1', startTime: '2026-06-20T01:00:00.000Z', endTime: '2026-06-25T10:00:00.000Z', status: 'in_progress', actualStartAt: '2026-06-20T01:00:00.000Z', createdAt: '2026-06-20T01:00:00.000Z', addedBy: 'pm-1', ...patch, }; } function devTask(patch: Partial = {}): DevTask { return { id: 'dev-1', taskNo: 'DEV-001', requirementId: 'req-1', title: 'Build warning panel', categoryId: 'frontend', assigneeId: 'dev-other', priority: 'P1', expectedStartAt: '2026-06-20T01:00:00.000Z', expectedEndAt: '2026-06-26T10:00:00.000Z', status: 'in_progress', isBlocked: false, actualStartAt: '2026-06-20T01:00:00.000Z', createdBy: 'pm-1', createdAt: '2026-06-20T01:00:00.000Z', updatedAt: '2026-06-24T02:00:00.000Z', ...patch, }; } function testCase(patch: Partial = {}): TestCase { return { id: 'tc-1', caseNo: 'TC-001', versionId: 'ver-1', title: 'Verify warning panel', categoryId: 'ui-interaction', priority: 'P2', assigneeId: 'qa-other', status: 'running', startedAt: '2026-06-20T03:00:00.000Z', createdBy: 'qa-other', createdAt: '2026-06-20T01:00:00.000Z', updatedAt: '2026-06-24T02:00:00.000Z', ...patch, }; } function bug(patch: Partial = {}): Bug { return { id: 'bug-1', bugNo: 'BUG-001', versionId: 'ver-1', testCaseId: 'tc-1', title: 'Warning count mismatch', description: 'The count is wrong.', severity: 'major', priority: 'P1', reportedBy: 'qa-other', assigneeId: 'dev-other', status: 'open', createdAt: '2026-06-20T01:00:00.000Z', updatedAt: '2026-06-24T02:00:00.000Z', ...patch, }; } test('buildVersionDailyEvidence groups version activity and detects no activity/no update', () => { const workItems = buildXiaobaoWorkItems({ plans: [plan()], devTasks: [devTask()], testCases: [testCase()], bugs: [bug()], versions: [{ id: 'ver-1', name: 'V1.0', productName: 'FTB', projectName: 'PM' }], requirementVersionMap: new Map([['req-1', 'ver-1']]), }); const activities: WorkActivity[] = [ { id: 'act-delivery', actorId: 'dev-other', date: '2026-06-29', occurredAt: '2026-06-29T02:00:00.000Z', sourceType: 'dev_task', sourceId: 'dev-1', action: 'dev_task_submitted', category: 'delivery', title: 'Build warning panel', summary: 'Submitted development task', }, { id: 'act-risk', actorId: 'qa-other', date: '2026-06-29', occurredAt: '2026-06-29T03:00:00.000Z', sourceType: 'bug', sourceId: 'bug-1', action: 'bug_blocked', category: 'risk', title: 'Warning count mismatch', summary: 'Blocked by missing data', }, ]; const worklogs: TaskWorklog[] = [ { id: 'wl-note', taskId: 'tc-1', userId: 'qa-other', date: '2026-06-29', hours: 1, workContent: 'Continued regression testing.', createdAt: '2026-06-29T04:00:00.000Z', }, ]; const evidence = buildVersionDailyEvidence({ versionId: 'ver-1', workItems, activities, worklogs, now: new Date('2026-06-29T08:00:00.000Z'), }); assert.deepEqual(evidence.todayDeliveries.map((item) => item.id), ['act-delivery']); assert.deepEqual(evidence.todayRisks.map((item) => item.id), ['act-risk']); assert.deepEqual(evidence.progressNotes.map((item) => item.id), ['wl-note']); assert.equal(evidence.recentActivityCount, 3); assert.equal(evidence.lastActivityAt, '2026-06-29T04:00:00.000Z'); assert.ok(evidence.silentRisks?.some((risk) => risk.key === 'no_update' && risk.itemId === 'plan-1')); assert.ok(evidence.silentRisks?.some((risk) => risk.key === 'no_activity' && risk.itemId === 'plan-1')); }); test('buildVersionDailyEvidence uses plan record work start time for daily hours', () => { const workItems = buildXiaobaoWorkItems({ plans: [ plan({ id: 'plan-cross-day', title: 'Product plan', actualStartAt: '2026-06-30T01:00:00.000Z', createdAt: '2026-06-30T01:00:00.000Z', }), ], versions: [{ id: 'ver-1', name: 'V1.0', productName: 'FTB', projectName: 'PM' }], requirementVersionMap: new Map(), }); const activities: WorkActivity[] = [ { id: 'act-plan-record', actorId: 'pm-1', date: '2026-07-01', occurredAt: '2026-07-01T04:00:00.000Z', sourceType: 'version_plan', sourceId: 'plan-cross-day', action: 'version_plan_requirement_progress', category: 'progress', title: 'Product plan', summary: 'Updated product plan requirement progress', metadata: { requirementId: 'req-1', workStartedAt: '2026-07-01T02:00:00.000Z', }, }, ]; const evidence = buildVersionDailyEvidence({ versionId: 'ver-1', workItems, activities, worklogs: [], now: new Date('2026-07-01T08:00:00.000Z'), }); assert.equal(evidence.todayActualHours, 2); }); test('buildVersionDailyEvidence uses previous progress next start time for dev task daily hours', () => { const workItems = buildXiaobaoWorkItems({ devTasks: [ devTask({ id: 'dev-cross-day-progress', title: 'Cross-day development', actualStartAt: '2026-07-01T01:30:00.000Z', updatedAt: '2026-07-02T10:00:00.000Z', }), ], versions: [{ id: 'ver-1', name: 'V1.0', productName: 'FTB', projectName: 'PM' }], requirementVersionMap: new Map([['req-1', 'ver-1']]), }); const activities: WorkActivity[] = [ { id: 'act-dev-progress-day-1', actorId: 'dev-1', date: '2026-07-01', occurredAt: '2026-07-01T10:00:00.000Z', sourceType: 'dev_task', sourceId: 'dev-cross-day-progress', action: 'progress_note_added', category: 'note', title: 'Cross-day development', summary: 'Updated today progress', metadata: { note: 'Finished the first part', nextStartAt: '2026-07-02T01:30:00.000Z', }, }, { id: 'act-dev-progress-day-2', actorId: 'dev-1', date: '2026-07-02', occurredAt: '2026-07-02T10:00:00.000Z', sourceType: 'dev_task', sourceId: 'dev-cross-day-progress', action: 'progress_note_added', category: 'note', title: 'Cross-day development', summary: 'Updated today progress', metadata: { note: 'Finished the second part', nextStartAt: '2026-07-03T01:30:00.000Z', }, }, ]; const evidence = buildVersionDailyEvidence({ versionId: 'ver-1', workItems, activities, worklogs: [], now: new Date('2026-07-02T10:00:00.000Z'), }); assert.equal(evidence.todayActualHours, 7.5); }); test('buildVersionDailyEvidence does not emit Infinity-day silent risk details', () => { const workItems = buildXiaobaoWorkItems({ plans: [ plan({ id: 'plan-no-evidence', title: 'Plan without evidence', createdAt: '2026-06-20T01:00:00.000Z', actualStartAt: '2026-06-20T01:00:00.000Z', }), ], versions: [{ id: 'ver-1', name: 'V1.0', productName: 'FTB', projectName: 'PM' }], requirementVersionMap: new Map(), }); const evidence = buildVersionDailyEvidence({ versionId: 'ver-1', workItems, activities: [], worklogs: [], now: new Date('2026-06-29T08:00:00.000Z'), }); assert.ok((evidence.silentRisks ?? []).length > 0); assert.equal((evidence.silentRisks ?? []).some((risk) => risk.detail.includes('Infinity')), false); }); test('buildVersionDailyEvidence mirrors daily report timestamp fallback evidence', () => { const workItems = buildXiaobaoWorkItems({ plans: [ plan({ id: 'plan-created', title: '新建产品方案', status: 'pending', createdAt: '2026-06-29T01:00:00.000Z', actualStartAt: undefined, }), ], devTasks: [ devTask({ id: 'dev-started', title: '开始开发风险列表', status: 'in_progress', actualStartAt: '2026-06-29T02:00:00.000Z', createdAt: '2026-06-28T01:00:00.000Z', updatedAt: '2026-06-29T02:00:00.000Z', }), devTask({ id: 'dev-submitted', title: '提交风险列表', status: 'submitted', actualStartAt: '2026-06-28T02:00:00.000Z', actualEndAt: '2026-06-29T03:00:00.000Z', createdAt: '2026-06-28T01:00:00.000Z', updatedAt: '2026-06-29T03:00:00.000Z', }), ], testCases: [ testCase({ id: 'tc-failed', title: '验证风险列表失败态', status: 'failed', startedAt: '2026-06-29T03:30:00.000Z', completedAt: '2026-06-29T04:00:00.000Z', createdAt: '2026-06-28T01:00:00.000Z', updatedAt: '2026-06-29T04:00:00.000Z', }), ], bugs: [ bug({ id: 'bug-fixed', title: '风险列表数量错误', status: 'fixed', createdAt: '2026-06-28T01:00:00.000Z', resolvedAt: '2026-06-29T05:00:00.000Z', updatedAt: '2026-06-29T05:00:00.000Z', }), ], versions: [{ id: 'ver-1', name: 'V1.0', productName: 'FTB', projectName: 'PM' }], requirementVersionMap: new Map([['req-1', 'ver-1']]), }); const evidence = buildVersionDailyEvidence({ versionId: 'ver-1', workItems, activities: [], worklogs: [], now: new Date('2026-06-29T08:00:00.000Z'), }); assert.deepEqual(evidence.todayCreations.map((item) => item.summary), ['新建计划:新建产品方案']); assert.ok(evidence.todayProgress.some((item) => item.summary === '开始开发:开始开发风险列表')); assert.ok(evidence.todayDeliveries.some((item) => item.summary === '已提测开发任务:提交风险列表')); assert.ok(evidence.todayDeliveries.some((item) => item.summary === '已修复 Bug:风险列表数量错误')); assert.ok(evidence.todayProgress.some((item) => item.summary === '开始测试:验证风险列表失败态')); assert.ok(evidence.todayRisks.some((item) => item.summary === '测试不通过:验证风险列表失败态')); assert.equal(evidence.totalActivityCount, 6); assert.equal(evidence.lastActivityAt, '2026-06-29T05:00:00.000Z'); assert.ok(evidence.todayActualHours > 0); }); test('buildXiaobaoWorkItems keeps all version work without current-user filtering', () => { const items = buildXiaobaoWorkItems({ plans: [plan({ owner: 'pm-other' })], devTasks: [devTask({ assigneeId: 'dev-other' })], testCases: [testCase({ assigneeId: 'qa-other' })], bugs: [bug({ assigneeId: 'dev-other' })], versions: [{ id: 'ver-1', name: 'V1.0', productName: 'FTB', projectName: 'PM' }], requirementVersionMap: new Map([['req-1', 'ver-1']]), }); assert.deepEqual(items.map((item) => item.id), ['plan-1', 'dev-1', 'tc-1', 'bug-1']); assert.deepEqual(items.map((item) => item.versionId), ['ver-1', 'ver-1', 'ver-1', 'ver-1']); }); test('buildXiaobaoWorkItems uses direct version id for no-requirement dev tasks', () => { const items = buildXiaobaoWorkItems({ devTasks: [ devTask({ id: 'dev-no-req', versionId: 'ver-1', requirementId: '', requirementName: '预入职人员支持', }), ], versions: [{ id: 'ver-1', name: 'V1.0', productName: 'FTB', projectName: 'PM' }], requirementVersionMap: new Map(), }); assert.equal(items[0].versionId, 'ver-1'); assert.equal(items[0].versionName, 'V1.0'); });