okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
ce3e329
main
1"""Idle kernel reaping persists Python state and explains restoration to the model.""" 2 3import sqlite3 4import subprocess 5import time 6import unittest 7 8from harness import alive ,latest_user 9from harness import Albedo ,Provider ,exclusive ,python ,text 10from stream_support import StreamProbe 11 12 13def notes_for (app ,session ,cursor ): 14return [ 15event ["data" ]["text" ] 16for event in app .stream_page (session ,cursor )["events" ] 17if event .get ("type" )== "note" 18 ] 19 20 21# Short enough that the test waits seconds, long enough to outlast a turn. 22IDLE_SECONDS = 2 23 24 25def kernels (home ): 26"""Pids of the kernels whose run directory is under this daemon's home.""" 27listing = subprocess .run ( 28 ["ps" ,"-axo" ,"pid=,command=" ],capture_output = True ,text = True ,check = True 29 ).stdout 30return [ 31int (line .split (None ,1 )[0 ]) 32for line in listing .splitlines () 33if "albedo_kernel.py" in line and f" { home } /run/" in line 34 ] 35 36 37# exclusive: changes the daemon idle timeout and counts all its kernels 38@exclusive 39class IdleReapTests (unittest .TestCase ): 40def setUp (self ): 41def script (request ): 42if request ["input" ][- 1 ].get ("role" )== "user" : 43prompt = latest_user (request ) 44if prompt .startswith ("remember" ): 45return python ("answer = 7\nrows = [1, 2, 3]" ) 46if prompt .startswith ("recall" ): 47return python ("print('recalled', answer, rows)" ) 48return text ("ok" ) 49 50provider = Provider (script ) 51self .addCleanup (provider .close ) 52self .provider = provider 53self .app = Albedo ( 54provider , 55protocol = "responses" , 56prepare = lambda app :app .env .update ( 57ALBEDO_IDLE_SECONDS = str (IDLE_SECONDS ),ALBEDO_INSPECT = "1" 58 ), 59 ) 60self .app .__enter__ () 61self .addCleanup (self .app .__exit__ ,None ,None ,None ) 62 63def turn (self ,session ,prompt ): 64self .app .prompt (session ,prompt ).close () 65self .app .idle (session ) 66 67def test_idle_kernel_reaps_and_restores_variables_with_notices (self ): 68app = self .app 69existing = set (kernels (app .home )) 70session = app .session () 71cursor = app .stream_page (session ) 72self .assertEqual ( 73set (kernels (app .home ))- existing , 74set (), 75"session must not eagerly open a kernel" , 76 ) 77self .turn (session ,"remember this" ) 78self .assertEqual (len (set (kernels (app .home ))- existing ),1 ) 79deadline = time .monotonic ()+ IDLE_SECONDS * 4 80while time .monotonic ()< deadline and set (kernels (app .home ))- existing : 81time .sleep (0.1 ) 82self .assertEqual ( 83set (kernels (app .home ))- existing ,set (),"idle kernel outlived its limit" 84 ) 85notes = notes_for (app ,session ,cursor ) 86self .assertIn ("released" ,notes [- 1 ]) 87self .assertIn ("2 variables saved to disk" ,notes [- 1 ]) 88self .assertTrue ((app .home / "kernels" / f" { session } .state" ).exists ()) 89 90self .turn (session ,"recall them" ) 91prompt = latest_user (self .provider .requests [- 1 ]["request" ]) 92self .assertTrue (prompt .startswith ("recall them<system-note>" ),prompt ) 93self .assertIn ("restored from disk: answer, rows" ,prompt ) 94self .assertTrue ( 95any ( 96"recalled 7 [1, 2, 3]" in item .get ("output" ,"" ) 97for item in self .provider .requests [- 1 ]["request" ]["input" ] 98 ) 99 ) 100notes = notes_for (app ,session ,cursor ) 101self .assertTrue ( 102any ("restored 2 variables from disk" in note for note in notes ),notes 103 ) 104 105def test_reattached_kernel_without_a_loaded_session_is_released (self ): 106app = self .app 107app .restart (prepare = lambda a :a .env .update (ALBEDO_IDLE_SECONDS = "600" )) 108session = app .session () 109self .turn (session ,"remember this" ) 110with sqlite3 .connect (app .home / "albedo.sqlite" )as database : 111 (kernel ,)= database .execute ( 112"SELECT pid FROM kernel_links WHERE session=?" , (session ,) 113 ).fetchone () 114saved = app .home / "kernels" / f" { session } .state" 115self .assertFalse (saved .exists ()) 116# A crash leaves the kernel waiting; the next daemon reattaches it 117# without loading the session. 118app .restart ( 119crash = True , 120prepare = lambda a :a .env .update (ALBEDO_IDLE_SECONDS = str (IDLE_SECONDS )), 121 ) 122deadline = time .monotonic ()+ IDLE_SECONDS * 5 123while time .monotonic ()< deadline and alive (kernel ): 124time .sleep (0.1 ) 125self .assertFalse (alive (kernel ),"reattached kernel outlived its limit" ) 126self .assertTrue (saved .exists ()) 127self .turn (session ,"recall them" ) 128self .assertTrue ( 129any ( 130"recalled 7 [1, 2, 3]" in item .get ("output" ,"" ) 131for item in self .provider .requests [- 1 ]["request" ]["input" ] 132 ) 133 ) 134 135def test_blocked_sweep_admits_one_worker_and_continues_after_owner_death (self ): 136app = self .app 137held ,survivor = app .session (),app .session () 138self .turn (held ,"remember held values" ) 139self .turn (survivor ,"remember surviving values" ) 140probe = StreamProbe (app ) 141result = probe .call ("maintenance_json" ,f'[<<" { held } ">>]' ) 142self .assertEqual (result ["admitted" ],1 ) 143self .assertEqual (result ["outcome" ],"completed" ) 144saved = app .home / "kernels" / f" { survivor } .state" 145deadline = time .monotonic ()+ IDLE_SECONDS * 4 146while not saved .exists ()and time .monotonic ()< deadline : 147time .sleep (0.1 ) 148self .assertTrue (saved .exists (),"one dead owner prevented survivor release" ) 149self .turn (survivor ,"recall surviving values" ) 150self .assertTrue ( 151any ( 152"recalled 7 [1, 2, 3]" in item .get ("output" ,"" ) 153for item in self .provider .requests [- 1 ]["request" ]["input" ] 154 ) 155 )