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