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:
@@ -33,6 +33,7 @@ export {
|
||||
type LaunchedAcpTestAgent,
|
||||
} from './launcher.ts'
|
||||
export {
|
||||
extractSnapshotSpillPaths,
|
||||
normalizeSessionLog,
|
||||
normalizeStdout,
|
||||
scrubRequestHeaders,
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user