fix(小宝预警): 稳定AI建议缓存签名
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
|||||||
buildRiskInterpretRequest,
|
buildRiskInterpretRequest,
|
||||||
findLatestRiskInsightForVersion,
|
findLatestRiskInsightForVersion,
|
||||||
findPreviousRiskSnapshot,
|
findPreviousRiskSnapshot,
|
||||||
|
getReusableInsight,
|
||||||
shouldRequestRiskInsight,
|
shouldRequestRiskInsight,
|
||||||
shouldRequestRiskInsightWithCacheGate,
|
shouldRequestRiskInsightWithCacheGate,
|
||||||
shouldRequestRiskInsightWithCooldown,
|
shouldRequestRiskInsightWithCooldown,
|
||||||
@@ -328,6 +329,38 @@ test('buildRiskInsightSignature changes when trend direction delta or activity m
|
|||||||
assert.notEqual(base, changed);
|
assert.notEqual(base, changed);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('buildRiskInsightSignature stays stable when only volatile same-day forecast timing changes', () => {
|
||||||
|
const base = buildRiskInsightSignature(risk({
|
||||||
|
forecastReleaseDate: '2026-07-14T03:06:58.211Z',
|
||||||
|
signals: { ...risk().signals, daysToExpectedRelease: 30.9 },
|
||||||
|
}));
|
||||||
|
const changed = buildRiskInsightSignature(risk({
|
||||||
|
forecastReleaseDate: '2026-07-14T03:37:15.903Z',
|
||||||
|
signals: { ...risk().signals, daysToExpectedRelease: 30.1 },
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert.equal(base, changed);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getReusableInsight reuses legacy signatures with volatile forecast timestamps', () => {
|
||||||
|
const current = risk({
|
||||||
|
forecastReleaseDate: '2026-07-14T03:37:15.903Z',
|
||||||
|
signals: { ...risk().signals, daysToExpectedRelease: 30.1 },
|
||||||
|
});
|
||||||
|
const legacy = insight({
|
||||||
|
riskSignature: JSON.stringify({
|
||||||
|
...JSON.parse(buildRiskInsightSignature(current)),
|
||||||
|
forecastReleaseDate: '2026-07-14T03:06:58.211Z',
|
||||||
|
signals: {
|
||||||
|
...current.signals,
|
||||||
|
daysToExpectedRelease: 30.9,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(getReusableInsight([legacy], current)?.insight.summary, legacy.insight.summary);
|
||||||
|
});
|
||||||
|
|
||||||
test('buildRiskInterpretRequest compresses frontend risk evidence for the backend AI contract', () => {
|
test('buildRiskInterpretRequest compresses frontend risk evidence for the backend AI contract', () => {
|
||||||
const payload = buildRiskInterpretRequest(risk());
|
const payload = buildRiskInterpretRequest(risk());
|
||||||
|
|
||||||
|
|||||||
@@ -108,8 +108,8 @@ export function buildRiskInsightSignature(risk: XiaobaoVersionRisk): string {
|
|||||||
versionId: risk.versionId,
|
versionId: risk.versionId,
|
||||||
riskScore: clampScore(risk.riskScore),
|
riskScore: clampScore(risk.riskScore),
|
||||||
riskLevel: risk.riskLevel,
|
riskLevel: risk.riskLevel,
|
||||||
expectedReleaseDate: risk.expectedReleaseDate ?? null,
|
expectedReleaseDate: normalizeDateKey(risk.expectedReleaseDate),
|
||||||
forecastReleaseDate: risk.forecastReleaseDate ?? null,
|
forecastReleaseDate: normalizeDateKey(risk.forecastReleaseDate),
|
||||||
delayDays: risk.delayDays,
|
delayDays: risk.delayDays,
|
||||||
confidence: clampScore(risk.confidence),
|
confidence: clampScore(risk.confidence),
|
||||||
signals: {
|
signals: {
|
||||||
@@ -119,7 +119,7 @@ export function buildRiskInsightSignature(risk: XiaobaoVersionRisk): string {
|
|||||||
failedTestCount: risk.signals.failedTestCount,
|
failedTestCount: risk.signals.failedTestCount,
|
||||||
blockedCount: risk.signals.blockedCount,
|
blockedCount: risk.signals.blockedCount,
|
||||||
silentRiskCount: risk.signals.silentRiskCount,
|
silentRiskCount: risk.signals.silentRiskCount,
|
||||||
daysToExpectedRelease: risk.signals.daysToExpectedRelease ?? null,
|
daysToExpectedRelease: normalizeDaysToExpectedRelease(risk.signals.daysToExpectedRelease),
|
||||||
},
|
},
|
||||||
trend: {
|
trend: {
|
||||||
direction: risk.trend.direction,
|
direction: risk.trend.direction,
|
||||||
@@ -145,7 +145,11 @@ export function getReusableInsight(
|
|||||||
cache: XiaobaoRiskInsightCacheItem[],
|
cache: XiaobaoRiskInsightCacheItem[],
|
||||||
risk: XiaobaoVersionRisk,
|
risk: XiaobaoVersionRisk,
|
||||||
): XiaobaoRiskInsightCacheItem | undefined {
|
): XiaobaoRiskInsightCacheItem | undefined {
|
||||||
return findCachedInsight(cache, risk.versionId, buildRiskInsightSignature(risk));
|
const currentSignature = buildRiskInsightSignature(risk);
|
||||||
|
return (
|
||||||
|
findCachedInsight(cache, risk.versionId, currentSignature) ??
|
||||||
|
cache.find((item) => item.versionId === risk.versionId && normalizeCachedRiskSignature(item.riskSignature) === currentSignature)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findPreviousRiskSnapshot(
|
export function findPreviousRiskSnapshot(
|
||||||
@@ -284,6 +288,43 @@ function parseRiskLevel(signature: string): XiaobaoVersionRisk['riskLevel'] | un
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeCachedRiskSignature(signature: string): string | undefined {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(signature) as {
|
||||||
|
expectedReleaseDate?: string | null;
|
||||||
|
forecastReleaseDate?: string | null;
|
||||||
|
signals?: { daysToExpectedRelease?: number | null };
|
||||||
|
};
|
||||||
|
return JSON.stringify({
|
||||||
|
...parsed,
|
||||||
|
expectedReleaseDate: normalizeDateKey(parsed.expectedReleaseDate),
|
||||||
|
forecastReleaseDate: normalizeDateKey(parsed.forecastReleaseDate),
|
||||||
|
signals: {
|
||||||
|
...parsed.signals,
|
||||||
|
daysToExpectedRelease: normalizeDaysToExpectedRelease(parsed.signals?.daysToExpectedRelease),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDateKey(value: string | null | undefined): string | null {
|
||||||
|
if (!value) return null;
|
||||||
|
const raw = value.trim();
|
||||||
|
if (raw.length === 0) return null;
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}/.test(raw)) return raw.slice(0, 10);
|
||||||
|
|
||||||
|
const time = new Date(raw).getTime();
|
||||||
|
if (!Number.isFinite(time)) return raw;
|
||||||
|
return new Date(time).toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDaysToExpectedRelease(value: number | null | undefined): number | null {
|
||||||
|
if (value === null || value === undefined || !Number.isFinite(value)) return null;
|
||||||
|
return value >= 0 ? Math.ceil(value) : Math.floor(value);
|
||||||
|
}
|
||||||
|
|
||||||
function getTime(value: string): number {
|
function getTime(value: string): number {
|
||||||
const time = new Date(value).getTime();
|
const time = new Date(value).getTime();
|
||||||
return Number.isFinite(time) ? time : Number.NaN;
|
return Number.isFinite(time) ? time : Number.NaN;
|
||||||
|
|||||||
Reference in New Issue
Block a user