diff --git a/apps/web/lib/xiaobao-risk.test.ts b/apps/web/lib/xiaobao-risk.test.ts index 78d9dfe..51f4e38 100644 --- a/apps/web/lib/xiaobao-risk.test.ts +++ b/apps/web/lib/xiaobao-risk.test.ts @@ -128,3 +128,55 @@ test('calcXiaobaoVersionRisk preserves daily evidence on the returned risk objec assert.equal(risk.dailyEvidence, dailyEvidence); }); + +test('calcXiaobaoVersionRisk uses silent risks from daily evidence', () => { + const silentRisk = { + key: 'no_progress_today', + title: '今日无进展', + detail: '进行中的任务今天没有动态。', + itemId: 'dev-1', + itemType: 'dev_task' as const, + }; + const dailyEvidence: VersionDailyEvidence = { + todayDeliveries: [], + todayProgress: [], + todayRisks: [], + progressNotes: [], + needsProgressItems: [], + recentActivityCount: 0, + silentRisks: [silentRisk], + }; + + const risk = calcXiaobaoVersionRisk({ + version: version(), + devTasks: [], + testCases: [], + bugs: [], + dailyEvidence, + now: new Date('2026-07-02T01:00:00.000Z'), + }); + + assert.deepEqual(risk.silentRisks, [silentRisk]); + assert.equal(risk.signals.silentRiskCount, 1); + assert.ok(risk.riskScore > 0); + assert.ok(risk.reasons.some((reason: { key: string }) => reason.key === 'silent_risk')); +}); + +test('calcXiaobaoVersionRisk preserves null expected release date as a compatible output value', () => { + const versionWithNullRelease = { + id: 'ver-null-release', + name: 'V1.1', + expectedReleaseDate: null, + members: [], + }; + + const risk = calcXiaobaoVersionRisk({ + version: versionWithNullRelease, + devTasks: [], + testCases: [], + bugs: [], + now: new Date('2026-07-02T01:00:00.000Z'), + }); + + assert.equal(risk.expectedReleaseDate, null); +}); diff --git a/apps/web/lib/xiaobao-risk.ts b/apps/web/lib/xiaobao-risk.ts index 0b151e6..b676c64 100644 --- a/apps/web/lib/xiaobao-risk.ts +++ b/apps/web/lib/xiaobao-risk.ts @@ -15,7 +15,7 @@ export interface XiaobaoVersionRef { status?: string; productName?: string; projectName?: string; - expectedReleaseDate?: string; + expectedReleaseDate?: string | null; members?: Array<{ id?: string; name: string; role?: string }>; } @@ -49,6 +49,7 @@ export interface XiaobaoVersionRisk { versionId: string; riskScore: number; riskLevel: XiaobaoRiskLevel; + expectedReleaseDate: string | null; confidence: number; confidenceLevel: XiaobaoConfidenceLevel; forecastReleaseDate?: string; @@ -86,7 +87,7 @@ export function calcXiaobaoVersionRisk(input: CalcXiaobaoVersionRiskInput): Xiao const devTasks = input.devTasks ?? []; const testCases = input.testCases ?? []; const bugs = input.bugs ?? []; - const silentRisks = input.silentRisks ?? []; + const silentRisks = input.dailyEvidence?.silentRisks ?? []; const remainingDevHours = devTasks.reduce((sum, task) => { const estimate = getEstimateHours(task); @@ -164,6 +165,15 @@ export function calcXiaobaoVersionRisk(input: CalcXiaobaoVersionRiskInput): Xiao count: failedTestCount, }); } + if (silentRisks.length > 0) { + reasons.push({ + key: 'silent_risk', + title: '静默风险', + detail: `发现 ${silentRisks.length} 个今日缺少进展或证据的风险项。`, + severity: 'warning', + count: silentRisks.length, + }); + } const signals: XiaobaoRiskSignals = { unfinishedCount, @@ -193,6 +203,7 @@ export function calcXiaobaoVersionRisk(input: CalcXiaobaoVersionRiskInput): Xiao versionId: input.version.id, riskScore, riskLevel, + expectedReleaseDate: input.version.expectedReleaseDate ?? null, confidence, confidenceLevel: getConfidenceLevel(confidence), forecastReleaseDate, @@ -297,7 +308,7 @@ function getConfidenceLevel(confidence: number): XiaobaoConfidenceLevel { return 'low'; } -function parseDate(iso?: string): Date | undefined { +function parseDate(iso?: string | null): Date | undefined { if (!iso) return undefined; const date = new Date(iso); return Number.isNaN(date.getTime()) ? undefined : date;