




























@@ -88,67 +88,162 @@ describe("tool mutation helpers", () => {
8888).toBe(false);
8989});
909091+it("populates structured fileTarget for file-mutating calls (#79024)", () => {
92+expect(buildToolMutationState("edit", { file_path: "/tmp/a" }).fileTarget).toEqual({
93+path: "/tmp/a",
94+});
95+expect(buildToolMutationState("write", { path: "/tmp/Foo|bar" }).fileTarget).toEqual({
96+path: "/tmp/foo|bar",
97+});
98+// Non-file-mutating tools never carry fileTarget, even with a path arg.
99+expect(buildToolMutationState("bash", { command: "rm /tmp/a" }).fileTarget).toBeUndefined();
100+expect(buildToolMutationState("exec", { command: "touch /tmp/a" }).fileTarget).toBeUndefined();
101+// apply_patch is excluded from file-mutating set, so no fileTarget even
102+// if a path-shaped arg is synthetically present.
103+expect(
104+buildToolMutationState("apply_patch", { input: "*** Update File: /tmp/a" }).fileTarget,
105+).toBeUndefined();
106+});
107+91108it("recognizes cross-tool file-mutation recovery on the same target (#79024)", () => {
92109expect(
93110isSameToolMutationAction(
94-{ toolName: "edit", actionFingerprint: "tool=edit|path=/tmp/a" },
95-{ toolName: "write", actionFingerprint: "tool=write|path=/tmp/a" },
111+{
112+toolName: "edit",
113+actionFingerprint: "tool=edit|path=/tmp/a",
114+fileTarget: { path: "/tmp/a" },
115+},
116+{
117+toolName: "write",
118+actionFingerprint: "tool=write|path=/tmp/a",
119+fileTarget: { path: "/tmp/a" },
120+},
96121),
97122).toBe(true);
98123expect(
99124isSameToolMutationAction(
100-{ toolName: "write", actionFingerprint: "tool=write|path=/tmp/a" },
101-{ toolName: "edit", actionFingerprint: "tool=edit|path=/tmp/a" },
125+{
126+toolName: "write",
127+actionFingerprint: "tool=write|path=/tmp/a",
128+fileTarget: { path: "/tmp/a" },
129+},
130+{
131+toolName: "edit",
132+actionFingerprint: "tool=edit|path=/tmp/a",
133+fileTarget: { path: "/tmp/a" },
134+},
102135),
103136).toBe(true);
104137// `apply_patch` is intentionally excluded from the file-mutating set
105138// because production `apply_patch` calls only carry opaque `input` text,
106-// so real fingerprints never have a `path=` segment to compare. Even a
107-// synthetic path-bearing fingerprint must not unlock recovery.
139+// so `extractFileTarget` returns `undefined` and the fail-closed branch
140+// refuses cross-tool recovery.
108141expect(
109142isSameToolMutationAction(
110-{ toolName: "edit", actionFingerprint: "tool=edit|path=/tmp/a" },
111-{ toolName: "apply_patch", actionFingerprint: "tool=apply_patch|path=/tmp/a" },
143+{
144+toolName: "edit",
145+actionFingerprint: "tool=edit|path=/tmp/a",
146+fileTarget: { path: "/tmp/a" },
147+},
148+{
149+toolName: "apply_patch",
150+actionFingerprint: "tool=apply_patch|path=/tmp/a",
151+fileTarget: { path: "/tmp/a" },
152+},
112153),
113154).toBe(false);
114155});
115156116157it("does not cross-recover file mutations on different targets (#79024)", () => {
117158expect(
118159isSameToolMutationAction(
119-{ toolName: "edit", actionFingerprint: "tool=edit|path=/tmp/a" },
120-{ toolName: "write", actionFingerprint: "tool=write|path=/tmp/b" },
160+{
161+toolName: "edit",
162+actionFingerprint: "tool=edit|path=/tmp/a",
163+fileTarget: { path: "/tmp/a" },
164+},
165+{
166+toolName: "write",
167+actionFingerprint: "tool=write|path=/tmp/b",
168+fileTarget: { path: "/tmp/b" },
169+},
121170),
122171).toBe(false);
123172});
124173174+it("does not over-match paths containing the fingerprint delimiter (#79024)", () => {
175+// The fingerprint string carries raw paths separated by `|`. A naive
176+// `split("|")` parser would extract `path=/tmp/a` from both fingerprints
177+// and incorrectly clear the prior failure. Structural fileTarget
178+// comparison fails closed for these distinct paths.
179+expect(
180+isSameToolMutationAction(
181+{
182+toolName: "edit",
183+actionFingerprint: "tool=edit|path=/tmp/a|left",
184+fileTarget: { path: "/tmp/a|left" },
185+},
186+{
187+toolName: "write",
188+actionFingerprint: "tool=write|path=/tmp/a|right",
189+fileTarget: { path: "/tmp/a|right" },
190+},
191+),
192+).toBe(false);
193+// Same delimiter-bearing path on both sides still matches.
194+expect(
195+isSameToolMutationAction(
196+{
197+toolName: "edit",
198+actionFingerprint: "tool=edit|path=/tmp/a|shared",
199+fileTarget: { path: "/tmp/a|shared" },
200+},
201+{
202+toolName: "write",
203+actionFingerprint: "tool=write|path=/tmp/a|shared",
204+fileTarget: { path: "/tmp/a|shared" },
205+},
206+),
207+).toBe(true);
208+});
209+125210it("does not cross-recover when the recovery tool is not file-mutating (#79024)", () => {
126211expect(
127212isSameToolMutationAction(
128-{ toolName: "edit", actionFingerprint: "tool=edit|path=/tmp/a" },
213+{
214+toolName: "edit",
215+actionFingerprint: "tool=edit|path=/tmp/a",
216+fileTarget: { path: "/tmp/a" },
217+},
129218{ toolName: "bash", actionFingerprint: "tool=bash|meta=cat /tmp/a" },
130219),
131220).toBe(false);
132221expect(
133222isSameToolMutationAction(
134-{ toolName: "edit", actionFingerprint: "tool=edit|path=/tmp/a" },
223+{
224+toolName: "edit",
225+actionFingerprint: "tool=edit|path=/tmp/a",
226+fileTarget: { path: "/tmp/a" },
227+},
135228{ toolName: "exec", actionFingerprint: "tool=exec|meta=touch /tmp/a" },
136229),
137230).toBe(false);
138231});
139232140233it("ignores call-specific noise when comparing the cross-tool target (#79024)", () => {
141-// `id=...` and `meta=...` segments must not block recovery when the
142-// stable `path=...` target still matches.
234+// `id=...` and `meta=...` segments differ between calls; structural
235+// fileTarget comparison is unaffected.
143236expect(
144237isSameToolMutationAction(
145238{
146239toolName: "edit",
147240actionFingerprint: "tool=edit|path=/tmp/a|id=42|meta=edit /tmp/a",
241+fileTarget: { path: "/tmp/a" },
148242},
149243{
150244toolName: "write",
151245actionFingerprint: "tool=write|path=/tmp/a|id=99|meta=write /tmp/a",
246+fileTarget: { path: "/tmp/a" },
152247},
153248),
154249).toBe(true);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。