From 14693070a12ec168effbebe5b080d2ae4ca87ad0 Mon Sep 17 00:00:00 2001 From: haelyra <49814733+haelyra@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:14:37 -0400 Subject: [PATCH] fix: normalize apostrophes inside quoted Windows paths (cherry picked from commit 8ab9358e18f28160436385c7b6d0ca876d4d49ce) --- scripts/lib/inspection.js | 5 +++-- tests/lib/inspection.test.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/scripts/lib/inspection.js b/scripts/lib/inspection.js index 8308c6b67..43ed7046e 100644 --- a/scripts/lib/inspection.js +++ b/scripts/lib/inspection.js @@ -21,8 +21,9 @@ 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, '') - // Quoted Windows paths preserve spaces in the final path component. - .replace(/(["'])(?:[a-z]:[\\/]|\\\\)[^\r\n]*?\1/gu, '$1$1') + // Quoted Windows paths preserve spaces and apostrophes inside path components. + // A closing quote must be followed by a delimiter, not another name character. + .replace(/(["'])(?:[a-z]:[\\/]|\\\\)[^\r\n]*?\1(?=[:\s),;.!?]|$)/gu, '$1$1') // Directories may contain spaces, but trailing whitespace separates diagnostic prose. // Spaced leaves need an extension before a colon, or quotes; preserve extensionless diagnostics. .replace(/(?:\b[a-z]:|\\)[\\/](?:[^\\/:*?"<>|\r\n]*[^\s\\/:*?"<>|][\\/])*[^\s\\/:*?"<>|(),;]+(?: [^\\/:*?"<>|\r\n]*\.[^\s\\/:*?"<>|]+(?=:))?[\\/]*/gu, '') diff --git a/tests/lib/inspection.test.js b/tests/lib/inspection.test.js index d6c806c38..9005dad28 100644 --- a/tests/lib/inspection.test.js +++ b/tests/lib/inspection.test.js @@ -96,6 +96,21 @@ async function runTests() { } })) passed += 1; else failed += 1; + if (await test('normalizeFailureReason keeps apostrophes inside quoted Windows paths', async () => { + for (const filePath of [ + String.raw`C:\Users\O'Brien\report.js`, + String.raw`D:\Users\D'Arcy\my report.js`, + String.raw`\\server\shared files\L'Heureux\report.js`, + ]) { + assert.strictEqual(normalizeFailureReason(`Cannot open '${filePath}': permission denied`), + "cannot open '': permission denied"); + } + assert.notStrictEqual( + normalizeFailureReason(String.raw`Cannot open 'C:\Users\O'Brien\report.js': permission denied`), + normalizeFailureReason(String.raw`Cannot open 'C:\Users\O'Brien\report.js': file missing`) + ); + })) 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`, @@ -267,6 +282,20 @@ async function runTests() { assert.strictEqual(generateReport(backupPatterns).status, 'attention_needed'); })) passed += 1; else failed += 1; + if (await test('inspect groups quoted Windows paths with apostrophes and preserves diagnostics', async () => { + const runs = [ + String.raw`Cannot open 'C:\Users\O'Brien\report.js': permission denied`, + String.raw`Cannot open 'D:\Users\D'Arcy\my report.js': permission denied`, + String.raw`Cannot open '\\server\shared files\L'Heureux\report.js': permission denied`, + ].map((failureReason, index) => makeSkillRun({ id: `apostrophe-${index}`, failureReason })); + const report = inspect({ getStatus: () => ({ skillRuns: { recent: runs } }) }); + assert.strictEqual(report.status, 'attention_needed'); + assert.strictEqual(report.patternCount, 1); + assert.strictEqual(report.patterns[0].count, 3); + assert.strictEqual(report.patterns[0].normalizedReason, "cannot open '': permission denied"); + assert.deepStrictEqual(report.patterns[0].rawReasons, runs.map(run => run.failureReason)); + })) passed += 1; else failed += 1; + if (await test('detectPatterns tracks unique session IDs and versions', async () => { const runs = [ makeSkillRun({ id: 'r1', sessionId: 'sess-1', skillVersion: '1.0.0', failureReason: 'err' }),