okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
5bdc0ee
main
1"""Long cells detach without losing output, deadlines, or completion wakes.""" 2 3import json 4import time 5import unittest 6 7from harness import Albedo ,Provider ,Reply ,exclusive ,latest_user ,python ,text 8 9 10def wait_for (predicate ,timeout = 40 ): 11deadline = time .monotonic ()+ timeout 12while time .monotonic ()< deadline : 13if result := predicate (): 14return result 15time .sleep (0.1 ) 16raise AssertionError ("background cell did not wake session" ) 17 18 19@exclusive 20class CellBackgroundTests (unittest .TestCase ): 21def setUp (self ): 22self .results = [] 23self .read_before_idle = False 24self .deadline = False 25self .synchronous = False 26self .cell_id = None 27 28def script (request ): 29last = request ["messages" ][- 1 ] 30user = latest_user (request ) 31if last .get ("role" )== "tool" : 32result = json .loads (last ["content" ]) 33self .results .append (result ) 34if result .get ("status" )== "backgrounded" : 35self .cell_id = result ["cell_id" ] 36return text ("waiting for the background cell" ) 37if "background python cells finished" in user : 38return text ("completion received" ) 39if "start cell" in user : 40code = ( 41"import asyncio\n" 42"cell_gate = asyncio.Event()\n" 43"cell_done = asyncio.Event()\n" 44"print('before detach')\n" 45"await cell_gate.wait()\n" 46"print('after detach')\n" 47"cell_done.set()\n" 48"{'answer': 42}" 49 ) 50if self .synchronous : 51code = "import threading\nprint('before detach')\nthreading.Event().wait(10)\nprint('after detach')" 52if self .deadline : 53return Reply ( 54"python" ,tool_arguments = {"code" :code ,"timeout_ms" :1800 } 55 ) 56return python (code ) 57if "release cell" in user : 58code = "print('other cell output')\ncell_gate.set()" 59if self .read_before_idle : 60code += f"\nawait cell_done.wait()\nprint(output.read( { self . cell_id !r } ))" 61return python (code ) 62if "cancel cell" in user : 63return python (f"print(await cells.cancel( { self . cell_id !r } ))" ) 64if "inspect cell" in user : 65return python ( 66f"print(await cells.info( { self . cell_id !r } ))\nprint(output.read( { self . cell_id !r } ))" 67 ) 68return text ("done" ) 69 70def prepare (app ): 71app .env ["ALBEDO_CELL_BACKGROUND_SECONDS" ]= "0.5" 72app .env ["ALBEDO_IDLE_SECONDS" ]= "2" 73 74self .provider = Provider (script ) 75self .addCleanup (self .provider .close ) 76self .app = Albedo (self .provider ,prepare = prepare ) 77self .app .__enter__ () 78self .addCleanup (self .app .__exit__ ,None ,None ,None ) 79 80def prompt (self ,session ,message ): 81self .app .prompt (session ,message ).close () 82self .app .idle (session ) 83 84def wakes (self ): 85return [ 86record 87for record in self .provider .requests 88if "background python cells finished" in latest_user (record ["request" ]) 89 ] 90 91def start (self ): 92session = self .app .session () 93self .prompt (session ,"start cell" ) 94result = self .results [0 ] 95self .assertEqual (result ["status" ],"backgrounded" ) 96self .assertIn ("before detach" ,result ["output" ]) 97self .assertIn ("do NOT poll or sleep" ,result ["output" ]) 98self .assertIn ("wake automatically" ,result ["output" ]) 99return session 100 101def test_cell_wakes_once_and_keeps_its_capture_separate (self ): 102session = self .start () 103self .prompt (session ,"inspect cell" ) 104self .assertIn ("'status': 'started'" ,self .results [- 1 ]["output" ]) 105self .assertIn ("before detach" ,self .results [- 1 ]["output" ]) 106self .prompt (session ,"release cell" ) 107self .assertEqual (self .results [- 1 ]["output" ].strip (),"other cell output" ) 108wait_for (lambda :self .wakes ()) 109self .app .idle (session ) 110self .assertEqual (len (self .wakes ()),1 ) 111notes = [ 112entry 113for entry in self .app .history (session )["items" ] 114if entry ["kind" ]== "note" 115and any ( 116part ["kind" ]== "text" and "cell finished" in part ["text" ] 117for part in entry ["content" ] 118 ) 119 ] 120self .assertEqual (len (notes ),1 ) 121shown = next ( 122part ["text" ]for part in notes [0 ]["content" ]if part ["kind" ]== "text" 123 ) 124self .assertTrue (shown .startswith ("cell finished (status=ok" ),shown ) 125# the transcript names the code, the model's notice the cell id 126self .assertTrue (shown .endswith ("import asyncio" ),shown ) 127self .assertNotIn ("<system-note>" ,shown ) 128self .assertIn (self .cell_id ,latest_user (self .wakes ()[0 ]["request" ])) 129self .prompt (session ,"inspect cell" ) 130self .assertIn ("'status': 'ok'" ,self .results [- 1 ]["output" ]) 131self .assertIn ("after detach" ,self .results [- 1 ]["output" ]) 132self .assertIn ("{'answer': 42}" ,self .results [- 1 ]["output" ]) 133self .assertNotIn ("other cell output" ,self .results [- 1 ]["output" ]) 134time .sleep (2.5 ) 135self .assertEqual (len (self .wakes ()),1 ) 136 137def test_reading_finished_output_suppresses_the_wake (self ): 138self .read_before_idle = True 139session = self .start () 140self .prompt (session ,"release cell" ) 141self .assertIn ("after detach" ,self .results [1 ]["output" ]) 142time .sleep (2.5 ) 143self .assertEqual (self .wakes (), []) 144 145def test_original_deadline_interrupts_the_background_cell (self ): 146self .deadline = True 147session = self .start () 148wait_for (lambda :self .wakes ()) 149self .app .idle (session ) 150self .assertIn ("status=interrupted" ,latest_user (self .wakes ()[0 ]["request" ])) 151self .prompt (session ,"inspect cell" ) 152self .assertIn ("'status': 'interrupted'" ,self .results [- 1 ]["output" ]) 153 154def test_background_cell_survives_idle_reaping_and_daemon_restart (self ): 155session = self .start () 156time .sleep (4 ) 157self .app .restart () 158self .prompt (session ,"release cell" ) 159wait_for (lambda :self .wakes ()) 160self .app .idle (session ) 161self .assertEqual (len (self .wakes ()),1 ) 162self .assertIn ("status=ok" ,latest_user (self .wakes ()[0 ]["request" ])) 163 164def test_cancelled_background_cell_does_not_wake (self ): 165session = self .start () 166self .prompt (session ,"cancel cell" ) 167self .assertEqual (self .results [1 ]["output" ].strip (),"True" ) 168time .sleep (2.5 ) 169self .prompt (session ,"inspect cell" ) 170self .assertIn ("'status': 'interrupted'" ,self .results [- 1 ]["output" ]) 171self .assertEqual (self .wakes (), []) 172 173def test_synchronous_cell_returns_early_and_keeps_its_deadline (self ): 174self .synchronous = True 175self .deadline = True 176session = self .start () 177wait_for (lambda :self .wakes ()) 178self .app .idle (session ) 179self .assertEqual (len (self .wakes ()),1 ) 180self .assertIn ("status=interrupted" ,latest_user (self .wakes ()[0 ]["request" ]))