246 lines
8.9 KiB
TypeScript
246 lines
8.9 KiB
TypeScript
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> = {}): 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> = {}): 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<XiaobaoRiskSnapshot>),
|
|
),
|
|
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.');
|
|
});
|