Files
ftb-project-management/apps/web/lib/workspace-daily-report.test.ts
Script Generator 8947aeaac7 feat(工作台): 细化计划进展工时证据
关键改动:

- 支持需求覆盖和调研方向开始工作记录

- 日报按计划记录开始时间和进度下次开始时间计算证据

- 更新版本编辑校验、项目展示和 workflow 说明

Co-Authored-By: Codex GPT-5 <codex@openai.com>
2026-07-01 11:02:38 +08:00

623 lines
17 KiB
TypeScript

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, '跨天开发任务');
});