Files
ftb-project-management/apps/web/lib/xiaobao-risk.test.ts
2026-06-29 19:50:33 +08:00

197 lines
5.6 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { calcXiaobaoVersionRisk } from './xiaobao-risk';
import type { DevTask } from './dev-task';
import type { TestCase } from './test-case';
import type { Bug } from './bug';
import type { VersionDailyEvidence } from './xiaobao-risk-evidence';
function version(patch: any = {}) {
return {
id: 'ver-1',
name: 'V1.0',
status: 'developing',
productName: 'FTB',
projectName: '项目管理',
expectedReleaseDate: '2026-07-03T10:00:00.000Z',
members: [{ name: '张三', role: 'frontend' }],
...patch,
};
}
function devTask(patch: Partial<DevTask> = {}): DevTask {
return {
id: 'dev-1',
taskNo: 'DEV-001',
requirementId: 'req-1',
title: '实现版本风险页',
categoryId: 'frontend',
assigneeId: 'm-1',
priority: 'P1',
expectedStartAt: '2026-07-01T09:00:00.000Z',
expectedEndAt: '2026-07-03T18:00:00.000Z',
estimateHours: 40,
status: 'todo',
isBlocked: false,
createdBy: 'm-1',
createdAt: '2026-07-01T09:00:00.000Z',
updatedAt: '2026-07-01T09:00:00.000Z',
...patch,
};
}
function testCase(patch: Partial<TestCase> = {}): TestCase {
return {
id: 'tc-1',
caseNo: 'TC-001',
versionId: 'ver-1',
title: '验收版本风险页',
categoryId: 'ui-interaction',
priority: 'P1',
assigneeId: 'm-2',
status: 'pending',
estimateHours: 16,
createdBy: 'm-2',
createdAt: '2026-07-01T09:00:00.000Z',
updatedAt: '2026-07-01T09:00:00.000Z',
...patch,
};
}
function bug(patch: Partial<Bug> = {}): Bug {
return {
id: 'bug-1',
bugNo: 'BUG-001',
versionId: 'ver-1',
testCaseId: 'tc-1',
title: '发布阻断缺陷',
description: '关键流程无法通过',
severity: 'critical',
priority: 'P1',
reportedBy: 'm-2',
assigneeId: 'm-1',
status: 'open',
createdAt: '2026-07-01T09:00:00.000Z',
updatedAt: '2026-07-01T09:00:00.000Z',
...patch,
};
}
test('calcXiaobaoVersionRisk predicts delay when remaining work exceeds release date', () => {
const risk = calcXiaobaoVersionRisk({
version: version(),
devTasks: [devTask()],
testCases: [testCase()],
bugs: [],
now: new Date('2026-07-02T01:00:00.000Z'),
});
assert.equal(risk.riskLevel, 'likely_delayed');
assert.ok(risk.riskScore >= 70);
assert.ok(risk.delayDays > 0);
assert.ok(risk.reasons.some((reason: { key: string }) => reason.key === 'remaining_work'));
});
test('calcXiaobaoVersionRisk marks blocked when critical Bug / blocked dev task / failed test exist', () => {
const risk = calcXiaobaoVersionRisk({
version: version(),
devTasks: [devTask({ isBlocked: true, blockReason: '依赖接口未完成' })],
testCases: [testCase({ status: 'failed', failReason: '核心流程失败' })],
bugs: [bug()],
now: new Date('2026-07-02T01:00:00.000Z'),
});
assert.equal(risk.riskLevel, 'blocked');
assert.ok(risk.reasons.some((reason: { key: string }) => reason.key === 'critical_bug'));
assert.ok(risk.reasons.some((reason: { key: string }) => reason.key === 'blocked_work'));
});
test('calcXiaobaoVersionRisk preserves daily evidence on the returned risk object', () => {
const dailyEvidence: VersionDailyEvidence = {
todayDeliveries: [{ id: 'ev-1', title: '提交提测', summary: '完成核心流程', occurredAt: '2026-07-02T02:00:00.000Z' }],
todayProgress: [],
todayRisks: [],
progressNotes: [],
needsProgressItems: [],
recentActivityCount: 1,
lastActivityAt: '2026-07-02T02:00:00.000Z',
};
const risk = calcXiaobaoVersionRisk({
version: version(),
devTasks: [],
testCases: [],
bugs: [],
dailyEvidence,
now: new Date('2026-07-02T01:00:00.000Z'),
});
assert.equal(risk.dailyEvidence, dailyEvidence);
});
test('calcXiaobaoVersionRisk uses silent risks from daily evidence', () => {
const silentRisk = {
key: 'no_progress_today',
title: '今日无进展',
detail: '进行中的任务今天没有动态。',
itemId: 'dev-1',
itemType: 'dev_task' as const,
};
const dailyEvidence: VersionDailyEvidence = {
todayDeliveries: [],
todayProgress: [],
todayRisks: [],
progressNotes: [],
needsProgressItems: [],
recentActivityCount: 0,
silentRisks: [silentRisk],
};
const risk = calcXiaobaoVersionRisk({
version: version(),
devTasks: [],
testCases: [],
bugs: [],
dailyEvidence,
now: new Date('2026-07-02T01:00:00.000Z'),
});
assert.deepEqual(risk.silentRisks, [silentRisk]);
assert.equal(risk.signals.silentRiskCount, 1);
assert.ok(risk.riskScore > 0);
assert.ok(risk.reasons.some((reason: { key: string }) => reason.key === 'silent_risk'));
});
test('calcXiaobaoVersionRisk preserves null expected release date as a compatible output value', () => {
const versionWithNullRelease = {
id: 'ver-null-release',
name: 'V1.1',
expectedReleaseDate: null,
members: [],
};
const risk = calcXiaobaoVersionRisk({
version: versionWithNullRelease,
devTasks: [],
testCases: [],
bugs: [],
now: new Date('2026-07-02T01:00:00.000Z'),
});
assert.equal(risk.expectedReleaseDate, null);
});
test('calcXiaobaoVersionRisk preserves version display context for AI interpretation', () => {
const risk = calcXiaobaoVersionRisk({
version: version({ name: 'V2.0', productName: 'FTB', projectName: 'Project PM' }),
devTasks: [],
testCases: [],
bugs: [],
now: new Date('2026-07-02T01:00:00.000Z'),
});
assert.equal(risk.versionName, 'V2.0');
assert.equal(risk.productName, 'FTB');
assert.equal(risk.projectName, 'Project PM');
});