char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
6276dba
main
1import { assertEquals , assertRejects } from "@std/assert" ; 2import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context" ; 3import { localEnvironment } from "@berry/api/environment.ts" ; 4import { projectFor , type ProjectIndex } from "@berry/api/projects.ts" ; 5import { homedir } from "node:os" ; 6import { join } from "node:path" ; 7import { type Environments , localOnly } from "./environment.ts" ; 8import { Projects } from "./projects.ts" ; 9import { setup } from "./fixture.ts" ; 10import { BerryError } from "./berry.ts" ; 11import { fauxAssistantMessage , fauxToolCall } from "@earendil-works/pi-ai/providers/faux" ; 12import { summaries , type Summary } from "./summaries.ts" ; 13import { tempDir } from "@berry/util" ; 14 15Deno . test ( "projects match workspace boundaries and the most specific root on the right machine" , () => { 16const projects :ProjectIndex = { 17berry :{ 18name :"Berry" , 19defaultRoot :0 , 20roots :[ 21{ environment :"local" , cwd :"/work/berry" , platform :"linux" }, 22{ environment :"local" , cwd :"/workspaces/feature" , platform :"linux" }, 23{ environment :"windows" , cwd :"C:\\Work\\Berry" , platform :"win32" }, 24{ environment :"local" , cwd :"/work/berry:odd" , platform :"linux" }, 25], 26}, 27nested :{ 28name :"Nested" , 29defaultRoot :0 , 30roots :[{ environment :"local" , cwd :"/work/berry/tools/" , platform :"linux" }], 31}, 32}; 33for ( 34const [ environment , cwd , expected ] of [ 35[ "local" , "/work/berry" , "berry" ], 36[ "local" , "/work/berry/src" , "berry" ], 37[ "local" , "/workspaces/feature/src" , "berry" ], 38[ "local" , "/work/berry-old" , null ], 39[ "other" , "/work/berry" , null ], 40[ "local" , "/work/berry/tools/parser" , "nested" ], 41[ "windows" , "c:/WORK/berry/src" , "berry" ], 42[ "windows" , "C:\\Work\\Berry-old" , null ], 43[ "local" , "/work/berry:odd/src" , "berry" ], 44[ "local" , "/Work/Berry" , null ], 45] as const 46) assertEquals ( projectFor ( projects , { environment, cwd}), expected ); 47assertEquals ( projectFor ( projects , undefined ), null ); 48}); 49 50Deno . test ( "project workspaces validate and canonicalize without changing any conversation" , async () => { 51await usingrootDir = await tempDir (); 52const root = rootDir . path ; 53const main = join ( root , "main" ); 54const workspace = join ( root , "workspace" ); 55await Deno . mkdir ( main ); 56await Deno . mkdir ( workspace ); 57await Deno . writeTextFile ( join ( root , "file" ), "" ); 58await Deno . symlink ( main , join ( root , "link" )); 59let offline = false ; 60const environments :Environments = { 61open ( name , cwd , during ) { 62if ( name !== "other" ) return localOnly . open ( name , cwd , during ); 63if ( offline ) return Promise . reject ( new Error ( "offline" )); 64return Promise . resolve ( Object . assign ( localEnvironment ( cwd ?? root ), { name, home :root })); 65}, 66close :() => Promise . resolve (), 67}; 68await usingharness = await setup ( environments ); 69const { berry} = harness ; 70const tid = await berry . create ({ extensions :{ environment :{ cwd :main } } }); 71const binding = await berry . state ( tid , "environment" ); 72const id = await berry . saveProject ( undefined , { 73name :" Berry " , 74defaultRoot :1 , 75roots :[ 76{ environment :"local" , cwd :join ( root , "link" ) }, 77{ environment :"other" , cwd :"workspace" }, 78], 79}); 80const project = ( await berry . harness . snapshot ( Projects , context )) ! [ id ]; 81assertEquals ( project . name , "Berry" ); 82assertEquals ( project . roots . map (({ environment, cwd}) => ({ environment, cwd})), [ 83{ environment :"local" , cwd :await Deno . realPath ( main ) }, 84{ environment :"other" , cwd :await Deno . realPath ( workspace ) }, 85]); 86assertEquals ( project . defaultRoot , 1 ); 87for ( const cwd of [ join ( root , "file" ), join ( root , "missing" ), main ]) { 88await assertRejects (() => 89berry . saveProject ( undefined , { 90name :"invalid" , 91defaultRoot :0 , 92roots :[{ environment :"local" , cwd}], 93}) 94); 95} 96await assertRejects (() => 97berry . saveProject ( undefined , { name :"invalid" , roots :[], defaultRoot :0 }) 98); 99await assertRejects (() => 100berry . saveProject ( undefined , { 101name :"invalid" , 102roots :[{ environment :"local" , cwd :workspace }], 103defaultRoot :2 , 104}) 105); 106await assertRejects (() => 107berry . saveProject ( undefined , { 108name :"invalid" , 109roots :[{ environment :"local" , cwd :workspace }, { environment :"local" , cwd :workspace }], 110defaultRoot :0 , 111}) 112); 113offline = true ; 114await berry . saveProject ( id , { ...project , name :"Renamed" }); 115assertEquals ( await berry . state ( tid , "environment" ), binding ); 116await berry . deleteProject ( id ); 117assertEquals ( await berry . harness . snapshot ( Projects , context ), {}); 118assertEquals ( await berry . state ( tid , "environment" ), binding ); 119}); 120 121Deno . test ( "live sidebar bindings follow cd, independently moved forks, branch switches, and unreachable machines" , async () => { 122await usingrootDir = await tempDir (); 123const root = rootDir . path ; 124await Deno . mkdir ( join ( root , "a" )); 125await Deno . mkdir ( join ( root , "b" )); 126await usingharness = await setup (); 127const { berry} = harness ; 128let watched :Awaited < ReturnType < typeof summaries >> | undefined ; 129try { 130const tid = await berry . create ({ extensions :{ environment :{ cwd :join ( root , "a" ) } } }); 131const misc = await berry . create ({ extensions :{} }); 132const conversation = ( await berry . conversation ( tid )) ! ; 133const point = ( await conversation . context ( context )). entries . at ( - 1 ) ! . id ; 134const fork = await conversation . fork ( point , { ownership :{ kind :"ownerless" } }, context ); 135const forkTid = await berry . tidOf ( fork . id ); 136watched = await summaries ( berry ); 137const feed = watched . feed (); 138let current = feed . value as Record < string , Summary >; 139let wake = () => {}; 140await feed . start (( value ) => { 141current = value as Record < string , Summary >; 142wake (); 143return Promise . resolve (); 144}); 145const until = async ( id :string , cwd :string ) => { 146while ( current [ id ]?. cwd !== cwd ) await new Promise < void >(( resolve ) => wake = resolve ); 147}; 148assertEquals ( current [ misc ]. cwd , homedir ()); 149await berry . command ( forkTid , "cd" , join ( root , "b" )); 150await until ( forkTid , join ( root , "b" )); 151assertEquals ( current [ tid ]. cwd , join ( root , "a" )); 152await berry . branch ( forkTid , point ); 153await until ( forkTid , join ( root , "a" )); 154await ( await berry . emit ( forkTid , "environment" , { 155type :"bind" , 156environment :"unreachable" , 157cwd :"/work" , 158})). wait ( context ); 159await until ( forkTid , "/work" ); 160assertEquals ( current [ forkTid ]. environment , "unreachable" ); 161assertEquals ( current [ tid ]. environment , "local" ); 162await feed . stop (); 163} finally { 164watched ?. stop (); 165} 166}); 167 168Deno . test ( "removing a workspace leaves its conversations in place unless moving is requested" , async () => { 169await usingroot = await tempDir (); 170const main = join ( root . path , "main" ); 171const work = join ( root . path , "work" ); 172await Deno . mkdir ( main ); 173await Deno . mkdir ( work ); 174await usingharness = await setup (); 175const { berry} = harness ; 176const id = await berry . saveProject ( undefined , { 177name :"Project" , 178roots :[{ environment :"local" , cwd :main }, { environment :"local" , cwd :work }], 179defaultRoot :0 , 180}); 181const tid = await berry . create ({ extensions :{ environment :{ cwd :work } } }); 182await berry . saveProject ( id , { 183name :"Project" , 184roots :[{ environment :"local" , cwd :main }], 185defaultRoot :0 , 186}); 187assertEquals ( await berry . state ( tid , "environment" ), { environment :"local" , cwd :work }); 188}); 189 190Deno . test ( "workspace removal moves current conversations and descendants, respecting nested workspaces and projects" , async () => { 191await usingroot = await tempDir (); 192const main = join ( root . path , "main" ); 193const work = join ( root . path , "work" ); 194const second = join ( root . path , "second" ); 195const child = join ( work , "src" ); 196const retained = join ( work , "retained" ); 197const nested = join ( work , "nested" ); 198const sibling = join ( root . path , "work-old" ); 199for ( const path of [ main , child , retained , nested , sibling , second ]) { 200await Deno . mkdir ( path , { recursive :true }); 201} 202let offline = false ; 203const environments :Environments = { 204open ( name , cwd , during ) { 205if ( name !== "destination" ) return localOnly . open ( name , cwd , during ); 206if ( offline ) return Promise . reject ( new Error ( "offline" )); 207return Promise . resolve ( Object . assign ( localEnvironment ( cwd ?? main ), { name})); 208}, 209close :() => Promise . resolve (), 210}; 211await usingharness = await setup ( environments ); 212const { berry} = harness ; 213const roots = [ 214{ environment :"local" , cwd :work }, 215{ environment :"local" , cwd :retained }, 216{ environment :"local" , cwd :second }, 217{ environment :"destination" , cwd :main }, 218{ environment :"local" , cwd :homedir () }, 219]; 220const id = await berry . saveProject ( undefined , { name :"Project" , roots, defaultRoot :0 }); 221await berry . saveProject ( undefined , { 222name :"Nested project" , 223roots :[{ environment :"local" , cwd :nested }], 224defaultRoot :0 , 225}); 226const moved = await Promise . all ( 227[ work , child , second ]. map (( cwd ) => berry . create ({ extensions :{ environment :{ cwd} } })), 228); 229const untouched = await Promise . all ([ retained , nested , sibling , main ]. map ( async ( cwd ) => { 230const tid = await berry . create ({ extensions :{ environment :{ cwd} } }); 231return { tid, binding :await berry . state ( tid , "environment" ) }; 232})); 233const wrongMachine = await berry . create ({ 234extensions :{ environment :{ environment :"destination" , cwd :work } }, 235}); 236untouched . push ({ tid :wrongMachine , binding :await berry . state ( wrongMachine , "environment" ) }); 237const conversation = ( await berry . conversation ( moved [ 0 ])) ! ; 238const point = ( await conversation . context ( context )). entries . at ( - 1 ) ! . id ; 239const fork = await conversation . fork ( point , { ownership :{ kind :"ownerless" } }, context ); 240moved . push ( await berry . tidOf ( fork . id )); 241moved . push ( await berry . create ({ extensions :{} })); 242await berry . command ( moved [ 0 ], "cd" , child ); 243await berry . branch ( moved [ 0 ], point ); 244await berry . archive ( moved [ 1 ], true ); 245await Deno . remove ( child , { recursive :true }); 246offline = true ; 247await berry . saveProject ( id , { 248name :"Project" , 249roots :[ roots [ 1 ], roots [ 3 ]], 250defaultRoot :1 , 251moveConversations :true , 252}); 253for ( const tid of moved ) { 254assertEquals ( await berry . state ( tid , "environment" ), { environment :"destination" , cwd :main }); 255} 256for ( const { tid, binding} of untouched ) { 257assertEquals ( await berry . state ( tid , "environment" ), binding ); 258} 259assertEquals ( await berry . state ( moved [ 0 ], "environment" , point ), { 260environment :"local" , 261cwd :work , 262}); 263}); 264 265Deno . test ( "a busy affected conversation prevents workspace removal and rolls back other moves" , async () => { 266await usingroot = await tempDir (); 267const main = join ( root . path , "main" ); 268const work = join ( root . path , "work" ); 269await Deno . mkdir ( main ); 270await Deno . mkdir ( work ); 271await usingharness = await setup (); 272const { berry, faux, model, gate} = harness ; 273const id = await berry . saveProject ( undefined , { 274name :"Project" , 275roots :[{ environment :"local" , cwd :main }, { environment :"local" , cwd :work }], 276defaultRoot :0 , 277}); 278const before = await berry . harness . snapshot ( Projects , context ); 279const idle = await berry . create ({ extensions :{ environment :{ cwd :work } } }); 280const busy = await berry . create ({ extensions :{ environment :{ cwd :work }, gate :{} }, model}); 281const conversation = ( await berry . conversation ( busy )) ! ; 282faux . setResponses ([ 283fauxAssistantMessage ( fauxToolCall ( "hold" , {}), { stopReason :"toolUse" }), 284fauxAssistantMessage ( "done" ), 285]); 286const submission = await conversation . submit ({ type :"input" , content :"go" }, context ); 287try { 288await gate . entered . promise ; 289await assertRejects ( 290() => 291berry . saveProject ( id , { 292name :"Project" , 293roots :[{ environment :"local" , cwd :main }], 294defaultRoot :0 , 295moveConversations :true , 296}), 297BerryError , 298"busy" , 299); 300assertEquals ( await berry . harness . snapshot ( Projects , context ), before ); 301assertEquals ( await berry . state ( idle , "environment" ), { environment :"local" , cwd :work }); 302assertEquals ( await berry . state ( busy , "environment" ), { environment :"local" , cwd :work }); 303} finally { 304gate . release . resolve ( "released" ); 305await submission . wait ( context ); 306await conversation . waitForIdle ( context ); 307} 308});