diff --git a/scripts/lib/inspection.js b/scripts/lib/inspection.js index 495a36524..709a6bb73 100644 --- a/scripts/lib/inspection.js +++ b/scripts/lib/inspection.js @@ -60,6 +60,14 @@ function groupFailures(skillRuns) { return groups; } +function compareTimestampsDescending(a, b) { + const timeA = Date.parse(a); + const timeB = Date.parse(b); + if (Number.isNaN(timeA)) return Number.isNaN(timeB) ? 0 : 1; + if (Number.isNaN(timeB)) return -1; + return timeB - timeA; +} + /** * Detect recurring failure patterns from skill runs. * @@ -79,7 +87,7 @@ function detectPatterns(skillRuns, options = {}) { } const sortedRuns = [...group.runs].sort( - (a, b) => (Date.parse(b.createdAt) || 0) - (Date.parse(a.createdAt) || 0) + (a, b) => compareTimestampsDescending(a.createdAt, b.createdAt) ); const firstSeen = sortedRuns[sortedRuns.length - 1].createdAt || null; @@ -106,7 +114,7 @@ function detectPatterns(skillRuns, options = {}) { // Sort by count descending, then by lastSeen descending return patterns.sort((a, b) => { if (b.count !== a.count) return b.count - a.count; - return (Date.parse(b.lastSeen) || 0) - (Date.parse(a.lastSeen) || 0); + return compareTimestampsDescending(a.lastSeen, b.lastSeen); }); } diff --git a/tests/lib/inspection.test.js b/tests/lib/inspection.test.js index a1bf21154..c9557faa6 100644 --- a/tests/lib/inspection.test.js +++ b/tests/lib/inspection.test.js @@ -149,6 +149,36 @@ async function runTests() { assert.strictEqual(patterns[2].lastSeen, null); })) passed += 1; else failed += 1; + if (await test('detectPatterns keeps undated runs and patterns stable after dated ones', async () => { + const historical = '1969-12-31T23:59:59Z'; + const epoch = '1970-01-01T00:00:00Z'; + const runs = [ + { ...makeSkillRun({ id: 'missing-a' }), createdAt: null }, + makeSkillRun({ id: 'historical', createdAt: historical }), + { ...makeSkillRun({ id: 'missing-b' }), createdAt: null }, + makeSkillRun({ id: 'epoch', createdAt: epoch }), + ]; + + const [pattern] = detectPatterns(runs, { threshold: 2 }); + assert.deepStrictEqual(pattern.runIds, ['epoch', 'historical', 'missing-a', 'missing-b']); + assert.strictEqual(pattern.lastSeen, epoch); + const patternRuns = [ + { ...makeSkillRun({ id: 'a1', skillId: 'undated-a' }), createdAt: null }, + { ...makeSkillRun({ id: 'a2', skillId: 'undated-a' }), createdAt: null }, + makeSkillRun({ id: 'h1', skillId: 'historical', createdAt: '1969-12-31T23:59:58Z' }), + makeSkillRun({ id: 'h2', skillId: 'historical', createdAt: '1969-12-31T23:59:59Z' }), + { ...makeSkillRun({ id: 'b1', skillId: 'undated-b' }), createdAt: null }, + { ...makeSkillRun({ id: 'b2', skillId: 'undated-b' }), createdAt: null }, + ]; + + const patterns = detectPatterns(patternRuns, { threshold: 2 }); + assert.deepStrictEqual(patterns.map(pattern => pattern.skillId), ['historical', 'undated-a', 'undated-b']); + assert.deepStrictEqual(patterns[1].runIds, ['a1', 'a2']); + assert.deepStrictEqual(patterns[2].runIds, ['b1', 'b2']); + assert.strictEqual(patterns[1].firstSeen, null); + assert.strictEqual(patterns[1].lastSeen, null); + })) passed += 1; else failed += 1; + if (await test('detectPatterns uses default threshold', async () => { const runs = Array.from({ length: DEFAULT_FAILURE_THRESHOLD }, (_, i) => makeSkillRun({ id: `r${i}`, failureReason: 'permission denied' })