okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
31c9e74
main
1"""Identified inputs retain their admission and deliver once through failure and restart.""" 2 3from concurrent .futures import ThreadPoolExecutor 4import base64 5import json 6import os 7from pathlib import Path 8import shlex 9import shutil 10import sqlite3 11import threading 12import time 13import unittest 14import urllib .error 15 16from harness import Albedo ,Provider ,exclusive ,operation_id ,python ,text 17from image_limits_test import png 18 19 20class InputScenario (unittest .TestCase ): 21def setUp (self ): 22self .provider = Provider (lambda _ :text ("done" )) 23self .addCleanup (self .provider .close ) 24self .app = Albedo (self .provider ,protocol = "responses" ) 25self .app .__enter__ () 26self .addCleanup (self .app .__exit__ ,None ,None ,None ) 27 28def put_input (self ,session ,input_id ,intent ): 29with self .app .api ( 30f"/sessions/ { session } /inputs/ { input_id } " ,intent ,method = "PUT" 31 )as response : 32return json .load (response ) 33 34def receipt (self ,session ,input_id ): 35with self .app .api (f"/sessions/ { session } /inputs/ { input_id } " )as response : 36return json .load (response ) 37 38def committed (self ,session ,input_id ): 39deadline = time .monotonic ()+ 30 40while time .monotonic ()< deadline : 41receipt = self .receipt (session ,input_id ) 42if receipt ["delivery" ]== "committed" : 43return receipt 44time .sleep (0.03 ) 45self .fail (f"input remained pending: { receipt } " ) 46 47def duplicates (self ,session ,input_id ,intent ): 48with ThreadPoolExecutor (max_workers = 6 )as executor : 49results = list ( 50executor .map ( 51lambda _ :self .put_input (session ,input_id ,intent ),range (6 ) 52 ) 53 ) 54self .assertTrue ( 55all ( 56result ["id" ]== input_id and result ["admission" ]== "accepted" 57for result in results 58 ) 59 ) 60self .assertEqual (len ({result ["acceptance_order" ]for result in results }),1 ) 61return results [0 ] 62 63def conflict (self ,session ,input_id ,intent ): 64with self .assertRaises (urllib .error .HTTPError )as failure : 65self .put_input (session ,input_id ,intent ) 66self .assertEqual (failure .exception .code ,409 ) 67self .assertEqual (json .load (failure .exception )["code" ],"input_conflict" ) 68 69def users (self ,session ): 70return [ 71entry 72for entry in self .app .history (session )["items" ] 73if entry ["kind" ]== "user" 74 ] 75 76def skill_intent (self ,session ,arguments ): 77with self .app .api (f"/sessions/ { session } /catalog?kind=skills" )as response : 78discovery = json .load (response )["discovery" ] 79candidate = next ( 80row for row in discovery ["candidates" ]if row ["title" ]== "retry-skill" 81 ) 82return { 83"kind" :"skill" , 84"candidate_id" :candidate ["id" ], 85"catalog_revision" :discovery ["revision" ], 86"arguments" :arguments , 87 } 88 89def write_skill (self ): 90skill = self .app .workspace / ".agents/skills/retry-skill/SKILL.md" 91skill .parent .mkdir (parents = True ) 92skill .write_text ( 93"---\nname: retry-skill\ndescription: test activation\n---\nOriginal saved instructions.\n" 94 ) 95return skill 96 97def assert_user_order (self ,session ,inputs ): 98users = self .users (session ) 99self .assertEqual ( 100 [entry ["input_id" ]for entry in users ], [input_id for input_id ,_ in inputs ] 101 ) 102self .assertEqual ( 103 [ 104part ["text" ] 105for entry in users 106for part in entry ["content" ] 107if part ["kind" ]== "text" 108 ], 109 [intent ["text" ]for _ ,intent in inputs ], 110 ) 111 112 113class OperationsTests (InputScenario ): 114def test_targeted_cancel_preserves_shared_turn_and_cannot_interrupt_a_later_turn ( 115self , 116 ): 117first_entered ,second_entered ,later_entered = [ 118threading .Event ()for _ in range (3 ) 119 ] 120first_release ,second_release ,later_release = [ 121threading .Event ()for _ in range (3 ) 122 ] 123for gate in (first_release ,second_release ,later_release ): 124self .addCleanup (gate .set ) 125calls = 0 126 127def reply (_request ): 128nonlocal calls 129calls += 1 130entered ,release = [ 131 (first_entered ,first_release ), 132 (second_entered ,second_release ), 133 (later_entered ,later_release ), 134 ][calls - 1 ] 135entered .set () 136if not release .wait (20 ): 137raise AssertionError ( 138"targeted cancellation scenario did not release provider" 139 ) 140return python ("1" )if calls == 1 else text ("completed" ) 141 142self .provider .script = reply 143session = self .app .session () 144identities = [operation_id ()for _ in range (3 )] 145self .put_input ( 146session ,identities [0 ], {"kind" :"message" ,"text" :"start shared turn" } 147 ) 148self .assertTrue (first_entered .wait (10 )) 149for index ,identity in enumerate (identities [1 :]): 150self .put_input ( 151session , 152identity , 153 {"kind" :"message" ,"text" :f"join shared turn { index } " }, 154 ) 155first_release .set () 156self .assertTrue (second_entered .wait (15 )) 157receipts = [self .committed (session ,identity )for identity in identities ] 158turn_ids = {receipt ["turn" ]["id" ]for receipt in receipts } 159self .assertEqual (len (turn_ids ),1 ) 160with self .app .api ( 161f"/sessions/ { session } /inputs/ { identities [ 1 ] } /cancel" , {} 162 )as response : 163cancellation = json .load (response ) 164self .assertEqual (cancellation ["result" ],"shared_running" ) 165with self .app .api (f"/sessions/ { session } ?tail=0" )as response : 166self .assertFalse (json .load (response )["status" ]["interrupt_requested" ]) 167second_release .set () 168self .app .idle (session ) 169self .assertTrue ( 170all ( 171self .receipt (session ,identity )["turn" ]["state" ]== "completed" 172for identity in identities 173 ) 174 ) 175later = operation_id () 176self .put_input ( 177session ,later , {"kind" :"message" ,"text" :"a later independent turn" } 178 ) 179self .assertTrue (later_entered .wait (10 )) 180with self .app .api ( 181f"/sessions/ { session } /inputs/ { identities [ 0 ] } /cancel" , {} 182 )as response : 183self .assertEqual (json .load (response )["result" ],"not_pending" ) 184with self .app .api (f"/sessions/ { session } ?tail=0" )as response : 185snapshot = json .load (response ) 186self .assertFalse (snapshot ["status" ]["interrupt_requested" ]) 187self .assertNotIn (snapshot ["status" ]["run_id" ],turn_ids ) 188later_release .set () 189self .app .idle (session ) 190self .assertEqual (self .receipt (session ,later )["turn" ]["state" ],"completed" ) 191 192def test_concurrent_creation_has_one_resource_and_immutable_provenance (self ): 193session_id = operation_id () 194resource = f"/sessions/ { session_id } " 195intent = { 196"kind" :"new" , 197"workspace" :str (self .app .workspace ), 198"provider_profile" :self .app .profile , 199 } 200 201def create (_ ): 202try : 203with self .app .api ( 204resource ,intent ,method = "PUT" ,headers = {"If-None-Match" :"*" } 205 )as response : 206return response .status ,json .load (response ) 207except urllib .error .HTTPError as failure : 208self .assertEqual (failure .code ,412 ) 209return failure .code ,None 210 211with ThreadPoolExecutor (max_workers = 6 )as executor : 212results = list (executor .map (create ,range (6 ))) 213self .assertEqual ([status for status ,_ in results ].count (201 ),1 ) 214with self .app .api (resource )as response : 215created = json .load (response ) 216self .assertEqual ( 217created ["creation" ]["submitted" ]["workspace" ],intent ["workspace" ] 218 ) 219self .assertEqual ( 220created ["creation" ]["resolved" ], 221 { 222key :created [key ] 223for key in ("workspace" ,"provider_profile" ,"model" ,"effort" ) 224 }, 225 ) 226configuration = created ["configuration_resource" ] 227with self .app .api ( 228configuration ["url" ], 229 {"name" :"edited after creation" }, 230method = "PATCH" , 231headers = {"If-Match" :configuration ["etag" ]}, 232 )as response : 233response .read () 234with self .app .api (resource )as response : 235edited = json .load (response ) 236self .assertEqual (edited ["creation" ],created ["creation" ]) 237self .assertEqual (edited ["name" ],"edited after creation" ) 238with self .app .api ("/sessions?scope=all" )as response : 239matches = [ 240row for row in json .load (response )["items" ]if row ["id" ]== session_id 241 ] 242self .assertEqual (len (matches ),1 ) 243 244def test_submission_identity_and_durable_echo (self ): 245session = self .app .session () 246first = operation_id () 247intent = {"kind" :"message" ,"text" :"same message" ,"client_id" :"first" } 248self .duplicates (session ,first ,intent ) 249duplicate = self .put_input (session ,first , {** intent ,"client_id" :"retry" }) 250self .assertEqual (duplicate ["client_id" ],"first" ) 251self .committed (session ,first ) 252self .app .idle (session ) 253self .conflict (session ,first , {** intent ,"text" :"changed message" }) 254self .conflict (session ,first , {"kind" :"continue" }) 255second = operation_id () 256self .put_input (session ,second , {** intent ,"client_id" :"second" }) 257self .committed (session ,second ) 258self .app .idle (session ) 259self .assert_user_order (session , [(first ,intent ), (second ,intent )]) 260 261def test_image_duplicates_and_changed_image_conflict (self ): 262session = self .app .session () 263input_id = operation_id () 264intent = { 265"kind" :"message" , 266"text" :"inspect image" , 267"images" : [ 268 { 269"mime_type" :"image/png" , 270"data" :base64 .b64encode (png (2 ,2 )).decode (), 271 } 272 ], 273 } 274self .duplicates (session ,input_id ,intent ) 275self .committed (session ,input_id ) 276self .app .idle (session ) 277self .conflict ( 278session , 279input_id , 280 { 281** intent , 282"images" : [ 283 { 284** intent ["images" ][0 ], 285"data" :base64 .b64encode (png (3 ,2 )).decode (), 286 } 287 ], 288 }, 289 ) 290users = self .users (session ) 291self .assertEqual ([entry ["input_id" ]for entry in users ], [input_id ]) 292image = next ( 293part ["image" ]for part in users [0 ]["content" ]if part ["kind" ]== "image" 294 ) 295self .assertEqual (image ["width" ],2 ) 296 297def test_continue_is_one_admission_without_an_extra_user_row (self ): 298session = self .app .session () 299self .app .prompt (session ,"start" ).close () 300self .app .idle (session ) 301input_id = operation_id () 302self .duplicates (session ,input_id , {"kind" :"continue" }) 303self .committed (session ,input_id ) 304self .app .idle (session ) 305self .assertEqual (len (self .users (session )),1 ) 306self .assertEqual (len (self .provider .requests ),2 ) 307self .assertIsNotNone (self .receipt (session ,input_id )["transcript_position" ]) 308 309def test_skill_admission_uses_resolved_content_once (self ): 310skill = self .write_skill () 311session = self .app .session () 312input_id = operation_id () 313intent = self .skill_intent (session ,"do this" ) 314accepted = self .duplicates (session ,input_id ,intent ) 315self .committed (session ,input_id ) 316self .app .idle (session ) 317skill .unlink () 318duplicate = self .put_input (session ,input_id ,intent ) 319self .assertEqual (duplicate ["acceptance_order" ],accepted ["acceptance_order" ]) 320self .conflict (session ,input_id , {** intent ,"arguments" :"other" }) 321self .assertIn ( 322"Original saved instructions." ,json .dumps (self .provider .requests ) 323 ) 324self .assertEqual ( 325 [entry ["input_id" ]for entry in self .users (session )], [input_id ] 326 ) 327 328 329@exclusive 330class WaitingOperationsTests (InputScenario ): 331def setUp (self ): 332self .gate = threading .Event () 333self .started = threading .Event () 334self .first = True 335 336def reply (_ ): 337if self .first : 338self .first = False 339self .started .set () 340self .gate .wait (40 ) 341return text ("done" ) 342 343self .provider = Provider (reply ) 344self .addCleanup (self .provider .close ) 345self .addCleanup (self .gate .set ) 346self .app = Albedo (self .provider ,protocol = "responses" ) 347self .app .__enter__ () 348self .addCleanup (self .app .__exit__ ,None ,None ,None ) 349 350def test_full_queue_duplicate_bypasses_limit_and_interrupt_cancels (self ): 351session = self .app .session () 352self .app .prompt (session ,"hold active generation" ).close () 353self .assertTrue (self .started .wait (10 )) 354inputs = [ 355 (operation_id (), {"kind" :"message" ,"text" :f"waiting { index } " }) 356for index in range (32 ) 357 ] 358results = [ 359self .put_input (session ,input_id ,intent )for input_id ,intent in inputs 360 ] 361self .assertTrue (all (result ["delivery" ]== "pending" for result in results )) 362self .assertEqual ( 363self .put_input (session ,* inputs [0 ])["acceptance_order" ], 364results [0 ]["acceptance_order" ], 365 ) 366with self .assertRaises (urllib .error .HTTPError )as full : 367self .put_input ( 368session ,operation_id (), {"kind" :"message" ,"text" :"queue overflow" } 369 ) 370self .assertEqual (full .exception .code ,429 ) 371with self .app .api (f"/sessions/ { session } ?tail=0" )as response : 372snapshot = json .load (response ) 373self .app .api ( 374f"/sessions/ { session } /interrupt" , 375 { 376"run_id" :snapshot ["status" ]["run_id" ], 377"through_input_order" :snapshot ["input_order" ], 378 }, 379 ).close () 380self .gate .set () 381self .app .idle (session ) 382self .app .restart (crash = True ) 383for (input_id ,intent ),accepted in zip (inputs ,results ): 384self .assertEqual (self .receipt (session ,input_id )["delivery" ],"cancelled" ) 385self .assertEqual ( 386self .put_input (session ,input_id ,intent )["acceptance_order" ], 387accepted ["acceptance_order" ], 388 ) 389 390def test_crash_recovers_order_images_and_resolved_skill_content (self ): 391skill = self .write_skill () 392session = self .app .session () 393with self .app .prompt (session ,"hold active generation" )as response : 394active_input = json .load (response )["id" ] 395self .assertTrue (self .started .wait (10 )) 396inputs = [ 397 (operation_id (), {"kind" :"message" ,"text" :"first waiting" }), 398 ( 399operation_id (), 400 { 401"kind" :"message" , 402"text" :"second image" , 403"images" : [ 404 { 405"mime_type" :"image/png" , 406"data" :base64 .b64encode (png (2 ,2 )).decode (), 407 } 408 ], 409 }, 410 ), 411 (operation_id (),self .skill_intent (session ,"saved arguments" )), 412 ] 413accepted = [ 414self .put_input (session ,input_id ,intent )for input_id ,intent in inputs 415 ] 416self .assertTrue ( 417all ( 418self .receipt (session ,input_id )["delivery" ]== "pending" 419for input_id ,_ in inputs 420 ) 421 ) 422skill .unlink () 423self .app .restart (crash = True ) 424self .assertEqual ( 425self .receipt (session ,active_input )["turn" ]["state" ],"abandoned" 426 ) 427self .gate .set () 428for input_id ,_ in inputs : 429self .committed (session ,input_id ) 430self .app .idle (session ) 431self .app .restart (crash = True ) 432for (input_id ,intent ),result in zip (inputs ,accepted ): 433self .assertEqual ( 434self .put_input (session ,input_id ,intent )["acceptance_order" ], 435result ["acceptance_order" ], 436 ) 437users = [ 438entry 439for entry in self .users (session ) 440if entry ["input_id" ]in {input_id for input_id ,_ in inputs } 441 ] 442self .assertEqual ( 443 [entry ["input_id" ]for entry in users ], [input_id for input_id ,_ in inputs ] 444 ) 445image = next ( 446part ["image" ]for part in users [1 ]["content" ]if part ["kind" ]== "image" 447 ) 448self .assertEqual (image ["width" ],2 ) 449self .assertIn ( 450"Original saved instructions." ,json .dumps (self .provider .requests ) 451 ) 452 453 454@exclusive 455class BlockedOperationsTests (InputScenario ): 456def test_full_blocked_queue_survives_restart_and_replays_rejection (self ): 457session = self .app .session () 458inputs = [ 459 (operation_id (), {"kind" :"message" ,"text" :f"blocked { index } " }) 460for index in range (32 ) 461 ] 462with sqlite3 .connect (self .app .home / "albedo.sqlite" )as database : 463database .execute ( 464"CREATE TRIGGER hold_inputs BEFORE INSERT ON transcript WHEN NEW.row_class='user' BEGIN SELECT RAISE(ABORT,'fixture'); END" 465 ) 466accepted = [ 467self .put_input (session ,input_id ,intent )for input_id ,intent in inputs 468 ] 469deadline = time .monotonic ()+ 10 470while time .monotonic ()< deadline : 471if self .receipt (session ,inputs [0 ][0 ])["blocking_reason" ]: 472break 473time .sleep (0.03 ) 474else : 475self .fail ("waiting inputs were never blocked" ) 476with self .app .api (f"/sessions/ { session } ?tail=0" )as response : 477snapshot = json .load (response ) 478self .assertEqual ( 479snapshot ["status" ]["blocking_reason" ]["detail" ], 480self .receipt (session ,inputs [0 ][0 ])["blocking_reason" ]["detail" ], 481 ) 482rejected_id = operation_id () 483rejected = {"kind" :"message" ,"text" :"blocked overflow" } 484with self .assertRaises (urllib .error .HTTPError )as failure : 485self .put_input (session ,rejected_id ,rejected ) 486self .assertEqual (failure .exception .code ,429 ) 487rejection = json .load (failure .exception ) 488self .assertEqual (self .receipt (session ,rejected_id )["admission" ],"rejected" ) 489self .app .restart (crash = True ) 490self .assertEqual ( 491self .put_input (session ,* inputs [- 1 ])["acceptance_order" ], 492accepted [- 1 ]["acceptance_order" ], 493 ) 494with self .assertRaises (urllib .error .HTTPError )as restored_full : 495self .put_input (session ,operation_id (), {"kind" :"continue" }) 496self .assertEqual (restored_full .exception .code ,429 ) 497with sqlite3 .connect (self .app .home / "albedo.sqlite" )as database : 498database .execute ("DROP TRIGGER hold_inputs" ) 499for input_id ,_ in inputs : 500self .committed (session ,input_id ) 501self .app .idle (session ) 502self .assert_user_order (session ,inputs ) 503with self .assertRaises (urllib .error .HTTPError )as replay : 504self .put_input (session ,rejected_id ,rejected ) 505self .assertEqual (replay .exception .code ,429 ) 506self .assertEqual (json .load (replay .exception ),rejection ) 507 508 509# exclusive: changes daemon PATH to gate kernel startup; also tests restart 510@exclusive 511class PreparingOperationsTests (InputScenario ): 512opened :Path 513release :Path 514 515def setUp (self ): 516provider = Provider (lambda request :text ("done" )) 517self .addCleanup (provider .close ) 518 519def prepare (app ): 520self .opened = app .root / "kernel-opening" 521self .release = app .root / "kernel-release" 522commands = app .root / "kernel-commands" 523commands .mkdir () 524executable = shutil .which ("python3" ) 525self .assertIsNotNone (executable ) 526wrapper = commands / "python3" 527wrapper .write_text ( 528"#!/bin/sh\n" 529# a detached kernel opens through its bridge's start or launch 530+ 'case "$*" in *albedo_kernel.py*|*"albedo_bridge.py start"*|*"albedo_bridge.py launch"*)\n' 531+ "touch " 532+ shlex .quote (str (self .opened )) 533+ "\n" 534+ "while [ ! -e " 535+ shlex .quote (str (self .release )) 536+ " ]; do sleep 0.05; done\n" 537+ ";; esac\nexec " 538+ shlex .quote (str (executable )) 539+ ' "$@"\n' 540 ) 541wrapper .chmod (0o755 ) 542app .env ["PATH" ]= str (commands )+ os .pathsep + app .env ["PATH" ] 543 544self .app = Albedo (provider ,protocol = "responses" ,prepare = prepare ) 545self .app .__enter__ () 546self .addCleanup (self .app .__exit__ ,None ,None ,None ) 547 548def test_kernel_preparation_admission_survives_restart (self ): 549session = self .app .session () 550input_id = operation_id () 551intent = {"kind" :"message" ,"text" :"stored while kernel opens" } 552accepted = self .put_input (session ,input_id ,intent ) 553deadline = time .monotonic ()+ 10 554while not self .opened .exists ()and time .monotonic ()< deadline : 555time .sleep (0.03 ) 556self .assertTrue (self .opened .exists ()) 557self .assertEqual (self .receipt (session ,input_id )["delivery" ],"pending" ) 558self .app .restart (crash = True ,prepare = lambda _ :self .release .touch ()) 559self .assertEqual ( 560self .put_input (session ,input_id ,intent )["acceptance_order" ], 561accepted ["acceptance_order" ], 562 ) 563self .committed (session ,input_id ) 564self .app .idle (session ) 565self .assertEqual ( 566 [entry ["input_id" ]for entry in self .users (session )], [input_id ] 567 ) 568 569def test_full_kernel_opening_queue_delivers_once_and_replays_rejection (self ): 570session = self .app .session () 571inputs = [ 572 (operation_id (), {"kind" :"message" ,"text" :f"opening { index } " }) 573for index in range (32 ) 574 ] 575results = [ 576self .put_input (session ,input_id ,intent )for input_id ,intent in inputs 577 ] 578deadline = time .monotonic ()+ 10 579while not self .opened .exists ()and time .monotonic ()< deadline : 580time .sleep (0.03 ) 581self .assertTrue (self .opened .exists ()) 582self .assertEqual ( 583self .put_input (session ,* inputs [0 ])["acceptance_order" ], 584results [0 ]["acceptance_order" ], 585 ) 586rejected_id = operation_id () 587intent = {"kind" :"continue" } 588with self .assertRaises (urllib .error .HTTPError )as failure : 589self .put_input (session ,rejected_id ,intent ) 590self .assertEqual (failure .exception .code ,429 ) 591rejection = json .load (failure .exception ) 592self .assertEqual (self .receipt (session ,rejected_id )["admission" ],"rejected" ) 593self .release .touch () 594for input_id ,_ in inputs : 595self .committed (session ,input_id ) 596self .app .idle (session ) 597self .assert_user_order (session ,inputs ) 598with self .assertRaises (urllib .error .HTTPError )as replay : 599self .put_input (session ,rejected_id ,intent ) 600self .assertEqual (replay .exception .code ,429 ) 601self .assertEqual (json .load (replay .exception ),rejection )