okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
cba5ff8
main
1"""An attachment during generation must recover text and thinking before replay. 2 3The provider pauses only after a responsive subscriber has observed its prefix; 4this exercises real actor capture, HTTP leases, and spill files without sleeps. 5""" 6 7import base64 8import json 9import threading 10import time 11import unittest 12import urllib .error 13import urllib .parse 14 15from harness import Albedo ,Provider ,Reply ,ROOT ,exclusive ,operation_id ,text 16from inspect_support import Inspection 17from tool_progress_test import current_progress ,tool_events 18 19 20def delta (text ,thinking = False ): 21return { 22"id" :"fixture" , 23"choices" : [ 24 { 25"index" :0 , 26"delta" : {"reasoning_content" if thinking else "content" :text }, 27"finish_reason" :None , 28 } 29 ], 30 } 31 32 33def reference_text (app ,reference ): 34pieces = [] 35token = None 36while True : 37url = urllib .parse .urlsplit (reference ["url" ]) 38query = urllib .parse .parse_qs (url .query ) 39if token is not None : 40query ["next" ]= [token ] 41path = urllib .parse .urlunsplit ( 42 ("" ,"" ,url .path ,urllib .parse .urlencode (query ,doseq = True ),"" ) 43 ) 44with app .api (path )as response : 45page = json .load (response ) 46for part in page ["parts" ]: 47if part ["field" ]== reference ["field" ]: 48pieces .append ( 49base64 .b64decode (part ["text" ]) 50if part ["encoding" ]== "base64" 51else part ["text" ].encode () 52 ) 53token = page ["next" ] 54if token is None : 55return b"" .join (pieces ).decode () 56 57 58def active_text (app ,descriptor ): 59return ( 60descriptor ["text" ] 61if descriptor ["text" ]is not None 62else reference_text (app ,descriptor ["reference" ]) 63 ) 64 65 66class ActiveOutputProbe : 67"""Inspect actor memory or age a retired lease without advancing wall time.""" 68 69def __init__ (self ,app ): 70self .inspection = Inspection ( 71app ,ROOT / "test/e2e/albedo_active_output_probe.erl" 72 ) 73 74def call (self ,function ,* arguments ): 75values = "," .join (f"<< { json . dumps ( str ( argument )) } >>" for argument in arguments ) 76return self .inspection .call_json (function ,f"[ { values } ]" ) 77 78 79class PausedOutput : 80"""One scripted response, with an actor-observed barrier and cleanup release.""" 81 82def __init__ (self ,test ,app ,provider ,chunks ,thinking = "" ,* ,session = None ): 83self .app ,self .prefix ,self .thinking = app ,"" .join (chunks ),thinking 84self .suffix = "FINAL_SUFFIX" 85self .release ,self .ready ,self .observed = (threading .Event ()for _ in range (3 )) 86self .events ,self .errors ,self .initial = [], [],None 87self .first_text_at = self .prefix_at = None 88test .addCleanup (self .release .set ) 89 90def response (): 91if thinking : 92yield delta (thinking ,True ) 93for chunk in chunks : 94yield delta (chunk ) 95if not self .release .wait (60 ): 96raise AssertionError ("paused provider was not released" ) 97yield delta (self .suffix ) 98yield { 99"id" :"fixture" , 100"choices" : [{"index" :0 ,"delta" : {},"finish_reason" :"stop" }], 101 } 102 103provider .script = lambda _ :Reply ("text" ,events = response ()) 104self .session = session or app .session () 105self .input_id = operation_id () 106 107def observe (): 108text_chars = 0 109try : 110with app .api ( 111f"/sessions/ { self . session } " ,headers = {"Accept" :"text/event-stream" } 112 )as stream : 113for line in stream : 114if not line .startswith (b"data: " ): 115continue 116batch = json .loads (line [6 :]) 117if self .initial is None : 118self .initial = batch 119self .ready .set () 120for event in batch ["events" ]: 121self .events .append (event ) 122if event ["type" ]== "text" : 123now = time .perf_counter () 124self .first_text_at = self .first_text_at or now 125text_chars += len (event ["data" ]["text" ]) 126if text_chars == len (self .prefix ): 127self .prefix_at = now 128self .observed .set () 129if event ["type" ]== "turn_completed" : 130return 131except Exception as error : 132self .errors .append (error ) 133self .ready .set () 134self .observed .set () 135 136self .thread = threading .Thread (target = observe ,daemon = True ) 137self .thread .start () 138test .assertTrue (self .ready .wait (10 ),"subscriber did not attach" ) 139app .api ( 140f"/sessions/ { self . session } /inputs/ { self . input_id } " , 141 {"kind" :"message" ,"text" :"generate the fixture" }, 142method = "PUT" , 143 ).close () 144test .assertTrue (self .observed .wait (20 ),"actor did not emit complete prefix" ) 145test .assertFalse (self .errors ) 146 147def snapshot (self ): 148with self .app .api (f"/sessions/ { self . session } ?tail=20" )as response : 149return json .load (response ) 150 151def finish (self ,test ): 152self .release .set () 153self .app .idle (self .session ) 154self .thread .join (10 ) 155test .assertFalse (self .thread .is_alive (),"subscriber did not see completion" ) 156test .assertFalse (self .errors ) 157 158 159class ActiveOutputTest (unittest .TestCase ): 160def setUp (self ): 161self .provider = Provider (lambda _ :Reply ("text" ,value = "unused" )) 162self .addCleanup (self .provider .close ) 163self .app = self .enterContext (Albedo (self .provider )) 164 165def assert_prefix (self ,paused ,snapshot ): 166descriptors = snapshot ["active_output" ] 167by_kind = {item ["kind" ]:item for item in descriptors } 168self .assertEqual (active_text (self .app ,by_kind ["text" ]),paused .prefix ) 169self .assertEqual (by_kind ["text" ]["bytes" ],len (paused .prefix .encode ())) 170if paused .thinking : 171self .assertEqual ( 172active_text (self .app ,by_kind ["thinking" ]),paused .thinking 173 ) 174self .assertTrue ( 175all (item ["message_id" ]and item ["run_id" ]for item in descriptors ) 176 ) 177return descriptors 178 179def test_fresh_attachment_restores_text_and_thinking_before_future_deltas (self ): 180paused = PausedOutput ( 181self ,self .app ,self .provider , ["prefix αš" ],"reasoning β" 182 ) 183batch = self .app .stream_page (paused .session ) 184descriptors = self .assert_prefix (paused ,batch ["snapshot" ]) 185paused .finish (self ) 186commits = [ 187event ["data" ]for event in paused .events if event ["type" ]== "committed" 188 ] 189replaced = { 190identifier 191for commit in commits 192for identifier in commit ["replaces_live_ids" ] 193 } 194self .assertTrue ({item ["message_id" ]for item in descriptors }<= replaced ) 195self .assertEqual (paused .snapshot ()["active_output" ], []) 196 197def test_evicted_cursor_restores_large_prefix_and_unflushed_tail (self ): 198chunks = [str (index ).zfill (6 )+ "x" * 250 for index in range (4096 )]+ [ 199"small tail αš" 200 ] 201paused = PausedOutput (self ,self .app ,self .provider ,chunks ,"thinking" * 10000 ) 202batch = self .app .stream_page (paused .session ,paused .initial ) 203self .assertEqual (batch ["events" ][0 ]["type" ],"reset" ) 204descriptors = self .assert_prefix (paused ,batch ["snapshot" ]) 205self .assertTrue (any (item ["reference" ]is not None for item in descriptors )) 206paused .finish (self ) 207 208def test_encoded_budget_spills_escaped_unicode_without_truncation (self ): 209paused = PausedOutput ( 210self ,self .app ,self .provider , ['"\\\nš' * 12000 ],"β" * 18000 211 ) 212snapshot = paused .snapshot () 213descriptors = self .assert_prefix (paused ,snapshot ) 214inline = [item ["text" ]for item in descriptors if item ["text" ]is not None ] 215self .assertLessEqual ( 216sum (len (json .dumps (text ,ensure_ascii = False ).encode ())for text in inline ), 21765536 , 218 ) 219self .assertTrue (any (item ["reference" ]is not None for item in descriptors )) 220paused .finish (self ) 221 222def test_json_string_budget_below_at_and_above_boundary (self ): 223for size in [65533 ,65534 ,65535 ]: 224with self .subTest (bytes = size ): 225paused = PausedOutput (self ,self .app ,self .provider , ["x" * size ]) 226descriptors = self .assert_prefix (paused ,paused .snapshot ()) 227self .assertEqual (descriptors [0 ]["reference" ]is not None ,size > 65534 ) 228paused .finish (self ) 229 230def test_snapshot_lease_remains_readable_after_commit (self ): 231paused = PausedOutput (self ,self .app ,self .provider , ["large αš" * 20000 ]) 232descriptors = paused .snapshot ()["active_output" ] 233reference = next ( 234item ["reference" ]for item in descriptors if item ["reference" ]is not None 235 ) 236path = urllib .parse .urlsplit (reference ["url" ]).path 237for query in ["" ,"?snapshot=" ]: 238with self .assertRaises (urllib .error .HTTPError )as caught : 239self .app .api (path + query ) 240self .assertEqual (caught .exception .code ,400 ) 241paused .finish (self ) 242self .assertEqual (reference_text (self .app ,reference ),paused .prefix ) 243 244def test_cancellation_clears_prefix_before_next_run (self ): 245paused = PausedOutput (self ,self .app ,self .provider , ["cancelled prefix αš" ]) 246old_ids = {item ["message_id" ]for item in paused .snapshot ()["active_output" ]} 247self .app .api ( 248f"/sessions/ { paused . session } /inputs/ { paused . input_id } /cancel" , {} 249 ).close () 250paused .release .set () 251self .app .idle (paused .session ) 252self .assertEqual (paused .snapshot ()["active_output" ], []) 253next_run = PausedOutput ( 254self ,self .app ,self .provider , ["new run prefix" ],session = paused .session 255 ) 256descriptors = self .assert_prefix (next_run ,next_run .snapshot ()) 257self .assertTrue (old_ids .isdisjoint (item ["message_id" ]for item in descriptors )) 258next_run .finish (self ) 259 260def gated_model_steps (self ,* ,first_completes ): 261gates = [threading .Event (),threading .Event ()] 262ready = [threading .Event (),threading .Event ()] 263for gate in gates : 264self .addCleanup (gate .set ) 265 266def model_events (index ): 267for event in [ 268delta (f"step { index } prefix αš" ), 269delta (f"step { index } thinking β" ,True ), 270 ]: 271event ["id" ]= "progress-response" 272yield event 273yield from tool_events ( 274"chat_completions" , 275 [(0 ,f"step- { index } " ,f"pass # step { index } " )], 276ready [index ], 277gates [index ], 278complete = index == 1 or first_completes , 279 ) 280 281self .provider .script = lambda _ : [ 282Reply ("text" ,events = model_events (0 )), 283Reply ("text" ,events = model_events (1 )), 284text ("done" ), 285 ] 286session = self .app .session () 287self .app .prompt (session ,"exercise model steps" ).close () 288self .assertTrue (ready [0 ].wait (20 )) 289old = current_progress (self .app ,session ,lambda values :len (values )== 1 )[ 290"snapshot" 291 ] 292old_ids = {item ["message_id" ]for item in old ["active_output" ]} 293self .assertEqual ( 294 {active_text (self .app ,item )for item in old ["active_output" ]}, 295 {"step 0 prefix αš" ,"step 0 thinking β" }, 296 ) 297gates [0 ].set () 298self .assertTrue (ready [1 ].wait (20 )) 299new = current_progress ( 300self .app , 301session , 302lambda values : ( 303len (values )== 1 304and values [0 ].get ("preview" , {}).get ("text" )== "pass # step 1" 305 ), 306 )["snapshot" ] 307self .assertEqual ( 308 {active_text (self .app ,item )for item in new ["active_output" ]}, 309 {"step 1 prefix αš" ,"step 1 thinking β" }, 310 ) 311self .assertTrue ( 312old_ids .isdisjoint (item ["message_id" ]for item in new ["active_output" ]) 313 ) 314gates [1 ].set () 315self .app .idle (session ) 316with self .app .api (f"/sessions/ { session } ?tail=20" )as response : 317self .assertEqual (json .load (response )["active_output" ], []) 318 319def test_retry_replaces_failed_attempt_output_before_reusing_indices (self ): 320self .gated_model_steps (first_completes = False ) 321 322def test_later_model_step_contains_only_its_own_output_segments (self ): 323self .gated_model_steps (first_completes = True ) 324 325 326@exclusive 327class ActiveOutputFailureTest (unittest .TestCase ): 328def test_daemon_restart_removes_spill_owned_by_crashed_incarnation (self ): 329provider = Provider (lambda _ :Reply ("text" ,value = "unused" )) 330self .addCleanup (provider .close ) 331with Albedo ( 332provider ,prepare = lambda app :app .daemon .env .update (ALBEDO_INSPECT = "1" ) 333 )as app : 334paused = PausedOutput (self ,app ,provider , ["crashed prefix " * 10000 ]) 335reference = next ( 336item ["reference" ] 337for item in paused .snapshot ()["active_output" ] 338if item ["reference" ]is not None 339 ) 340app .restart (crash = True ) 341paused .release .set () 342with self .assertRaises (urllib .error .HTTPError )as caught : 343reference_text (app ,reference ) 344# A daemon restart rotates the signing secret, invalidating old tokens. 345self .assertEqual (caught .exception .code ,400 ) 346content_id = urllib .parse .urlsplit (reference ["url" ]).path .rsplit ("/" ,1 )[1 ] 347probe = ActiveOutputProbe (app ) 348probe .call ("expire" ,app .home ,content_id ) 349probe .call ("sweep" ,app .home ) 350self .assertEqual (list ((app .home / "active-output" ).glob ("*.data" )), []) 351 352def test_expired_retired_lease_is_unreadable_and_swept (self ): 353provider = Provider (lambda _ :Reply ("text" ,value = "unused" )) 354self .addCleanup (provider .close ) 355with Albedo ( 356provider ,prepare = lambda app :app .daemon .env .update (ALBEDO_INSPECT = "1" ) 357 )as app : 358paused = PausedOutput (self ,app ,provider , ["expiry prefix " * 10000 ]) 359reference = next ( 360item ["reference" ] 361for item in paused .snapshot ()["active_output" ] 362if item ["reference" ]is not None 363 ) 364content_id = urllib .parse .urlsplit (reference ["url" ]).path .rsplit ("/" ,1 )[1 ] 365paused .finish (self ) 366probe = ActiveOutputProbe (app ) 367probe .call ("expire" ,app .home ,content_id ) 368with self .assertRaises (urllib .error .HTTPError )as caught : 369reference_text (app ,reference ) 370self .assertEqual (caught .exception .code ,410 ) 371probe .call ("sweep" ,app .home ) 372self .assertFalse ( 373 (app .home / "active-output" / f" { content_id } .data" ).exists () 374 ) 375self .assertFalse ( 376 (app .home / "active-output" / f" { content_id } .meta" ).exists () 377 ) 378 379def test_deleted_session_revokes_snapshot_and_removes_spill (self ): 380provider = Provider (lambda _ :Reply ("text" ,value = "unused" )) 381self .addCleanup (provider .close ) 382with Albedo (provider )as app : 383paused = PausedOutput (self ,app ,provider , ["delete prefix " * 20000 ]) 384reference = next ( 385item ["reference" ] 386for item in paused .snapshot ()["active_output" ] 387if item ["reference" ]is not None 388 ) 389app .api ( 390f"/sessions/ { paused . session } /inputs/ { paused . input_id } /cancel" , {} 391 ).close () 392paused .finish (self ) 393resource = f"/sessions/ { paused . session } ?view=configuration" 394with app .api (resource )as response : 395revision = response .getheader ("ETag" ) 396response .read () 397app .api (resource ,method = "DELETE" ,headers = {"If-Match" :revision }).close () 398paused .release .set () 399with self .assertRaises (urllib .error .HTTPError )as caught : 400reference_text (app ,reference ) 401self .assertEqual (caught .exception .code ,410 ) 402self .assertEqual (list ((app .home / "active-output" ).glob ("*.data" )), []) 403 404def test_spill_read_failure_is_visible_but_durable_commit_survives (self ): 405provider = Provider (lambda _ :Reply ("text" ,value = "unused" )) 406self .addCleanup (provider .close ) 407with Albedo (provider )as app : 408paused = PausedOutput (self ,app ,provider , ["read failure prefix " * 10000 ]) 409reference = next ( 410item ["reference" ] 411for item in paused .snapshot ()["active_output" ] 412if item ["reference" ]is not None 413 ) 414content_id = urllib .parse .urlsplit (reference ["url" ]).path .rsplit ("/" ,1 )[1 ] 415spill = app .home / "active-output" / f" { content_id } .data" 416spill .unlink () 417spill .mkdir () 418with self .assertRaises (urllib .error .HTTPError )as caught : 419reference_text (app ,reference ) 420self .assertEqual (caught .exception .code ,503 ) 421paused .finish (self ) 422self .assertEqual (paused .snapshot ()["active_output" ], []) 423self .assertTrue ( 424any (event ["type" ]== "committed" for event in paused .events ) 425 ) 426 427def test_spill_creation_failure_does_not_interrupt_generation (self ): 428provider = Provider (lambda _ :Reply ("text" ,value = "unused" )) 429self .addCleanup (provider .close ) 430 431def prepare (app ): 432 (app .home / "active-output" ).write_text ("injected creation failure" ) 433 434with Albedo (provider ,prepare = prepare )as app : 435paused = PausedOutput (self ,app ,provider , ["unavailable spill " * 12000 ]) 436with self .assertRaises (urllib .error .HTTPError )as caught : 437paused .snapshot () 438self .assertEqual (caught .exception .code ,503 ) 439paused .finish (self ) 440snapshot = paused .snapshot () 441self .assertEqual (snapshot ["active_output" ], []) 442assistant = [ 443entry 444for entry in snapshot ["history" ]["items" ] 445if entry ["kind" ]== "assistant" 446 ] 447self .assertTrue (assistant ,snapshot )