char-slop/berry
multipurpose self-modifiable agentic harness
git clone https://git.t4t.associates/char-slop/berry
32b51b2
main
1import { assertEquals , assertExists , assertStringIncludes } from "@std/assert" ; 2import { withBerry } from "./testing.ts" ; 3import { fill } from "@berry/util/dom.ts" ; 4import { until } from "@berry/util" ; 5 6const initial = { 7userName :"Charlotte" , 8generalGuidelines :"Ask $USER first." , 9}; 10 11Deno . test ( "available providers start collapsed, expand for search, and remember manual expansion" , async () => { 12await withBerry ( async ({ scope, server, settle}) => { 13const available = [ 14{ 15provider :"alpha" , 16id :"a1" , 17name :"Alpha One" , 18input :[ "text" ], 19reasoning :false , 20thinkingLevels :[ "off" ], 21contextWindow :1000 , 22cost :{ input :0 , output :0 }, 23}, 24{ 25provider :"beta" , 26id :"b1" , 27name :"Beta One" , 28input :[ "text" ], 29reasoning :false , 30thinkingLevels :[ "off" ], 31contextWindow :1000 , 32cost :{ input :0 , output :0 }, 33}, 34]; 35server . respond (({ path}) => path === "/api/models" ?available :[]); 36const { app} = await import ( "./app.tsx" ); 37document . body . append ( app ( scope )); 38server . snapshot ( "settings" , { 39models :[{ provider :"alpha" , modelId :"a1" }], 40systemPrompt :initial , 41}); 42server . snapshot ( "extensions" , []); 43await settle (); 44const groups = [ 45 ...document . querySelectorAll < HTMLDetailsElement >( ".model-list details.provider" ), 46]; 47assertEquals ( groups . length , 2 ); 48assertEquals ( 49groups . map (( group ) => group . open ), 50[ false , false ], 51); 52groups [ 0 ]. open = true ; 53await settle (); 54server . snapshot ( "settings" , { models :[], systemPrompt :initial }); 55await settle (); 56assertEquals ( document . querySelector < HTMLDetailsElement >( ".model-list details" ) ! . open , true ); 57 58const filter = document . querySelector < HTMLInputElement >( ".settings input.filter" ) ! ; 59filter . focus (); 60fill ( filter , "beta" ); 61await settle (); 62const shown = [ ...document . querySelectorAll ( ".model-list details.provider" )]; 63assertEquals ( shown . length , 1 ); 64assertEquals (( shown [ 0 ] as HTMLDetailsElement ). open , true ); 65assertStringIncludes ( shown [ 0 ]. textContent , "Beta One" ); 66fill ( filter , "" ); 67await settle (); 68assertEquals ( 69[ ...document . querySelectorAll < HTMLDetailsElement >( ".model-list details" )]. map (( group ) => 70group . open 71), 72[ true , false ], 73); 74}, { url :"http://berry.test/#/settings" }); 75}); 76 77Deno . test ( "global prompt drafts survive remote updates, save only changed fields, and recover from failure" , async () => { 78await withBerry ( async ({ scope, server, settle}) => { 79server . respond (() => []); 80const { app} = await import ( "./app.tsx" ); 81document . body . append ( app ( scope )); 82server . snapshot ( "settings" , { models :[], systemPrompt :initial }); 83server . snapshot ( "extensions" , []); 84await settle (); 85 86const editor = document . querySelector < HTMLFieldSetElement >( ".system-prompt" ); 87assertExists ( editor ); 88const name = editor . querySelector < HTMLInputElement >( "input" ) ! ; 89const general = editor . querySelector < HTMLTextAreaElement >( "textarea" ) ! ; 90assertEquals ([ name . value , general . value ], Object . values ( initial )); 91 92general . focus (); 93fill ( general , "" ); 94const remote = { ...initial , userName :"Sam" }; 95server . snapshot ( "settings" , { models :[], systemPrompt :remote }); 96await settle (); 97assertEquals ([ name . value , general . value ], [ "Sam" , "" ]); 98 99server . respond (() => new Response ( JSON . stringify ({ error :"disk full" }), { status :500 })); 100general . blur (); 101await settle (); 102assertEquals ( general . value , "" ); 103assertEquals ( editor . querySelector ( '[role="status"]' ) ! . textContent , "disk full" ); 104assertEquals ( editor . disabled , false ); 105 106server . respond (() => ({ models :[], systemPrompt :{ ...remote , generalGuidelines :"" } })); 107general . focus (); 108general . blur (); 109await settle (); 110assertEquals ( server . changes (). at ( - 1 ), { 111method :"PATCH" , 112path :"/api/settings" , 113body :{ systemPrompt :{ generalGuidelines :"" } }, 114}); 115server . snapshot ( "settings" , { 116models :[], 117systemPrompt :{ ...remote , generalGuidelines :"New global rule." }, 118}); 119await settle (); 120assertEquals ( general . value , "New global rule." ); 121}, { url :"http://berry.test/#/settings" }); 122}); 123 124Deno . test ( "blur saves are queued without blocking editing or overwriting newer drafts" , async () => { 125await withBerry ( async ({ scope, server, settle}) => { 126server . respond (() => []); 127const { app} = await import ( "./app.tsx" ); 128document . body . append ( app ( scope )); 129server . snapshot ( "settings" , { models :[], systemPrompt :initial }); 130server . snapshot ( "extensions" , []); 131await settle (); 132 133const editor = document . querySelector < HTMLFieldSetElement >( ".system-prompt" ) ! ; 134const name = editor . querySelector < HTMLInputElement >( "input" ) ! ; 135const general = editor . querySelector < HTMLTextAreaElement >( "textarea" ) ! ; 136const entered = Promise . withResolvers < void >(); 137const first = Promise . withResolvers < unknown >(); 138let saved = { ...initial }; 139server . respond (({ method, body}) => { 140if ( method !== "PATCH" ) return []; 141if ( server . changes (). length === 1 ) { 142entered . resolve (); 143return first . promise ; 144} 145saved = { ...saved , ...( body as { systemPrompt :Partial < typeof initial > }). systemPrompt }; 146return { systemPrompt :saved }; 147}); 148 149general . focus (); 150fill ( general , "First edit" ); 151assertEquals ( server . changes (). length , 0 ); 152name . focus (); 153await entered . promise ; 154assertEquals ( editor . disabled , false ); 155 156general . focus (); 157fill ( general , "Second edit" ); 158name . focus (); 159fill ( name , "Alex" ); 160assertEquals ( server . changes (). length , 1 ); 161 162saved = { ...saved , generalGuidelines :"First edit" }; 163server . snapshot ( "settings" , { models :[], systemPrompt :saved }); 164first . resolve ({ systemPrompt :saved }); 165await until (() => server . changes (). length === 2 ); 166await settle (); 167assertEquals ( general . value , "Second edit" ); 168assertEquals ( name . value , "Alex" ); 169assertEquals ( server . changes (). map (({ body}) => body ), [ 170{ systemPrompt :{ generalGuidelines :"First edit" } }, 171{ systemPrompt :{ generalGuidelines :"Second edit" } }, 172]); 173 174name . blur (); 175await settle (); 176assertEquals ( server . changes (). at ( - 1 )?. body , { systemPrompt :{ userName :"Alex" } }); 177const count = server . changes (). length ; 178general . focus (); 179general . blur (); 180await settle (); 181assertEquals ( server . changes (). length , count ); 182}, { url :"http://berry.test/#/settings" }); 183}); 184 185Deno . test ( "extension prose saves per extension and keeps dirty drafts across remote updates" , async () => { 186await withBerry ( async ({ scope, server, settle}) => { 187server . respond (() => []); 188const { app} = await import ( "./app.tsx" ); 189document . body . append ( app ( scope )); 190const extensions = [{ 191name :"code" , 192directory :"/ext/code" , 193description :"edit files" , 194status :"loaded" , 195error :null , 196running :true , 197builtin :false , 198alwaysOn :false , 199defaultEnabled :false , 200web :null , 201formerly :[], 202configuration :null , 203promptProse :{ default :"Default prose." }, 204startup :null , 205}]; 206server . snapshot ( "extensions" , extensions ); 207await settle (); 208 209const row = document . querySelector ( ".extension" ) ! ; 210const area = row . querySelector < HTMLTextAreaElement >( "textarea" ) ! ; 211const fieldset = row . querySelector < HTMLFieldSetElement >( "fieldset" ) ! ; 212assertExists ( area ); 213assertEquals ( fieldset . disabled , true ); 214assertEquals ( area . value , "" ); 215 216server . snapshot ( "settings" , { models :[], systemPrompt :{ ...initial , promptProse :{} } }); 217await settle (); 218assertEquals ( fieldset . disabled , false ); 219assertEquals ( area . value , "Default prose." ); 220 221area . focus (); 222fill ( area , "Edited prose." ); 223server . snapshot ( "settings" , { 224models :[], 225systemPrompt :{ ...initial , userName :"Sam" , promptProse :{} }, 226}); 227await settle (); 228assertEquals ( area . value , "Edited prose." ); 229 230server . respond (() => ({ 231systemPrompt :{ ...initial , promptProse :{ code :"Edited prose." } }, 232})); 233area . blur (); 234await settle (); 235assertEquals ( server . changes (). at ( - 1 ), { 236method :"PATCH" , 237path :"/api/settings" , 238body :{ systemPrompt :{ promptProse :{ code :"Edited prose." } } }, 239}); 240 241server . snapshot ( "settings" , { 242models :[], 243systemPrompt :{ ...initial , promptProse :{ code :"Remote prose." } }, 244}); 245await settle (); 246assertEquals ( area . value , "Remote prose." ); 247 248// a reloaded default reaches an untouched draft, but never overwrites an edited one 249server . snapshot ( "settings" , { 250models :[], 251systemPrompt :{ ...initial , promptProse :{} }, 252}); 253await settle (); 254server . snapshot ( "extensions" , [{ 255 ...extensions [ 0 ], 256promptProse :{ default :"Reloaded prose." }, 257}]); 258await settle (); 259const freshRow = document . querySelector ( ".extension" ) ! ; 260const freshArea = freshRow . querySelector < HTMLTextAreaElement >( "textarea" ) ! ; 261assertEquals ( freshArea . value , "Reloaded prose." ); 262assertEquals ( freshArea , area ); 263const reloaded = freshArea ; 264reloaded . focus (); 265fill ( reloaded , "Draft prose." ); 266assertEquals ( document . activeElement , reloaded ); 267server . snapshot ( "extensions" , [{ 268 ...extensions [ 0 ], 269description :"new description" , 270promptProse :{ default :"Newer prose." }, 271}]); 272await settle (); 273assertEquals ( reloaded . value , "Draft prose." ); 274assertEquals ( document . activeElement , reloaded ); 275assertEquals ( document . querySelector ( ".extension" ) ! , freshRow ); 276assertEquals ( document . querySelector ( ".extension textarea" ), reloaded ); 277assertEquals ( freshRow . querySelector ( ".description" ) ! . textContent , "new description" ); 278 279const entered = Promise . withResolvers < void >(); 280const first = Promise . withResolvers < unknown >(); 281let kept = "Draft prose." ; 282server . respond (({ method, body}) => { 283if ( method !== "PATCH" ) return []; 284if ( server . changes (). length === 2 ) { 285entered . resolve (); 286return first . promise ; 287} 288kept = ( body as { systemPrompt :{ promptProse :{ code :string } } }). systemPrompt . promptProse 289. code ; 290return { systemPrompt :{ ...initial , promptProse :{ code :kept } } }; 291}); 292reloaded . focus (); 293reloaded . blur (); 294await entered . promise ; 295reloaded . focus (); 296fill ( reloaded , "Second queued." ); 297reloaded . blur (); 298await settle (); 299assertEquals ( reloaded . value , "Second queued." ); 300first . resolve ({ systemPrompt :{ ...initial , promptProse :{ code :"Draft prose." } } }); 301await until (() => server . changes (). length === 3 ); 302await settle (); 303assertEquals ( reloaded . value , "Second queued." ); 304assertEquals ( server . changes (). slice ( 1 ). map (({ body}) => body ), [ 305{ systemPrompt :{ promptProse :{ code :"Draft prose." } } }, 306{ systemPrompt :{ promptProse :{ code :"Second queued." } } }, 307]); 308 309server . respond (() => new Response ( JSON . stringify ({ error :"disk full" }), { status :500 })); 310reloaded . focus (); 311fill ( reloaded , "Failing edit." ); 312reloaded . blur (); 313await settle (); 314assertEquals ( reloaded . value , "Failing edit." ); 315assertEquals ( 316document . querySelector ( ".extension" ) ! . querySelector ( '[role="status"]' ) ! . textContent , 317"disk full" , 318); 319}, { url :"http://berry.test/#/settings" }); 320}); 321 322Deno . test ( "the local environment toggle follows global settings and restores its value after a failed save" , async () => { 323await withBerry ( async ({ scope, server, settle}) => { 324server . respond (() => []); 325const { app} = await import ( "./app.tsx" ); 326document . body . append ( app ( scope )); 327server . snapshot ( "settings" , { models :[] }); 328server . snapshot ( "extensions" , []); 329await settle (); 330 331const local = document . querySelector < HTMLInputElement >( '.environments input[type="checkbox"]' ); 332assertExists ( local ); 333assertEquals ( local . checked , true ); 334server . respond (() => ({ allowLocalEnvironment :false })); 335local . click (); 336await settle (); 337assertEquals ( server . changes (). at ( - 1 ), { 338method :"PATCH" , 339path :"/api/settings" , 340body :{ allowLocalEnvironment :false }, 341}); 342assertEquals ( local . checked , false ); 343 344server . respond (() => new Response ( JSON . stringify ({ error :"disk full" }), { status :500 })); 345local . click (); 346await settle (); 347assertEquals ( local . checked , false ); 348assertEquals ( local . disabled , false ); 349assertEquals ( document . querySelector ( ".environments .error" )?. textContent , "disk full" ); 350 351server . snapshot ( "settings" , { models :[], allowLocalEnvironment :true }); 352await settle (); 353assertEquals ( local . checked , true ); 354}, { url :"http://berry.test/#/settings" }); 355}); 356 357Deno . test ( "the notification switch explains a page that cannot ask, and stores only a granted choice" , async () => { 358await withBerry ( async ({ scope, server, settle}) => { 359server . respond (() => []); 360const { app} = await import ( "./app.tsx" ); 361document . body . append ( app ( scope )); 362server . snapshot ( "settings" , { models :[] }); 363server . snapshot ( "extensions" , []); 364await settle (); 365const input = document . querySelector < HTMLInputElement >( ".notify input[type=checkbox]" ); 366assertExists ( input ); 367assertEquals ( input . disabled , true ); 368assertEquals ( input . checked , false ); 369assertStringIncludes ( 370document . querySelector ( ".notify [role=status]" )?. textContent ?? "" , 371"secure" , 372); 373}, { url :"http://berry.test/#/settings" }); 374 375class Granted { 376static permission :NotificationPermission = "granted" ; 377static requestPermission = () => Promise . resolve ( "granted" as const ); 378constructor () {} 379close () {} 380} 381await withBerry ( async ({ scope, server, settle}) => { 382server . respond (() => []); 383const { app} = await import ( "./app.tsx" ); 384document . body . append ( app ( scope )); 385server . snapshot ( "settings" , { models :[] }); 386server . snapshot ( "extensions" , []); 387await settle (); 388const input = document . querySelector < HTMLInputElement >( ".notify input[type=checkbox]" ); 389assertExists ( input ); 390assertEquals ( input . disabled , false ); 391input . click (); 392await settle (); 393assertEquals ( input . checked , true ); 394assertEquals ( localStorage . getItem ( "berry.notifications" ), "on" ); 395input . click (); 396await settle (); 397assertEquals ( input . checked , false ); 398assertEquals ( localStorage . getItem ( "berry.notifications" ), null ); 399}, { url :"http://berry.test/#/settings" , globals :{ Notification :Granted } }); 400});