char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
cb11725
main
1import { 2assert , 3assertEquals , 4assertGreater , 5assertObjectMatch , 6assertRejects , 7assertStrictEquals , 8assertStringIncludes , 9} from "@std/assert" ; 10import { BACKGROUND_CONTEXT as context , withAbortSignal } from "@earendil-works/chord/context" ; 11import { fauxAssistantMessage , fauxToolCall } from "@earendil-works/pi-ai/providers/faux" ; 12import { getOrThrow } from "@earendil-works/pi-durable/env" ; 13import { registerEnvConformance } from "@earendil-works/pi-durable/testing" ; 14import code from "../../extensions/code/mod.ts" ; 15import { configuredEnvironments , type EnvironmentSettings } from "../environment.ts" ; 16import { lastToolResult , sectionsSeen , setup , turn } from "../fixture.ts" ; 17import { BOOTSTRAP , Connection , RemoteEnvironment } from "./connection.ts" ; 18 19/** The worker on this machine, as ssh would start it on another, telling itself apart by `BERRY_MACHINE`. */ 20const here = ( name :string ) => 21Connection . open ( name , [ 22"env" , 23`BERRY_MACHINE= ${ name } ` , 24Deno . execPath (), 25"eval" , 26"--quiet" , 27BOOTSTRAP , 28]); 29 30Deno . test ( "the bootstrap passes through any remote shell's double quotes as it is" , () => { 31// sh, cmd, and powershell each give one of these a meaning inside double quotes 32for ( const special of [ '"' , "$" , "`" , "\\" , "%" , "!" , "\n" ]) { 33assert ( ! BOOTSTRAP . includes ( special ), `the bootstrap holds ${ JSON . stringify ( special )} ` ); 34} 35}); 36 37/** Cases a local NodeExecutionEnv fails as well when deno runs it, which a worker cannot do better at. */ 38const UPSTREAM = new Set ([ "watch follows a directory replaced at the same path" ]); 39 40{ 41let connection :Connection | undefined ; 42const cases :{ name :string ; test :() => Promise < void > }[] = []; 43registerEnvConformance ( 44{ 45describe ( name , suite ) { 46suite (); 47// one worker for every case, as a conversation's calls share one 48Deno . test ( name , async ( t ) => { 49connection = await here ( "conformance" ); 50try { 51for ( const { name, test} of cases ) { 52await t . step ({ name, fn :test , ignore :UPSTREAM . has ( name ) }); 53} 54} finally { 55await connection . close (); 56} 57}); 58}, 59it :( name , test ) => void cases . push ({ name, test}), 60expect :( actual ) => ({ 61toBe :( expected :unknown ) => assertStrictEquals ( actual , expected ), 62toBeGreaterThan :( expected :number ) => assertGreater ( actual as number , expected ), 63toEqual :( expected :unknown ) => assertEquals ( actual , expected ), 64toMatchObject :( expected :Record < PropertyKey , unknown >) => 65assertObjectMatch ( actual as Record < PropertyKey , unknown >, expected ), 66toBeTruthy :() => assert ( actual ), 67rejects :{ 68toThrow :( expected ?:string ) => 69assertRejects (() => actual as Promise < unknown >, Error , expected ), 70}, 71}), 72}, 73"a remote environment behaves as a local one" , 74async ( use ) => { 75const root = new RemoteEnvironment ( "conformance" , connection ! , "/" ); 76const dir = getOrThrow ( await root . createTempDir ( "berry-conformance-" , context )); 77try { 78await use ( new RemoteEnvironment ( "conformance" , connection ! , dir )); 79} finally { 80await root . remove ( dir , { recursive :true , force :true }, context ); 81} 82}, 83); 84} 85 86Deno . test ( "each machine keeps one connection, until it is lost or its settings change" , async () => { 87let settings :Record < string , EnvironmentSettings > = { far :{ ssh :"far" } }; 88const started :Connection [] = []; 89const environments = configuredEnvironments ( 90() => Promise . resolve ( settings ), 91async ( name ) => { 92const connection = await here ( name ); 93started . push ( connection ); 94return connection ; 95}, 96); 97try { 98const [ first , second ] = await Promise . all ([ 99environments . open ( "far" , null , context ), 100environments . open ( "far" , "/" , context ), 101]); 102assertEquals ( started . length , 1 ); 103assertEquals ([ first . cwd , second . cwd ], [ first . home , "/" ]); 104assertEquals ( first . variables . BERRY_MACHINE , "far" ); 105 106await started [ 0 ]. close (); 107const lost = await first . fileInfo ( "/" , context ); 108assert ( ! lost . ok ); 109assertStringIncludes ( lost . error . message , "far: lost" ); 110assert (( await ( await environments . open ( "far" , null , context )). fileInfo ( "/" , context )). ok ); 111assertEquals ( started . length , 2 ); 112 113settings = { far :{ ssh :"far" , deno :"deno" } }; 114await environments . open ( "far" , null , context ); 115assertEquals ( started . length , 3 ); 116 117settings = {}; 118await assertRejects ( 119() => environments . open ( "far" , null , context ), 120Error , 121"No environment named far" , 122); 123assertEquals (( await environments . open ( "local" , null , context )). name , "local" ); 124} finally { 125await environments . close (); 126} 127}); 128 129Deno . test ( "a worker that cannot start says why" , async () => { 130await assertRejects ( 131() => Connection . open ( "nowhere" , [ "sh" , "-c" , "echo no deno here >&2; exit 127" ]), 132Error , 133"no deno here" , 134); 135}); 136 137Deno . test ( "aborting a call stops the command on the other machine" , async () => { 138const connection = await here ( "far" ); 139try { 140const env = new RemoteEnvironment ( "far" , connection , "/" ); 141const controller = new AbortController (); 142const started = performance . now (); 143setTimeout (() => controller . abort (), 100 ); 144const result = await env . exec ( 145"sleep 30" , 146undefined , 147withAbortSignal ( controller . signal , context ), 148); 149assert ( ! result . ok ); 150assertEquals ( result . error . code , "aborted" ); 151assert ( performance . now () - started < 10_000 ); 152} finally { 153await connection . close (); 154} 155}); 156 157Deno . test ( "a conversation bound to a configured machine works there" , async () => { 158const environments = configuredEnvironments ( 159() => Promise . resolve ({ far :{ ssh :"far" } }), 160( name ) => here ( name ), 161); 162await usingharness = await setup ( environments ); 163const { berry, model} = harness ; 164await berry . install ( code ); 165const tid = await berry . create ({ 166extensions :{ environment :{ environment :"far" }, code :{} }, 167 model, 168}); 169const home = ( await berry . environment ( tid )). home ; 170const seen = await turn ( harness , tid , [ 171fauxAssistantMessage ( 172fauxToolCall ( "bash" , { command :"echo $BERRY_MACHINE; pwd" , timeout :10 }), 173{ 174stopReason :"toolUse" , 175}, 176), 177]); 178assertStringIncludes ( lastToolResult ( seen [ 1 ]). text , `far\n ${ home } ` ); 179const sections = sectionsSeen ( seen [ 0 ])[ 0 ]; 180assertStringIncludes ( sections . code ! , `current working directory: ${ home } ` ); 181assertStringIncludes ( sections . environment ! , "files and commands are on far" ); 182}); 183 184Deno . test ( "programs and sockets on the other machine speak both ways, and end with it" , async () => { 185const connection = await here ( "far" ); 186const env = new RemoteEnvironment ( "far" , connection , "/" ); 187const echo = Deno . listen ({ hostname :"127.0.0.1" , port :0 }); 188const echoed = ( async () => { 189const accepted = await echo . accept (); 190await accepted . readable . pipeTo ( accepted . writable ); 191})(); 192try { 193const cat = await env . spawn ([ "cat" ], {}, context ); 194const writer = cat . stdin . getWriter (); 195await writer . write ( new TextEncoder (). encode ( "hello" )); 196await writer . close (); 197assertEquals ( await new Response ( cat . stdout ). text (), "hello" ); 198assertEquals ( await cat . exited , 0 ); 199 200const told = await env . spawn ([ "sh" , "-c" , "echo $BERRY_MACHINE $THERE; exit 3" ], { 201env :{ THERE :"too" }, 202}, context ); 203assertEquals ( await new Response ( told . stdout ). text (), "far too\n" ); 204assertEquals ( await told . exited , 3 ); 205 206await assertRejects (() => env . spawn ([ "berry-no-such-program" ], {}, context )); 207const sleeping = await env . spawn ([ "sleep" , "30" ], {}, context ); 208sleeping . kill (); 209assertEquals ( await sleeping . exited , null ); 210 211// the worker's loopback, which here is this machine's 212const socket = await env . connect ( "127.0.0.1" , ( echo . addr as Deno . NetAddr ). port , context ); 213const socketWriter = socket . writable . getWriter (); 214await socketWriter . write ( new TextEncoder (). encode ( "ping" )); 215const { value} = await socket . readable . getReader (). read (); 216assertEquals ( new TextDecoder (). decode ( value ), "ping" ); 217socket . close (); 218await echoed ; 219 220const orphan = await env . spawn ([ "sleep" , "30" ], {}, context ); 221await connection . close (); 222assertEquals ( await orphan . exited , null ); 223await assertRejects (() => new Response ( orphan . stdout ). text (), Error , "far: lost" ); 224} finally { 225echo . close (); 226await connection . close (); 227} 228});