@@ -88,6 +88,184 @@ test("Edit returns candidate match snippets when old_string is not unique", asyn
8888 assert . match ( candidates [ 0 ] ?. preview ?? "" , / c i t y / ) ;
8989} ) ;
9090
91+ test ( "Edit returns closest matches only above threshold with surrounding context" , async ( ) => {
92+ const workspace = createTempWorkspace ( ) ;
93+ const filePath = path . join ( workspace , "closest.ts" ) ;
94+ fs . writeFileSync (
95+ filePath ,
96+ [
97+ "const before = true;" ,
98+ "function computeSubtotal(value: number) {" ,
99+ " return value;" ,
100+ "}" ,
101+ "const after = true;" ,
102+ ] . join ( "\n" ) ,
103+ "utf8"
104+ ) ;
105+
106+ const sessionId = "closest-match-context" ;
107+ await handleReadTool ( { file_path : filePath } , createContext ( sessionId , workspace ) ) ;
108+
109+ const closeResult = await handleEditTool (
110+ {
111+ file_path : filePath ,
112+ old_string : "function computeTotal(value: number) {" ,
113+ new_string : "function computeTotal(input: number) {" ,
114+ } ,
115+ createContext ( sessionId , workspace )
116+ ) ;
117+
118+ assert . equal ( closeResult . ok , false ) ;
119+ assert . equal ( closeResult . error , "old_string not found in file." ) ;
120+ const closestMatch = closeResult . metadata ?. closest_match as
121+ | { snippet_id ?: string ; start_line ?: number ; end_line ?: number ; similarity ?: number ; preview ?: string }
122+ | undefined ;
123+ assert . ok ( closestMatch ?. snippet_id ) ;
124+ assert . equal ( closestMatch . start_line , 1 ) ;
125+ assert . equal ( closestMatch . end_line , 4 ) ;
126+ assert . ok ( ( closestMatch . similarity ?? 0 ) >= 0.8 ) ;
127+ assert . match ( closestMatch . preview ?? "" , / c o n s t b e f o r e = t r u e / ) ;
128+ assert . match ( closestMatch . preview ?? "" , / r e t u r n v a l u e / ) ;
129+
130+ const lowResult = await handleEditTool (
131+ {
132+ file_path : filePath ,
133+ old_string : 'query: string = Field(description="search query")' ,
134+ new_string : "query: string" ,
135+ } ,
136+ createContext ( sessionId , workspace )
137+ ) ;
138+
139+ assert . equal ( lowResult . ok , false ) ;
140+ assert . equal ( lowResult . error , "old_string not found in file." ) ;
141+ assert . equal ( lowResult . metadata ?. closest_match , undefined ) ;
142+
143+ const partialRead = await handleReadTool (
144+ { file_path : filePath , offset : 2 , limit : 2 } ,
145+ createContext ( sessionId , workspace )
146+ ) ;
147+ const snippet = ( partialRead . metadata ?. snippet ?? null ) as { id : string } | null ;
148+ assert . ok ( snippet ) ;
149+
150+ const scopedCloseResult = await handleEditTool (
151+ {
152+ snippet_id : snippet . id ,
153+ old_string : "function computeTotal(value: number) {" ,
154+ new_string : "function computeTotal(input: number) {" ,
155+ } ,
156+ createContext ( sessionId , workspace )
157+ ) ;
158+
159+ assert . equal ( scopedCloseResult . ok , false ) ;
160+ const scopedClosestMatch = scopedCloseResult . metadata ?. closest_match as
161+ | { start_line ?: number ; end_line ?: number ; preview ?: string }
162+ | undefined ;
163+ assert . equal ( scopedClosestMatch ?. start_line , 2 ) ;
164+ assert . equal ( scopedClosestMatch ?. end_line , 3 ) ;
165+ assert . doesNotMatch ( scopedClosestMatch ?. preview ?? "" , / c o n s t b e f o r e = t r u e / ) ;
166+ } ) ;
167+
168+ test ( "Edit allows outdated snippet matches but reports outdated snippet when no match is found" , async ( ) => {
169+ const workspace = createTempWorkspace ( ) ;
170+ const filePath = path . join ( workspace , "snippet-outdated.txt" ) ;
171+ fs . writeFileSync ( filePath , [ "alpha = 1" , "beta = 1" , "gamma = 1" ] . join ( "\n" ) , "utf8" ) ;
172+
173+ const sessionId = "outdated-snippet-miss" ;
174+ const readResult = await handleReadTool ( { file_path : filePath } , createContext ( sessionId , workspace ) ) ;
175+ const snippet = ( readResult . metadata ?. snippet ?? null ) as { id : string } | null ;
176+ assert . ok ( snippet ) ;
177+
178+ const firstEdit = await handleEditTool (
179+ {
180+ snippet_id : snippet . id ,
181+ old_string : "alpha = 1" ,
182+ new_string : "alpha = 2" ,
183+ } ,
184+ createContext ( sessionId , workspace )
185+ ) ;
186+ assert . equal ( firstEdit . ok , true ) ;
187+
188+ const secondEdit = await handleEditTool (
189+ {
190+ snippet_id : snippet . id ,
191+ old_string : "beta = 1" ,
192+ new_string : "beta = 2" ,
193+ } ,
194+ createContext ( sessionId , workspace )
195+ ) ;
196+ assert . equal ( secondEdit . ok , true ) ;
197+ assert . equal ( fs . readFileSync ( filePath , "utf8" ) , [ "alpha = 2" , "beta = 2" , "gamma = 1" ] . join ( "\n" ) ) ;
198+
199+ const missingEdit = await handleEditTool (
200+ {
201+ snippet_id : snippet . id ,
202+ old_string : "delta = 1" ,
203+ new_string : "delta = 2" ,
204+ } ,
205+ createContext ( sessionId , workspace )
206+ ) ;
207+
208+ assert . equal ( missingEdit . ok , false ) ;
209+ assert . equal (
210+ missingEdit . error ,
211+ "old_string was not found in this snippet scope. The file has changed since this snippet was created. Read the file again before editing."
212+ ) ;
213+ const outdatedScope = ( missingEdit . metadata ?. scope ?? { } ) as { snippet_id ?: string } ;
214+ assert . equal ( outdatedScope . snippet_id , snippet . id ) ;
215+
216+ const freshRead = await handleReadTool ( { file_path : filePath } , createContext ( sessionId , workspace ) ) ;
217+ const freshSnippet = ( freshRead . metadata ?. snippet ?? null ) as { id : string } | null ;
218+ assert . ok ( freshSnippet ) ;
219+
220+ const freshMissingEdit = await handleEditTool (
221+ {
222+ snippet_id : freshSnippet . id ,
223+ old_string : "delta = 1" ,
224+ new_string : "delta = 2" ,
225+ } ,
226+ createContext ( sessionId , workspace )
227+ ) ;
228+
229+ assert . equal ( freshMissingEdit . ok , false ) ;
230+ assert . equal ( freshMissingEdit . error , "old_string not found in file." ) ;
231+ } ) ;
232+
233+ test ( "Edit reports outdated snippet when a later Write changes the file and snippet matching fails" , async ( ) => {
234+ const workspace = createTempWorkspace ( ) ;
235+ const filePath = path . join ( workspace , "write-outdated.txt" ) ;
236+ fs . writeFileSync ( filePath , [ "alpha = 1" , "beta = 1" ] . join ( "\n" ) , "utf8" ) ;
237+
238+ const sessionId = "write-outdated-snippet" ;
239+ const readResult = await handleReadTool ( { file_path : filePath } , createContext ( sessionId , workspace ) ) ;
240+ const snippet = ( readResult . metadata ?. snippet ?? null ) as { id : string } | null ;
241+ assert . ok ( snippet ) ;
242+
243+ const writeResult = await handleWriteTool (
244+ {
245+ file_path : filePath ,
246+ content : [ "alpha = 2" , "gamma = 2" ] . join ( "\n" ) ,
247+ } ,
248+ createContext ( sessionId , workspace )
249+ ) ;
250+
251+ assert . equal ( writeResult . ok , true ) ;
252+
253+ const editResult = await handleEditTool (
254+ {
255+ snippet_id : snippet . id ,
256+ old_string : "beta = 1" ,
257+ new_string : "beta = 2" ,
258+ } ,
259+ createContext ( sessionId , workspace )
260+ ) ;
261+
262+ assert . equal ( editResult . ok , false ) ;
263+ assert . equal (
264+ editResult . error ,
265+ "old_string was not found in this snippet scope. The file has changed since this snippet was created. Read the file again before editing."
266+ ) ;
267+ } ) ;
268+
91269test ( "replace_all requires expected_occurrences for broad short-fragment replacements" , async ( ) => {
92270 const workspace = createTempWorkspace ( ) ;
93271 const filePath = path . join ( workspace , "openapi.yaml" ) ;
0 commit comments