okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
c0d0428
main
1"""Extension catalog, commands, live reload, and compaction through a real daemon.""" 2 3import json 4import sqlite3 5from pathlib import Path 6import threading 7import time 8import unittest 9import urllib .error 10import urllib .parse 11 12from harness import Albedo ,Provider ,exclusive ,operation_id ,python ,text 13 14 15SKILL = "---\nname: demo\ndescription: catalog-only fixture description\n---\nBODY_MUST_NOT_AUTOLOAD\n" 16 17 18def scripted (request ): 19messages = request ["input" ] 20prompt = next ( 21 ( 22str (item .get ("content" ,"" )) 23for item in reversed (messages ) 24if item .get ("role" )== "user" 25 ), 26"" , 27 ) 28if "<newly-evicted-history>" in prompt : 29return text ("older conversation summary" ) 30if messages [- 1 ].get ("type" )== "function_call_output" : 31return text ("done" ) 32if "model probe via python" in prompt : 33return python ( 34"selection = await commands.model()\n" 35"assert selection['model'] == 'fixture', selection\n" 36"try:\n" 37" await commands.model('unreachable-model')\n" 38" assert False, 'switch must refuse from the model'\n" 39"except CommandsError as error:\n" 40" assert 'user action' in str(error), error\n" 41"print('MODEL_COMMAND_OK')" 42 ) 43if "activate demo via python" in prompt : 44return python ( 45"activation = await commands.demo('python argument')\n" 46"assert 'BODY_MUST_NOT_AUTOLOAD' in activation['instructions']\n" 47"assert activation['arguments'] == 'python argument'\n" 48"print('SKILL_PYTHON_ACTIVATION_OK')" 49 ) 50if "hot probe before reload" in prompt : 51return python ( 52"names = [c['name'] for c in await commands.catalog()]\n" 53"assert '/late' not in names, names\n" 54"hot_marker = 'kernel-kept-running'\nprint('BEFORE_RELOAD_OK')" 55 ) 56if "hot probe after reload" in prompt : 57return python ( 58"names = [c['name'] for c in await commands.catalog()]\n" 59"assert '/late' in names, names\n" 60"assert hot_marker == 'kernel-kept-running', 'kernel was restarted'\n" 61"print('AFTER_RELOAD_OK')" 62 ) 63if "hold a cell across a crash" in prompt : 64return python ( 65"import asyncio\nfrom pathlib import Path\n" 66"Path('held-cell-started').write_text('')\n" 67"await asyncio.sleep(30)" 68 ) 69if "lcm tool probe" in prompt : 70return python ("first turn" ,tool_name = "lcm_grep" ) 71if "fold list probe" in prompt : 72return python ("" ,tool_name = "lcm_list" ) 73return text ("older conversation summary" ) 74 75 76def assert_result_follows_call (test ,request ): 77"""The call's result comes right after it, and the capability note saved 78while the call ran comes after that.""" 79items = request ["input" ] 80 (call ,)= [i for i ,item in enumerate (items )if item .get ("type" )== "function_call" ] 81following = items [call + 1 ] 82test .assertEqual ( 83 (following .get ("type" ),following .get ("call_id" )), 84 ("function_call_output" ,items [call ]["call_id" ]), 85 ) 86 (note ,)= [ 87i 88for i ,item in enumerate (items ) 89if "capabilities changed" in str (item .get ("content" ,"" )) 90 ] 91test .assertGreater (note ,call + 1 ) 92 93 94def capability_notes (request ): 95"""The capability notes a request carries as inputs of their own; the 96rolling recap may quote the newest one too.""" 97return [ 98item ["content" ] 99for item in request ["input" ] 100if item .get ("role" )== "user" 101and str (item .get ("content" ,"" )).startswith ( 102'<system-note origin="capabilities changed">' 103 ) 104 ] 105 106 107class ExtensionTests (unittest .TestCase ): 108def setUp (self ): 109self .provider = Provider (scripted ) 110self .addCleanup (self .provider .close ) 111self .app = Albedo ( 112self .provider , 113protocol = "responses" , 114providers = { 115f"fixture- { self . provider . route } " : { 116"extension" :"openai" , 117"baseUrl" :self .provider .url , 118"apiKey" :"fixture-key" , 119"model" :"fixture" , 120"protocol" :"responses" , 121 } 122 }, 123 ) 124self .app .__enter__ () 125self .addCleanup (self .app .__exit__ ,None ,None ,None ) 126self .skill = self .app .workspace / ".albedo/skills/demo/SKILL.md" 127self .skill .parent .mkdir (parents = True ) 128self .skill .write_text (SKILL ) 129self .sid = self .app .session () 130 131def get (self ,path ): 132with self .app .api (path )as response : 133return json .load (response ) 134 135def post (self ,path ,body ): 136with self .app .api (path ,body )as response : 137return json .load (response ) 138 139def catalog (self ): 140return self .get (f"/sessions/ { self . sid } /catalog" ) 141 142def commands (self ): 143return self .catalog ()["loaded" ]["commands" ] 144 145def reload (self ): 146return self .post (f"/sessions/ { self . sid } /reload" , {"target" :"session" }) 147 148def compact (self ,strategy = None ): 149return self .post ( 150f"/sessions/ { self . sid } /compaction" , 151 {"strategy" :strategy }if strategy else {}, 152 ) 153 154def change (self ,fields ,session = None ): 155route = f"/sessions/ { session or self . sid } ?view=configuration" 156with self .app .api (route )as response : 157json .load (response ) 158etag = response .headers ["ETag" ] 159with self .app .api ( 160route ,fields ,method = "PATCH" ,headers = {"If-Match" :etag } 161 )as response : 162return json .load (response ) 163 164def snapshot (self ): 165return self .get (f"/sessions/ { self . sid } ?tail=0" ) 166 167def extensions (self ): 168return self .snapshot ()["selection" ]["effective" ]["extensions" ] 169 170def section_text (self ,section ): 171snapshot = self .get (f"/sessions/ { self . sid } /context" ) 172descriptor = next (row for row in snapshot ["sections" ]if row ["id" ]== section ) 173chunks = [] 174for page in range (descriptor ["page_count" ]): 175query = urllib .parse .urlencode ( 176 { 177"view" :"section" , 178"snapshot_id" :snapshot ["snapshot_id" ], 179"section_id" :section , 180"page" :page , 181 } 182 ) 183chunks .append (self .get (f"/sessions/ { self . sid } /context? { query } " )["text" ]) 184return "" .join (chunks ) 185 186def activate (self ,name ,* ,arguments = "" ,client_id = None ): 187catalog = self .catalog ()["discovery" ] 188candidate = next ( 189row 190for row in catalog ["candidates" ] 191if row ["kind" ]== "skill" 192and row ["preference_key" ]== name .removeprefix ("/" ) 193 ) 194body = { 195"kind" :"skill" , 196"candidate_id" :candidate ["id" ], 197"catalog_revision" :catalog ["revision" ], 198"arguments" :arguments , 199 } 200if client_id is not None : 201body ["client_id" ]= client_id 202with self .app .api ( 203f"/sessions/ { self . sid } /inputs/ { operation_id () } " ,body ,method = "PUT" 204 )as response : 205return json .load (response ) 206 207def turn (self ,prompt ,session = None ,* ,expected = 1 ): 208sid = session or self .sid 209before = len (self .provider .requests ) 210self .app .prompt (sid ,prompt ).close () 211self .app .idle (sid ) 212self .assertEqual (len (self .provider .requests )- before ,expected ) 213return [entry ["request" ]for entry in self .provider .requests [before :]] 214 215def restart (self ): 216self .app .restart () 217 218def output (self ,request ): 219return next ( 220item ["output" ] 221for item in reversed (request ["input" ]) 222if item .get ("type" )== "function_call_output" 223 ) 224 225def test_workspace_system_files_replace_base_and_append_before_project_instructions ( 226self , 227 ): 228 (self .app .workspace / "SYSTEM.md" ).write_text ("CUSTOM_BASE_ONLY" ) 229 (self .app .workspace / "APPEND_SYSTEM.md" ).write_text ("CUSTOM_APPEND_ONLY" ) 230 (self .app .workspace / "AGENTS.md" ).write_text ("PROJECT_CONVENTION_ONLY" ) 231 (request ,)= self .turn ("inspect system files" ) 232prompt = request ["instructions" ] 233self .assertTrue (prompt .startswith ("CUSTOM_BASE_ONLY\n" )) 234self .assertNotIn ("You are a coding agent operating inside albedo" ,prompt ) 235self .assertLess ( 236prompt .index ("Local workspace context supplied by an enabled extension" ), 237prompt .index ("CUSTOM_APPEND_ONLY" ), 238 ) 239self .assertLess ( 240prompt .index ('name="commands"' ),prompt .index ("CUSTOM_APPEND_ONLY" ) 241 ) 242self .assertLess ( 243prompt .index ("CUSTOM_APPEND_ONLY" ),prompt .index ("PROJECT_CONVENTION_ONLY" ) 244 ) 245self .assertNotIn ("CUSTOM_APPEND_ONLY" ,json .dumps (request ["input" ])) 246 (self .app .workspace / "SYSTEM.md" ).write_text ("NEW_BASE_ONLY" ) 247 (self .app .workspace / "APPEND_SYSTEM.md" ).write_text ("NEW_APPEND_ONLY" ) 248self .reload () 249 (pinned ,)= self .turn ("after system file reload" ) 250self .assertEqual (pinned ["instructions" ],prompt ) 251self .assertIn ("NEW_BASE_ONLY" ,json .dumps (pinned ["input" ])) 252self .assertIn ("NEW_APPEND_ONLY" ,json .dumps (pinned ["input" ])) 253self .compact () 254 (current ,)= self .turn ("after prompt compaction" ) 255self .assertTrue (current ["instructions" ].startswith ("NEW_BASE_ONLY\n" )) 256self .assertIn ("NEW_APPEND_ONLY" ,current ["instructions" ]) 257self .assertNotIn ("CUSTOM_APPEND_ONLY" ,current ["instructions" ]) 258 259# exclusive: writes global home instruction files 260@exclusive 261def test_system_files_share_instruction_discovery_paths (self ): 262home = self .app .root / "user-home" 263locations = [ 264self .app .workspace , 265self .app .workspace / ".agents" , 266self .app .workspace / ".albedo" , 267home / ".agents" , 268home / ".albedo" , 269 ] 270system = [] 271for i ,directory in enumerate (locations ): 272directory .mkdir (exist_ok = True ) 273base = directory / "SYSTEM.md" 274append = directory / "APPEND_SYSTEM.md" 275if i : 276base .write_text (f"BASE_ { i } " ) 277system .append (base ) 278self .addCleanup (base .unlink ,missing_ok = True ) 279append .write_text (f"APPEND_ { i } " ) 280self .addCleanup (append .unlink ,missing_ok = True ) 281 (self .app .workspace / "AGENTS.md" ).write_text ("AGENT_CONVENTIONS" ) 282 (request ,)= self .turn ("discover all prompt paths" ) 283prompt = request ["instructions" ] 284self .assertTrue (prompt .startswith ("BASE_1\n" )) 285self .assertEqual ([prompt .count (f"APPEND_ { i } " )for i in range (5 )], [1 ]* 5 ) 286self .assertEqual ([prompt .count (f"BASE_ { i } " )for i in range (1 ,5 )], [1 ,0 ,0 ,0 ]) 287positions = [prompt .index (f"APPEND_ { i } " )for i in range (5 )] 288self .assertEqual (positions ,sorted (positions )) 289self .assertLess (positions [- 1 ],prompt .index ("AGENT_CONVENTIONS" )) 290 291root = locations [0 ]/ "SYSTEM.md" 292root .write_text ("BASE_0" ) 293self .addCleanup (root .unlink ,missing_ok = True ) 294for expected ,removed in [ 295 (0 ,root ), 296 (1 ,system [0 ]), 297 (2 ,system [1 ]), 298 (3 ,system [2 ]), 299 (4 ,system [3 ]), 300 ]: 301session = self .app .session () 302 (next_request ,)= self .turn (f"system priority { expected } " ,session ) 303self .assertTrue ( 304next_request ["instructions" ].startswith (f"BASE_ { expected } \n" ) 305 ) 306removed .unlink () 307 308def test_oversized_agent_instructions_warn_and_allow_turns (self ): 309before = self .app .stream_page (self .sid ) 310 (self .app .workspace / "AGENTS.md" ).write_text ("X" * (1024 * 1024 + 1 )) 311 (self .app .workspace / "CLAUDE.md" ).write_text ("USABLE_CONVENTION" ) 312 (request ,)= self .turn ("continue without oversized instructions" ) 313self .assertIn ("USABLE_CONVENTION" ,request ["instructions" ]) 314self .assertNotIn ("X" * 128 ,request ["instructions" ]) 315events = self .app .stream_page (self .sid ,before )["events" ] 316warnings = [ 317event ["data" ]["text" ] 318for event in events 319if event ["type" ]== "note" and "AGENTS.md" in event ["data" ]["text" ] 320 ] 321self .assertEqual ( 322warnings , ["Warning: AGENTS.md exceeds 1 MiB and was not loaded" ] 323 ) 324self .turn ("next turn still works" ) 325 326# exclusive: writes global home instruction files 327@exclusive 328def test_instruction_locations_concatenate_in_scope_and_directory_order (self ): 329home = self .app .root / "user-home" 330files = [ 331 (self .app .workspace / "AGENTS.md" ,"ROOT_AGENTS" ), 332 (self .app .workspace / "cLaUdE.Md" ,"ROOT_CLAUDE" ), 333 (self .app .workspace / ".agents/a.MD" ,"PROJECT_AGENTS_A" ), 334 (self .app .workspace / ".agents/z.md" ,"PROJECT_AGENTS_Z" ), 335 (self .app .workspace / ".albedo/a.md" ,"PROJECT_ALBEDO" ), 336 (home / ".agents/a.md" ,"GLOBAL_AGENTS" ), 337 (home / ".albedo/a.md" ,"GLOBAL_ALBEDO" ), 338 ] 339for path ,marker in reversed (files ): 340path .parent .mkdir (exist_ok = True ) 341path .write_text (marker ) 342 (self .app .workspace / "SYSTEM.md" ).write_text ("ROOT_SYSTEM" ) 343 (self .app .workspace / ".agents/SyStEm.Md" ).write_text ("UNSELECTED_SYSTEM" ) 344 (self .app .workspace / ".agents/ApPeNd_SyStEm.Md" ).write_text ("APPENDED_SYSTEM" ) 345 (self .app .workspace / ".agents/ignored.txt" ).write_text ("NON_MARKDOWN" ) 346 347 (request ,)= self .turn ("inspect instruction ordering" ) 348prompt = request ["instructions" ] 349positions = [prompt .index (marker )for _ ,marker in files ] 350self .assertEqual (positions ,sorted (positions )) 351self .assertEqual ( 352 [prompt .count (marker )for _ ,marker in files ], [1 ]* len (files ) 353 ) 354self .assertLess (prompt .index ("APPENDED_SYSTEM" ),positions [0 ]) 355self .assertEqual (prompt .count ("APPENDED_SYSTEM" ),1 ) 356self .assertNotIn ("UNSELECTED_SYSTEM" ,prompt ) 357self .assertNotIn ("NON_MARKDOWN" ,prompt ) 358self .assertLess (positions [4 ],prompt .index ("## Global user preferences" )) 359self .assertLess (prompt .index ("## Global user preferences" ),positions [5 ]) 360self .assertIn (".agents/a.MD" ,prompt ) 361self .assertIn ("~/.agents/a.md" ,prompt ) 362 363def test_system_replacement_reads_only_highest_priority_match (self ): 364root = self .app .workspace / "SYSTEM.md" 365root .write_text ("VALID_REPLACEMENT" ) 366directory = self .app .workspace / ".agents" 367directory .mkdir () 368 (directory / "system.md" ).write_bytes (b"\xff" ) 369 370 (request ,)= self .turn ("ignore unreadable lower priority prompt" ) 371self .assertTrue (request ["instructions" ].startswith ("VALID_REPLACEMENT\n" )) 372root .unlink () 373outcome = self .reload () 374self .assertEqual (outcome ["session" ]["state" ],"failed" ) 375error = json .dumps (outcome ["session" ]["failure" ]) 376self .assertIn (".agents/system.md must be UTF-8 text" ,error ) 377 378# exclusive: writes daemon-wide capabilities.json 379@exclusive 380def test_instruction_discovery_limit_applies_before_capability_filtering (self ): 381directory = self .app .workspace / ".agents" 382directory .mkdir () 383choices = {} 384for i in range (128 ): 385name = f"instruction- { i :03 } .md" 386 (directory / name ).write_text ("DISABLED_INSTRUCTION" ) 387choices [f"project:.agents/ { name } " ]= False 388 (self .app .home / "capabilities.json" ).write_text ( 389json .dumps ({"global" : {"instructions" :choices }}) 390 ) 391 (request ,)= self .turn ("128 disabled instructions remain usable" ) 392self .assertNotIn ("DISABLED_INSTRUCTION" ,request ["instructions" ]) 393 394 (directory / "overflow.md" ).write_text ("OVERFLOW_INSTRUCTION" ) 395choices ["project:.agents/overflow.md" ]= False 396 (self .app .home / "capabilities.json" ).write_text ( 397json .dumps ({"global" : {"instructions" :choices }}) 398 ) 399outcome = self .reload () 400self .assertEqual (outcome ["session" ]["state" ],"failed" ) 401error = json .dumps (outcome ["session" ]["failure" ]) 402self .assertIn ("more than 128 instruction files were discovered" ,error ) 403 404def test_workspace_system_files_are_optional (self ): 405 (request ,)= self .turn ("no custom system files" ) 406self .assertTrue ( 407request ["instructions" ].startswith ( 408"You are a coding agent operating inside albedo" 409 ) 410 ) 411 412def test_catalog_is_lazy_and_activation_submits_one_turn (self ): 413initial = self .catalog () 414self .assertFalse (self .provider .requests ) 415self .assertIsNone (self .get (f"/sessions/ { self . sid } /context" )["snapshot_id" ]) 416 (request ,)= self .turn ("first turn" ) 417context = request ["instructions" ] 418self .assertIn ("catalog-only fixture description" ,context ) 419self .assertIn (str (self .skill .resolve ()),context ) 420self .assertNotIn ("BODY_MUST_NOT_AUTOLOAD" ,json .dumps (request )) 421skills = next ( 422row 423for row in initial ["discovery" ]["candidates" ] 424if row ["kind" ]== "extension" and row ["preference_key" ]== "skills" 425 ) 426self .assertEqual (skills ["dependencies" ], ["python" ,"commands" ]) 427self .assertEqual (skills ["extension" ]["tools" ], []) 428catalog = self .commands () 429demo = next (row for row in catalog if row ["slash_name" ]== "/demo" ) 430self .assertEqual (demo ["delivery" ],"input" ) 431self .assertTrue ({"human" ,"model" }<= set (demo ["caller_permissions" ])) 432before = len (self .provider .requests ) 433receipt = self .activate ( 434"/demo" ,arguments = "one two" ,client_id = "fixture-client" 435 ) 436self .assertEqual ( 437 (receipt ["admission" ],receipt ["client_id" ]), ("accepted" ,"fixture-client" ) 438 ) 439self .app .idle (self .sid ) 440self .assertEqual (len (self .provider .requests ),before + 1 ) 441activation = json .dumps (self .provider .requests [- 1 ]["request" ]["input" ]) 442for marker in ("BODY_MUST_NOT_AUTOLOAD" ,"one two" ,str (self .skill .resolve ())): 443self .assertIn (marker ,activation ) 444self .assertEqual (self .commands (),catalog ) 445self .skill .write_text ( 446SKILL .replace ("catalog-only fixture description" ,"changed on disk" ) 447 ) 448self .assertEqual (self .commands (),catalog ) 449self .skill .write_text (SKILL ) 450before = len (self .provider .requests ) 451self .activate ("/demo" ,arguments = "slash argument" ) 452self .app .idle (self .sid ) 453self .assertEqual (len (self .provider .requests ),before + 1 ) 454user = next ( 455item ["content" ] 456for item in reversed (self .provider .requests [- 1 ]["request" ]["input" ]) 457if item .get ("role" )== "user" 458 ) 459activation = json .loads (user .split ("\n" ,1 )[1 ]) 460self .assertEqual ( 461 (activation ["name" ],activation ["arguments" ],activation ["source" ]), 462 ("demo" ,"slash argument" ,str (self .skill .resolve ())), 463 ) 464self .assertIn ("BODY_MUST_NOT_AUTOLOAD" ,activation ["instructions" ]) 465 466# exclusive: writes models.json and /model saves global defaults 467# exclusive: model catalog seeded on disk 468@exclusive 469def test_python_activation_and_user_only_model_switch (self ): 470 (self .app .home / "models.json" ).write_text ("{}" ) 471requests = self .turn ("activate demo via python" ,expected = 2 ) 472self .assertIn ("SKILL_PYTHON_ACTIVATION_OK" ,self .output (requests [- 1 ])) 473session = self .app .session () 474requests = self .turn ("model probe via python" ,session ,expected = 2 ) 475self .assertIn ("MODEL_COMMAND_OK" ,self .output (requests [- 1 ])) 476selected = self .change ({"model" :"switched-model" },session )["session" ] 477self .assertEqual ( 478 (selected ["model" ],selected ["provider_profile" ]), 479 ("switched-model" ,self .app .profile ), 480 ) 481selected = self .change ({"model" :"o3-mini" },session )["session" ] 482self .assertEqual ((selected ["model" ],selected ["effort" ]), ("o3-mini" ,"medium" )) 483with self .assertRaises (urllib .error .HTTPError ): 484self .change ({"effort" :"bogus" },session ) 485selected = self .change ({"effort" :"high" },session )["session" ] 486self .assertEqual (selected ["effort" ],"high" ) 487self .assertEqual ( 488self .change ({"model" :"o3-mini" ,"effort" :"low" },session )["session" ][ 489"effort" 490 ], 491"low" , 492 ) 493for fields in ( 494 {"model" :"o3-mini" ,"effort" :"max" }, 495 {"model" :"switched-model" ,"effort" :"low" }, 496 ): 497with self .assertRaises (urllib .error .HTTPError ): 498self .change (fields ,session ) 499self .assertEqual (self .get (f"/sessions/ { session } " )["model" ],"o3-mini" ) 500 501def test_work_page_queues_agent_note_without_starting_turn (self ): 502route = "/extensions/work/items?" + urllib .parse .urlencode ( 503 {"workspace" :str (self .app .workspace )} 504 ) 505page = self .get (route )["page" ] 506self .assertEqual (page ["title" ],"work" ) 507before = len (self .provider .requests ) 508added = self .post ( 509route , {"title" :"write the release notes" ,"session_id" :self .sid } 510 ) 511self .assertEqual (added ["notification" ]["state" ],"queued" ) 512self .assertEqual (len (self .provider .requests ),before ) 513 (request ,)= self .turn ("anything new on the ledger?" ) 514context = json .dumps (request ["input" ]) 515self .assertIn ("The user added work item" ,context ) 516note = next ( 517item ["content" ] 518for item in request ["input" ] 519if item .get ("role" )== "user" 520and "The user added work item" in item .get ("content" ,"" ) 521 ) 522self .assertEqual ( 523 (note .count ("<system-note>" ),note .count ("</system-note>" )), (1 ,1 ) 524 ) 525self .assertLess ( 526context .index ("The user added work item" ), 527context .index ("anything new on the ledger?" ), 528 ) 529self .assertTrue ( 530any ( 531resource ["value" ]["title" ]== "write the release notes" 532for resource in self .get (route )["items" ] 533 ) 534 ) 535 536def test_work_resources_are_scoped_to_each_workspace (self ): 537workspaces = [self .app .root / "work-one" ,self .app .root / "work-two" ] 538for workspace in workspaces : 539workspace .mkdir () 540self .app .session (workspace ) 541routes = [ 542"/extensions/work/items?" 543+ urllib .parse .urlencode ({"workspace" :str (workspace )}) 544for workspace in workspaces 545 ] 546self .post (routes [0 ], {"title" :"first workspace item" }) 547self .assertIn ("first workspace item" ,json .dumps (self .get (routes [0 ]))) 548self .assertNotIn ("first workspace item" ,json .dumps (self .get (routes [1 ]))) 549self .post (routes [1 ], {"title" :"second workspace item" }) 550self .assertNotIn ("second workspace item" ,json .dumps (self .get (routes [0 ]))) 551self .assertIn ("second workspace item" ,json .dumps (self .get (routes [1 ]))) 552 553def test_manual_compaction_preserves_durable_history_and_reuses_summary (self ): 554cursor = self .app .stream_page (self .sid ) 555for prompt in ("first turn" ,"second turn" ,"third turn" ,"fourth turn" ): 556self .turn (prompt ) 557original = self .app .history (self .sid )["items" ] 558before = len (self .provider .requests ) 559outcome = self .compact () 560self .assertEqual ( 561 (outcome ["effective_strategy" ],outcome ["state" ]), ("rolling" ,"compacted" ) 562 ) 563self .assertFalse (outcome ["selection_applied" ]) 564self .assertIsNone (outcome ["failure" ]) 565self .assertEqual (outcome ["observation" ]["strategy" ],"rolling" ) 566self .assertGreater (outcome ["observation" ]["evicted_entries" ],0 ) 567self .assertEqual (len (self .provider .requests ),before + 2 ) 568current = self .app .history (self .sid )["items" ] 569self .assertEqual ( 570 [ 571entry 572for entry in current 573if entry ["position" ]<= original [- 1 ]["position" ] 574 ], 575original , 576 ) 577 (request ,)= self .turn ("after manual compaction" ) 578self .assertIn ("older conversation summary" ,json .dumps (request ["input" ])) 579context = self .get (f"/sessions/ { self . sid } /context" ) 580self .assertEqual ( 581 (context ["state" ],context ["compaction" ]["status" ]), ("ready" ,"compacted" ) 582 ) 583events = self .app .stream_page (self .sid ,cursor )["events" ] 584compacted = next ( 585event ["data" ]for event in events if event ["type" ]== "compacted" 586 ) 587self .assertEqual (compacted ,outcome ["observation" ]) 588self .assertGreater (compacted ["evicted_entries" ],0 ) 589self .assertIn ("older conversation summary" ,compacted ["summary" ]) 590self .assertIn ("older conversation summary" ,self .section_text ("history" )) 591 592def test_every_shipped_skill_loads (self ): 593self .reload () 594shipped = sorted ( 595path .name for path in (Path (__file__ ).parents [2 ]/ "priv/skills" ).iterdir () 596 ) 597listed = {c ["slash_name" ]for c in self .commands ()} 598self .assertEqual ([name for name in shipped if f"/ { name } " not in listed ], []) 599 600def test_builtin_skill_is_listed_and_yields_to_a_workspace_skill_of_the_same_name ( 601self , 602 ): 603def description (): 604return next ( 605c ["description" ] 606for c in self .commands () 607if c ["slash_name" ]== "/customize-albedo" 608 ) 609 610self .reload () 611catalog_route = f"/sessions/ { self . sid } /catalog" 612builtin = next ( 613row 614for row in self .get (catalog_route )["discovery" ]["candidates" ] 615if row ["preference_key" ]== "customize-albedo" 616 ) 617self .assertTrue (builtin ["valid" ]) 618self .assertTrue (builtin ["eligible" ]) 619self .assertEqual (description (),builtin ["description" ]) 620override = self .app .workspace / ".agents/skills/customize-albedo/SKILL.md" 621override .parent .mkdir (parents = True ) 622override .write_text ( 623"---\nname: customize-albedo\ndescription: workspace override\n---\nmine\n" 624 ) 625self .reload () 626self .assertEqual (description (),"workspace override" ) 627candidates = [ 628row 629for row in self .get (catalog_route )["discovery" ]["candidates" ] 630if row ["preference_key" ]== "customize-albedo" 631 ] 632selected = next (row for row in candidates if row ["source" ]== str (override )) 633shipped = next (row for row in candidates if row ["id" ]== builtin ["id" ]) 634self .assertTrue (selected ["eligible" ]) 635self .assertTrue (shipped ["valid" ]) 636self .assertFalse (shipped ["eligible" ]) 637self .assertEqual (shipped ["shadowed_by" ],selected ["id" ]) 638 (request ,)= self .turn ("after override" ) 639self .assertNotIn ("duplicate skill" ,json .dumps (request ["input" ])) 640 641# exclusive: restarts the daemon 642@exclusive 643def test_reload_pins_system_prompt_until_compaction (self ): 644self .turn ("first turn" ) 645catalog_route = f"/sessions/ { self . sid } /catalog" 646original_catalog = self .get (catalog_route )["discovery" ] 647late = self .app .workspace / ".agents/skills/late/SKILL.md" 648late .parent .mkdir (parents = True ) 649late .write_text ( 650"---\nname: late\ndescription: added after the session opened\n---\nLATE_BODY\n" 651 ) 652self .skill .write_text ( 653SKILL .replace ( 654"catalog-only fixture description" ,"updated demo description" 655 ) 656 ) 657self .assertNotIn ("/late" , {c ["slash_name" ]for c in self .commands ()}) 658fresh_catalog = self .get (catalog_route )["discovery" ] 659self .assertNotEqual (fresh_catalog ["revision" ],original_catalog ["revision" ]) 660late_row = next ( 661row 662for row in fresh_catalog ["candidates" ] 663if row ["preference_key" ]== "late" 664 ) 665self .assertTrue (late_row ["eligible" ]) 666self .assertEqual ( 667self .get (catalog_route )["discovery" ]["revision" ],fresh_catalog ["revision" ] 668 ) 669requests = self .turn ("hot probe before reload" ,expected = 2 ) 670self .assertIn ("BEFORE_RELOAD_OK" ,self .output (requests [- 1 ])) 671cached = requests [- 1 ] 672reloaded = self .reload () 673self .assertEqual (reloaded ["session" ]["state" ],"applied" ) 674self .assertIn ("/late" , {c ["slash_name" ]for c in self .commands ()}) 675current_catalog = self .get (catalog_route )["discovery" ] 676self .assertEqual (current_catalog ["revision" ],fresh_catalog ["revision" ]) 677self .assertEqual ( 678next ( 679row ["id" ] 680for row in current_catalog ["candidates" ] 681if row ["preference_key" ]== "late" 682 ), 683late_row ["id" ], 684 ) 685 (request ,)= self .turn ("after session reload" ) 686self .assertEqual (request ["instructions" ],cached ["instructions" ]) 687updates = [ 688item ["content" ] 689for item in request ["input" ] 690if item .get ("role" )== "user" 691and str (item .get ("content" ,"" )).startswith ("<system-note" ) 692and "capabilities changed" in item ["content" ] 693 ] 694self .assertEqual (len (updates ),1 ) 695self .assertIn ("commands.catalog()" ,updates [0 ]) 696self .assertIn ("updated demo description" ,updates [0 ]) 697self .assertIn ("<name>demo</name>" ,updates [0 ]) 698self .assertIn ("/late" ,updates [0 ]) 699self .assertNotIn ("catalog-only fixture description" ,updates [0 ]) 700self .assertNotIn ("LATE_BODY" ,updates [0 ]) 701self .assertNotIn ("BODY_MUST_NOT_AUTOLOAD" ,updates [0 ]) 702requests = self .turn ("hot probe after reload" ,expected = 2 ) 703self .assertIn ("AFTER_RELOAD_OK" ,self .output (requests [- 1 ])) 704newer = self .app .workspace / ".agents/skills/newer/SKILL.md" 705newer .parent .mkdir (parents = True ) 706newer .write_text ( 707"---\nname: newer\ndescription: another live skill\n---\nNEWER_BODY\n" 708 ) 709late .unlink () 710self .reload () 711 (request ,)= self .turn ("after second reload" ) 712self .assertEqual (request ["instructions" ],cached ["instructions" ]) 713update = next ( 714item ["content" ] 715for item in reversed (request ["input" ]) 716if item .get ("role" )== "user" 717and "capabilities changed" in str (item .get ("content" ,"" )) 718 ) 719self .assertIn ("another live skill" ,update ) 720self .assertIn ("supersedes any earlier version" ,update ) 721self .assertNotIn ("<command>/late</command>" ,update ) 722self .assertNotIn (" /late " ,update ) 723self .assertNotIn ("catalog-only fixture description" ,update ) 724self .assertNotIn ("NEWER_BODY" ,update ) 725self .assertIn ("/newer" ,update ) 726self .assertEqual ( 727sum ( 728"capabilities changed" in str (item .get ("content" ,"" )) 729for item in request ["input" ] 730if item .get ("role" )== "user" 731 ), 7322 , 733 ) 734self .restart () 735 (request ,)= self .turn ("reload pin survives restart" ) 736self .assertEqual (request ["instructions" ],cached ["instructions" ]) 737self .turn ("one more turn before compaction" ) 738self .compact () 739self .app .idle (self .sid ) 740snapshot = self .section_text ("instructions" ) 741self .assertNotIn ("added after the session opened" ,snapshot ) 742self .assertIn ("updated demo description" ,snapshot ) 743self .assertIn ("another live skill" ,snapshot ) 744 (request ,)= self .turn ("after compaction following reload" ) 745self .assertNotIn ("added after the session opened" ,request ["instructions" ]) 746self .assertIn ("updated demo description" ,request ["instructions" ]) 747self .assertIn ("another live skill" ,request ["instructions" ]) 748 (request ,)= self .turn ("pin stays released" ) 749self .assertIn ("another live skill" ,request ["instructions" ]) 750 751# exclusive: restarts the daemon 752@exclusive 753def test_restart_keeps_sent_system_prompt_until_compaction (self ): 754for index in range (3 ): 755self .turn (f"older turn { index } " ) 756self .compact () 757self .app .idle (self .sid ) 758 (sent ,)= self .turn ("turn after compaction" ) 759self .assertIn ("[older conversation summary;" ,json .dumps (sent ["input" ])) 760self .skill .write_text ( 761SKILL .replace ( 762"catalog-only fixture description" ,"edited while the daemon was down" 763 ) 764 ) 765self .restart () 766 (request ,)= self .turn ("first turn after restart" ) 767self .assertEqual (request ["instructions" ],sent ["instructions" ]) 768updates = capability_notes (request ) 769self .assertEqual (len (updates ),1 ) 770self .assertIn ("edited while the daemon was down" ,updates [0 ]) 771self .restart () 772 (request ,)= self .turn ("unchanged restart" ) 773self .assertEqual (request ["instructions" ],sent ["instructions" ]) 774self .assertEqual (capability_notes (request ),updates ) 775 776# exclusive: crashes and restarts the daemon 777@exclusive 778def test_a_capability_note_saved_during_a_call_follows_its_result (self ): 779self .app .prompt (self .sid ,"hold a cell across a crash" ).close () 780started = self .app .workspace / "held-cell-started" 781deadline = time .monotonic ()+ 15 782while not started .exists (): 783self .assertLess (time .monotonic (),deadline ,"the held cell never started" ) 784time .sleep (0.05 ) 785self .skill .write_text ( 786SKILL .replace ("catalog-only fixture description" ,"edited during a call" ) 787 ) 788before = len (self .provider .requests ) 789# The reattached kernel brings the edited prompt, whose note is saved 790# before the resumed turn records the interrupted call's result. 791self .app .restart (crash = True ) 792self .app .idle (self .sid ) 793assert_result_follows_call (self ,self .provider .requests [before ]["request" ]) 794self .restart () 795 (request ,)= self .turn ("after a clean restart" ) 796assert_result_follows_call (self ,request ) 797 798def follows_call (db ): 799 (call ,)= db .execute ( 800"SELECT seq FROM transcript_calls WHERE session=?" , (self .sid ,) 801 ).fetchone () 802return [ 803seq 804for (seq ,)in db .execute ( 805"SELECT seq FROM transcript WHERE session=? AND seq>? ORDER BY seq LIMIT 2" , 806 (self .sid ,call ), 807 ) 808 ] 809 810# A transcript saved before this fix keeps the note between them. 811def unsettle (app ): 812columns = "payload,timestamp,provider,thought_ms,row_class,turn_id,calls_indexed,summary_indexed" 813with sqlite3 .connect (app .home / "albedo.sqlite" )as db : 814result ,note = follows_call (db ) 815saved = { 816seq :db .execute ( 817f"SELECT { columns } FROM transcript WHERE seq=?" , (seq ,) 818 ).fetchone () 819for seq in (result ,note ) 820 } 821assignments = "," .join (f" { c } =?" for c in columns .split ("," )) 822for seq ,other in ((result ,note ), (note ,result )): 823db .execute ( 824f"UPDATE transcript SET { assignments } WHERE seq=?" , 825 (* saved [other ],seq ), 826 ) 827 828self .app .restart (prepare = unsettle ) 829 (request ,)= self .turn ("after an unsettled restart" ) 830assert_result_follows_call (self ,request ) 831with sqlite3 .connect (self .app .home / "albedo.sqlite" )as db : 832classes = [ 833db .execute ( 834"SELECT row_class FROM transcript WHERE seq=?" , (seq ,) 835 ).fetchone ()[0 ] 836for seq in follows_call (db ) 837 ] 838self .assertEqual (classes , ["other" ,"user" ]) 839 840# exclusive: changes global compaction settings and restarts the daemon 841@exclusive 842def test_auto_compaction_releases_pin_on_first_turn_after_restart (self ): 843for index in range (4 ): 844self .turn (f"older turn { index } : " + "history " * 8000 ) 845self .skill .write_text ( 846SKILL .replace ( 847"catalog-only fixture description" ,"reloaded skill description" 848 ) 849 ) 850self .reload () 851settings_path = self .app .home / "extensions.json" 852settings = json .loads (settings_path .read_text ()) 853settings ["rolling" ]= {"contextWindowTokens" :50000 } 854settings_path .write_text (json .dumps (settings )) 855self .restart () 856before = len (self .provider .requests ) 857self .app .prompt (self .sid ,"first turn after restart" ).close () 858self .app .idle (self .sid ) 859requests = [entry ["request" ]for entry in self .provider .requests [before :]] 860self .assertGreater (len (requests ),1 ,"expected automatic summarization" ) 861self .assertIn ("reloaded skill description" ,requests [- 1 ]["instructions" ]) 862self .assertNotIn ( 863"catalog-only fixture description" ,requests [- 1 ]["instructions" ] 864 ) 865self .assertIn ("older conversation summary" ,json .dumps (requests [- 1 ]["input" ])) 866 867def test_failed_snapshot_retains_kernel_and_saved_choice_for_retry (self ): 868self .turn ("hot probe before reload" ,expected = 2 ) 869before = self .snapshot ()["kernel" ]["instance_id" ] 870with sqlite3 .connect (self .app .home / "albedo.sqlite" )as db : 871 (run_dir ,)= db .execute ( 872"SELECT run_dir FROM kernel_links WHERE session=?" , (self .sid ,) 873 ).fetchone () 874blocked = Path (run_dir )/ "namespace.state" / "taken" 875blocked .mkdir (parents = True ) 876self .change ({"selection" : {"extensions" : {"skills" :False }}}) 877refused = self .reload ()["session" ] 878self .assertEqual (refused ["state" ],"failed" ,refused ) 879self .assertFalse (self .extensions ()["skills" ],"the desired choice stays saved" ) 880self .assertEqual (self .snapshot ()["kernel" ]["instance_id" ],before ) 881self .assertIsNotNone (refused ["loaded_revision" ]) 882blocked .rmdir () 883blocked .parent .rmdir () 884applied = self .reload ()["session" ] 885self .assertEqual (applied ["state" ],"applied" ,applied ) 886self .assertNotEqual (self .snapshot ()["kernel" ]["instance_id" ],before ) 887self .assertNotIn ("/demo" , [entry ["slash_name" ]for entry in self .commands ()]) 888 889def test_compaction_applies_saved_strategy_and_keeps_matching_kernel (self ): 890self .turn ("first turn" ) 891self .turn ("second turn" ) 892before = self .snapshot ()["kernel" ]["instance_id" ] 893self .change ({"selection" : {"extensions" : {"lcm" :True }}}) 894# Saving is separate from applying. Named compaction must compare with 895# the loaded strategy, even when the desired choice already matches. 896report = self .compact ("lcm" ) 897self .assertTrue (report ["selection_applied" ],report ) 898self .assertEqual (report ["effective_strategy" ],"lcm" ,report ) 899self .assertEqual (report ["state" ],"compacted" ,report ) 900after = self .snapshot ()["kernel" ]["instance_id" ] 901self .assertEqual (after ,before ) 902 903# exclusive: restarts the daemon 904@exclusive 905def test_live_toggles_guard_dependencies_busy_turns_and_persist (self ): 906self .turn ("first turn" ) 907installed = self .extensions () 908for name in ("not-installed" ,"python" ): 909with self .assertRaises (urllib .error .HTTPError ): 910self .change ({"selection" : {"extensions" : {name :False }}}) 911self .assertEqual (self .extensions (),installed ) 912self .change ({"selection" : {"extensions" : {"skills" :False }}}) 913self .reload () 914disabled = self .extensions () 915self .assertFalse (disabled ["skills" ]) 916with self .assertRaises (urllib .error .HTTPError ): 917self .activate ("/demo" ,arguments = "must not run" ) 918pid = self .app .connection ["pid" ] 919 (request ,)= self .turn ("extension disabled" ) 920self .assertIn ("Context removed: skills" ,json .dumps (request ["input" ])) 921self .assertEqual ( 922json .loads ((self .app .home / "daemon.json" ).read_text ())["pid" ],pid 923 ) 924entered ,release = threading .Event (),threading .Event () 925self .addCleanup (release .set ) 926 927def held (_request ): 928entered .set () 929release .wait (30 ) 930return text ("done" ) 931 932self .provider .script = held 933self .app .prompt (self .sid ,"hold this turn" ).close () 934self .assertTrue (entered .wait (10 )) 935for fields in ( 936 {"selection" : {"extensions" : {"skills" :True }}}, 937 {"model" :"mid-run" }, 938 ): 939with self .assertRaises (urllib .error .HTTPError )as caught : 940self .change (fields ) 941self .assertEqual (caught .exception .code ,409 ) 942with self .assertRaises (urllib .error .HTTPError )as caught : 943self .compact () 944self .assertEqual (caught .exception .code ,409 ) 945self .assertEqual (self .extensions (),disabled ) 946release .set () 947self .app .idle (self .sid ) 948self .provider .script = scripted 949self .restart () 950self .assertFalse (self .extensions ()["skills" ]) 951self .skill .write_text ( 952SKILL .replace ( 953"catalog-only fixture description" ,"refreshed catalog description" 954 ) 955 ) 956self .change ({"selection" : {"extensions" : {"skills" :True }}}) 957self .reload () 958refreshed = next ( 959command for command in self .commands ()if command ["slash_name" ]== "/demo" 960 ) 961self .assertEqual (refreshed ["description" ],"refreshed catalog description" ) 962 (request ,)= self .turn ("extension reloaded" ) 963self .assertNotIn ("BODY_MUST_NOT_AUTOLOAD" ,request ["instructions" ]) 964 965# exclusive: restarts the daemon 966@exclusive 967def test_compaction_strategies_preserve_lcm_folds_across_switch_and_restart (self ): 968for prompt in ("first turn" ,"second turn" ,"third turn" ,"fourth turn" ): 969self .turn (prompt ) 970refused = self .compact ("skills" ) 971self .assertEqual (refused ["state" ],"failed" ) 972self .assertFalse (refused ["selection_applied" ]) 973self .assertIsNotNone (refused ["failure" ]) 974self .assertFalse (self .extensions ()["lcm" ]) 975result = self .compact ("lcm" ) 976self .assertEqual ( 977 (result ["effective_strategy" ],result ["state" ]), ("lcm" ,"compacted" ) 978 ) 979self .assertTrue (result ["selection_applied" ]) 980self .assertIsNone (result ["failure" ]) 981self .assertEqual (result ["observation" ]["strategy" ],"lcm" ) 982self .assertTrue (self .extensions ()["lcm" ]) 983self .assertFalse (self .extensions ()["rolling" ]) 984self .change ({"selection" : {"extensions" : {"lcm" :False }}}) 985configured = self .get (f"/sessions/ { self . sid } ?view=configuration" ) 986self .assertFalse (configured ["selection" ]["extensions" ]["lcm" ]) 987refused_reload = self .reload () 988self .assertEqual (refused_reload ["session" ]["state" ],"failed" ) 989self .assertIsNotNone (refused_reload ["session" ]["failure" ]) 990 (request ,)= self .turn ("lcm preflight" ) 991self .assertTrue ( 992 {"lcm_list" ,"lcm_grep" ,"lcm_describe" ,"lcm_expand" } 993<= {tool ["name" ]for tool in request ["tools" ]} 994 ) 995requests = self .turn ("lcm tool probe" ,expected = 2 ) 996self .assertIn ("LCM summary node #" ,json .dumps (requests [0 ]["input" ])) 997self .assertIn ("source_count" ,self .output (requests [- 1 ])) 998self .assertIn ("first turn" ,self .output (requests [- 1 ])) 999self .change ({"selection" : {"extensions" : {"lcm" :True }}}) 1000self .assertEqual (self .reload ()["session" ]["state" ],"applied" ) 1001configuration_url = f"/sessions/ { self . sid } ?view=configuration" 1002before_conflict = self .get (configuration_url ) 1003with self .assertRaises (urllib .error .HTTPError )as conflict : 1004self .change ({"selection" : {"extensions" : {"lcm" :True ,"rolling" :True }}}) 1005self .assertEqual (conflict .exception .code ,400 ) 1006self .assertEqual (json .load (conflict .exception )["code" ],"selection_conflict" ) 1007self .assertEqual (self .get (configuration_url ),before_conflict ) 1008self .change ({"selection" : {"extensions" : {"rolling" :True }}}) 1009self .assertEqual (self .reload ()["session" ]["state" ],"applied" ) 1010self .assertTrue (self .extensions ()["rolling" ]) 1011self .assertFalse (self .extensions ()["lcm" ]) 1012 (request ,)= self .turn ("folded under rolling" ) 1013self .assertIn ("LCM summary node #" ,json .dumps (request ["input" ])) 1014requests = self .turn ("fold list probe" ,expected = 2 ) 1015self .assertGreater (json .loads (self .output (requests [- 1 ]))["total" ],0 ) 1016self .assertEqual ( 1017self .get (f"/sessions/ { self . sid } /context" )["compaction" ]["strategy" ], 1018"rolling" , 1019 ) 1020self .restart () 1021self .assertTrue (self .extensions ()["rolling" ]) 1022self .assertFalse (self .extensions ()["lcm" ]) 1023 (request ,)= self .turn ("folded after restart" ) 1024self .assertIn ("LCM summary node #" ,json .dumps (request ["input" ])) 1025 1026def test_rest_extension_edits_compare_the_displayed_resource_at_commit (self ): 1027"""Concurrent clients cannot overwrite a newer edit or delete its row.""" 1028cases = [ 1029 ( 1030"/extensions/work/items?" 1031+ urllib .parse .urlencode ({"workspace" :str (self .app .workspace )}), 1032 {"title" :"original" }, 1033"title" , 1034"😀" * 1025 , 1035 ), 1036 ( 1037"/extensions/paperclips/items" , 1038 {"message" :"original" }, 1039"reply" , 1040"😀" * 4097 , 1041 ), 1042 ( 1043"/extensions/schedule/jobs" , 1044 { 1045"session_id" :self .sid , 1046"kind" :"once" , 1047"prompt" :"original" , 1048"delay_seconds" :86400 , 1049 }, 1050"prompt" , 1051"😀" * 1025 , 1052 ), 1053 ] 1054before = len (self .provider .requests ) 1055for collection ,creation ,field ,oversize in cases : 1056with self .subTest (collection = collection ): 1057created = self .post (collection ,creation )["resource" ] 1058route = created ["url" ] 1059try : 1060with self .app .api (route )as response : 1061original = json .load (response ) 1062observed = response .headers ["ETag" ] 1063self .assertEqual (original ,created ["value" ]) 1064for patch ,status in (({field :"unguarded" },428 ),): 1065with self .assertRaises (urllib .error .HTTPError )as error : 1066self .app .api (route ,patch ,method = "PATCH" ) 1067self .assertEqual (error .exception .code ,status ) 1068for patch in ( 1069 {"revision" :"invented" }, 1070 {field :None }, 1071 {field :oversize }, 1072 ): 1073with self .assertRaises (urllib .error .HTTPError )as error : 1074self .app .api ( 1075route , 1076patch , 1077method = "PATCH" , 1078headers = {"If-Match" :observed }, 1079 ) 1080self .assertIn (error .exception .code , (400 ,413 )) 1081self .assertEqual (self .get (route ),original ) 1082 1083barrier = threading .Barrier (3 ) 1084outcomes = [] 1085 1086def edit (value ): 1087barrier .wait () 1088try : 1089with self .app .api ( 1090route , 1091 {field :value }, 1092method = "PATCH" , 1093headers = {"If-Match" :observed }, 1094 )as response : 1095outcomes .append ((response .status ,json .load (response ))) 1096except urllib .error .HTTPError as error : 1097outcomes .append ((error .code ,json .load (error ))) 1098 1099threads = [ 1100threading .Thread (target = edit ,args = (value ,)) 1101for value in ("first" ,"second" ) 1102 ] 1103for thread in threads : 1104thread .start () 1105barrier .wait () 1106for thread in threads : 1107thread .join (timeout = 10 ) 1108self .assertFalse (thread .is_alive ()) 1109self .assertEqual ( 1110sorted (status for status ,_ in outcomes ), [200 ,412 ] 1111 ) 1112winner = next ( 1113body ["resource" ]for status ,body in outcomes if status == 200 1114 ) 1115self .assertEqual (self .get (route ),winner ["value" ]) 1116with self .assertRaises (urllib .error .HTTPError )as error : 1117self .app .api ( 1118route ,method = "DELETE" ,headers = {"If-Match" :observed } 1119 ) 1120self .assertEqual (error .exception .code ,412 ) 1121self .assertEqual (self .get (route ),winner ["value" ]) 1122finally : 1123with self .app .api (route )as response : 1124json .load (response ) 1125current = response .headers ["ETag" ] 1126with self .app .api ( 1127route ,method = "DELETE" ,headers = {"If-Match" :current } 1128 )as response : 1129self .assertEqual ( 1130json .load (response )["id" ],created ["value" ]["id" ] 1131 ) 1132self .assertEqual (len (self .provider .requests ),before ) 1133 1134def test_links_validator_does_not_reappear_after_link_and_unlink (self ): 1135"""An old singleton observation stays stale after membership returns.""" 1136other = self .app .root / "linked-rest-peer" 1137other .mkdir () 1138own = str (self .app .workspace ) 1139route = "/extensions/links/groups?" + urllib .parse .urlencode ( 1140 {"workspace" :own ,"view" :"configuration" } 1141 ) 1142peer = "/extensions/links/groups?" + urllib .parse .urlencode ( 1143 {"workspace" :str (other ),"view" :"configuration" } 1144 ) 1145with self .app .api (route )as response : 1146before = json .load (response ) 1147old = response .headers ["ETag" ] 1148with self .app .api (peer )as response : 1149json .load (response ) 1150peer_etag = response .headers ["ETag" ] 1151with self .app .api ( 1152route , 1153 {"other_workspace" :str (other ),"other_etag" :peer_etag }, 1154headers = {"If-Match" :old }, 1155 )as response : 1156merged = json .load (response )["resource" ] 1157self .assertEqual (set (merged ["value" ]["members" ]), {own ,str (other )}) 1158unlink = route + "&" + urllib .parse .urlencode ({"member" :str (other )}) 1159with self .app .api ( 1160unlink ,method = "DELETE" ,headers = {"If-Match" :merged ["etag" ]} 1161 )as response : 1162unlinked = json .load (response )["resource" ] 1163self .assertEqual (unlinked ["value" ]["members" ],before ["members" ]) 1164self .assertNotEqual (unlinked ["etag" ],old ) 1165with self .app .api (peer )as response : 1166json .load (response ) 1167peer_etag = response .headers ["ETag" ] 1168with self .assertRaises (urllib .error .HTTPError )as error : 1169self .app .api ( 1170route , 1171 {"other_workspace" :str (other ),"other_etag" :peer_etag }, 1172headers = {"If-Match" :old }, 1173 ) 1174self .assertEqual (error .exception .code ,412 ) 1175self .assertEqual (self .get (route )["members" ],before ["members" ])