import test from 'node:test'; import assert from 'node:assert/strict'; import type { TaskWorklog } from './task-worklog'; import type { WorkActivity } from './work-activity'; import type { WorkItem } from './workspace-engine'; import { getWorkspaceDailyReport } from './workspace-daily-report'; const workItems: WorkItem[] = [ { id: 'task-1', type: 'devTask', title: '实现登录接口', status: 'in_progress', completed: false, productName: 'FTB', projectName: '项目管理', versionName: 'V1.0', versionId: 'version-1', extra: { actualStartAt: '2026-06-26T01:00:00.000Z' }, raw: {} as any, }, { id: 'task-2', type: 'bug', title: '修复菜单错位', status: 'fixing', completed: false, productName: 'FTB', projectName: '项目管理', versionName: 'V1.0', versionId: 'version-1', raw: {} as any, }, { id: 'task-3', type: 'devTask', title: '跨天开发任务', status: 'in_progress', completed: false, productName: 'FTB', projectName: '项目管理', versionName: 'V1.0', versionId: 'version-1', extra: { actualStartAt: '2026-06-25T02:00:00.000Z' }, raw: {} as any, }, ]; const worklogs: TaskWorklog[] = [ { id: 'wl-1', taskId: 'task-1', userId: '张三', date: '2026-06-26', hours: 1.5, workContent: '完成登录接口联调', createdAt: '2026-06-26T02:00:00.000Z', }, { id: 'wl-2', taskId: 'task-2', userId: '张三', date: '2026-06-26', hours: 0.5, workContent: '定位菜单错位原因', createdAt: '2026-06-26T03:00:00.000Z', }, { id: 'wl-other-user', taskId: 'task-1', userId: '李四', date: '2026-06-26', hours: 8, workContent: '其他人的日报', createdAt: '2026-06-26T04:00:00.000Z', }, { id: 'wl-other-date', taskId: 'task-1', userId: '张三', date: '2026-06-25', hours: 8, workContent: '昨天的日报', createdAt: '2026-06-25T04:00:00.000Z', }, ]; const activities: WorkActivity[] = [ { id: 'act-delivery', actorId: '张三', date: '2026-06-26', occurredAt: '2026-06-26T06:00:00.000Z', sourceType: 'dev_task', sourceId: 'task-1', action: 'dev_task_submitted', category: 'delivery', title: '实现登录接口', summary: '已提测开发任务:实现登录接口', }, { id: 'act-risk', actorId: '张三', date: '2026-06-26', occurredAt: '2026-06-26T05:00:00.000Z', sourceType: 'bug', sourceId: 'task-2', action: 'bug_blocked', category: 'risk', title: '修复菜单错位', summary: '标记阻塞:等待设计确认', metadata: { blocker: '等待设计确认', helperId: '李四' }, }, { id: 'act-note', actorId: '张三', date: '2026-06-26', occurredAt: '2026-06-26T04:30:00.000Z', sourceType: 'manual', sourceId: 'task-1', action: 'progress_note_added', category: 'note', title: '实现登录接口', summary: '补充进展:完成接口联调', metadata: { note: '完成接口联调' }, }, { id: 'act-other-user', actorId: '李四', date: '2026-06-26', occurredAt: '2026-06-26T07:00:00.000Z', sourceType: 'dev_task', sourceId: 'task-1', action: 'dev_task_started', category: 'progress', title: '其他人的任务', summary: '其他人的日报活动', }, { id: 'act-other-date', actorId: '张三', date: '2026-06-25', occurredAt: '2026-06-25T07:00:00.000Z', sourceType: 'dev_task', sourceId: 'task-1', action: 'dev_task_started', category: 'progress', title: '昨天的任务', summary: '昨天的日报活动', }, ]; test('getWorkspaceDailyReport filters by current user and date', () => { const report = getWorkspaceDailyReport({ worklogs, workItems, userId: '张三', date: '2026-06-26', }); assert.equal(report.totalHours, 2); assert.equal(report.totalCount, 2); assert.deepEqual(report.items.map((item) => item.id), ['wl-2', 'wl-1']); }); test('getWorkspaceDailyReport resolves task title and workspace context', () => { const report = getWorkspaceDailyReport({ worklogs, workItems, userId: '张三', date: '2026-06-26', }); assert.equal(report.items[0].taskTitle, '修复菜单错位'); assert.equal(report.items[0].productName, 'FTB'); assert.equal(report.items[0].projectName, '项目管理'); assert.equal(report.items[0].versionName, 'V1.0'); }); test('getWorkspaceDailyReport keeps logs whose task is no longer visible', () => { const report = getWorkspaceDailyReport({ worklogs: [ { id: 'wl-missing', taskId: 'deleted-task', userId: '张三', date: '2026-06-26', hours: 0.5, workContent: '处理历史任务', createdAt: '2026-06-26T01:00:00.000Z', }, ], workItems: [], userId: '张三', date: '2026-06-26', }); assert.equal(report.totalHours, 0.5); assert.equal(report.totalCount, 1); assert.equal(report.items[0].taskTitle, '未知任务'); assert.equal(report.items[0].workContent, '处理历史任务'); }); test('getWorkspaceDailyReport groups current user activities by category', () => { const report = getWorkspaceDailyReport({ activities, worklogs, workItems, userId: '张三', date: '2026-06-26', }); assert.deepEqual(report.groups.delivery.map((item) => item.id), ['act-delivery']); assert.deepEqual(report.groups.risk.map((item) => item.id), ['act-risk']); assert.deepEqual(report.groups.note.map((item) => item.id), ['act-note']); assert.equal(report.totalCount, 5); assert.equal(report.groups.delivery[0].context, 'FTB / 项目管理 / V1.0'); }); test('getWorkspaceDailyReport counts actual hours from today activity sources', () => { const report = getWorkspaceDailyReport({ activities: [ { id: 'act-started', actorId: 'Alice', date: '2026-06-26', occurredAt: '2026-06-26T02:00:00.000Z', sourceType: 'dev_task', sourceId: 'task-activity-hours', action: 'dev_task_started', category: 'progress', title: 'Daily report hours', summary: 'Started development: Daily report hours', }, { id: 'act-submitted', actorId: 'Alice', date: '2026-06-26', occurredAt: '2026-06-26T04:00:00.000Z', sourceType: 'dev_task', sourceId: 'task-activity-hours', action: 'dev_task_submitted', category: 'delivery', title: 'Daily report hours', summary: 'Submitted development task: Daily report hours', }, ], worklogs: [], workItems: [ { id: 'task-activity-hours', type: 'devTask', title: 'Daily report hours', status: 'submitted', completed: true, productName: 'FTB', projectName: 'Project Management', versionName: 'V1.0', versionId: 'version-1', extra: { actualStartAt: '2026-06-26T02:00:00.000Z', actualEndAt: '2026-06-26T04:00:00.000Z', }, raw: { id: 'task-activity-hours', actualStartAt: '2026-06-26T02:00:00.000Z', actualEndAt: '2026-06-26T04:00:00.000Z', status: 'submitted', updatedAt: '2026-06-26T04:00:00.000Z', } as any, }, ], userId: 'Alice', date: '2026-06-26', }); assert.equal(report.totalHours, 2); assert.equal(report.totalCount, 2); }); test('getWorkspaceDailyReport does not double count overlapping activity time ranges', () => { const report = getWorkspaceDailyReport({ activities: [ { id: 'act-overlap-a', actorId: 'Alice', date: '2026-06-26', occurredAt: '2026-06-26T02:10:00.000Z', sourceType: 'dev_task', sourceId: 'task-overlap-a', action: 'dev_task_started', category: 'progress', title: 'Overlap A', summary: 'Started development: Overlap A', }, { id: 'act-overlap-b', actorId: 'Alice', date: '2026-06-26', occurredAt: '2026-06-26T02:20:00.000Z', sourceType: 'dev_task', sourceId: 'task-overlap-b', action: 'dev_task_started', category: 'progress', title: 'Overlap B', summary: 'Started development: Overlap B', }, ], worklogs: [], workItems: [ { id: 'task-overlap-a', type: 'devTask', title: 'Overlap A', status: 'submitted', completed: true, productName: 'FTB', projectName: 'Project Management', versionName: 'V1.0', versionId: 'version-1', extra: { actualStartAt: '2026-06-26T02:00:00.000Z', actualEndAt: '2026-06-26T04:00:00.000Z', }, raw: { id: 'task-overlap-a', actualStartAt: '2026-06-26T02:00:00.000Z', actualEndAt: '2026-06-26T04:00:00.000Z', status: 'submitted', updatedAt: '2026-06-26T04:00:00.000Z', } as any, }, { id: 'task-overlap-b', type: 'devTask', title: 'Overlap B', status: 'submitted', completed: true, productName: 'FTB', projectName: 'Project Management', versionName: 'V1.0', versionId: 'version-1', extra: { actualStartAt: '2026-06-26T02:00:00.000Z', actualEndAt: '2026-06-26T04:00:00.000Z', }, raw: { id: 'task-overlap-b', actualStartAt: '2026-06-26T02:00:00.000Z', actualEndAt: '2026-06-26T04:00:00.000Z', status: 'submitted', updatedAt: '2026-06-26T04:00:00.000Z', } as any, }, ], userId: 'Alice', date: '2026-06-26', }); assert.equal(report.totalHours, 2); assert.equal(report.totalCount, 2); }); test('getWorkspaceDailyReport uses plan record work start time for daily report hours', () => { const report = getWorkspaceDailyReport({ activities: [ { id: 'act-plan-record', actorId: 'Alice', date: '2026-06-26', occurredAt: '2026-06-26T04: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: { workStartedAt: '2026-06-26T02:00:00.000Z', }, }, ], worklogs: [], workItems: [ { id: 'plan-cross-day', type: 'plan_product', title: 'Product plan', status: 'in_progress', completed: false, productName: 'FTB', projectName: 'Project Management', versionName: 'V1.0', versionId: 'version-1', extra: { actualStartAt: '2026-06-25T01:00:00.000Z', }, raw: { id: 'plan-cross-day', actualStartAt: '2026-06-25T01:00:00.000Z', status: 'in_progress', } as any, }, ], userId: 'Alice', date: '2026-06-26', now: new Date('2026-06-26T08:00:00.000Z'), }); assert.equal(report.totalHours, 2); assert.equal(report.totalCount, 1); }); test('getWorkspaceDailyReport uses previous progress next start time for dev task daily hours', () => { const report = getWorkspaceDailyReport({ activities: [ { id: 'act-dev-progress-day-1', actorId: 'Alice', date: '2026-07-01', occurredAt: '2026-07-01T10:00:00.000Z', sourceType: 'dev_task', sourceId: 'task-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: 'Alice', date: '2026-07-02', occurredAt: '2026-07-02T10:00:00.000Z', sourceType: 'dev_task', sourceId: 'task-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', }, }, ], worklogs: [], workItems: [ { id: 'task-cross-day-progress', type: 'devTask', title: 'Cross-day development', status: 'in_progress', completed: false, productName: 'FTB', projectName: 'Project Management', versionName: 'V1.0', versionId: 'version-1', extra: { actualStartAt: '2026-07-01T01:30:00.000Z', }, raw: { id: 'task-cross-day-progress', actualStartAt: '2026-07-01T01:30:00.000Z', status: 'in_progress', } as any, }, ], userId: 'Alice', date: '2026-07-02', now: new Date('2026-07-02T10:00:00.000Z'), }); assert.equal(report.totalHours, 7.5); assert.equal(report.totalCount, 1); }); test('getWorkspaceDailyReport rebuilds activity evidence from persisted work item timestamps', () => { const report = getWorkspaceDailyReport({ activities: [], worklogs: [], workItems: [ { id: 'task-refresh-recovery', type: 'devTask', title: 'Refresh recovery', status: 'submitted', completed: true, productName: 'FTB', projectName: 'Project Management', versionName: 'V1.0', versionId: 'version-1', extra: { actualStartAt: '2026-06-26T02:00:00.000Z', actualEndAt: '2026-06-26T04:00:00.000Z', }, raw: { id: 'task-refresh-recovery', title: 'Refresh recovery', actualStartAt: '2026-06-26T02:00:00.000Z', actualEndAt: '2026-06-26T04:00:00.000Z', status: 'submitted', updatedAt: '2026-06-26T04:00:00.000Z', } as any, }, ], userId: 'Alice', date: '2026-06-26', }); assert.deepEqual(report.groups.progress.map((item) => item.action), ['dev_task_started']); assert.deepEqual(report.groups.delivery.map((item) => item.action), ['dev_task_submitted']); assert.equal(report.totalHours, 2); assert.equal(report.totalCount, 2); }); test('getWorkspaceDailyReport treats same-requirement plan progress as today evidence', () => { const report = getWorkspaceDailyReport({ activities: [ { id: 'act-plan-requirement-progress', actorId: 'Alice', date: '2026-07-01', occurredAt: '2026-07-01T03:00:00.000Z', sourceType: 'version_plan', sourceId: 'plan-product', action: 'version_plan_requirement_progress', category: 'progress', title: 'Product plan', summary: 'Updated requirement progress', metadata: { requirementId: 'req-shared', workStartedAt: '2026-07-01T02:00:00.000Z', }, }, ], worklogs: [], workItems: [ { id: 'plan-product', type: 'plan_product', title: 'Product plan', status: 'in_progress', completed: false, productName: 'FTB', projectName: 'Project Management', versionName: 'V1.0', versionId: 'version-1', extra: { actualStartAt: '2026-06-30T01:00:00.000Z', }, raw: { id: 'plan-product', actualStartAt: '2026-06-30T01:00:00.000Z', status: 'in_progress', } as any, }, { id: 'task-same-requirement', type: 'devTask', title: 'Same requirement task', status: 'in_progress', completed: false, productName: 'FTB', projectName: 'Project Management', versionName: 'V1.0', versionId: 'version-1', extra: { actualStartAt: '2026-06-30T02:00:00.000Z', }, raw: { id: 'task-same-requirement', requirementId: 'req-shared', actualStartAt: '2026-06-30T02:00:00.000Z', status: 'in_progress', } as any, }, { id: 'task-other-requirement', type: 'devTask', title: 'Other requirement task', status: 'in_progress', completed: false, productName: 'FTB', projectName: 'Project Management', versionName: 'V1.0', versionId: 'version-1', extra: { actualStartAt: '2026-06-30T02:00:00.000Z', }, raw: { id: 'task-other-requirement', requirementId: 'req-other', actualStartAt: '2026-06-30T02:00:00.000Z', status: 'in_progress', } as any, }, ], userId: 'Alice', date: '2026-07-01', now: new Date('2026-07-01T04:00:00.000Z'), }); assert.deepEqual(report.needsProgressItems.map((item) => item.id), ['task-other-requirement']); }); test('getWorkspaceDailyReport flags multi-day in-progress items without today progress', () => { const report = getWorkspaceDailyReport({ activities, worklogs, workItems, userId: '张三', date: '2026-06-26', }); assert.deepEqual(report.needsProgressItems.map((item) => item.id), ['task-3']); assert.equal(report.needsProgressItems[0].title, '跨天开发任务'); });