关键改动: - 优化风险证据的日报工时与静默风险计算 - 稳定 AI 触发签名,避免证据细节变化造成重复请求 - 调整预警建议更新状态与相关测试 Co-Authored-By: Codex GPT-5 <codex@openai.com>
366 lines
11 KiB
TypeScript
366 lines
11 KiB
TypeScript
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']);
|
||
});
|