fix: normalize Windows paths containing spaces and Unicode

This commit is contained in:
Eneko Ruiz committed 2026-10-16 15:22:29 +02:00
1 parent e0de4739a9
commit 25b775edc1
2 files changed
+46 -6

No files matched your search

+5 -1
View File
@@ -21,7 +21,11 @@ function normalizeFailureReason(reason) {
.replace(/\d{4}-\d{2}-\d{2}[t ]\d{2}:\d{2}:\d{2}[.\dz]*/g, '<timestamp>')
// 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, '<uuid>')
// Strip file paths
// Quoting disambiguates spaces in the final Windows path component.
.replace(/(["'])(?:[a-z]:[\\/]|\\\\)[^\r\n]*?\1/gu, '$1<path>$1')
// Directories may contain spaces, but trailing whitespace separates diagnostic prose.
.replace(/(?:\b[a-z]:|\\)[\\/](?:[^\\/:*?"<>|\r\n]*[^\s\\/:*?"<>|][\\/])*[^\s\\/:*?"<>|(),;]+/gu, '<path>')
// Strip remaining file paths
.replace(/(?:\b[a-z]:)?[\\/][\w./\\-]+/g, '<path>')
// Collapse whitespace
.replace(/\s+/g, ' ')
+41 -5
View File
@@ -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: <path>');
}
@@ -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}<path>${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`), '<path>: permission denied');
}
assert.strictEqual(normalizeFailureReason('Request failed: https://example.com/node'), 'request failed: https:<path>');
assert.strictEqual(normalizeFailureReason('Cannot run /usr/local/bin/node: permission denied'), 'cannot run <path>: permission denied');
assert.strictEqual(normalizeFailureReason('Cannot open D:/Users/Alice Smith/one.js permission denied in /tmp/log'),
'cannot open <path> permission denied in <path>');
assert.strictEqual(normalizeFailureReason('Cannot run C:/tools/node failed to open /tmp/log'),
'cannot run <path> failed to open <path>');
assert.strictEqual(normalizeFailureReason('D:/Program Files/node permission denied in /tmp/log'),
'<path> permission denied in <path>');
})) 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: <path>');
assert.strictEqual(generateReport(patterns).status, 'attention_needed');
assert.strictEqual(patterns[0].normalizedReason, 'cannot open <path>: 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 () => {