fix(acp-snapshot): stabilize /private prefix and spill paths in snapshots

macOS symlinks /tmp → /private/tmp; when the session header cwd lacks
the /private prefix but fs tools resolve symlinks, tool result paths
get the prefix and the cwd replacement never matches.  Strip the prefix
before cwd→{{cwd}} replacement so both forms normalize identically.

Also extend refreshFixtureReplacements to match spill paths by filename
suffix, preventing raw fixture churn when a refresh runs from a
different session directory.
This commit is contained in:
_Kerman
2026-07-24 23:00:17 +08:00
parent b36a82fc98
commit 6b4c26a1b9
11 changed files with 55 additions and 10 deletions

View File

@@ -33,6 +33,7 @@ export {
type LaunchedAcpTestAgent,
} from './launcher.ts'
export {
extractSnapshotSpillPaths,
normalizeSessionLog,
normalizeStdout,
scrubRequestHeaders,

View File

@@ -30,6 +30,19 @@ const SNAPSHOT_SPILL_PATH_RE = new RegExp(
'g',
)
/**
* Extract every snapshot-mode spill path from a session log, keyed by spill
* filename. Used by refresh write-back to keep spill paths stable across runs.
*/
export function extractSnapshotSpillPaths(content: string): Map<string, string> {
const result = new Map<string, string>()
for (const match of content.matchAll(SNAPSHOT_SPILL_PATH_RE)) {
const name = match[1]
if (name) result.set(name, match[0])
}
return result
}
/** Convert separators only inside generated path-bearing text markers. */
function canonicalizeEmbeddedPaths(value: string): string {
return value
@@ -61,6 +74,11 @@ function scrubString(value: string, ctx: NormalizeContext, cwdPathMode: CwdPathM
let out = value
// cwd first (longest, most specific), then explicit session ids, then any
// residual UUID (covers ids that appear in places we didn't enumerate).
// macOS symlinks /tmp → /private/tmp and /var → /private/var. The session
// header cwd may be recorded without the /private prefix while fs tools
// resolve symlinks, producing paths with the prefix inside tool results.
// Strip the prefix before the cwd replacement so both forms match.
out = out.split(`/private${ctx.cwd}`).join(ctx.cwd)
out = out.split(ctx.cwd).join(CWD)
out = out.split(`/private${CWD}`).join(CWD)
if (cwdPathMode === 'canonical') {

View File

@@ -23,6 +23,7 @@ import { type AgentUnderTest, type HarvestedLog, type InputScript, runScenario }
import {
type CwdPathMode,
type NormalizeContext,
extractSnapshotSpillPaths,
normalizeSessionLog,
normalizeStdout,
scrubRequestHeaders,
@@ -443,7 +444,7 @@ export function unknownToolCallIds(rawLog: string): string[] {
}
/**
* Build the cross-log id/cwd replacements used by refresh write-back.
* Build the cross-log id/cwd/spill-path replacements used by refresh write-back.
*
* @param logs The freshly harvested logs, in fixture order.
* @param fixtures The existing fixture contents, in matching order.
@@ -461,6 +462,16 @@ export function refreshFixtureReplacements(logs: HarvestedLog[], fixtures: strin
replacements.push({ from, to })
}
}
// Stabilize snapshot spill paths: match by filename suffix so the raw
// fixture does not churn on every refresh from a different session run.
const freshSpills = extractSnapshotSpillPaths((logs[i] as HarvestedLog).content)
const existingSpills = extractSnapshotSpillPaths(fixtures[i] ?? '')
for (const [name, existingPath] of existingSpills) {
const freshPath = freshSpills.get(name)
if (freshPath !== undefined && freshPath !== existingPath) {
replacements.push({ from: freshPath, to: existingPath })
}
}
}
return replacements
}

View File

@@ -192,6 +192,21 @@ describe('normalizeSessionLog', () => {
expect(out).not.toContain('/private{{spillLocator')
})
it('scrubs macOS /private prefix on cwd-rooted fs tool result paths', () => {
const ev = JSON.stringify({
type: 'tool/result', seq: 2, time: 5,
data: {
content: [{
type: 'text',
text: `The file /private${ctx.cwd}/config.txt has been updated successfully.`,
}],
},
})
const out = normalizeSessionLog(`${header({ cwd: ctx.cwd })}\n${ev}\n`, ctx)
expect(out).toContain('{{cwd}}/config.txt')
expect(out).not.toContain('/private{{cwd}}')
})
it('scrubs fixed snapshot spill paths', () => {
const ev = JSON.stringify({
type: 'tool/result', seq: 2, time: 5,