char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
cb11725
main
1import { assertEquals , assertRejects , assertStringIncludes } from "@std/assert" ; 2import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context" ; 3import code from "../extensions/code/mod.ts" ; 4import { ShellDoc , ShellEntry } from "./shell.ts" ; 5import { setup , turn } from "./fixture.ts" ; 6import { tempDir } from "@berry/util" ; 7 8async function prepare () { 9const harness = await setup (); 10const temporary = await tempDir ({ prefix :"user-shell-test-" }); 11const root = temporary . path ; 12await harness . berry . install ( code ); 13const tid = await harness . berry . create ({ 14extensions :{ environment :{ cwd :root }, code :{} }, 15model :harness . model , 16}); 17const conversation = ( await harness . berry . conversation ( tid )) ! ; 18return { 19 ...harness , 20 root, 21 tid, 22 conversation, 23async entries () { 24return ( await conversation . entries ({ order :"ascending" }, 100 , undefined , context )). items 25. filter ( ShellEntry . is ); 26}, 27async run ( command :string , excluded = false ) { 28const task = await harness . berry . shell ( tid , command , excluded ); 29const settled = await harness . berry . harness . waitForTask ( task , context ); 30assertEquals ( settled . state . outcome . status , "completed" ); 31await conversation . waitForIdle ( context ); 32}, 33async output () { 34for ( let i = 0 ; i < 200 ; i ++ ) { 35const run = ( await harness . berry . harness . snapshot ( ShellDoc , conversation . id , context ))?. run ; 36if (( run ?. details as { rawOutput ?:string } | null )?. rawOutput ) return run ! ; 37await new Promise (( resolve ) => setTimeout ( resolve , 10 )); 38} 39throw new Error ( "Shell command did not produce live output" ); 40}, 41async [ Symbol . asyncDispose ]() { 42await conversation . abort ( context ); 43await harness . berry . close (); 44await temporary [ Symbol . asyncDispose ](); 45}, 46}; 47} 48 49Deno . test ( "user shell commands persist output at the working directory without starting the model" , async () => { 50await usingh = await prepare (); 51await h . run ( "printf 'visible'; printf ' stderr' >&2; pwd; exit 7" ); 52await h . run ( "printf 'private'" , true ); 53assertEquals ( h . faux . state . callCount , 0 ); 54const entries = await h . entries (); 55assertEquals ( entries . length , 2 ); 56const first = entries [ 0 ]. data . result as { output :string ; exitCode :number }; 57assertStringIncludes ( first . output , "visible" ); 58assertStringIncludes ( first . output , " stderr" ); 59assertStringIncludes ( first . output , h . root ); 60assertEquals ( first . exitCode , 7 ); 61assertEquals ( entries [ 0 ]. data . status , "ok" ); 62assertEquals ( entries [ 1 ]. model , undefined ); 63assertEquals ( entries [ 1 ]. data . excludeFromContext , true ); 64assertEquals ( entries [ 1 ]. data . result , { output :"private" , exitCode :0 }); 65const [ messages ] = await turn ( h , h . tid , []); 66const text = JSON . stringify ( messages ); 67assertStringIncludes ( text , "visible" ); 68assertStringIncludes ( text , "exit code 7" ); 69assertEquals ( text . includes ( "private" ), false ); 70const fork = await h . conversation . fork ( 71entries [ 1 ]. id , 72{ ownership :{ kind :"ownerless" } }, 73context , 74); 75const forkTid = await h . berry . tidOf ( fork . id ); 76const [ forkMessages ] = await turn ( h , forkTid , []); 77assertStringIncludes ( JSON . stringify ( forkMessages ), "visible" ); 78assertEquals ( JSON . stringify ( forkMessages ). includes ( "private" ), false ); 79}); 80 81Deno . test ( "user shell output streams with ANSI intact, refuses concurrent commands and can be aborted" , async () => { 82await usingh = await prepare (); 83const task = await h . berry . shell ( h . tid , "printf '\\033[31mred'; sleep 30" ); 84const live = await h . output (); 85assertEquals ( live . status , "running" ); 86assertEquals ( live . details , { rawOutput :"\x1b[31mred" }); 87await assertRejects (() => h . berry . shell ( h . tid , "touch should-not-run" ), Error , "Busy" ); 88await h . conversation . abort ( context ); 89await h . berry . harness . waitForTask ( task , context ); 90const [ entry ] = await h . entries (); 91assertEquals ( entry . data . status , "cancelled" ); 92assertEquals ( entry . data . details , live . details ); 93assertStringIncludes ( JSON . stringify ( entry . model ), "command cancelled" ); 94assertEquals ( 95( await h . berry . harness . snapshot ( ShellDoc , h . conversation . id , context ))?. run , 96undefined , 97); 98assertEquals ( h . faux . state . callCount , 0 ); 99await h . run ( "printf 'after cancellation'" ); 100assertEquals (( await h . entries ()). length , 2 ); 101}); 102 103Deno . test ( "shell commands require enabled code, nonempty input and an idle assistant" , async () => { 104await usingh = await prepare (); 105let running :ReturnType < typeof turn > | undefined ; 106try { 107await assertRejects (() => h . berry . shell ( h . tid , " \n " ), Error , "Expected a shell command" ); 108const disabled = await h . berry . create ({ extensions :{} }); 109await assertRejects ( 110() => h . berry . shell ( disabled , "touch should-not-run" ), 111Error , 112"code extension" , 113); 114await h . berry . configure ( h . tid , { extensions :[ "gate" , "code" ] }); 115const { fauxAssistantMessage, fauxToolCall} = await import ( 116"@earendil-works/pi-ai/providers/faux" 117); 118running = turn ( h , h . tid , [ 119fauxAssistantMessage ( fauxToolCall ( "hold" , {}), { stopReason :"toolUse" }), 120]); 121await h . gate . entered . promise ; 122await assertRejects (() => h . berry . shell ( h . tid , "touch should-not-run" ), Error , "Busy" ); 123h . gate . release . resolve ( "done" ); 124await running ; 125assertEquals ( await h . entries (), []); 126} finally { 127h . gate . release . resolve ( "done" ); 128await running ; 129} 130}); 131 132Deno . test ( "a restart records interrupted shell execution instead of replaying its side effects" , async () => { 133const { openNodeSqliteStorage} = await import ( "@earendil-works/pi-durable/storage/sqlite/node" ); 134const { createModels} = await import ( "@earendil-works/pi-ai/models" ); 135const { fauxProvider} = await import ( "@earendil-works/pi-ai/providers/faux" ); 136const { openBerry} = await import ( "./berry.ts" ); 137const models = createModels (); 138models . setProvider ( fauxProvider (). provider ); 139await usingrootDir = await tempDir ({ prefix :"shell-restart-test-" }); 140const root = rootDir . path ; 141const path = ` ${ root } /berry.sqlite` ; 142let berry = await openBerry ( await openNodeSqliteStorage ( path ), models ); 143try { 144await berry . install ( code ); 145const tid = await berry . create ({ extensions :{ environment :{ cwd :root }, code :{} } }); 146const conversation = ( await berry . conversation ( tid )) ! ; 147await berry . shell ( tid , "echo effect >> effects; printf started; sleep 30" ); 148for ( let i = 0 ; i < 200 ; i ++ ) { 149const run = ( await berry . harness . snapshot ( ShellDoc , conversation . id , context ))?. run ; 150if (( run ?. details as { rawOutput ?:string } | null )?. rawOutput === "started" ) break ; 151if ( i === 199 ) throw new Error ( "Command never started" ); 152await new Promise (( resolve ) => setTimeout ( resolve , 10 )); 153} 154await berry . close (); 155berry = await openBerry ( await openNodeSqliteStorage ( path ), models ); 156await berry . install ( code ); 157await berry . harness . waitForIdle ( context ); 158const reopened = ( await berry . conversation ( tid )) ! ; 159const entries = ( await reopened . entries ({ order :"ascending" }, 100 , undefined , context )). items ; 160const shell = entries . filter ( ShellEntry . is ); 161assertEquals ( shell . length , 1 ); 162assertEquals ( shell [ 0 ]. data . status , "error" ); 163assertStringIncludes ( shell [ 0 ]. data . error ! , "not rerun" ); 164assertEquals ( await Deno . readTextFile ( ` ${ root } /effects` ), "effect\n" ); 165assertEquals (( await berry . harness . snapshot ( ShellDoc , reopened . id , context ))?. run , undefined ); 166} finally { 167await berry . close (); 168} 169});