import assert from 'node:assert/strict'; import test from 'node:test'; import type { XiaobaoVersionRisk } from './xiaobao-risk'; import type { XiaobaoRiskSnapshot } from './xiaobao-risk-trend'; import { buildRiskInsightSignature, buildRiskInterpretRequest, shouldRequestRiskInsight, } from './xiaobao-risk-ai'; function snapshot(patch: Partial = {}): XiaobaoRiskSnapshot { return { versionId: 'ver-1', date: '2026-06-28', riskScore: 35, riskLevel: 'attention', forecastReleaseDate: '2026-07-02T10:00:00.000Z', openBugCount: 0, failedTestCount: 0, blockedCount: 0, silentRiskCount: 0, confidence: 80, createdAt: '2026-06-28T10:00:00.000Z', ...patch, }; } function risk(patch: Partial = {}): XiaobaoVersionRisk { return { versionId: 'ver-1', versionName: 'V1.0', productName: 'FTB', projectName: 'PM', riskScore: 68, riskLevel: 'attention', expectedReleaseDate: '2026-07-01T10:00:00.000Z', confidence: 80, confidenceLevel: 'high', forecastReleaseDate: '2026-07-02T10:00:00.000Z', delayDays: 1, remainingWorkHours: 12, reasons: [ { key: 'failed_test', title: 'Failed tests', detail: '1 test failed.', severity: 'warning', count: 1 }, { key: 'critical_bug', title: 'Critical bug', detail: 'P1 bug exists.', severity: 'danger', count: 1 }, ], silentRisks: [{ key: 'no_update', title: 'No update', detail: 'No update for 5 days.' }], dailyEvidence: { todayDeliveries: [{ id: 'ev-1', title: 'Delivery', summary: 'Submitted core flow.', occurredAt: '2026-06-29T09:00:00.000Z' }], todayProgress: [{ id: 'ev-2', title: 'Progress', summary: 'Fixed login issue.', occurredAt: '2026-06-29T10:00:00.000Z' }], todayRisks: [{ id: 'ev-3', title: 'Risk', summary: 'Regression failed.', occurredAt: '2026-06-29T11:00:00.000Z' }], progressNotes: [{ id: 'ev-4', title: 'Note', summary: 'Need QA retest.', occurredAt: '2026-06-29T12:00:00.000Z' }], needsProgressItems: [{ id: 'ev-5', title: 'Need update', summary: 'Backend task has no update.', occurredAt: '2026-06-29T13:00:00.000Z' }], recentActivityCount: 3, lastActivityAt: '2026-06-29T13:00:00.000Z', }, signals: { unfinishedCount: 4, openBugCount: 3, criticalBugCount: 1, failedTestCount: 1, blockedCount: 0, silentRiskCount: 1, daysToExpectedRelease: 1, }, currentSnapshot: snapshot({ riskScore: 68, openBugCount: 3, failedTestCount: 1, silentRiskCount: 1 }), trend: { direction: 'up', delta: 33, summary: 'Risk rose by 33 points.', pattern: 'score_delta' }, ...patch, } as unknown as XiaobaoVersionRisk; } test('shouldRequestRiskInsight skips on_track', () => { assert.equal(shouldRequestRiskInsight(risk({ riskLevel: 'on_track', riskScore: 10 }), undefined), false); }); test('shouldRequestRiskInsight does not trigger ordinary attention without a previous worsening signal', () => { assert.equal(shouldRequestRiskInsight(risk({ riskLevel: 'attention', riskScore: 42, signals: { ...risk().signals, unfinishedCount: 0, daysToExpectedRelease: 5 } }), undefined), false); }); test('shouldRequestRiskInsight triggers attention when risk score jumps', () => { assert.equal(shouldRequestRiskInsight(risk({ riskLevel: 'attention', riskScore: 68 }), snapshot({ riskScore: 35 })), true); }); test('shouldRequestRiskInsight triggers attention when risk signals worsen without level change', () => { assert.equal( shouldRequestRiskInsight( risk({ riskLevel: 'attention', riskScore: 52, signals: { unfinishedCount: 4, openBugCount: 3, criticalBugCount: 3, failedTestCount: 1, blockedCount: 1, silentRiskCount: 1, daysToExpectedRelease: 1, }, }), snapshot({ riskScore: 50, openBugCount: 0, failedTestCount: 0, blockedCount: 0, silentRiskCount: 0 }), ), true, ); }); test('shouldRequestRiskInsight triggers attention when critical bugs increase even if open bug total is unchanged', () => { assert.equal( shouldRequestRiskInsight( risk({ riskLevel: 'attention', riskScore: 45, signals: { unfinishedCount: 0, openBugCount: 2, criticalBugCount: 1, failedTestCount: 0, blockedCount: 0, silentRiskCount: 0, daysToExpectedRelease: 5, }, }), snapshot({ riskScore: 44, openBugCount: 2, criticalBugCount: 0 } as Partial), ), true, ); }); test('shouldRequestRiskInsight triggers attention on continuously rising trend', () => { assert.equal( shouldRequestRiskInsight( risk({ riskLevel: 'attention', riskScore: 46, signals: { unfinishedCount: 0, openBugCount: 0, criticalBugCount: 0, failedTestCount: 0, blockedCount: 0, silentRiskCount: 0, daysToExpectedRelease: 5, }, trend: { direction: 'up', delta: 16, summary: 'Risk rose continuously from 30 to 46.', pattern: 'continuous_rising' }, }), snapshot({ riskScore: 38 }), ), true, ); }); test('shouldRequestRiskInsight triggers attention on continuously rising trend without previous snapshot', () => { assert.equal( shouldRequestRiskInsight( risk({ riskLevel: 'attention', riskScore: 46, signals: { unfinishedCount: 0, openBugCount: 0, criticalBugCount: 0, failedTestCount: 0, blockedCount: 0, silentRiskCount: 0, daysToExpectedRelease: 5, }, trend: { direction: 'up', delta: 16, summary: 'Risk rose continuously from 30 to 46.', pattern: 'continuous_rising' }, }), undefined, ), true, ); }); test('shouldRequestRiskInsight triggers attention near release with unfinished work even without history', () => { assert.equal( shouldRequestRiskInsight(risk({ riskLevel: 'attention', signals: { ...risk().signals, unfinishedCount: 2, daysToExpectedRelease: 1 } }), undefined), true, ); }); test('shouldRequestRiskInsight triggers high risk levels', () => { assert.equal(shouldRequestRiskInsight(risk({ riskLevel: 'at_risk' }), undefined), true); assert.equal(shouldRequestRiskInsight(risk({ riskLevel: 'likely_delayed' }), undefined), true); assert.equal(shouldRequestRiskInsight(risk({ riskLevel: 'blocked' }), undefined), true); }); test('buildRiskInsightSignature uses all open bugs, not only critical bugs', () => { const base = buildRiskInsightSignature(risk({ signals: { ...risk().signals, openBugCount: 1, criticalBugCount: 0 }, currentSnapshot: snapshot({ openBugCount: 1, riskScore: 45 }), })); const changed = buildRiskInsightSignature(risk({ signals: { ...risk().signals, openBugCount: 2, criticalBugCount: 0 }, currentSnapshot: snapshot({ openBugCount: 2, riskScore: 45 }), })); assert.notEqual(base, changed); }); test('buildRiskInsightSignature changes when trend or evidence changes', () => { const base = buildRiskInsightSignature(risk({ trend: { direction: 'flat', delta: 0, summary: 'Risk is stable.', pattern: 'stable' }, })); const changed = buildRiskInsightSignature(risk({ trend: { direction: 'up', delta: 16, summary: 'Risk rose continuously from 30 to 46.', pattern: 'continuous_rising' }, dailyEvidence: { ...risk().dailyEvidence!, todayRisks: [{ id: 'ev-new', title: 'Risk', summary: 'New P1 regression appeared.', occurredAt: '2026-06-29T15:00:00.000Z' }], }, })); assert.notEqual(base, changed); }); test('buildRiskInsightSignature changes when trend direction delta or activity metadata changes', () => { const base = buildRiskInsightSignature(risk({ trend: { direction: 'up', delta: 10, summary: 'Risk changed.', pattern: 'score_delta' }, dailyEvidence: { ...risk().dailyEvidence!, recentActivityCount: 1, lastActivityAt: '2026-06-29T10:00:00.000Z', }, })); const changed = buildRiskInsightSignature(risk({ trend: { direction: 'down', delta: -10, summary: 'Risk changed.', pattern: 'score_delta' }, dailyEvidence: { ...risk().dailyEvidence!, recentActivityCount: 2, lastActivityAt: '2026-06-29T11:00:00.000Z', }, })); assert.notEqual(base, changed); }); test('buildRiskInterpretRequest compresses frontend risk evidence for the backend AI contract', () => { const payload = buildRiskInterpretRequest(risk()); assert.equal(payload.versionName, 'V1.0'); assert.equal(payload.reasons[0].label, 'Failed tests'); assert.equal(payload.reasons[0].severity, 'medium'); assert.equal(payload.reasons[1].severity, 'critical'); assert.deepEqual(payload.dailyEvidence.todayDeliveries, ['Submitted core flow.']); assert.deepEqual(payload.dailyEvidence.needsProgressItems, ['Backend task has no update.']); assert.equal(payload.silentRisks[0].detail, 'No update for 5 days.'); });