okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
e9df547
main
1"""Quota readings land raw, each account keeps its own cadence, and failures back off. 2 3The daemon-wide poller maps every account it can see — an OAuth account from 4creds.json, an API-key profile whose base url names a feed — onto provide-usage 5credentials and records each report as it came. This is the only place that 6wiring is visible from outside, so it is E2E: the usage core itself is a fake 7executable answering the `usage advance -` envelope (the real one needs 8accounts), pointed at by ALBEDO_USAGE_CORE with short poll settings. 9 10The cadence is asserted through the interval between one account's consecutive 11readings, not through poll counts over wall time: counts race the machine, and 12a hyper poll arriving a second late was read as "the busy cadence did not kick 13in" when the schedule was exactly as configured. The intervals are what the 14poller actually kept. 15""" 16 17import json 18from datetime import datetime 19import os 20import stat 21import tempfile 22import time 23import unittest 24import urllib .error 25 26from harness import Albedo ,Provider ,exclusive ,text 27 28 29def milliseconds (timestamp ): 30return round ( 31datetime .fromisoformat (timestamp .replace ("Z" ,"+00:00" )).timestamp ()* 1000 32 ) 33 34 35# The busy cadence must stay distinguishable from the ordinary one, and the 36# first failure backoff waits 2x the ordinary cadence, which sets the length. 37# How the backoff grows and where it stops is test/daemon/quota_test.gleam. 38POLL_SECONDS = 2 39BUSY_SECONDS = 1 40 41FAKE_USAGE = """#!/usr/bin/env python3 42# Fake usage-core: one `advance -` envelope line in, one report line out. Each 43# account is answered immediately, so one poll is one process. The provider in 44# the envelope picks the script: anthropic is ordinary (5 percent), hyper is 45# busy (85 percent, resetting soon), deepseek fails (an error report is data, 46# not a driver failure). The happy path touches no file, so nothing can make 47# it exit 1; anything unexpected is logged to fake-error.txt and answered as 48# an error report rather than crashing, which the driver would report as the 49# CLI itself failing. 50import json, os, sys, time, traceback 51 52ERRORS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fake-error.txt") 53RECOVERED = os.path.join(os.path.dirname(os.path.abspath(__file__)), "recovered") 54 55def report_for(provider, credential): 56if provider == "anthropic" and credential.get("accountId") == "recovering-account": 57# Fails its first poll, then answers for good. A failure with no limits 58# is filed under the empty limit id, which a later success never reuses. 59if not os.path.exists(RECOVERED): 60open(RECOVERED, "w").close() 61return {"error": "the fixture key failed once"} 62return {"limits": [{"id": "weekly", "label": "Weekly usage", "usedPercent": 7, 63"windowLabel": "7 days", "status": "ok"}]} 64if provider == "anthropic": 65return {"planType": "pro", 66"limits": [{"id": "weekly", "label": "Weekly usage", 67"usedPercent": 5, "windowLabel": "7 days", 68"status": "ok"}]} 69if provider == "google-antigravity": 70now_ms = int(time.time() * 1000) 71return { 72"limits": [ 73{ 74"id": "gemini", 75"label": "Gemini", 76"usedPercent": 65, 77"windowLabel": "1 hour", 78"windowSeconds": 3600, 79"resetsAt": now_ms + 3600000, 80"status": "ok", 81}, 82{ 83"id": "claude", 84"label": "Claude", 85"usedPercent": 100, 86"windowLabel": "6 days", 87"windowSeconds": 518400, 88"resetsAt": 1799999999000, 89"status": "exceeded", 90}, 91{ 92"id": "gpt-oss", 93"label": "GPT-OSS", 94"usedPercent": 100, 95"windowLabel": "6 days", 96"windowSeconds": 518400, 97"resetsAt": 1799999999000, 98"status": "exceeded", 99}, 100] 101} 102if provider == "deepseek": 103return {"error": "the fixture deepseek key has no quota"} 104return {"limits": [{"id": "primary", "label": "Primary", "usedPercent": 85, 105"windowLabel": "1 hour", "windowSeconds": 3600, 106"resetsAt": int(time.time() * 1000) + 600000, 107"status": "ok"}]} 108 109try: 110line = sys.stdin.readline() 111envelope = json.loads(line) 112print(json.dumps({"report": report_for(envelope["provider"], envelope.get("credential", {}))})) 113except Exception: 114# A bad envelope lands here too: record it and answer an error report, so 115# the failure shows up as a reading rather than as an exit-1 crash. 116try: 117with open(ERRORS, "a") as errors: 118errors.write(traceback.format_exc()) 119except Exception: 120pass 121print(json.dumps({"report": {"error": "the fake usage core failed"}})) 122""" 123 124 125# exclusive: configures global quota cadence, credentials, and usage-core environment 126@exclusive 127class QuotaTests (unittest .TestCase ): 128def setUp (self ): 129self .provider = Provider (lambda _request :text ("ok" )) 130self .addCleanup (self .provider .close ) 131self .core = tempfile .TemporaryDirectory (prefix = "quota-core-" ) 132self .addCleanup (self .core .cleanup ) 133self .usage = os .path .join (self .core .name ,"usage" ) 134with open (self .usage ,"w" )as out : 135out .write (FAKE_USAGE ) 136os .chmod (self .usage ,os .stat (self .usage ).st_mode | stat .S_IEXEC ) 137 138def prepare (app ): 139# The poller resolves the core through the daemon's environment. 140app .env ["ALBEDO_USAGE_CORE" ]= self .usage 141app .write_extensions ( 142 { 143"quota" : { 144"pollSeconds" :POLL_SECONDS , 145"busyPollSeconds" :BUSY_SECONDS , 146 } 147 } 148 ) 149app .store_secrets ( 150"accounts" , 151 { 152"anthropic" : [ 153 { 154"type" :"oauth" , 155"access" :"fixture-anthropic-access" , 156"refresh" :"fixture-anthropic-refresh" , 157"expires" :int (time .time ()* 1000 )+ 3600000 , 158"accountId" :"fixture-account" , 159 }, 160 { 161"type" :"oauth" , 162"access" :"fixture-recovering-access" , 163"refresh" :"fixture-recovering-refresh" , 164"expires" :int (time .time ()* 1000 )+ 3600000 , 165"accountId" :"recovering-account" , 166 }, 167 ], 168"google-antigravity" : [ 169 { 170"type" :"oauth" , 171"access" :"fixture-antigravity-access" , 172"refresh" :"fixture-antigravity-refresh" , 173"expires" :int (time .time ()* 1000 )+ 3600000 , 174"projectId" :"fixture-project" , 175"email" :"antigravity@example.com" , 176"limitedUntil" :1799999999000 , 177 } 178 ], 179 }, 180 ) 181 182self .app = Albedo ( 183self .provider , 184providers = { 185"fixture" : { 186"baseUrl" :self .provider .url , 187"apiKey" :"fixture-key" , 188"model" :"fixture-model" , 189"protocol" :"chat_completions" , 190 }, 191"charm-hyper" : { 192"baseUrl" :"https://hyper.charm.land/v1" , 193"apiKey" :"fixture-hyper-key" , 194"model" :"fixture-model" , 195"protocol" :"chat_completions" , 196 }, 197"deepseek-fixture" : { 198"baseUrl" :"https://api.deepseek.com/v1" , 199"apiKey" :"fixture-deepseek-key" , 200"model" :"fixture-model" , 201"protocol" :"chat_completions" , 202 }, 203 }, 204prepare = prepare , 205 ) 206# The daemon boots with the fake core once prepare has named it, so 207# its cadence starts here. 208self .since = time .time ()* 1000 209self .app .__enter__ () 210self .addCleanup (self .app .__exit__ ,None ,None ,None ) 211 212def readings (self ): 213with self .app .api ("/server" )as response : 214return json .load (response )["quota" ] 215 216def samples (self ): 217"""Every stored sample, newest first, through the history pages.""" 218rows ,token = [],None 219while len (rows )< 1000 : 220query = "&next=" + token if token else "" 221with self .app .api ( 222f"/server?include=quota_history&limit=200 { query } " 223 )as response : 224page = json .load (response )["quota_history" ] 225rows .extend (page ["items" ]) 226if page ["next" ]is None : 227break 228token = page ["next" ] 229return rows 230 231def observed (self ,account ): 232"""One timestamp per poll of one account since the boot, oldest first.""" 233return sorted ( 234 { 235milliseconds (row ["observed_at" ]) 236for row in self .samples () 237if row ["account_id" ]== account 238and milliseconds (row ["observed_at" ])> self .since 239 } 240 ) 241 242def intervals (self ,account ): 243times = self .observed (account ) 244return [later - earlier for earlier ,later in zip (times ,times [1 :])] 245 246def fake_errors (self ): 247try : 248with open (os .path .join (self .core .name ,"fake-error.txt" ))as errors : 249return errors .read () 250except FileNotFoundError : 251return "" 252 253def wait_for (self ,condition ,timeout ,message ): 254deadline = time .monotonic ()+ timeout 255while time .monotonic ()< deadline : 256if condition (): 257return 258time .sleep (0.25 ) 259self .fail (message ) 260 261def test_readings_land_and_each_account_keeps_its_cadence (self ): 262# Readings land for every account: the OAuth account from creds.json, 263# the hyper-charm profile, and the deepseek profile. 264wanted = { 265 ("anthropic" ,"weekly" ), 266 ("hyper" ,"primary" ), 267 ("deepseek" ,"" ), 268 ("google-antigravity" ,"gemini" ), 269 ("google-antigravity" ,"claude" ), 270 ("google-antigravity" ,"gpt-oss" ), 271 } 272reading = {} 273deadline = time .monotonic ()+ 40 274while time .monotonic ()< deadline : 275reading = {(r ["provider" ],r ["limit_id" ]):r for r in self .readings ()} 276if wanted .issubset (reading ): 277break 278time .sleep (0.25 ) 279else : 280self .fail ( 281"quota readings did not land: " 282+ json .dumps (self .readings ()) 283+ self .fake_errors () 284 ) 285hyper = reading [("hyper" ,"primary" )] 286self .assertEqual (hyper ["account_id" ],"charm-hyper" ) 287self .assertEqual (hyper ["used_percent" ],85.0 ) 288self .assertEqual (hyper ["window_seconds" ],3600 ) 289self .assertGreater (milliseconds (hyper ["resets_at" ]),int (time .time ()* 1000 )) 290self .assertEqual (hyper ["status" ],"ok" ) 291self .assertEqual (hyper ["source" ],"poll" ) 292anthropic = reading [("anthropic" ,"weekly" )] 293self .assertEqual (anthropic ["account_id" ],"fixture-account" ) 294self .assertEqual (anthropic ["used_percent" ],5.0 ) 295# The report plan and each limit's window label are readings too, kept 296# exactly as reported; where the report had neither, they stay null. 297self .assertEqual (anthropic ["plan" ],"pro" ) 298self .assertEqual (anthropic ["window_label" ],"7 days" ) 299self .assertIsNone (hyper ["plan" ]) 300self .assertEqual (hyper ["window_label" ],"1 hour" ) 301# A failed report is recorded as a reading too, error and all; a 302# missing percentage stays unknown rather than becoming zero. 303deepseek = reading [("deepseek" ,"" )] 304self .assertEqual (set (deepseek ["error" ]), {"code" ,"detail" }) 305self .assertEqual (deepseek ["error" ]["code" ],"quota_failed" ) 306self .assertIn ("no quota" ,deepseek ["error" ]["detail" ]) 307self .assertLessEqual (len (deepseek ["error" ]["detail" ]),4096 ) 308self .assertIsNone (deepseek ["used_percent" ]) 309self .assertIsNone (deepseek ["plan" ]) 310 311# Google Antigravity readings for all three model-family meters 312gemini = reading [("google-antigravity" ,"gemini" )] 313self .assertEqual (gemini ["account_id" ],"antigravity@example.com" ) 314self .assertEqual (gemini ["used_percent" ],65.0 ) 315self .assertEqual (gemini ["window_label" ],"1 hour" ) 316self .assertEqual (gemini ["window_seconds" ],3600 ) 317self .assertEqual (gemini ["status" ],"ok" ) 318 319claude = reading [("google-antigravity" ,"claude" )] 320self .assertEqual (claude ["account_id" ],"antigravity@example.com" ) 321self .assertEqual (claude ["used_percent" ],100.0 ) 322self .assertEqual (claude ["window_label" ],"6 days" ) 323self .assertEqual (claude ["window_seconds" ],518400 ) 324self .assertEqual (claude ["status" ],"exceeded" ) 325 326gpt_oss = reading [("google-antigravity" ,"gpt-oss" )] 327self .assertEqual (gpt_oss ["account_id" ],"antigravity@example.com" ) 328self .assertEqual (gpt_oss ["used_percent" ],100.0 ) 329self .assertEqual (gpt_oss ["window_label" ],"6 days" ) 330self .assertEqual (gpt_oss ["window_seconds" ],518400 ) 331self .assertEqual (gpt_oss ["status" ],"exceeded" ) 332 333# The poller heal re-keyed creds.json with legacy limitedUntil replaced by limitedMeters 334def healed (): 335creds_path = self .app .home / "creds.json" 336if not creds_path .exists (): 337return False 338data = ( 339json .loads (creds_path .read_text ()) 340 .get ("accounts" , {}) 341 .get ("google-antigravity" , []) 342 ) 343return bool (data and "limitedMeters" in data [0 ]) 344 345self .wait_for (healed ,15 ,"poller heal did not re-key creds.json" ) 346creds = json .loads ((self .app .home / "creds.json" ).read_text ())["accounts" ][ 347"google-antigravity" 348 ][0 ] 349self .assertNotIn ("limitedUntil" ,creds ) 350self .assertIn ("limitedMeters" ,creds ) 351self .assertEqual (creds ["limitedMeters" ]["claude" ],1799999999000 ) 352 353# Enough polls of each account to read a cadence off the intervals. 354self .wait_for ( 355lambda :len (self .observed ("charm-hyper" ))>= 5 , 35660 , 357"the busy cadence never produced five polls: " 358+ json .dumps (self .intervals ("charm-hyper" )) 359+ self .fake_errors (), 360 ) 361self .wait_for ( 362lambda :len (self .observed ("fixture-account" ))>= 3 , 36360 , 364"the ordinary cadence never produced three polls: " 365+ json .dumps (self .intervals ("fixture-account" )) 366+ self .fake_errors (), 367 ) 368self .wait_for ( 369lambda :len (self .observed ("deepseek-fixture" ))>= 2 , 37060 , 371"the failure backoff never produced a second poll: " 372+ json .dumps (self .intervals ("deepseek-fixture" )) 373+ self .fake_errors (), 374 ) 375 376# The schedule as the poller kept it: one account's consecutive 377# readings are one poll duration plus its wait, so each cadence shows 378# up as an interval with generous bounds either side of its setting. 379# The busy cadence (85 percent, resetting soon) is busyPollSeconds, 380# the ordinary one is pollSeconds, and the failing account backs off 381# past the ordinary cadence. 382busy = self .intervals ("charm-hyper" ) 383ordinary = self .intervals ("fixture-account" ) 384backed_off = self .intervals ("deepseek-fixture" ) 385 386def median (values ): 387return sorted (values )[len (values )// 2 ] 388 389message = ( 390f"busy { busy } , ordinary { ordinary } , backed off { backed_off } , " 391f"fake errors { self . fake_errors ()!r } " 392 ) 393self .assertGreaterEqual (median (busy ),BUSY_SECONDS * 900 ,message ) 394self .assertLess ( 395median (busy ), 396POLL_SECONDS * 900 , 397message + " — the busy account polled at the ordinary cadence" , 398 ) 399self .assertGreaterEqual (median (ordinary ),POLL_SECONDS * 900 ,message ) 400self .assertLess (median (ordinary ),POLL_SECONDS * 1700 ,message ) 401self .assertGreaterEqual ( 402min (backed_off ), 403POLL_SECONDS * 1800 , 404message + " — failures did not back off" , 405 ) 406 407# The feed is the fake, so a driver failure ("usage is not built", the 408# CLI crashing, the feed not finishing) is a bug in the host half, not 409# weather. An exit-1 burst would surface here as readings carrying it. 410failures = [ 411row 412for row in self .samples () 413if milliseconds (row ["observed_at" ])> self .since 414and row ["error" ] 415and "usage" in row ["error" ]["detail" ] 416 ] 417self .assertEqual (failures , [],"the usage driver failed against the fake" ) 418self .assertEqual (self .fake_errors (),"" ) 419 420# The latest reading per account and limit is the newest sample, and 421# history pages from it by row id, newest first. 422latest = {(r ["provider" ],r ["limit_id" ]):r for r in self .readings ()} 423with self .app .api ("/server?include=quota_history&limit=200" )as response : 424page = json .load (response )["quota_history" ] 425samples = page ["items" ] 426ids = [sample ["sequence" ]for sample in samples ] 427self .assertTrue ( 428all (later < earlier for earlier ,later in zip (ids ,ids [1 :])), 429f"history is not newest first: { ids } " , 430 ) 431newest = max ( 432 (s for s in samples if s ["provider" ]== "hyper" ), 433key = lambda s :s ["sequence" ], 434 ) 435self .assertEqual ( 436newest ["observed_at" ],latest [("hyper" ,"primary" )]["observed_at" ] 437 ) 438with self .app .api ("/server?include=quota_history&limit=2" )as response : 439first = json .load (response )["quota_history" ] 440self .assertEqual (len (first ["items" ]),2 ) 441self .assertIsNotNone (first ["next" ]) 442with self .app .api ( 443f"/server?include=quota_history&limit=2&next= { first [ 'next' ] } " 444 )as response : 445older = json .load (response )["quota_history" ] 446self .assertTrue (older ["items" ]) 447self .assertTrue ( 448all ( 449sample ["sequence" ]< first ["items" ][- 1 ]["sequence" ] 450for sample in older ["items" ] 451 ), 452"quota continuation reached newer rows" , 453 ) 454 455# An account that failed once and then answered has an old failure filed 456# under the empty limit id. The latest readings drop it once a later 457# reading of a real limit shows the account answering; deepseek, which 458# never answers, keeps its failure. 459self .wait_for ( 460lambda :any ( 461r ["account_id" ]!= "fixture-account" 462and r ["provider" ]== "anthropic" 463and r ["limit_id" ]== "weekly" 464for r in self .readings () 465 ), 46640 , 467"the recovering account never answered" , 468 ) 469recovered = [ 470r 471for r in self .readings () 472if r ["provider" ]== "anthropic" and r ["account_id" ]!= "fixture-account" 473 ] 474self .assertEqual ( 475 [r ["limit_id" ]for r in recovered ], 476 ["weekly" ], 477f"a failure the account recovered from is still its latest reading: { recovered } " , 478 ) 479failures = [ 480 (r ["provider" ],r ["limit_id" ])for r in self .readings ()if r ["error" ] 481 ] 482self .assertIn (("deepseek" ,"" ),failures ) 483self .assertTrue ( 484any ( 485row ["limit_id" ]== "" and row ["error" ] 486for row in self .samples () 487if row ["provider" ]== "anthropic" 488and row ["account_id" ]!= "fixture-account" 489 ), 490"the recovering account never recorded the failure this checks is dropped" , 491 ) 492 493# Filtered history returns only that limit's rows newest-first. 494hyper_filter = ( 495"quota_provider=hyper"a_account=charm-hyper"a_limit=primary" 496 ) 497with self .app .api ( 498f"/server?include=quota_history&limit=200& { hyper_filter } " 499 )as response : 500hyper_history = json .load (response )["quota_history" ]["items" ] 501self .assertGreaterEqual (len (hyper_history ),5 ) 502self .assertTrue ( 503all ( 504s ["provider" ]== "hyper" 505and s ["account_id" ]== "charm-hyper" 506and s ["limit_id" ]== "primary" 507for s in hyper_history 508 ), 509"filtered history returned observations from other limits" , 510 ) 511hyper_ids = [s ["sequence" ]for s in hyper_history ] 512self .assertTrue ( 513all (later < earlier for earlier ,later in zip (hyper_ids ,hyper_ids [1 :])), 514f"filtered history is not newest first: { hyper_ids } " , 515 ) 516 517# quota_since in the future returns zero rows and next null. 518future_since = "2099-01-01T00:00:00Z" 519with self .app .api ( 520f"/server?include=quota_history&limit=200& { hyper_filter } "a_since= { future_since } " 521 )as response : 522future_page = json .load (response )["quota_history" ] 523self .assertEqual (future_page ["items" ], []) 524self .assertIsNone (future_page ["next" ]) 525 526# since between two readings drops the older. 527cutoff = hyper_history [1 ]["observed_at" ] 528with self .app .api ( 529f"/server?include=quota_history&limit=200& { hyper_filter } "a_since= { cutoff } " 530 )as response : 531bounded_items = json .load (response )["quota_history" ]["items" ] 532self .assertTrue ( 533all ( 534milliseconds (s ["observed_at" ])>= milliseconds (cutoff ) 535for s in bounded_items 536 ), 537"quota_since kept observations older than cutoff" , 538 ) 539bounded_ids = [s ["sequence" ]for s in bounded_items ] 540self .assertIn (hyper_history [0 ]["sequence" ],bounded_ids ) 541self .assertIn (hyper_history [1 ]["sequence" ],bounded_ids ) 542self .assertNotIn (hyper_history [- 1 ]["sequence" ],bounded_ids ) 543 544# Paging a filtered history with limit=1 follows next across all of that 545# limit's rows with strictly decreasing sequence. 546paged_seqs = [] 547next_cursor = None 548while True : 549cursor_param = f"&next= { next_cursor } " if next_cursor else "" 550with self .app .api ( 551f"/server?include=quota_history&limit=1& { hyper_filter } { cursor_param } " 552 )as response : 553page_data = json .load (response )["quota_history" ] 554self .assertEqual (len (page_data ["items" ]),1 ) 555paged_seqs .append (page_data ["items" ][0 ]["sequence" ]) 556next_cursor = page_data ["next" ] 557if not next_cursor : 558break 559self .assertEqual (paged_seqs ,hyper_ids ) 560self .assertTrue ( 561all (later < earlier for earlier ,later in zip (paged_seqs ,paged_seqs [1 :])), 562"paged filtered sequence is not strictly decreasing" , 563 ) 564 565# A cursor from one filter replayed on another is rejected 400. 566with self .app .api ( 567f"/server?include=quota_history&limit=1& { hyper_filter } " 568 )as response : 569token = json .load (response )["quota_history" ]["next" ] 570self .assertIsNotNone (token ) 571anthropic_filter = ( 572"quota_provider=anthropic"a_account=fixture-account"a_limit=weekly" 573 ) 574with self .assertRaises (urllib .error .HTTPError )as cm : 575self .app .api ( 576f"/server?include=quota_history&limit=1& { anthropic_filter } &next= { token } " 577 ) 578self .assertEqual (cm .exception .code ,400 ) 579 580# Partial filter (only quota_account) is 400. 581with self .assertRaises (urllib .error .HTTPError )as cm : 582self .app .api ("/server?include=quota_history"a_account=charm-hyper" ) 583self .assertEqual (cm .exception .code ,400 ) 584 585# quota_since without the trio is 400. 586with self .assertRaises (urllib .error .HTTPError )as cm : 587self .app .api (f"/server?include=quota_history"a_since= { future_since } " ) 588self .assertEqual (cm .exception .code ,400 ) 589 590# Bad since is 400. 591with self .assertRaises (urllib .error .HTTPError )as cm : 592self .app .api ( 593f"/server?include=quota_history& { hyper_filter } "a_since=not-a-timestamp" 594 ) 595self .assertEqual (cm .exception .code ,400 ) 596 597# Filter parameters require include=quota_history. 598with self .assertRaises (urllib .error .HTTPError )as cm : 599self .app .api (f"/server? { hyper_filter } " ) 600self .assertEqual (cm .exception .code ,400 )