Files
ftb-project-management/apps/web/lib/xiaobao-risk-evidence.test.ts
2026-07-01 13:33:04 +08:00

384 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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> = {}): 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> = {}): 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> = {}): 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> = {}): 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');
});