diff --git a/scripts/lib/inspection.js b/scripts/lib/inspection.js index e3e257b06..798ff2ce7 100644 --- a/scripts/lib/inspection.js +++ b/scripts/lib/inspection.js @@ -24,8 +24,8 @@ function normalizeFailureReason(reason) { // Quoted Windows paths preserve spaces in the final path component. .replace(/(["'])(?:[a-z]:[\\/]|\\\\)[^\r\n]*?\1/gu, '$1$1') // Directories may contain spaces, but trailing whitespace separates diagnostic prose. - // A colon delimits spaced leaves; trailing separators belong to the path. - .replace(/(?:\b[a-z]:|\\)[\\/](?:[^\\/:*?"<>|\r\n]*[^\s\\/:*?"<>|][\\/])*[^\s\\/:*?"<>|(),;]+(?: [^\\/:*?"<>|\r\n]+(?=:))?[\\/]*/gu, '') + // Spaced leaves need an extension before a colon, or quotes; preserve extensionless diagnostics. + .replace(/(?:\b[a-z]:|\\)[\\/](?:[^\\/:*?"<>|\r\n]*[^\s\\/:*?"<>|][\\/])*[^\s\\/:*?"<>|(),;]+(?: [^\\/:*?"<>|\r\n]*\.[\p{L}\p{N}_-]+(?=:))?[\\/]*/gu, '') // Strip remaining file paths .replace(/(?:\b[a-z]:)?[\\/][\w./\\-]+/g, '') // Collapse whitespace diff --git a/tests/lib/inspection.test.js b/tests/lib/inspection.test.js index 9199a8734..7e92f72aa 100644 --- a/tests/lib/inspection.test.js +++ b/tests/lib/inspection.test.js @@ -87,6 +87,7 @@ async function runTests() { String.raw`C:\Usuarios\José Pérez\mi archivo.js`, String.raw`D:\用户\张三\我的 文件.js`, String.raw`\\servidor\recurso compartido\José\mi archivo.js`, + String.raw`C:\Program Files\my node`, ]) { for (const quote of ['"', "'"]) { assert.strictEqual(normalizeFailureReason(`Cannot open ${quote}${filePath}${quote}: permission denied`), @@ -116,6 +117,9 @@ async function runTests() { ' permission denied in '); assert.strictEqual(normalizeFailureReason('Cannot run C:/tools/node failed to open /tmp/log: permission denied'), 'cannot run failed to open : permission denied'); + const denied = normalizeFailureReason(String.raw`C:\Program Files\node permission denied: EACCES`); + assert.strictEqual(denied, ' permission denied: eacces'); + assert.notStrictEqual(denied, normalizeFailureReason(String.raw`C:\Program Files\node: EACCES`)); })) passed += 1; else failed += 1; if (await test('groupFailures groups by skillId and normalized reason', async () => { @@ -138,6 +142,18 @@ async function runTests() { assert.strictEqual(skillBGroup.runs.length, 1); })) passed += 1; else failed += 1; + if (await test('keeps distinct diagnostics after extensionless Windows paths separate', async () => { + const runs = [ + String.raw`Command C:\tools\node exited with code: 1`, + String.raw`Command D:\tools\node exited with signal: 1`, + String.raw`Command E:\tools\node failed with status: 1`, + ].map((failureReason, index) => makeSkillRun({ id: `diagnostic-${index}`, failureReason })); + const patterns = detectPatterns(runs); + assert.strictEqual(generateReport(patterns).status, 'clean'); + assert.deepStrictEqual(patterns, []); + assert.strictEqual(groupFailures(runs).size, 3); + })) passed += 1; else failed += 1; + if (await test('groupFailures handles mixed outcome casing', async () => { const runs = [ makeSkillRun({ id: 'r1', outcome: 'FAILURE', failureReason: 'timeout' }),