char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
0575637
main
1import { 2assertEquals , 3assertMatch , 4assertNotEquals , 5assertNotMatch , 6assertRejects , 7} from "@std/assert" ; 8import { existsSync } from "node:fs" ; 9import { tempDir , until as eventually } from "@berry/util" ; 10import { BACKGROUND_CONTEXT as context , withCancel } from "@earendil-works/chord/context" ; 11import { type Environment , localEnvironment , type Pty } from "@berry/api/environment.ts" ; 12import { BOOTSTRAP , Connection , RemoteEnvironment } from "./remote/connection.ts" ; 13 14/** What the program has written so far, read until `pattern` turns up in it. */ 15async function until ( 16reader :ReadableStreamDefaultReader < Uint8Array >, 17seen :string [], 18pattern :RegExp , 19) { 20const decoder = new TextDecoder (); 21while ( ! pattern . test ( seen . join ( "" ))) { 22const { value, done} = await reader . read (); 23if ( done ) throw new Error ( `ended before ${ pattern } : ${ JSON . stringify ( seen . join ( "" ))} ` ); 24seen . push ( decoder . decode ( value , { stream :true })); 25} 26return seen . join ( "" ); 27} 28 29const SCRIPT = [ 30"tty" , 31"stty size" , 32"read line" , 33'echo "got $line"' , 34"stty size" , 35'trap "echo interrupted; exit 5" INT' , 36"echo trapped" , 37"sleep 30 & wait" , 38]. join ( "; " ); 39 40async function converse ( env :Environment ) { 41const pty :Pty = await env . pty ([ "sh" , "-c" , SCRIPT ], { cols :80 , rows :24 }, context ); 42const reader = pty . readable . getReader (); 43const writer = pty . writable . getWriter (); 44const seen :string [] = []; 45assertMatch ( await until ( reader , seen , / 24 80 / ), / ^\/dev\/pts\/\d+\r\n / ); 46pty . resize ( 100 , 30 ); 47// the terminal turns a typed return into the newline `read` waits for 48await writer . write ( new TextEncoder (). encode ( "hello\r" )); 49await until ( reader , seen , / got hello\r\n30 100\r\ntrapped\r\n / ); 50// ctrl-c reaches the program as a signal, which only a terminal of its own makes so 51await writer . write ( new TextEncoder (). encode ( "\x03" )); 52await until ( reader , seen , / interrupted / ); 53assertEquals ( await pty . exited , 5 ); 54for ( let next = await reader . read (); ! next . done ; next = await reader . read ()); 55} 56 57/** A program that leaves something holding its terminal, to write there after it has exited. */ 58async function leaves ( env :Environment ) { 59await usingdir = await tempDir (); 60const status = ` ${ dir . path } /status` ; 61const pty = await env . pty ( 62[ 63"sh" , 64"-c" , 65// quoted apart, so the leftover's words are not the command's 66`trap "" HUP; (sleep 1; echo left""over; echo $? > ${ status } ) & echo started` , 67], 68{ cols :80 , rows :24 }, 69context , 70); 71const output = await new Response ( pty . readable ). text (); 72assertMatch ( output , / started / ); 73assertNotMatch ( output , / leftover / ); 74assertEquals ( await pty . exited , 0 ); 75// the terminal hung up under it 76await eventually (() => existsSync ( status )); 77assertNotEquals (( await Deno . readTextFile ( status )). trim (), "0" ); 78} 79 80Deno . test ({ 81name :"a program on a pty has a terminal of its own: sized, resized, typed into and interrupted" , 82ignore :Deno . build . os !== "linux" , 83fn :() => converse ( localEnvironment ( Deno . cwd ())), 84}); 85 86Deno . test ({ 87name :"a pty hangs up once its program exits, whatever that left holding it" , 88ignore :Deno . build . os !== "linux" , 89fn :() => leaves ( localEnvironment ( Deno . cwd ())), 90}); 91 92Deno . test ({ 93name :"a pty hangs up when it is killed" , 94ignore :Deno . build . os !== "linux" , 95async fn () { 96const pty = await localEnvironment ( Deno . cwd ()). pty ([ "sh" , "-c" , "sleep 30" ], { 97cols :80 , 98rows :24 , 99}, context ); 100pty . kill (); 101assertEquals ( await pty . exited , null ); 102await pty . readable . pipeTo ( new WritableStream ()); 103}, 104}); 105 106Deno . test ({ 107name :"aborting a pty wakes blocked readers and writers" , 108ignore :Deno . build . os !== "linux" , 109async fn () { 110const cancelled = withCancel ( context ); 111const pty = await localEnvironment ( Deno . cwd ()). pty ( 112[ "sh" , "-c" , "stty raw -echo; echo ready; exec sleep 30" ], 113{ cols :80 , rows :24 }, 114cancelled . context , 115); 116const reader = pty . readable . getReader (); 117const writer = pty . writable . getWriter (); 118try { 119await until ( reader , [], / ready\n / ); 120let readEnded = false ; 121let writeEnded = false ; 122const reading = reader . read (). then (( next ) => { 123readEnded = true ; 124return next ; 125}); 126const writing = writer . write ( new Uint8Array ( 1024 * 1024 ). fill ( 97 )); 127const rejected = assertRejects (() => writing , Error ). then (() => writeEnded = true ); 128await new Promise (( resolve ) => setTimeout ( resolve , 20 )); 129assertEquals ([ readEnded , writeEnded ], [ false , false ]); 130cancelled . cancel (); 131await eventually (() => readEnded && writeEnded ); 132assertEquals (( await reading ). done , true ); 133await rejected ; 134assertEquals ( await pty . exited , null ); 135} finally { 136pty . kill (); 137await reader . cancel (); 138await pty . exited ; 139} 140}, 141}); 142 143Deno . test ({ 144name :"a pty transfers input larger than its buffer without losing bytes" , 145ignore :Deno . build . os !== "linux" , 146async fn () { 147const pty = await localEnvironment ( Deno . cwd ()). pty ( 148[ "sh" , "-c" , "stty raw -echo; echo ready; exec cat" ], 149{ cols :80 , rows :24 }, 150context , 151); 152const reader = pty . readable . getReader (); 153const writer = pty . writable . getWriter (); 154try { 155await until ( reader , [], / ready\n / ); 156const input = new Uint8Array ( 256 * 1024 ); 157for ( let i = 0 ; i < input . length ; i ++ ) input [ i ] = i % 256 ; 158const output = new Uint8Array ( input . length ); 159await Promise . all ([ 160writer . write ( input ), 161( async () => { 162for ( let at = 0 ; at < output . length ;) { 163const { value, done} = await reader . read (); 164if ( done ) throw new Error ( "Terminal ended before echoing its input" ); 165output . set ( value , at ); 166at += value . length ; 167} 168})(), 169]); 170assertEquals ( output , input ); 171} finally { 172pty . kill (); 173await reader . cancel (); 174await pty . exited ; 175} 176}, 177}); 178 179Deno . test ({ 180name : 181"a pty starts its program where it is asked, with the variables it is given, or says why not" , 182ignore :Deno . build . os !== "linux" , 183async fn () { 184const env = localEnvironment ( "/" ); 185const pty = await env . pty ([ "sh" , "-c" , 'echo "$PWD $THERE"' ], { 186cwd :"/tmp" , 187env :{ THERE :"here" }, 188cols :80 , 189rows :24 , 190}, context ); 191assertEquals ( await new Response ( pty . readable ). text (), "/tmp here\r\n" ); 192assertEquals ( await pty . exited , 0 ); 193await assertRejects ( 194() => env . pty ([ "berry-no-such-program" ], { cols :80 , rows :24 }, context ), 195Error , 196"No such program" , 197); 198}, 199}); 200 201Deno . test ({ 202name :"a pty on another machine speaks, resizes, and hangs up through its worker" , 203ignore :Deno . build . os !== "linux" , 204async fn () { 205const connection = await Connection . open ( "far" , [ 206Deno . execPath (), 207"eval" , 208"--quiet" , 209BOOTSTRAP , 210]); 211try { 212await converse ( new RemoteEnvironment ( "far" , connection , "/" )); 213await leaves ( new RemoteEnvironment ( "far" , connection , "/" )); 214} finally { 215await connection . close (); 216} 217}, 218});