@@ -373,6 +373,138 @@ describe("Harness-native Agent Runtime", () => {
373373 }
374374 } ) ;
375375
376+ it ( "preflights and applies a combined existing-file and new-file edit batch" , async ( ) => {
377+ const worktree = await mkdtemp ( join ( tmpdir ( ) , "agentpatchcheck-native-runtime-" ) ) ;
378+ try {
379+ await mkdir ( join ( worktree , "docs" ) , { recursive : true } ) ;
380+ await writeFile ( join ( worktree , "README.md" ) , "before\n" , "utf8" ) ;
381+ const provider : HarnessNativeModelProvider = {
382+ id : "test-provider" ,
383+ decide : async ( { observations } ) =>
384+ observations . length === 0
385+ ? {
386+ decision : {
387+ kind : "tool" ,
388+ tool : "apply-edit-batch" ,
389+ arguments : {
390+ patches : [
391+ {
392+ path : "README.md" ,
393+ expectedText : "before" ,
394+ replacementText : "after" ,
395+ } ,
396+ ] ,
397+ creates : [ { path : "docs/usage.md" , content : "usage\n" } ] ,
398+ } ,
399+ } ,
400+ }
401+ : { decision : { kind : "finish" } } ,
402+ } ;
403+
404+ const result = await runHarnessNativeRuntime ( {
405+ policy : testNativePolicy ( { maxIterations : 2 , maxToolCalls : 1 } ) ,
406+ prompt : "Update README and add usage documentation." ,
407+ model : "test-model" ,
408+ worktreePath : worktree ,
409+ provider,
410+ timeoutMs : 1_000 ,
411+ } ) ;
412+
413+ expect ( result ) . toMatchObject ( { status : "succeeded" , toolCalls : 1 } ) ;
414+ expect ( result . trajectory [ 0 ] ) . toMatchObject ( { tool : "apply-edit-batch" , toolStatus : "ok" } ) ;
415+ expect ( await readFile ( join ( worktree , "README.md" ) , "utf8" ) ) . toBe ( "after\n" ) ;
416+ expect ( await readFile ( join ( worktree , "docs" , "usage.md" ) , "utf8" ) ) . toBe ( "usage\n" ) ;
417+ } finally {
418+ await rm ( worktree , { recursive : true , force : true } ) ;
419+ }
420+ } ) ;
421+
422+ it ( "rejects a combined edit batch before changing any target when creation preflight fails" , async ( ) => {
423+ const worktree = await mkdtemp ( join ( tmpdir ( ) , "agentpatchcheck-native-runtime-" ) ) ;
424+ try {
425+ await writeFile ( join ( worktree , "README.md" ) , "before\n" , "utf8" ) ;
426+ await writeFile ( join ( worktree , "existing.md" ) , "original\n" , "utf8" ) ;
427+ const provider : HarnessNativeModelProvider = {
428+ id : "test-provider" ,
429+ decide : async ( { observations } ) =>
430+ observations . length === 0
431+ ? {
432+ decision : {
433+ kind : "tool" ,
434+ tool : "apply-edit-batch" ,
435+ arguments : {
436+ patches : [
437+ {
438+ path : "README.md" ,
439+ expectedText : "before" ,
440+ replacementText : "after" ,
441+ } ,
442+ ] ,
443+ creates : [ { path : "existing.md" , content : "overwrite\n" } ] ,
444+ } ,
445+ } ,
446+ }
447+ : { decision : { kind : "finish" } } ,
448+ } ;
449+
450+ const result = await runHarnessNativeRuntime ( {
451+ policy : testNativePolicy ( { maxIterations : 2 , maxToolCalls : 1 } ) ,
452+ prompt : "Try an invalid batch." ,
453+ model : "test-model" ,
454+ worktreePath : worktree ,
455+ provider,
456+ timeoutMs : 1_000 ,
457+ } ) ;
458+
459+ expect ( result . trajectory [ 0 ] ) . toMatchObject ( { tool : "apply-edit-batch" , toolStatus : "rejected" } ) ;
460+ expect ( await readFile ( join ( worktree , "README.md" ) , "utf8" ) ) . toBe ( "before\n" ) ;
461+ expect ( await readFile ( join ( worktree , "existing.md" ) , "utf8" ) ) . toBe ( "original\n" ) ;
462+ } finally {
463+ await rm ( worktree , { recursive : true , force : true } ) ;
464+ }
465+ } ) ;
466+
467+ it ( "rejects more than eight combined edits before changing any target" , async ( ) => {
468+ const worktree = await mkdtemp ( join ( tmpdir ( ) , "agentpatchcheck-native-runtime-" ) ) ;
469+ try {
470+ await writeFile ( join ( worktree , "README.md" ) , "before\n" , "utf8" ) ;
471+ const provider : HarnessNativeModelProvider = {
472+ id : "test-provider" ,
473+ decide : async ( { observations } ) =>
474+ observations . length === 0
475+ ? {
476+ decision : {
477+ kind : "tool" ,
478+ tool : "apply-edit-batch" ,
479+ arguments : {
480+ patches : [ { path : "README.md" , expectedText : "before" , replacementText : "after" } ] ,
481+ creates : Array . from ( { length : 8 } , ( _ , index ) => ( {
482+ path : `new-${ index } .txt` ,
483+ content : "new\n" ,
484+ } ) ) ,
485+ } ,
486+ } ,
487+ }
488+ : { decision : { kind : "finish" } } ,
489+ } ;
490+
491+ const result = await runHarnessNativeRuntime ( {
492+ policy : testNativePolicy ( { maxIterations : 2 , maxToolCalls : 1 } ) ,
493+ prompt : "Try an oversized batch." ,
494+ model : "test-model" ,
495+ worktreePath : worktree ,
496+ provider,
497+ timeoutMs : 1_000 ,
498+ } ) ;
499+
500+ expect ( result . trajectory [ 0 ] ) . toMatchObject ( { tool : "apply-edit-batch" , toolStatus : "rejected" } ) ;
501+ expect ( await readFile ( join ( worktree , "README.md" ) , "utf8" ) ) . toBe ( "before\n" ) ;
502+ await expect ( readFile ( join ( worktree , "new-0.txt" ) , "utf8" ) ) . rejects . toMatchObject ( { code : "ENOENT" } ) ;
503+ } finally {
504+ await rm ( worktree , { recursive : true , force : true } ) ;
505+ }
506+ } ) ;
507+
376508 it ( "applies a multi-line LF request to a CRLF file while preserving CRLF output" , async ( ) => {
377509 const worktree = await mkdtemp ( join ( tmpdir ( ) , "agentpatchcheck-native-runtime-" ) ) ;
378510 try {
0 commit comments