关键改动: - 优化风险证据的日报工时与静默风险计算 - 稳定 AI 触发签名,避免证据细节变化造成重复请求 - 调整预警建议更新状态与相关测试 Co-Authored-By: Codex GPT-5 <codex@openai.com>
238 lines
8.0 KiB
TypeScript
238 lines
8.0 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { buildRiskInsightSignature } from './xiaobao-risk-ai';
|
|
import type { XiaobaoRiskInsightCacheItem } from './xiaobao-risk-cache';
|
|
import type { XiaobaoVersionRisk } from './xiaobao-risk';
|
|
import { attachXiaobaoRiskSuggestion, buildXiaobaoRiskInsightPendingKey } from './xiaobao-risk-suggestion';
|
|
|
|
function risk(patch: Partial<XiaobaoVersionRisk> = {}): XiaobaoVersionRisk {
|
|
return {
|
|
versionId: 'ver-1',
|
|
versionName: 'V1.0',
|
|
productId: 'prod-1',
|
|
productName: 'Product A',
|
|
projectId: 'proj-1',
|
|
projectName: 'Project A',
|
|
riskScore: 68,
|
|
riskLevel: 'attention',
|
|
expectedReleaseDate: '2026-07-05',
|
|
confidence: 80,
|
|
confidenceLevel: 'high',
|
|
forecastReleaseDate: '2026-07-05T10:00:00.000Z',
|
|
delayDays: 0,
|
|
remainingWorkHours: 8,
|
|
reasons: [
|
|
{
|
|
key: 'remaining_work',
|
|
title: '剩余工作量',
|
|
detail: '预计还剩 8h 工作量。',
|
|
severity: 'warning',
|
|
},
|
|
],
|
|
silentRisks: [],
|
|
signals: {
|
|
unfinishedCount: 1,
|
|
openBugCount: 0,
|
|
criticalBugCount: 0,
|
|
failedTestCount: 0,
|
|
blockedCount: 0,
|
|
silentRiskCount: 0,
|
|
daysToExpectedRelease: 2,
|
|
},
|
|
currentSnapshot: {
|
|
versionId: 'ver-1',
|
|
date: '2026-06-30',
|
|
riskScore: 68,
|
|
riskLevel: 'attention',
|
|
forecastReleaseDate: '2026-07-05T10:00:00.000Z',
|
|
openBugCount: 0,
|
|
criticalBugCount: 0,
|
|
failedTestCount: 0,
|
|
blockedCount: 0,
|
|
silentRiskCount: 0,
|
|
confidence: 80,
|
|
createdAt: '2026-06-30T10:00:00.000Z',
|
|
},
|
|
trend: { direction: 'flat', delta: 0, summary: '风险保持稳定。', pattern: 'stable' },
|
|
...patch,
|
|
};
|
|
}
|
|
|
|
function insight(itemRisk: XiaobaoVersionRisk, patch: Partial<XiaobaoRiskInsightCacheItem> = {}): XiaobaoRiskInsightCacheItem {
|
|
return {
|
|
versionId: itemRisk.versionId,
|
|
riskSignature: buildRiskInsightSignature(itemRisk),
|
|
insight: {
|
|
summary: '旧的小宝建议',
|
|
why: ['仍有工作未完成'],
|
|
forecast: '当前建议继续观察。',
|
|
suggestedActions: ['确认剩余任务负责人和完成时间。'],
|
|
ownerHints: [],
|
|
generatedAt: '2026-06-30T09:00:00.000Z',
|
|
},
|
|
generatedAt: '2026-06-30T09:00:00.000Z',
|
|
...patch,
|
|
};
|
|
}
|
|
|
|
test('attachXiaobaoRiskSuggestion provides a rule suggestion when AI cache is empty', () => {
|
|
const result = attachXiaobaoRiskSuggestion(risk(), {
|
|
insights: [],
|
|
pendingInsightKeys: [],
|
|
});
|
|
|
|
assert.equal(result.aiInsightSource, 'rule');
|
|
assert.equal(result.aiInsightUpdating, false);
|
|
assert.ok(result.aiInsight?.summary);
|
|
assert.ok(result.aiInsight?.forecast);
|
|
assert.ok((result.aiInsight?.suggestedActions.length ?? 0) > 0);
|
|
});
|
|
|
|
test('attachXiaobaoRiskSuggestion keeps previous AI suggestion while a new one is updating', () => {
|
|
const previous = risk({ riskScore: 52, riskLevel: 'attention' });
|
|
const current = risk({ riskScore: 82, riskLevel: 'at_risk', delayDays: 1 });
|
|
const pendingKey = buildXiaobaoRiskInsightPendingKey(current);
|
|
|
|
const result = attachXiaobaoRiskSuggestion(current, {
|
|
insights: [insight(previous)],
|
|
pendingInsightKeys: [pendingKey],
|
|
});
|
|
|
|
assert.equal(result.aiInsightSource, 'previous_ai');
|
|
assert.equal(result.aiInsightUpdating, true);
|
|
assert.equal(result.aiInsight?.summary, '旧的小宝建议');
|
|
});
|
|
|
|
test('attachXiaobaoRiskSuggestion hides stale pending update notices', () => {
|
|
const previous = risk({ riskScore: 52, riskLevel: 'attention' });
|
|
const current = risk({ riskScore: 82, riskLevel: 'at_risk', delayDays: 1 });
|
|
const pendingKey = buildXiaobaoRiskInsightPendingKey(current);
|
|
|
|
const result = attachXiaobaoRiskSuggestion(current, {
|
|
insights: [insight(previous)],
|
|
pendingInsightKeys: [pendingKey],
|
|
pendingInsightAttempts: { [pendingKey]: '2026-06-30T10:00:00.000Z' },
|
|
now: new Date('2026-06-30T10:04:00.000Z'),
|
|
});
|
|
|
|
assert.equal(result.aiInsightSource, 'previous_ai');
|
|
assert.equal(result.aiInsightUpdating, false);
|
|
assert.equal(result.aiInsight?.summary, insight(previous).insight.summary);
|
|
});
|
|
|
|
test('attachXiaobaoRiskSuggestion uses the current AI suggestion when the signature matches', () => {
|
|
const current = risk({ riskScore: 82, riskLevel: 'at_risk', delayDays: 1 });
|
|
|
|
const result = attachXiaobaoRiskSuggestion(current, {
|
|
insights: [insight(current, {
|
|
insight: {
|
|
summary: '新的小宝建议',
|
|
why: ['风险已升高'],
|
|
forecast: '预计延期 1 天。',
|
|
suggestedActions: ['先处理阻塞和失败用例。'],
|
|
ownerHints: [],
|
|
generatedAt: '2026-06-30T10:00:00.000Z',
|
|
},
|
|
generatedAt: '2026-06-30T10:00:00.000Z',
|
|
})],
|
|
pendingInsightKeys: [],
|
|
});
|
|
|
|
assert.equal(result.aiInsightSource, 'current_ai');
|
|
assert.equal(result.aiInsightUpdating, false);
|
|
assert.equal(result.aiInsight?.summary, '新的小宝建议');
|
|
});
|
|
|
|
test('buildXiaobaoRiskInsightPendingKey stays stable for refresh-only risk drift', () => {
|
|
const before = risk({
|
|
riskLevel: 'likely_delayed',
|
|
riskScore: 100,
|
|
confidence: 65,
|
|
forecastReleaseDate: '2026-07-14T03:37:15.903Z',
|
|
signals: { ...risk().signals, failedTestCount: 14, silentRiskCount: 58, daysToExpectedRelease: 1 },
|
|
dailyEvidence: {
|
|
todayDeliveries: [],
|
|
todayProgress: [
|
|
{ id: 'ev-2', title: 'Progress', summary: 'Fixed login issue.', occurredAt: '2026-06-29T03:37:15.903Z' },
|
|
],
|
|
todayCreations: [],
|
|
todayRisks: [],
|
|
progressNotes: [],
|
|
needsProgressItems: [],
|
|
recentActivityCount: 12,
|
|
totalActivityCount: 6,
|
|
todayActualHours: 1.5,
|
|
lastActivityAt: '2026-06-29T03:37:15.903Z',
|
|
},
|
|
});
|
|
const after = risk({
|
|
riskLevel: 'likely_delayed',
|
|
riskScore: 100,
|
|
confidence: 65,
|
|
forecastReleaseDate: '2026-07-15T06:21:38.597Z',
|
|
signals: { ...risk().signals, failedTestCount: 14, silentRiskCount: 58, daysToExpectedRelease: 0 },
|
|
dailyEvidence: {
|
|
todayDeliveries: [],
|
|
todayProgress: [
|
|
{ id: 'ev-2', title: 'Progress', summary: 'Fixed login issue.', occurredAt: '2026-06-29T06:21:38.597Z' },
|
|
],
|
|
todayCreations: [],
|
|
todayRisks: [],
|
|
progressNotes: [],
|
|
needsProgressItems: [],
|
|
recentActivityCount: 13,
|
|
totalActivityCount: 6,
|
|
todayActualHours: 3,
|
|
lastActivityAt: '2026-06-29T06:21:38.597Z',
|
|
},
|
|
});
|
|
|
|
assert.equal(buildXiaobaoRiskInsightPendingKey(before), buildXiaobaoRiskInsightPendingKey(after));
|
|
});
|
|
|
|
test('buildXiaobaoRiskInsightPendingKey stays stable when only daily report evidence details change', () => {
|
|
const before = risk({
|
|
riskLevel: 'at_risk',
|
|
riskScore: 82,
|
|
confidence: 70,
|
|
dailyEvidence: {
|
|
todayDeliveries: [],
|
|
todayProgress: [
|
|
{ id: 'ev-old', title: 'Progress', summary: 'Worked on checkout flow.', occurredAt: '2026-06-30T03:00:00.000Z' },
|
|
],
|
|
todayCreations: [],
|
|
todayRisks: [],
|
|
progressNotes: [],
|
|
needsProgressItems: [],
|
|
recentActivityCount: 2,
|
|
totalActivityCount: 1,
|
|
todayActualHours: 1,
|
|
lastActivityAt: '2026-06-30T03:00:00.000Z',
|
|
},
|
|
});
|
|
const after = risk({
|
|
riskLevel: 'at_risk',
|
|
riskScore: 82,
|
|
confidence: 70,
|
|
dailyEvidence: {
|
|
todayDeliveries: [],
|
|
todayProgress: [
|
|
{ id: 'ev-new', title: 'Progress', summary: 'Updated checkout flow and added handoff notes.', occurredAt: '2026-07-01T04:00:00.000Z' },
|
|
{ id: 'ev-new-2', title: 'Progress', summary: 'Confirmed QA scope.', occurredAt: '2026-07-01T05:00:00.000Z' },
|
|
],
|
|
todayCreations: [],
|
|
todayRisks: [],
|
|
progressNotes: [
|
|
{ id: 'note-new', title: 'Note', summary: 'Next work starts tomorrow morning.', occurredAt: '2026-07-01T06:00:00.000Z' },
|
|
],
|
|
needsProgressItems: [],
|
|
recentActivityCount: 6,
|
|
totalActivityCount: 3,
|
|
todayActualHours: 4,
|
|
lastActivityAt: '2026-07-01T06:00:00.000Z',
|
|
},
|
|
});
|
|
|
|
assert.equal(buildXiaobaoRiskInsightPendingKey(before), buildXiaobaoRiskInsightPendingKey(after));
|
|
});
|