import test from 'node:test'; import assert from 'node:assert/strict'; import type { Bug } from './bug'; import { calcXiaobaoVersionRisk } from './xiaobao-risk'; import { buildRiskSignature, findLatestDailySnapshot, shouldSaveRiskSnapshot, summarizeRiskTrend } from './xiaobao-risk-trend'; import type { XiaobaoRiskSnapshot } from './xiaobao-risk-trend'; function snapshot(patch: Partial): XiaobaoRiskSnapshot { return { versionId: 'ver-1', date: '2026-06-27', riskScore: 38, riskLevel: 'attention', openBugCount: 0, failedTestCount: 0, blockedCount: 0, silentRiskCount: 0, confidence: 80, createdAt: '2026-06-27T01:00:00.000Z', ...patch, }; } test('summarizeRiskTrend detects continuous rising risk across three snapshots', () => { const trend = summarizeRiskTrend([ snapshot({ riskScore: 71, createdAt: '2026-06-29T01:00:00.000Z', date: '2026-06-29' }), snapshot({ riskScore: 38, createdAt: '2026-06-27T01:00:00.000Z', date: '2026-06-27' }), snapshot({ riskScore: 52, createdAt: '2026-06-28T01:00:00.000Z', date: '2026-06-28' }), ]); assert.equal(trend.direction, 'up'); assert.equal(trend.delta, 33); assert.equal(trend.pattern, 'continuous_rising'); }); test('buildRiskSignature changes when score and bug counts change', () => { const base = buildRiskSignature(snapshot({ riskScore: 52, openBugCount: 1 })); const changed = buildRiskSignature(snapshot({ riskScore: 71, openBugCount: 2 })); assert.notEqual(base, changed); }); test('buildRiskSignature changes when critical bug count changes without open bug count change', () => { const base = buildRiskSignature(snapshot({ openBugCount: 2, criticalBugCount: 0 })); const changed = buildRiskSignature(snapshot({ openBugCount: 2, criticalBugCount: 1 })); assert.notEqual(base, changed); }); test('findLatestDailySnapshot returns latest same-day snapshot for a version', () => { const latest = findLatestDailySnapshot([ snapshot({ versionId: 'ver-1', date: '2026-06-29', riskScore: 40, createdAt: '2026-06-29T09:00:00.000Z' }), snapshot({ versionId: 'ver-1', date: '2026-06-29', riskScore: 45, createdAt: '2026-06-29T10:00:00.000Z' }), snapshot({ versionId: 'ver-1', date: '2026-06-28', riskScore: 70, createdAt: '2026-06-28T10:00:00.000Z' }), snapshot({ versionId: 'ver-2', date: '2026-06-29', riskScore: 90, createdAt: '2026-06-29T11:00:00.000Z' }), ], 'ver-1', '2026-06-29'); assert.equal(latest?.riskScore, 45); }); test('shouldSaveRiskSnapshot skips small same-day changes inside throttle window', () => { const previous = snapshot({ date: '2026-06-29', riskScore: 40, createdAt: '2026-06-29T10:00:00.000Z' }); const current = snapshot({ date: '2026-06-29', riskScore: 43, createdAt: '2026-06-29T10:03:00.000Z' }); assert.equal(shouldSaveRiskSnapshot(current, previous, new Date('2026-06-29T10:03:00.000Z')), false); }); test('shouldSaveRiskSnapshot saves material signal changes immediately', () => { const previous = snapshot({ date: '2026-06-29', riskScore: 40, criticalBugCount: 0, createdAt: '2026-06-29T10:00:00.000Z' }); const current = snapshot({ date: '2026-06-29', riskScore: 41, criticalBugCount: 1, createdAt: '2026-06-29T10:02:00.000Z' }); assert.equal(shouldSaveRiskSnapshot(current, previous, new Date('2026-06-29T10:02:00.000Z')), true); }); test('shouldSaveRiskSnapshot saves minor signature changes after throttle window', () => { const previous = snapshot({ date: '2026-06-29', riskScore: 40, createdAt: '2026-06-29T10:00:00.000Z' }); const current = snapshot({ date: '2026-06-29', riskScore: 43, createdAt: '2026-06-29T10:12:00.000Z' }); assert.equal(shouldSaveRiskSnapshot(current, previous, new Date('2026-06-29T10:12:00.000Z')), true); }); test('calcXiaobaoVersionRisk uses all open bugs in the current trend snapshot signature', () => { const now = new Date('2026-07-02T01:00:00.000Z'); const risk = calcXiaobaoVersionRisk({ version: { id: 'ver-1', name: 'V1.0', expectedReleaseDate: '2026-07-03T10:00:00.000Z', members: [{ name: 'Alice' }], }, devTasks: [], testCases: [], bugs: [bug({ severity: 'major', priority: 'P2' })], now, }); const currentSignature = (risk.trend as { currentSignature?: string }).currentSignature; assert.equal(risk.signals.openBugCount, 1); assert.equal(risk.signals.criticalBugCount, 0); assert.equal(risk.currentSnapshot.openBugCount, 1); assert.equal(risk.currentSnapshot.criticalBugCount, 0); assert.equal(currentSignature, buildRiskSignature(risk.currentSnapshot)); assert.equal(buildRiskSignature(risk.currentSnapshot).split('|')[5], '1'); }); function bug(patch: Partial = {}): Bug { return { id: 'bug-1', bugNo: 'BUG-001', versionId: 'ver-1', testCaseId: 'tc-1', title: 'Non-critical open bug', description: 'Open but not critical.', severity: 'major', priority: 'P2', reportedBy: 'qa-1', assigneeId: 'dev-1', status: 'open', createdAt: '2026-07-01T09:00:00.000Z', updatedAt: '2026-07-01T09:00:00.000Z', ...patch, }; }