diff --git a/packages/support/acp-snapshot/src/normalize.ts b/packages/support/acp-snapshot/src/normalize.ts index f984c231ea..e010afebcb 100644 --- a/packages/support/acp-snapshot/src/normalize.ts +++ b/packages/support/acp-snapshot/src/normalize.ts @@ -22,6 +22,7 @@ const EVENT_READ_OMITTED_BYTES_RE = /(\r?\n\r?\n\(Omitted )\d+( bytes\.)/g const EVENT_READ_TARGET_REGION_RE = /^Session [^\r\n]+ — [^\r\n]+\r?\nTarget event seq \d+:\r?\n```json\r?\n\{\r?\n[\s\S]*?(?=\r?\n```(?:\r?\n|$)|\r?\n\r?\n\(Omitted )/ const PATH_TEXT_BOUNDARY_RE = /[\s<>'"`()\[\]{},;:!?=]/ +const FILE_URI_PATH_PREFIX_RE = /(?:^|[^a-z0-9+.-])file:\/\/\/?$/i /** A UUID v4 string, the shape `randomUUID()` produces for session ids. */ const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi @@ -97,12 +98,14 @@ function isCwdMatch(value: string, start: number, length: number): boolean { const before = value[start - 1] const after = value[start + length] const afterPunctuation = value[start + length + 1] - const startsAtBoundary = before === undefined || PATH_TEXT_BOUNDARY_RE.test(before) + const startsAtBoundary = before === undefined + || PATH_TEXT_BOUNDARY_RE.test(before) + || FILE_URI_PATH_PREFIX_RE.test(value.slice(0, start)) const endsAtBoundary = after === undefined || after === '/' || after === '\\' || PATH_TEXT_BOUNDARY_RE.test(after) - || after === '.' && (afterPunctuation === undefined || /\s/.test(afterPunctuation)) + || after === '.' && (afterPunctuation === undefined || PATH_TEXT_BOUNDARY_RE.test(afterPunctuation)) return startsAtBoundary && endsAtBoundary } diff --git a/packages/support/acp-snapshot/tests/normalize.spec.ts b/packages/support/acp-snapshot/tests/normalize.spec.ts index a2c824519e..8bc6a66d14 100644 --- a/packages/support/acp-snapshot/tests/normalize.spec.ts +++ b/packages/support/acp-snapshot/tests/normalize.spec.ts @@ -46,6 +46,28 @@ describe('normalizeStdout', () => { expect(out).not.toContain(ctx.sessionIds[0] as string) }) + it('scrubs cwd at file URI and chained-punctuation boundaries', () => { + const raw = JSON.stringify({ + jsonrpc: '2.0', + method: 'session/update', + params: { + uri: `file://${ctx.cwd}/proof.txt`, + punctuated: `${ctx.cwd}.,`, + dottedSegment: `${ctx.cwd}.backup`, + dashedSegment: `${ctx.cwd}-backup`, + }, + }) + const frame = JSON.parse(normalizeStdout(raw, ctx)) as { + params: Record + } + expect(frame.params).toEqual({ + uri: 'file://{{cwd}}/proof.txt', + punctuated: '{{cwd}}.,', + dottedSegment: `${ctx.cwd}.backup`, + dashedSegment: `${ctx.cwd}-backup`, + }) + }) + it('scrubs every filesystem spelling of the cwd longest-first', () => { const longCwd = String.raw`C:\Users\runneradmin\AppData\Local\Temp\acp-snapshot` const aliasedCtx: NormalizeContext = { @@ -213,6 +235,22 @@ describe('normalizeSessionLog', () => { expect(out).not.toContain(ctx.cwd) }) + it('scrubs cwd at file URI and chained-punctuation boundaries in event data', () => { + const ev = JSON.stringify({ + type: 'tool/result', + seq: 2, + time: 5, + data: { + uri: `file://${ctx.cwd}/proof.txt`, + punctuated: `${ctx.cwd}.,`, + }, + }) + const out = normalizeSessionLog(`${header({ cwd: ctx.cwd })}\n${ev}\n`, ctx) + expect(out).toContain('file://{{cwd}}/proof.txt') + expect(out).toContain('{{cwd}}.,') + expect(out).not.toContain(`file://${ctx.cwd}`) + }) + it('scrubs random local spill paths under the snapshot cwd', () => { const ev = JSON.stringify({ type: 'tool/result', seq: 2, time: 5,