char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
0ad1bcc
main
1import { assert , assertEquals , assertMatch , assertRejects } from "@std/assert" ; 2import { decodeBase64 } from "@std/encoding/base64" ; 3import { PNG , tempDir } from "@berry/util" ; 4import { readFile , stat , symlink , writeFile } from "node:fs/promises" ; 5import { join } from "node:path" ; 6import { localEnvironment } from "@berry/api/environment.ts" ; 7import { testExtension } from "@berry/api/testing.ts" ; 8import code from "./mod.ts" ; 9 10type Fixture = { 11cwd :string ; 12path :string ; 13call ( name :string , args ?:Record < string , unknown >, signal ?:AbortSignal ) :Promise < string >; 14}; 15 16function test ( name :string , run :( f :Fixture ) => Promise < void >, content :string = "" ) { 17Deno . test ( name , async () => { 18await usingdir = await tempDir (); 19const path = join ( dir . path , "file.txt" ); 20await writeFile ( path , content ); 21const extension = testExtension ( code , { env :localEnvironment ( dir . path ) }); 22await run ({ 23cwd :dir . path , 24 path, 25call :async ( name , args = {}, signal ) => 26await extension . call ( name , { path :"file.txt" , ...args } as never , { signal}) as string , 27}); 28}); 29} 30 31const anchors = ( output :string ) => 32output . split ( "\n" ). filter (( row ) => row . includes ( "│" )). map (( row ) => row . split ( "│" )[ 0 ]); 33const unanchored = ( output :string ) => 34output . replace ( / ^([+-])[A-Za-z]{4}│ / gm , "$1" ). split ( "\n" ). slice ( 2 ); 35 36test ( "diffs carry the new lines' hashes and keep replacement text literal" , async ( f ) => { 37const [ old ] = anchors ( await f . call ( "read" )); 38assertMatch ( old , / ^[A-Za-z]{4}$ / ); 39const output = await f . call ( "edit" , { edits :[{ old :[ old ], new :[ "ABCD│literal" ] }] }); 40assert ( output . includes ( `- ${ old } │original` )); 41assertMatch ( output , / ^\+[A-Za-z]{4}│ABCD│literal$ / m ); 42}, "original\n" ); 43 44test ( 45"batched diffs merge nearby context without duplicating it and separate distant hunks" , 46async ( f ) => { 47const old = anchors ( await f . call ( "read" )); 48const output = await f . call ( "edit" , { 49edits :[ 50{ old :[ old [ 16 ]], new :[ "last" ] }, 51{ old :[ old [ 2 ]], new :[ "first" , "extra" ] }, 52{ old :[ old [ 5 ]], new :[] }, 53], 54}); 55assertEquals ( unanchored ( output ), [ 56"@@ original line 1 @@" , 57" line 0" , 58" line 1" , 59"-line 2" , 60"+first" , 61"+extra" , 62" line 3" , 63" line 4" , 64"-line 5" , 65" line 6" , 66" line 7" , 67" line 8" , 68"@@ original line 14 @@" , 69" line 13" , 70" line 14" , 71" line 15" , 72"-line 16" , 73"+last" , 74" line 17" , 75" line 18" , 76" line 19" , 77]); 78}, 79Array . from ({ length :20 }, ( _ , i ) => `line ${ i } ` ). join ( "\n" ), 80); 81 82test ( "references survive external line movement" , async ( f ) => { 83const [ first , target , last ] = anchors ( await f . call ( "read" )); 84await writeFile ( f . path , "inserted\nlast\nfirst\ntarget\n" ); 85assertEquals ( anchors ( await f . call ( "read" )). slice ( 1 ), [ last , first , target ]); 86await f . call ( "edit" , { edits :[{ old :[ target ], new :[ "changed" ] }] }); 87assertEquals ( await readFile ( f . path , "utf8" ), "inserted\nlast\nfirst\nchanged\n" ); 88}, "first\ntarget\nlast\n" ); 89 90test ( "duplicate lines need unique adjacent context, which is preserved" , async ( f ) => { 91const original = "left\nsame\nright\nleft\nsame\nother\n" ; 92const [ left , same , right , leftAgain , sameAgain ] = anchors ( await f . call ( "read" )); 93assertEquals ( left , leftAgain ); 94assertEquals ( same , sameAgain ); 95await assertRejects ( 96() => f . call ( "edit" , { edits :[{ old :[ same ], new :[ "changed" ] }] }), 97Error , 98"ambiguous" , 99); 100await assertRejects ( 101() => f . call ( "edit" , { edits :[{ before :[ left ], old :[ same ], new :[ "changed" ] }] }), 102Error , 103"ambiguous" , 104); 105assertEquals ( await readFile ( f . path , "utf8" ), original ); 106await f . call ( "edit" , { 107edits :[{ before :[ left ], old :[ same ], after :[ right ], new :[ "changed" ] }], 108}); 109assertEquals ( await readFile ( f . path , "utf8" ), "left\nchanged\nright\nleft\nsame\nother\n" ); 110}, "left\nsame\nright\nleft\nsame\nother\n" ); 111 112test ( 113"batches resolve against the original, regardless of order or line count changes" , 114async ( f ) => { 115const [ one , two , three , four ] = anchors ( await f . call ( "read" )); 116await f . call ( "edit" , { 117edits :[{ old :[ four ], new :[ "last" ] }, { old :[ one ], new :[ "first" , "extra" ] }], 118}); 119assertEquals ( await readFile ( f . path , "utf8" ), "first\nextra\ntwo\nthree\nlast\n" ); 120assertEquals ( anchors ( await f . call ( "read" )). slice ( 2 , 4 ), [ two , three ]); 121}, 122"one\ntwo\nthree\nfour\n" , 123); 124 125test ( "a stale interior line refuses the entire batch without writing" , async ( f ) => { 126const [ one , two , three , four ] = anchors ( await f . call ( "read" )); 127const external = "one\nexternal\nthree\nfour\n" ; 128await writeFile ( f . path , external ); 129await assertRejects ( 130() => 131f . call ( "edit" , { 132edits :[{ old :[ four ], new :[ "would change" ] }, { 133old :[ one , two , three ], 134new :[ "replacement" ], 135}], 136}), 137Error , 138"stale" , 139); 140assertEquals ( await readFile ( f . path , "utf8" ), external ); 141}, "one\ntwo\nthree\nfour\n" ); 142 143test ( "hashes include trailing whitespace and the entire line" , async ( f ) => { 144const long = "x" . repeat ( 1000 ); 145const original = `text\ntext \n ${ long } a\n ${ long } b\n` ; 146await writeFile ( f . path , original ); 147const hashes = anchors ( await f . call ( "read" )); 148assertEquals ( new Set ( hashes ). size , 4 ); 149await writeFile ( f . path , original . replace ( "text \n" , "text \n" )); 150await assertRejects ( 151() => f . call ( "edit" , { edits :[{ old :[ hashes [ 1 ]], new :[ "changed" ] }] }), 152Error , 153"stale" , 154); 155}); 156 157test ( "overlapping ranges and same-position insertions are refused" , async ( f ) => { 158const original = "one\ntwo\nthree\n" ; 159const [ one , two , three ] = anchors ( await f . call ( "read" )); 160for ( 161const edits of [ 162[{ old :[ one , two ], new :[] }, { old :[ two , three ], new :[] }], 163[{ before :[ one ], old :[], new :[ "x" ] }, { after :[ two ], old :[], new :[ "y" ] }], 164] 165) { 166await assertRejects (() => f . call ( "edit" , { edits}), Error , "Overlapping" ); 167assertEquals ( await readFile ( f . path , "utf8" ), original ); 168} 169}, "one\ntwo\nthree\n" ); 170 171test ( "context may overlap other edits, but is always checked against the original" , async ( f ) => { 172const [ one , two , three ] = anchors ( await f . call ( "read" )); 173await f . call ( "edit" , { 174edits :[ 175{ old :[ one ], after :[ two ], new :[ "first" ] }, 176{ before :[ one ], old :[ two ], after :[ three ], new :[ "second" ] }, 177], 178}); 179assertEquals ( await readFile ( f . path , "utf8" ), "first\nsecond\nthree\n" ); 180}, "one\ntwo\nthree\n" ); 181 182test ( "insert before/after, delete, and reuse hashes returned by edit" , async ( f ) => { 183const [ middle ] = anchors ( await f . call ( "read" )); 184const diff = await f . call ( "edit" , { 185edits :[{ old :[], after :[ middle ], new :[ "first" ] }, { 186before :[ middle ], 187old :[], 188new :[ "last" ], 189}], 190}); 191assertEquals ( await readFile ( f . path , "utf8" ), "first\nmiddle\nlast\n" ); 192const [ first , last ] = anchors ( diff ). map (( anchor ) => anchor . slice ( 1 )); 193await f . call ( "edit" , { edits :[{ old :[ first , middle , last ], new :[] }] }); 194assertEquals ( await readFile ( f . path , "utf8" ), "" ); 195}, "middle\n" ); 196 197test ( "empty files can be seeded, but nonempty files require a selector" , async ( f ) => { 198assertMatch ( await f . call ( "read" ), / Empty file / ); 199await f . call ( "edit" , { edits :[{ old :[], new :[ "seed" ] }] }); 200assertEquals ( await readFile ( f . path , "utf8" ), "seed\n" ); 201await assertRejects ( 202() => f . call ( "edit" , { edits :[{ old :[], new :[ "unsafe" ] }] }), 203Error , 204"context" , 205); 206}); 207 208test ( "BOM, mixed line endings, and missing final newline survive edits" , async ( f ) => { 209const [ one , two , three ] = anchors ( await f . call ( "read" )); 210await f . call ( "edit" , { edits :[{ old :[ one ], new :[ "first" ] }] }); 211assertEquals ( await readFile ( f . path , "utf8" ), "\uFEFFfirst\r\ntwo\nthree" ); 212await f . call ( "edit" , { edits :[{ before :[ three ], old :[], new :[ "last" ] }] }); 213assertEquals ( await readFile ( f . path , "utf8" ), "\uFEFFfirst\r\ntwo\nthree\r\nlast" ); 214assertEquals ( anchors ( await f . call ( "read" ))[ 1 ], two ); 215}, "\uFEFFone\r\ntwo\nthree" ); 216 217test ( "blank replacement lines are not deletion, including at unterminated EOF" , async ( f ) => { 218const [, two ] = anchors ( await f . call ( "read" )); 219await f . call ( "edit" , { edits :[{ old :[ two ], new :[ "" ] }] }); 220assertEquals ( await readFile ( f . path , "utf8" ), "one\n\n" ); 221}, "one\ntwo" ); 222 223test ( "no-op edits do not write, and replacement text is literal" , async ( f ) => { 224const [ one ] = anchors ( await f . call ( "read" )); 225const before = await stat ( f . path ); 226assertMatch ( await f . call ( "edit" , { edits :[{ old :[ one ], new :[ "one" ] }] }), / No changes / ); 227assertEquals (( await stat ( f . path )). mtimeMs , before . mtimeMs ); 228await f . call ( "edit" , { edits :[{ old :[ one ], new :[ ` ${ one } │one` , "one" , "one" ] }] }); 229assertEquals ( await readFile ( f . path , "utf8" ), ` ${ one } │one\none\none\n` ); 230}, "one\n" ); 231 232test ( "invalid text and embedded newlines are refused without changing bytes" , async ( f ) => { 233for ( 234const content of [ 235new Uint8Array ([ 0xff , 0xfe , 65 , 0 ]), 236new Uint8Array ([ 97 , 0 , 98 ]), 237new Uint8Array ([ 0xff ]), 238] 239) { 240await writeFile ( f . path , content ); 241await assertRejects (() => f . call ( "read" ), Error , "UTF-8" ); 242await assertRejects ( 243() => f . call ( "edit" , { edits :[{ old :[], new :[ "unsafe" ] }] }), 244Error , 245"UTF-8" , 246); 247assertEquals ( new Uint8Array ( await readFile ( f . path )), content ); 248} 249await writeFile ( f . path , "one\n" ); 250const [ one ] = anchors ( await f . call ( "read" )); 251for ( const line of [ "two\nthree" , "two\rthree" , "two\0three" ]) { 252await assertRejects (() => f . call ( "edit" , { edits :[{ old :[ one ], new :[ line ] }] })); 253assertEquals ( await readFile ( f . path , "utf8" ), "one\n" ); 254} 255}); 256 257test ( "paged reads provide the same anchors and respect line and byte limits" , async ( f ) => { 258const page = await f . call ( "read" ); 259assertEquals ( anchors ( page ). length , 2000 ); 260assertMatch ( page , / offset=2001 / ); 261const small = await f . call ( "read" , { offset :2 , limit :2 }); 262assertEquals ( anchors ( small ), anchors ( page ). slice ( 1 , 3 )); 263assertMatch ( small , / offset=4 / ); 264assertEquals ( anchors ( await f . call ( "read" , { offset :2001 })). length , 5 ); 265await assertRejects (() => f . call ( "read" , { offset :2006 }), Error , "beyond" ); 266 267await writeFile ( f . path , ( "x" . repeat ( 30_000 ) + "\n" ). repeat ( 3 )); 268const bytes = await f . call ( "read" ); 269assertEquals ( anchors ( bytes ). length , 1 ); 270assertMatch ( bytes , / offset=2 / ); 271await writeFile ( f . path , "x" . repeat ( 60_000 )); 272await assertRejects (() => f . call ( "read" ), Error , "exceeds" ); 273}, Array . from ({ length :2005 }, ( _ , i ) => i . toString ( 36 )). join ( "\n" )); 274 275test ( "parallel edits through symlink aliases do not overwrite each other" , async ( f ) => { 276const alias = f . path + ".link" ; 277await symlink ( f . path , alias ); 278const [ one , two ] = anchors ( await f . call ( "read" )); 279await Promise . all ([ 280f . call ( "edit" , { edits :[{ old :[ one ], new :[ "first" ] }] }), 281f . call ( "edit" , { path :alias , edits :[{ old :[ two ], new :[ "second" ] }] }), 282]); 283assertEquals ( await readFile ( f . path , "utf8" ), "first\nsecond\n" ); 284}, "one\ntwo\n" ); 285 286test ( "images resolve to an image block" , async ( f ) => { 287await writeFile ( join ( f . cwd , "pixel.png" ), decodeBase64 ( PNG )); 288assertEquals ( await f . call ( "read" , { path :"pixel.png" }), { 289type :"image" , 290data :PNG , 291mimeType :"image/png" , 292} as unknown as string ); 293}); 294 295test ( "aborted calls do not write" , async ( f ) => { 296const [ one ] = anchors ( await f . call ( "read" )); 297await assertRejects (() => 298f . call ( "edit" , { edits :[{ old :[ one ], new :[ "changed" ] }] }, AbortSignal . abort ()) 299); 300assertEquals ( await readFile ( f . path , "utf8" ), "one\n" ); 301}, "one\n" ); 302 303test ( "text edits replace exact substrings and return reusable line hashes" , async ( f ) => { 304const output = await f . call ( "edit" , { 305edits :[{ oldText :"const x = 1;\nconst y" , newText :'const x = "$&";\nconst z' }], 306}); 307assertEquals ( await readFile ( f . path , "utf8" ), 'const x = "$&";\nconst z = 2;\n' ); 308const added = anchors ( output ). filter (( anchor ) => anchor . startsWith ( "+" )). map (( a ) => a . slice ( 1 )); 309await f . call ( "edit" , { edits :[{ old :[ added [ 1 ]], new :[ "done" ] }] }); 310assertEquals ( await readFile ( f . path , "utf8" ), 'const x = "$&";\ndone\n' ); 311}, "const x = 1;\nconst y = 2;\n" ); 312 313test ( "text and hash edits share original coordinates even within one line" , async ( f ) => { 314const [ heading ] = anchors ( await f . call ( "read" )); 315await f . call ( "edit" , { 316edits :[ 317{ oldText :"right" , newText :"R" }, 318{ old :[ heading ], new :[ "new heading" , "extra" ] }, 319{ oldText :"left" , newText :"longer left" }, 320], 321}); 322assertEquals ( await readFile ( f . path , "utf8" ), "new heading\nextra\nlonger left + R\n" ); 323}, "heading\nleft + right\n" ); 324 325test ( "ambiguous text includes overlapping occurrences and can be disambiguated" , async ( f ) => { 326await assertRejects ( 327() => f . call ( "edit" , { edits :[{ oldText :"aa" , newText :"x" }] }), 328Error , 329"ambiguous" , 330); 331assertEquals ( await readFile ( f . path , "utf8" ), "aaa\n" ); 332await f . call ( "edit" , { edits :[{ oldText :"aa\n" , newText :"x\n" }] }); 333assertEquals ( await readFile ( f . path , "utf8" ), "ax\n" ); 334}, "aaa\n" ); 335 336test ( 337"missing text refuses the entire batch rather than matching earlier replacements" , 338async ( f ) => { 339await assertRejects ( 340() => 341f . call ( "edit" , { 342edits :[{ oldText :"one" , newText :"two" }, { oldText :"two" , newText :"three" }], 343}), 344Error , 345"not found" , 346); 347assertEquals ( await readFile ( f . path , "utf8" ), "one\n" ); 348}, 349"one\n" , 350); 351 352test ( "overlap between text and hash ranges refuses the entire batch" , async ( f ) => { 353const [ line ] = anchors ( await f . call ( "read" )); 354await assertRejects ( 355() => 356f . call ( "edit" , { 357edits :[{ old :[ line ], new :[ "whole" ] }, { oldText :"part" , newText :"piece" }], 358}), 359Error , 360"Overlapping" , 361); 362assertEquals ( await readFile ( f . path , "utf8" ), "a part of a line\n" ); 363}, "a part of a line\n" ); 364 365test ( "empty oldText seeds only empty files and each entry must choose one form" , async ( f ) => { 366await f . call ( "edit" , { edits :[{ oldText :"" , newText :"seed" }] }); 367assertEquals ( await readFile ( f . path , "utf8" ), "seed" ); 368await assertRejects (() => f . call ( "edit" , { edits :[{ oldText :"" , newText :"unsafe" }] })); 369await assertRejects (() => 370f . call ( "edit" , { edits :[{ oldText :"seed" , newText :"text" , old :[], new :[ "hash" ] }] }) 371); 372assertEquals ( await readFile ( f . path , "utf8" ), "seed" ); 373}); 374 375test ( 376"text edits preserve BOM and literal line endings, including deleting through EOF" , 377async ( f ) => { 378const before = await stat ( f . path ); 379assertMatch ( 380await f . call ( "edit" , { edits :[{ oldText :"one" , newText :"one" }] }), 381/ No changes / , 382); 383assertEquals (( await stat ( f . path )). mtimeMs , before . mtimeMs ); 384await f . call ( "edit" , { 385edits :[{ oldText :"one\r\ntwo\n" , newText :"first\nsecond\r\n" }], 386}); 387assertEquals ( await readFile ( f . path , "utf8" ), "\uFEFFfirst\nsecond\r\nthree" ); 388await f . call ( "edit" , { edits :[{ oldText :"\r\nthree" , newText :"" }] }); 389assertEquals ( await readFile ( f . path , "utf8" ), "\uFEFFfirst\nsecond" ); 390}, 391"\uFEFFone\r\ntwo\nthree" , 392); 393 394test ( "hash replacement can keep an unterminated line and add a following line" , async ( f ) => { 395const [ line ] = anchors ( await f . call ( "read" )); 396await f . call ( "edit" , { edits :[{ old :[ line ], new :[ "one" , "two" ] }] }); 397assertEquals ( await readFile ( f . path , "utf8" ), "one\ntwo" ); 398}, "one" );