okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
6b74a53
main
1"""A replay cursor cannot skip durable events from a replacement session actor.""" 2 3import json 4import sqlite3 5import unittest 6import urllib .error 7 8from inspect_support import Inspection 9from harness import Albedo ,Provider ,ROOT ,Reply ,exclusive ,text 10 11 12def restart_actor (app ,session ): 13inspection = Inspection (app ,ROOT / "test/e2e/albedo_stream_replay_probe.erl" ) 14result = inspection .call ("restart" ,f"[<< { json . dumps ( session ) } >>]" ) 15if result != "actor_stopped" : 16raise AssertionError (result ) 17 18 19def interleaved (pairs ): 20for _ in range (pairs ): 21for delta in [{"reasoning_content" :"thought " }, {"content" :"answer " }]: 22yield { 23"id" :"fixture" , 24"choices" : [{"index" :0 ,"delta" :delta ,"finish_reason" :None }], 25 } 26yield { 27"id" :"fixture" , 28"choices" : [{"index" :0 ,"delta" : {},"finish_reason" :"stop" }], 29 } 30 31 32class StreamReplayTest (unittest .TestCase ): 33def setUp (self ): 34self .provider = Provider (lambda _ :text ("answer" )) 35self .addCleanup (self .provider .close ) 36 37def turn (self ,app ,session ,prompt ): 38app .prompt (session ,prompt ).close () 39app .idle (session ) 40 41def assert_reset_history (self ,page ,prompts ): 42self .assertEqual (page ["events" ][0 ]["type" ],"reset" ) 43self .assertEqual ( 44 [ 45part ["text" ] 46for entry in page ["snapshot" ]["history" ]["items" ] 47if entry ["kind" ]== "user" 48for part in entry ["content" ] 49if part ["kind" ]== "text" 50 ], 51prompts , 52 ) 53 54# exclusive: enables inspection and loads a deliberately malformed probe 55@exclusive 56def test_inspection_reports_rpc_and_invalid_result_failures (self ): 57with Albedo ( 58self .provider , 59prepare = lambda app :app .daemon .env .update (ALBEDO_INSPECT = "1" ), 60 )as app : 61inspection = Inspection (app ) 62with self .assertRaisesRegex (AssertionError ,"rpc_failed" ): 63inspection .evaluate ("Call(albedo_missing_probe, missing, [])" ) 64source = app .root / "albedo_invalid_probe.erl" 65source .write_text ( 66"-module(albedo_invalid_probe).\n-export([answer/0]).\n" 67'answer() -> <<"not JSON">>.\n' 68 ) 69with self .assertRaisesRegex (AssertionError ,"returned invalid JSON" ): 70Inspection (app ,source ).call_json ("answer" ) 71 72# exclusive: enables daemon inspection at startup 73@exclusive 74def test_old_actor_cursor_inside_replacement_window_resets_durable_history (self ): 75def prepare (app ): 76app .daemon .env ["ALBEDO_INSPECT" ]= "1" 77 78with Albedo (self .provider ,prepare = prepare )as app : 79session = app .session () 80self .turn (app ,session ,"before actor restart" ) 81old = app .stream_page (session ) 82restart_actor (app ,session ) 83self .turn (app ,session ,"after actor restart" ) 84current = app .stream_page (session ) 85replay = app .stream_page (session ,old ) 86self .assert_reset_history ( 87replay , ["before actor restart" ,"after actor restart" ] 88 ) 89self .assertNotEqual (replay ["generation" ],old ["generation" ]) 90self .assertEqual (replay ["generation" ],current ["generation" ]) 91 92# exclusive: restarts the daemon 93@exclusive 94def test_old_daemon_cursor_resets_durable_history (self ): 95with Albedo (self .provider )as app : 96session = app .session () 97self .turn (app ,session ,"before daemon restart" ) 98old = app .stream_page (session ) 99app .restart (crash = True ) 100self .turn (app ,session ,"after daemon restart" ) 101current = app .stream_page (session ) 102replay = app .stream_page (session ,old ) 103self .assert_reset_history ( 104replay , ["before daemon restart" ,"after daemon restart" ] 105 ) 106self .assertNotEqual (replay ["generation" ],old ["generation" ]) 107self .assertEqual (replay ["generation" ],current ["generation" ]) 108 109def test_same_generation_replays_only_subsequent_events_in_order (self ): 110with Albedo (self .provider )as app : 111session = app .session () 112self .turn (app ,session ,"already consumed" ) 113old = app .stream_page (session ) 114self .turn (app ,session ,"next prompt" ) 115self .turn (app ,session ,"last prompt" ) 116replay = app .stream_page (session ,old ) 117self .assertEqual (replay ["generation" ],old ["generation" ]) 118self .assertGreater (replay ["cursor" ],old ["cursor" ]) 119self .assertNotIn ("reset" , [event ["type" ]for event in replay ["events" ]]) 120self .assertEqual ( 121 [ 122part ["text" ] 123for event in replay ["events" ] 124if event ["type" ]== "message" 125and event ["data" ]["entry" ]["kind" ]== "user" 126for part in event ["data" ]["entry" ]["content" ] 127if part ["kind" ]== "text" 128 ], 129 ["next prompt" ,"last prompt" ], 130 ) 131keepalive = app .stream_page (session ,replay ) 132self .assertEqual (keepalive ["events" ], []) 133self .assertEqual (keepalive ["generation" ],replay ["generation" ]) 134self .assertEqual (keepalive ["cursor" ],replay ["cursor" ]) 135 136# exclusive: renames the global transcript table 137@exclusive 138def test_failed_transcript_read_emits_failure_without_replacement_cursor (self ): 139with Albedo (self .provider )as app : 140session = app .session () 141self .turn (app ,session ,"committed history" ) 142old = app .stream_page (session ) 143other_generation = app .stream_page (app .session ())["generation" ] 144with sqlite3 .connect (app .home / "albedo.sqlite" )as database : 145database .execute ( 146"ALTER TABLE transcript RENAME TO unavailable_transcript" 147 ) 148with self .assertRaises (urllib .error .HTTPError )as caught : 149app .api ( 150f"/sessions/ { session } ?after_generation= { other_generation } &after_seq= { old [ 'cursor' ] } &tail=2" , 151headers = {"Accept" :"text/event-stream" }, 152 ) 153self .assertEqual (caught .exception .code ,503 ) 154problem = json .load (caught .exception ) 155self .assertEqual (problem ["code" ],"request_unavailable" ) 156self .assertNotIn ("snapshot" ,problem ) 157self .assertNotIn ("generation" ,problem ) 158self .assertNotIn ("cursor" ,problem ) 159 160def test_evicted_cursor_resets_in_same_generation_and_initial_tail_pages_history ( 161self , 162 ): 163with Albedo (self .provider )as app : 164session = app .session () 165self .turn (app ,session ,"oldest prompt" ) 166old = app .stream_page (session ) 167# Alternating thinking and text never merge into one event, so a 168# long interleaved answer evicts the count-limited replay window. 169self .provider .script = lambda _ :Reply ("text" ,events = interleaved (150 )) 170self .turn (app ,session ,"newest prompt" ) 171replay = app .stream_page (session ,old ) 172self .assertEqual (replay ["generation" ],old ["generation" ]) 173self .assert_reset_history (replay , ["oldest prompt" ,"newest prompt" ]) 174initial = app .stream_page (session ,tail = 3 ) 175self .assert_reset_history (initial , ["newest prompt" ]) 176history = initial ["snapshot" ]["history" ] 177self .assertIsNotNone (history ["older" ]) 178with app .api ( 179f"/sessions/ { session } /history?limit=3&next= { history [ 'older' ] } " 180 )as response : 181older = json .load (response ) 182self .assertEqual ( 183 [ 184part ["text" ] 185for entry in older ["items" ] 186if entry ["kind" ]== "user" 187for part in entry ["content" ] 188if part ["kind" ]== "text" 189 ], 190 ["oldest prompt" ], 191 )