okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
61c43ca
main
1"""Management discovery must predict runtime selection without loading context.""" 2 3import json 4import os 5import unittest 6import urllib .error 7 8from harness import Albedo ,Provider ,exclusive ,text 9 10 11class CatalogTest (unittest .TestCase ): 12def setUp (self ): 13self .provider = Provider (lambda _request :text ("ok" )) 14self .addCleanup (self .provider .close ) 15self .app = Albedo (self .provider ,protocol = "responses" ) 16self .app .__enter__ () 17self .addCleanup (self .app .__exit__ ,None ,None ,None ) 18self .session = self .app .session () 19self .route = f"/sessions/ { self . session } /catalog" 20 21def request (self ,path ,body = None ,** options ): 22with self .app .api (path ,body ,** options )as response : 23return json .load (response ) 24 25def discovery (self ,session = None ): 26return self .request (f"/sessions/ { session or self . session } /catalog" )["discovery" ] 27 28def commands (self ,session = None ): 29return self .request (f"/sessions/ { session or self . session } /catalog" )["loaded" ][ 30"commands" 31 ] 32 33def snapshot (self ): 34return self .request (f"/sessions/ { self . session } ?tail=0" ) 35 36def change (self ,patch ,session = None ): 37route = f"/sessions/ { session or self . session } ?view=configuration" 38with self .app .api (route )as response : 39json .load (response ) 40revision = response .headers ["ETag" ] 41return self .request ( 42route ,patch ,method = "PATCH" ,headers = {"If-Match" :revision } 43 ) 44 45def test_reappearing_skill_replaces_its_missing_candidate_choice (self ): 46first = self .write_skill ( 47self .app .workspace / ".albedo/skills" ,"returning" ,"FIRST_SOURCE" 48 ) 49catalog = self .discovery () 50original = next ( 51row for row in catalog ["candidates" ]if row ["source" ]== str (first ) 52 ) 53self .change ( 54 { 55"catalog_revision" :catalog ["revision" ], 56"selection" : {"skills" : {original ["id" ]:False }}, 57 } 58 ) 59self .reload () 60self .assertNotIn ("/returning" , {row ["slash_name" ]for row in self .commands ()}) 61first .unlink () 62replacement = self .write_skill ( 63self .app .workspace / ".agents/skills" ,"returning" ,"REPLACEMENT_SOURCE" 64 ) 65catalog = self .discovery () 66fresh = next ( 67row for row in catalog ["candidates" ]if row ["source" ]== str (replacement ) 68 ) 69self .assertNotEqual (fresh ["id" ],original ["id" ]) 70self .change ( 71 { 72"catalog_revision" :catalog ["revision" ], 73"selection" : {"skills" : {fresh ["id" ]:True }}, 74 } 75 ) 76configuration = self .request (f"/sessions/ { self . session } ?view=configuration" ) 77self .assertEqual (configuration ["selection" ]["skills" ], {fresh ["id" ]:True }) 78self .reload () 79command = next ( 80row for row in self .commands ()if row ["slash_name" ]== "/returning" 81 ) 82self .assertEqual (command ["description" ],"REPLACEMENT_SOURCE" ) 83catalog = self .discovery () 84self .change ( 85 { 86"catalog_revision" :catalog ["revision" ], 87"selection" : {"skills" : {fresh ["id" ]:None }}, 88 } 89 ) 90self .assertEqual ( 91self .request (f"/sessions/ { self . session } ?view=configuration" )["selection" ][ 92"skills" 93 ], 94 {}, 95 ) 96self .reload () 97self .assertIn ("/returning" , {row ["slash_name" ]for row in self .commands ()}) 98 99def write_skill (self ,base ,name ,description ): 100path = base / name / "SKILL.md" 101path .parent .mkdir (parents = True ,exist_ok = True ) 102path .write_text (f"---\nname: { name } \ndescription: { description } \n---\nbody\n" ) 103return path 104 105def reload (self ): 106self .request ( 107f"/sessions/ { self . session } /reload" , 108 {"target" :"session" }, 109 ) 110 111def test_rejected_and_shadowed_candidates_do_not_hide_valid_runtime_winners (self ): 112high = self .app .workspace / ".albedo/skills" 113low = self .app .workspace / ".agents/skills" 114malformed = self .write_skill (high ,"fallback" ,"invalid" ) 115malformed .write_text ("---\nname: [\ndescription: bad\n---\n" ) 116fallback = self .write_skill (low ,"fallback" ,"LOWER_WINNER" ) 117mismatch = self .write_skill (high ,"mismatch" ,"invalid" ) 118mismatch .write_text ("---\nname: other\ndescription: bad\n---\n" ) 119oversized = self .write_skill (high ,"oversized" ,"invalid" ) 120oversized .write_text ("x" * (1048576 + 1 )) 121winner = self .write_skill (high ,"duplicate" ,"HIGHER_WINNER" ) 122shadowed = self .write_skill (low ,"duplicate" ,"SHADOWED_DESCRIPTION" ) 123target = self .write_skill (high / "container" ,"linked" ,"LINKED_WINNER" ) 124 (high / "linked" ).symlink_to (target .parent ,target_is_directory = True ) 125 126catalog = self .discovery () 127by_source = {row ["source" ]:row for row in catalog ["candidates" ]} 128for path in (malformed ,mismatch ,oversized ): 129row = by_source [str (path )] 130self .assertFalse (row ["valid" ]) 131self .assertTrue (row ["diagnostic" ]) 132self .assertFalse (row ["eligible" ]) 133self .assertTrue (by_source [str (fallback )]["eligible" ]) 134self .assertTrue (by_source [str (winner )]["eligible" ]) 135self .assertTrue (by_source [str (shadowed )]["valid" ]) 136self .assertFalse (by_source [str (shadowed )]["eligible" ]) 137self .assertEqual ( 138by_source [str (shadowed )]["shadowed_by" ],by_source [str (winner )]["id" ] 139 ) 140self .assertNotEqual ( 141by_source [str (winner )]["id" ],by_source [str (shadowed )]["id" ] 142 ) 143linked = by_source [str (high / "linked/SKILL.md" )] 144self .assertTrue (linked ["valid" ]) 145self .assertEqual (linked ["resolved_source" ],str (target )) 146self .reload () 147commands = self .commands () 148descriptions = {row ["slash_name" ]:row ["description" ]for row in commands } 149self .assertEqual (descriptions ["/fallback" ],"LOWER_WINNER" ) 150self .assertEqual (descriptions ["/duplicate" ],"HIGHER_WINNER" ) 151self .assertEqual (descriptions ["/linked" ],"LINKED_WINNER" ) 152self .assertTrue ({"/mismatch" ,"/oversized" }.isdisjoint (descriptions )) 153 154self .app .prompt (self .session ,"inspect selected skills" ).close () 155self .app .idle (self .session ) 156prompt = self .provider .requests [- 1 ]["request" ]["instructions" ] 157for description in ("LOWER_WINNER" ,"HIGHER_WINNER" ,"LINKED_WINNER" ): 158self .assertIn (description ,prompt ) 159self .assertNotIn ("SHADOWED_DESCRIPTION" ,prompt ) 160 161replacement_target = self .write_skill ( 162high / "replacement" ,"linked" ,"RETARGETED_WINNER" 163 ) 164 (high / "linked" ).unlink () 165 (high / "linked" ).symlink_to ( 166replacement_target .parent ,target_is_directory = True 167 ) 168retargeted = self .discovery () 169self .assertNotEqual (retargeted ["revision" ],catalog ["revision" ]) 170retargeted_row = next ( 171row for row in retargeted ["candidates" ]if row ["id" ]== linked ["id" ] 172 ) 173self .assertEqual (retargeted_row ["resolved_source" ],str (replacement_target )) 174self .assertEqual (retargeted_row ["description" ],"RETARGETED_WINNER" ) 175 176original_id = by_source [str (malformed )]["id" ] 177self .write_skill (high ,"fallback" ,"REPAIRED_WINNER" ) 178repaired = self .discovery () 179self .assertNotEqual (repaired ["revision" ],catalog ["revision" ]) 180row = next ( 181row for row in repaired ["candidates" ]if row ["source" ]== str (malformed ) 182 ) 183self .assertEqual (row ["id" ],original_id ) 184self .assertTrue (row ["eligible" ]) 185self .reload () 186self .assertEqual ( 187next ( 188row ["description" ] 189for row in self .commands () 190if row ["slash_name" ]== "/fallback" 191 ), 192"REPAIRED_WINNER" , 193 ) 194 195def test_instruction_links_follow_daemon_policy_and_exclude_system_files (self ): 196directory = self .app .workspace / ".agents" 197directory .mkdir () 198external = self .app .workspace .with_name ( 199self .app .workspace .name + "-external.md" 200 ) 201external .write_text ("LINKED_INSTRUCTION" ) 202 (directory / "linked.md" ).symlink_to (external ) 203 (directory / "broken.md" ).symlink_to (self .app .workspace / "missing" ) 204 (directory / "directory.md" ).symlink_to ( 205self .app .workspace ,target_is_directory = True 206 ) 207 (directory / "SYSTEM.md" ).write_text ("SYSTEM_REPLACEMENT" ) 208 (directory / "APPEND_SYSTEM.md" ).write_text ("SYSTEM_APPEND" ) 209 (directory / "oversized.md" ).write_text ("x" * (1048576 + 1 )) 210catalog = self .discovery () 211instructions = { 212row ["title" ]:row 213for row in catalog ["candidates" ] 214if row ["kind" ]== "instruction" 215 } 216self .assertTrue (instructions [".agents/linked.md" ]["eligible" ]) 217oversized = instructions [".agents/oversized.md" ] 218self .assertFalse (oversized ["valid" ]) 219self .assertTrue (oversized ["diagnostic" ]) 220self .assertFalse (oversized ["eligible" ]) 221for name in ("broken.md" ,"directory.md" ,"SYSTEM.md" ,"APPEND_SYSTEM.md" ): 222self .assertNotIn (f".agents/ { name } " ,instructions ) 223self .app .prompt (self .session ,"inspect linked instruction" ).close () 224self .app .idle (self .session ) 225self .assertIn ( 226"LINKED_INSTRUCTION" ,self .provider .requests [- 1 ]["request" ]["instructions" ] 227 ) 228 229# exclusive: GET /settings validates every profile, so it fails while any concurrent test holds an invalid one 230@exclusive 231def test_workspace_move_changes_catalog_without_using_client_workspace (self ): 232first = self .write_skill ( 233self .app .workspace / ".albedo/skills" ,"first" ,"FIRST_WORKSPACE" 234 ) 235 (self .app .workspace / "AGENTS.md" ).write_text ("FIRST_INSTRUCTION" ) 236other = self .app .workspace .with_name (self .app .workspace .name + "-other" ) 237other .mkdir () 238second = self .write_skill ( 239other / ".albedo/skills" ,"second" ,"SECOND_WORKSPACE" 240 ) 241 (other / "AGENTS.md" ).write_text ("SECOND_INSTRUCTION" ) 242before = self .discovery () 243self .assertEqual (before ["workspace" ],str (self .app .workspace )) 244self .assertIn (str (first ), {row ["source" ]for row in before ["candidates" ]}) 245self .assertNotIn (str (second ), {row ["source" ]for row in before ["candidates" ]}) 246self .change ( 247 { 248"workspace" :str (other ), 249"family_revision" :self .snapshot ()["family_revision" ], 250 } 251 ) 252after = self .discovery () 253self .assertEqual (after ["workspace" ],str (other )) 254self .assertNotEqual (after ["revision" ],before ["revision" ]) 255self .assertNotIn (str (first ), {row ["source" ]for row in after ["candidates" ]}) 256self .assertIn (str (second ), {row ["source" ]for row in after ["candidates" ]}) 257self .reload () 258commands = {row ["slash_name" ]for row in self .commands ()} 259self .assertIn ("/second" ,commands ) 260self .assertNotIn ("/first" ,commands ) 261preferences = self .snapshot ()["configuration_resource" ]["value" ]["selection" ] 262previous = next ( 263row for row in before ["candidates" ]if row ["source" ]== str (first ) 264 ) 265with self .assertRaises (urllib .error .HTTPError )as failure : 266self .change ( 267 { 268"catalog_revision" :before ["revision" ], 269"selection" : {"skills" : {previous ["id" ]:False }}, 270 } 271 ) 272self .assertEqual (failure .exception .code ,409 ) 273self .assertEqual (json .load (failure .exception )["code" ],"catalog_changed" ) 274self .assertEqual ( 275self .snapshot ()["configuration_resource" ]["value" ]["selection" ], 276preferences , 277 ) 278self .assertEqual ( 279 {row ["slash_name" ]for row in self .commands ()}, 280commands , 281 ) 282 283# exclusive: writes global preferences and asserts unchanged global file bytes 284@exclusive 285def test_stale_updates_do_not_write_preferences_or_reload_disk_changes (self ): 286skill = self .write_skill ( 287self .app .workspace / ".albedo/skills" ,"stale" ,"PREPARED_DESCRIPTION" 288 ) 289self .reload () 290initial = self .discovery () 291row = next ( 292row for row in initial ["candidates" ]if row ["preference_key" ]== "stale" 293 ) 294body = { 295"catalog_revision" :initial ["revision" ], 296"selection" : {"skills" : {row ["id" ]:False }}, 297 } 298settings_file = self .app .home / "capabilities.json" 299 300def assert_stale_without_side_effects (): 301before = settings_file .read_bytes ()if settings_file .exists ()else None 302configuration = self .request (f"/sessions/ { self . session } ?view=configuration" ) 303with self .assertRaises (urllib .error .HTTPError )as failure : 304self .change (body ) 305self .assertEqual (failure .exception .code ,409 ) 306self .assertEqual (json .load (failure .exception )["code" ],"catalog_changed" ) 307self .assertEqual ( 308settings_file .read_bytes ()if settings_file .exists ()else None ,before 309 ) 310self .assertEqual ( 311self .request (f"/sessions/ { self . session } ?view=configuration" ), 312configuration , 313 ) 314commands = self .commands () 315self .assertEqual ( 316next ( 317row ["description" ] 318for row in commands 319if row ["slash_name" ]== "/stale" 320 ), 321"PREPARED_DESCRIPTION" , 322 ) 323 324original_stat = skill .stat () 325original_contents = skill .read_bytes () 326replacement = original_contents .replace (b"body" ,b"edit" ) 327self .assertEqual (len (replacement ),len (original_contents )) 328skill .write_bytes (replacement ) 329os .utime (skill ,ns = (original_stat .st_atime_ns ,original_stat .st_mtime_ns )) 330self .assertEqual (skill .stat ().st_mtime_ns ,original_stat .st_mtime_ns ) 331assert_stale_without_side_effects () 332current = self .discovery () 333self .assertNotEqual (current ["revision" ],initial ["revision" ]) 334self .assertEqual (self .discovery ()["revision" ],current ["revision" ]) 335body ["catalog_revision" ]= current ["revision" ] 336settings_file .write_text ( 337json .dumps ({"global" : {"skills" : {"unrelated" :False }}}) 338 ) 339assert_stale_without_side_effects () 340body ["catalog_revision" ]= self .discovery ()["revision" ] 341self .change (body ) 342self .reload () 343self .assertNotIn ( 344"/stale" , 345 {row ["slash_name" ]for row in self .commands ()}, 346 ) 347acknowledged = next ( 348candidate 349for candidate in self .discovery ()["candidates" ] 350if candidate ["id" ]== row ["id" ] 351 ) 352self .assertFalse (acknowledged ["session_override" ]) 353self .assertFalse (acknowledged ["eligible" ]) 354 355before_extension_change = self .discovery () 356body ["catalog_revision" ]= before_extension_change ["revision" ] 357self .change ({"selection" : {"extensions" : {"skills" :False }}}) 358disabled = self .discovery () 359self .assertFalse ( 360self .snapshot ()["selection" ]["effective" ]["extensions" ]["skills" ] 361 ) 362self .assertNotEqual (disabled ["revision" ],before_extension_change ["revision" ]) 363before_skills = { 364row ["id" ] 365for row in before_extension_change ["candidates" ] 366if row ["kind" ]== "skill" 367 } 368disabled_skills = [ 369row for row in disabled ["candidates" ]if row ["kind" ]== "skill" 370 ] 371self .assertEqual ({row ["id" ]for row in disabled_skills },before_skills ) 372self .assertTrue (all (not row ["eligible" ]for row in disabled_skills )) 373preferences = settings_file .read_bytes () 374commands = self .commands () 375with self .assertRaises (urllib .error .HTTPError )as failure : 376self .change (body ) 377self .assertEqual (failure .exception .code ,409 ) 378self .assertEqual (json .load (failure .exception )["code" ],"catalog_changed" ) 379self .assertEqual (settings_file .read_bytes (),preferences ) 380self .assertEqual (self .commands (),commands ) 381 382# exclusive: GET /settings validates every profile, so it fails while any concurrent test holds an invalid one 383@exclusive 384def test_instruction_content_change_rejects_stale_save_with_restored_mtime (self ): 385instruction = self .app .workspace / "AGENTS.md" 386instruction .write_text ("FIRST_INSTRUCTION" ) 387self .reload () 388initial = self .discovery () 389self .assertEqual (self .discovery ()["revision" ],initial ["revision" ]) 390row = next ( 391row for row in initial ["candidates" ]if row ["source" ]== str (instruction ) 392 ) 393row_id = row ["id" ] 394preferences = self .snapshot ()["configuration_resource" ]["value" ]["selection" ] 395original_stat = instruction .stat () 396original_contents = instruction .read_bytes () 397replacement = original_contents .replace (b"FIRST" ,b"OTHER" ) 398self .assertEqual (len (replacement ),len (original_contents )) 399instruction .write_bytes (replacement ) 400os .utime (instruction ,ns = (original_stat .st_atime_ns ,original_stat .st_mtime_ns )) 401self .assertEqual (instruction .stat ().st_mtime_ns ,original_stat .st_mtime_ns ) 402with self .assertRaises (urllib .error .HTTPError )as failure : 403self .change ( 404 { 405"catalog_revision" :initial ["revision" ], 406"selection" : {"instructions" : {row ["id" ]:False }}, 407 } 408 ) 409self .assertEqual (failure .exception .code ,409 ) 410self .assertEqual (json .load (failure .exception )["code" ],"catalog_changed" ) 411self .assertEqual ( 412self .snapshot ()["configuration_resource" ]["value" ]["selection" ], 413preferences , 414 ) 415refreshed = self .discovery () 416self .assertNotEqual (refreshed ["revision" ],initial ["revision" ]) 417self .assertEqual (self .discovery ()["revision" ],refreshed ["revision" ]) 418self .assertTrue ( 419next ( 420row ["eligible" ] 421for row in refreshed ["candidates" ] 422if row ["id" ]== row_id 423 ) 424 ) 425 426# exclusive: writes skills and instructions in the daemon user home 427@exclusive 428def test_session_in_daemon_home_has_unique_rows_and_distinct_instruction_choices ( 429self , 430 ): 431home = self .app .root / "user-home" 432skill = self .write_skill (home / ".albedo/skills" ,"home-skill" ,"HOME_SKILL" ) 433instruction = home / ".agents/shared.md" 434instruction .parent .mkdir (exist_ok = True ) 435instruction .write_text ("HOME_INSTRUCTION" ) 436session = self .app .session (home ) 437catalog = self .discovery (session ) 438ids = [row ["id" ]for row in catalog ["candidates" ]] 439self .assertEqual (len (ids ),len (set (ids ))) 440skills = [row for row in catalog ["candidates" ]if row ["source" ]== str (skill )] 441self .assertEqual (sum (row ["eligible" ]for row in skills ),1 ) 442self .request (f"/sessions/ { session } /reload" , {"target" :"session" }) 443commands = self .commands (session ) 444self .assertEqual (sum (row ["slash_name" ]== "/home-skill" for row in commands ),1 ) 445instructions = [ 446row for row in catalog ["candidates" ]if row ["source" ]== str (instruction ) 447 ] 448self .assertEqual ( 449 {row ["preference_key" ]for row in instructions }, 450 {"project:.agents/shared.md" ,"global:~/.agents/shared.md" }, 451 ) 452project = next ( 453row for row in instructions if row ["preference_key" ].startswith ("project:" ) 454 ) 455self .change ( 456 { 457"catalog_revision" :catalog ["revision" ], 458"selection" : {"instructions" : {project ["id" ]:False }}, 459 }, 460session , 461 ) 462updated = self .discovery (session ) 463instructions = [ 464row for row in updated ["candidates" ]if row ["source" ]== str (instruction ) 465 ] 466self .assertFalse ( 467next (row ["eligible" ]for row in instructions if row ["id" ]== project ["id" ]) 468 ) 469self .assertTrue ( 470next (row ["eligible" ]for row in instructions if row ["id" ]!= project ["id" ]) 471 ) 472self .app .api (f"/sessions/ { session } /reload" , {"target" :"session" }).close () 473self .app .prompt (session ,"inspect home instruction scopes" ).close () 474self .app .idle (session ) 475self .assertEqual ( 476self .provider .requests [- 1 ]["request" ]["instructions" ].count ( 477"HOME_INSTRUCTION" 478 ), 4791 , 480 )