okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
5bdc0ee
main
1"""Concurrent child kernel boot stays responsive and forwards all answers.""" 2 3import json 4import os 5import sqlite3 6import sys 7import time 8import unittest 9import urllib .error 10 11from harness import ( 12Albedo , 13Provider , 14exclusive , 15latest_user , 16operation_id , 17python , 18release_fifo , 19text , 20wait_until , 21) 22 23HEAVY = os .environ .get ("ALBEDO_E2E_HEAVY" )== "1" 24ROOTS = 4 if HEAVY else 2 25CHILDREN = 12 if HEAVY else 4 26 27 28class AgentsSwarmTests (unittest .TestCase ): 29def test_children_boot_without_blocking_actors_or_losing_answers (self ): 30def script (request ): 31last = request ["messages" ][- 1 ] 32if last .get ("role" )== "user" and 'kind="task"' in latest_user (request ): 33return python ("print('cell ran')" ) 34return text ("done" ) 35 36provider = Provider (script ) 37try : 38with Albedo (provider )as app : 39roots = [app .session ()for _ in range (ROOTS )] 40children = [] 41started = time .monotonic () 42for root in roots : 43for number in range (CHILDREN ): 44child = operation_id () 45with app .api ( 46f"/sessions/ { child } " , 47 { 48"kind" :"child" , 49"parent_id" :root , 50"address" :f"sp- { number } " , 51"name" :f"sp- { number } " , 52"initial_input_id" :operation_id (), 53"task" :f"count to { number } " , 54 }, 55method = "PUT" , 56headers = {"If-None-Match" :"*" }, 57 )as response : 58self .assertEqual (json .load (response )["id" ],child ) 59children .append (child ) 60self .assertLess ( 61time .monotonic ()- started ,30 ,"spawn must not await kernel boot" 62 ) 63slowest = 0.0 64deadline = time .monotonic ()+ 300 65while time .monotonic ()< deadline : 66answers = self .answers (app ,roots ) 67tick = time .monotonic () 68for root in roots : 69with app .api ( 70f"/sessions?scope=all&family_id= { root } " 71 )as response : 72self .assertEqual ( 73len (json .load (response )["items" ]),CHILDREN + 1 74 ) 75for child in children [::6 ]: 76with app .api (f"/sessions/ { child } ?tail=0" )as response : 77json .load (response ) 78slowest = max (slowest ,time .monotonic ()- tick ) 79if sum (answers )== ROOTS * CHILDREN : 80break 81time .sleep (0.5 ) 82self .assertEqual (self .answers (app ,roots ), [CHILDREN ]* ROOTS ) 83self .assertLess ( 84slowest ,5 ,"status and tree calls must stay responsive" 85 ) 86# Fast kernels can finish before the first status poll. Their 87# completed Python outputs prove every child actually booted. 88completed = [ 89record 90for record in provider .requests 91if record ["request" ]["messages" ][- 1 ].get ("role" )== "tool" 92and "cell ran" 93in record ["request" ]["messages" ][- 1 ].get ("content" ,"" ) 94 ] 95self .assertEqual (len (completed ),ROOTS * CHILDREN ) 96with app .api ("/server" )as response : 97self .assertEqual (json .load (response )["state" ],"ready" ) 98finally : 99provider .close () 100 101@exclusive 102def test_closed_child_does_not_start_when_its_kernel_finishes_booting (self ): 103def script (request ): 104if request ["messages" ][- 1 ].get ("role" )!= "user" : 105return text ("done" ) 106if latest_user (request )== "spawn held child" : 107return python ( 108"models = await agents.models()\n" 109"child = await agents.self.spawn('held task', name='held', model=models[0])\n" 110"print(child.id)" 111 ) 112if latest_user (request )== "close held child" : 113return python ( 114"await child.close()\nprint((await mail.submit(child, 'late task')).status)" 115 ) 116return text ("done" ) 117 118def prepare (app ): 119# The parent boots normally; creating this FIFO holds later launches. 120wrapper = app .home / "bin" / "python3" 121wrapper .parent .mkdir () 122gate = app .workspace / "boot-gate" 123wrapper .write_text ( 124f"#! { sys . executable } \n" 125"import os, sys\n" 126"from pathlib import Path\n" 127f"gate = Path( { str ( gate )!r } )\n" 128"if ('launch' in sys.argv or 'start' in sys.argv) and gate.exists():\n" 129f" Path( { str ( app . workspace / 'boot-ready' )!r } ).touch()\n" 130" with gate.open('rb') as reader: reader.read(1)\n" 131f"os.execv( { sys . executable !r } , [ { sys . executable !r } , *sys.argv[1:]])\n" 132 ) 133wrapper .chmod (0o700 ) 134app .env ["PATH" ]= str (wrapper .parent )+ os .pathsep + app .env ["PATH" ] 135 136provider = Provider (script ) 137self .addCleanup (provider .close ) 138with Albedo (provider ,prepare = prepare )as app : 139parent = app .session () 140app .prompt (parent ,"prime parent kernel" ).close () 141app .idle (parent ) 142gate = app .workspace / "boot-gate" 143os .mkfifo (gate ) 144app .prompt (parent ,"spawn held child" ).close () 145app .idle (parent ) 146with app .api (f"/sessions?scope=all&family_id= { parent } " )as response : 147child = next ( 148item ["id" ] 149for item in json .load (response )["items" ] 150if item ["parent_id" ]== parent 151 ) 152with sqlite3 .connect (app .home / "albedo.sqlite" )as database : 153self .assertEqual ( 154database .execute ( 155"SELECT count(*) FROM mail WHERE recipient=? AND delivered_at IS NULL" , 156 (child ,), 157 ).fetchone ()[0 ], 1581 , 159 ) 160self .assertEqual (app .history (child )["items" ], []) 161wait_until ( 162 (app .workspace / "boot-ready" ).exists ,10 ,"child did not start booting" 163 ) 164app .prompt (parent ,"close held child" ).close () 165app .idle (parent ) 166wait_until ( 167lambda :release_fifo (gate ),10 ,"child did not reach its boot gate" 168 ) 169gate .unlink () 170# Wait for the child's preparation to finish, not an arbitrary sleep. 171app .idle (child ) 172self .assertEqual (app .history (child )["items" ], []) 173with sqlite3 .connect (app .home / "albedo.sqlite" )as database : 174self .assertIsNotNone ( 175database .execute ( 176"SELECT closed_at FROM session_family WHERE session=?" , 177 (child ,), 178 ).fetchone ()[0 ] 179 ) 180self .assertEqual ( 181database .execute ( 182"SELECT count(*) FROM mail WHERE recipient=? AND delivered_at IS NULL" , 183 (child ,), 184 ).fetchone ()[0 ], 1852 , 186 ) 187self .assertFalse ( 188any ( 189"held task" in latest_user (record ["request" ]) 190for record in provider .requests 191 ) 192 ) 193# A manual chat can still visit a closed child without consuming its letters. 194app .prompt (child ,"manual visit" ).close () 195app .idle (child ) 196self .assertTrue ( 197any ( 198latest_user (record ["request" ])== "manual visit" 199for record in provider .requests 200 ) 201 ) 202# Closing one child must not prevent a later sibling from starting. 203sibling = operation_id () 204with app .api ( 205f"/sessions/ { sibling } " , 206 { 207"kind" :"child" , 208"parent_id" :parent , 209"address" :"sibling" , 210"name" :"sibling" , 211"initial_input_id" :operation_id (), 212"task" :"normal task" , 213 }, 214method = "PUT" , 215headers = {"If-None-Match" :"*" }, 216 )as response : 217self .assertEqual (json .load (response )["id" ],sibling ) 218app .idle (sibling ) 219self .assertTrue ( 220any ( 221"normal task" in latest_user (record ["request" ]) 222for record in provider .requests 223 ) 224 ) 225 226def test_explicit_session_provider_binds_and_unknown_profile_is_rejected (self ): 227provider = Provider (lambda _request :text ("ok" )) 228try : 229with Albedo (provider )as app : 230session = app .session () 231with app .api ("/sessions" )as response : 232info = next ( 233item 234for item in json .load (response )["items" ] 235if item ["id" ]== session 236 ) 237self .assertEqual (info ["provider_profile" ],app .profile ) 238with self .assertRaises (urllib .error .HTTPError )as rejected : 239app .api ( 240f"/sessions/ { operation_id () } " , 241 { 242"kind" :"new" , 243"workspace" :str (app .workspace ), 244"provider_profile" :"not-configured" , 245 }, 246method = "PUT" , 247headers = {"If-None-Match" :"*" }, 248 ).close () 249self .assertEqual (rejected .exception .code ,400 ) 250finally : 251provider .close () 252 253@staticmethod 254def answers (app ,roots ): 255counts = [] 256for root in roots : 257with app .api (f"/sessions/ { root } /history?limit=200" )as response : 258items = json .load (response )["items" ] 259counts .append ( 260sum ( 261item ["kind" ]== "user" 262and (item .get ("mail" )or {}).get ("kind" )== "unreviewed" 263for item in items 264 ) 265 ) 266return counts