diff --git a/scripts/lib/inspection.js b/scripts/lib/inspection.js index 21d89a7b4..2150ccf83 100644 --- a/scripts/lib/inspection.js +++ b/scripts/lib/inspection.js @@ -21,7 +21,11 @@ function normalizeFailureReason(reason) { .replace(/\d{4}-\d{2}-\d{2}[t ]\d{2}:\d{2}:\d{2}[.\dz]*/g, '') // Strip UUIDs (already lowercased) .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g, '') - // Strip file paths + // Quoting disambiguates spaces in the final Windows path component. + .replace(/(["'])(?:[a-z]:[\\/]|\\\\)[^\r\n]*?\1/gu, '$1$1') + // Directories may contain spaces, but trailing whitespace separates diagnostic prose. + .replace(/(?:\b[a-z]:|\\)[\\/](?:[^\\/:*?"<>|\r\n]*[^\s\\/:*?"<>|][\\/])*[^\s\\/:*?"<>|(),;]+/gu, '') + // Strip remaining file paths .replace(/(?:\b[a-z]:)?[\\/][\w./\\-]+/g, '') // Collapse whitespace .replace(/\s+/g, ' ') diff --git a/tests/lib/inspection.test.js b/tests/lib/inspection.test.js index bf948cf8f..f354ef6d6 100644 --- a/tests/lib/inspection.test.js +++ b/tests/lib/inspection.test.js @@ -8,6 +8,7 @@ const { normalizeFailureReason, groupFailures, detectPatterns, + inspect, generateReport, suggestAction, DEFAULT_FAILURE_THRESHOLD, @@ -63,6 +64,8 @@ async function runTests() { String.raw`File not found: C:\project-a\src\one.js`, 'File not found: D:/project-b/src/two.js', String.raw`File not found: \\server\share\three.js`, + String.raw`File not found: C:/project-a\src\one.js`, + String.raw`File not found: D:\project-b/src/two.js`, ]) { assert.strictEqual(normalizeFailureReason(reason), 'file not found: '); } @@ -73,6 +76,38 @@ async function runTests() { assert.strictEqual(normalizeFailureReason(null), 'unknown'); assert.strictEqual(normalizeFailureReason(''), 'unknown'); assert.strictEqual(normalizeFailureReason(undefined), 'unknown'); + assert.strictEqual(normalizeFailureReason(123), 'unknown'); + })) passed += 1; else failed += 1; + + if (await test('normalizeFailureReason strips quoted Windows paths with spaces and Unicode', async () => { + for (const filePath of [ + String.raw`C:\Usuarios\José Pérez\mi archivo.js`, + String.raw`D:\用户\张三\我的 文件.js`, + String.raw`\\servidor\recurso compartido\José\mi archivo.js`, + ]) { + for (const quote of ['"', "'"]) { + assert.strictEqual(normalizeFailureReason(`Cannot open ${quote}${filePath}${quote}: permission denied`), + `cannot open ${quote}${quote}: permission denied`); + } + } + })) passed += 1; else failed += 1; + + if (await test('normalizeFailureReason preserves diagnostics after unquoted Windows paths', async () => { + for (const filePath of [ + String.raw`C:\Program Files\node`, + 'D:/Program Files/node', + String.raw`\\server\shared files\node`, + ]) { + assert.strictEqual(normalizeFailureReason(`${filePath}: permission denied`), ': permission denied'); + } + assert.strictEqual(normalizeFailureReason('Request failed: https://example.com/node'), 'request failed: https:'); + assert.strictEqual(normalizeFailureReason('Cannot run /usr/local/bin/node: permission denied'), 'cannot run : permission denied'); + assert.strictEqual(normalizeFailureReason('Cannot open D:/Users/Alice Smith/one.js permission denied in /tmp/log'), + 'cannot open permission denied in '); + assert.strictEqual(normalizeFailureReason('Cannot run C:/tools/node failed to open /tmp/log'), + 'cannot run failed to open '); + assert.strictEqual(normalizeFailureReason('D:/Program Files/node permission denied in /tmp/log'), + ' permission denied in '); })) passed += 1; else failed += 1; if (await test('groupFailures groups by skillId and normalized reason', async () => { @@ -179,15 +214,15 @@ async function runTests() { if (await test('detectPatterns groups failures from different Windows paths', async () => { const runs = [ - makeSkillRun({ id: 'r1', failureReason: String.raw`File not found: C:\project-a\one.js` }), - makeSkillRun({ id: 'r2', failureReason: 'File not found: D:/project-b/two.js' }), - makeSkillRun({ id: 'r3', failureReason: String.raw`File not found: E:\project-c\three.js` }), + makeSkillRun({ id: 'r1', failureReason: String.raw`Cannot open C:\Users\Alice Smith\one.js: permission denied` }), + makeSkillRun({ id: 'r2', failureReason: 'Cannot open D:/Users/Bob Jones/two.js: permission denied' }), + makeSkillRun({ id: 'r3', failureReason: String.raw`Cannot open E:\Users\Carol White\three.js: permission denied` }), ]; const patterns = detectPatterns(runs); assert.strictEqual(patterns.length, 1); assert.strictEqual(patterns[0].count, 3); - assert.strictEqual(patterns[0].normalizedReason, 'file not found: '); - assert.strictEqual(generateReport(patterns).status, 'attention_needed'); + assert.strictEqual(patterns[0].normalizedReason, 'cannot open : permission denied'); + assert.strictEqual(inspect({ getStatus: () => ({ skillRuns: { recent: runs } }) }).status, 'attention_needed'); })) passed += 1; else failed += 1; if (await test('detectPatterns tracks unique session IDs and versions', async () => { @@ -209,6 +244,7 @@ async function runTests() { assert.strictEqual(report.patternCount, 0); assert.ok(report.summary.includes('No recurring')); assert.ok(report.generatedAt); + assert.strictEqual(inspect({ getStatus: () => ({ skillRuns: {} }) }, { threshold: 1, windowSize: 1 }).status, 'clean'); })) passed += 1; else failed += 1; if (await test('generateReport produces structured report from patterns', async () => {