claude-multiacc 2.0.50 → 2.0.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/codex +55 -11
- package/bin/codex-accounts +34 -40
- package/bin/multiacc-select +6 -1
- package/docs/CODEX.md +118 -0
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
- package/lib/__pycache__/report.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
- package/lib/autoresume.py +7 -2
- package/lib/autoresume_probe.py +3 -3
- package/lib/codex_credit_controls.py +27 -0
- package/lib/codex_credit_local_scope.py +35 -0
- package/lib/codex_credit_selection.py +172 -0
- package/lib/codex_credits.py +158 -0
- package/lib/codex_limits_lock.py +56 -0
- package/lib/codex_usage_fetch.py +85 -0
- package/lib/report.py +6 -0
- package/lib/selector_credit_scope.py +85 -0
- package/lib/selector_credits.py +105 -0
- package/lib/selector_funding.py +113 -0
- package/lib/selector_policy.py +20 -2
- package/lib/selector_primitives.py +20 -2
- package/package.json +1 -1
- package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
- package/tests/test_codex_reset_credits.py +154 -0
- package/tests/test_codex_reset_credits_polling.py +151 -0
- package/tests/test_codex_reset_credits_report.py +85 -0
- package/tests/test_codex_reset_fetch.py +123 -0
- package/tests/test_selector_cached_credits.py +135 -0
- package/tests/test_selector_credit_scope.py +118 -0
- package/tests/test_selector_credit_shim.py +139 -0
- package/tests/test_selector_credits.py +140 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Real shell shim simulations with isolated accounts and a fake provider CLI."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
import time
|
|
10
|
+
import unittest
|
|
11
|
+
|
|
12
|
+
from test_selector import ROOT
|
|
13
|
+
from codex_credit_selection import credit_refusal_view, record_launch, record_refusal
|
|
14
|
+
from autoresume_probe import parse_probe, probe_allows
|
|
15
|
+
import test_autoresume as watcher_fixtures
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CreditShimTests(unittest.TestCase):
|
|
19
|
+
def setUp(self):
|
|
20
|
+
self.temporary = tempfile.TemporaryDirectory(prefix="codex-credit-shim-")
|
|
21
|
+
self.addCleanup(self.temporary.cleanup)
|
|
22
|
+
self.home = Path(self.temporary.name)
|
|
23
|
+
self.pool, self.fake = self.home / "pool", self.home / "fake"
|
|
24
|
+
self.pool.mkdir()
|
|
25
|
+
self.fake.mkdir()
|
|
26
|
+
(self.pool / "accounts.json").write_text('{"accounts":[],"threshold":90}')
|
|
27
|
+
binary = self.fake / "codex"
|
|
28
|
+
binary.write_text('#!/bin/sh\nprintf "chosen=%s\\n" "${CODEX_HOME##*/}"\n'
|
|
29
|
+
'if [ -n "$FAKE_ERROR" ]; then printf "%s\\n" "$FAKE_ERROR" >&2; exit 1; fi\n')
|
|
30
|
+
binary.chmod(0o755)
|
|
31
|
+
self.env = {key: value for key, value in os.environ.items()
|
|
32
|
+
if not key.startswith(("CODEX_", "CLAUDE_"))}
|
|
33
|
+
self.env.update(HOME=str(self.home), CODEX_ACCOUNTS_ROOT=str(self.pool),
|
|
34
|
+
PATH=str(self.fake) + os.pathsep + os.environ["PATH"],
|
|
35
|
+
CODEX_MULTIACC_AUTORESUME="0", CLAUDE_MULTIACC_PATH_PROBE="0", FAKE_ERROR="")
|
|
36
|
+
|
|
37
|
+
def account(self, name, weekly=100, balance="100", **patch):
|
|
38
|
+
directory = self.pool / name
|
|
39
|
+
directory.mkdir(exist_ok=True)
|
|
40
|
+
(directory / "auth.json").write_text('{"tokens":{"access_token":"fake"}}')
|
|
41
|
+
now = time.time()
|
|
42
|
+
credits = {"has_credits": True, "unlimited": False, "balance": balance,
|
|
43
|
+
"fetched_at": datetime.fromtimestamp(now, timezone.utc).isoformat(),
|
|
44
|
+
"account_id": "wallet-" + name, "ordinary_usage_allowed": weekly < 100,
|
|
45
|
+
"spend_control_reached": False, "limit_reached_type": "rate_limit_reached"}
|
|
46
|
+
credits.update(patch)
|
|
47
|
+
data = {"fetched_at": int(now), "max_percent": weekly, "weekly_percent": weekly,
|
|
48
|
+
"session_percent": 10, "credits": credits}
|
|
49
|
+
(directory / "limits.json").write_text(json.dumps(data, indent=1))
|
|
50
|
+
if weekly >= 90:
|
|
51
|
+
(directory / ".limited").write_text(
|
|
52
|
+
f"{int(now) + 86400}\nbucket=weekly percent={weekly} reason=limits\n")
|
|
53
|
+
return directory
|
|
54
|
+
|
|
55
|
+
def run_shim(self, probe=True, **environment):
|
|
56
|
+
env = dict(self.env, **environment)
|
|
57
|
+
if probe:
|
|
58
|
+
env["CODEX_MULTIACC_AR_PROBE"] = "1"
|
|
59
|
+
with open(os.devnull) as source:
|
|
60
|
+
return subprocess.run([str(ROOT / "bin/codex"), "exec", "test"],
|
|
61
|
+
env=env, stdin=source, capture_output=True, text=True, timeout=15)
|
|
62
|
+
|
|
63
|
+
def test_paid_after_included_and_reserve_and_kill_switch(self):
|
|
64
|
+
paid = self.account("acct-01")
|
|
65
|
+
result = self.run_shim()
|
|
66
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
67
|
+
self.assertIn("pick=acct-01 tier=credits", result.stdout)
|
|
68
|
+
self.assertFalse((paid / ".credit-sessions").exists(), "probe must not reserve funds")
|
|
69
|
+
self.account("acct-02", weekly=95)
|
|
70
|
+
self.assertIn("pick=acct-02 tier=soft", self.run_shim().stdout)
|
|
71
|
+
self.account("acct-03", weekly=20)
|
|
72
|
+
self.assertIn("pick=acct-03 tier=eligible", self.run_shim().stdout)
|
|
73
|
+
(self.pool / "acct-02" / "auth.json").unlink()
|
|
74
|
+
(self.pool / "acct-03" / "auth.json").unlink()
|
|
75
|
+
self.assertIn("tier=hard", self.run_shim(CODEX_MULTIACC_CREDITS="0").stdout)
|
|
76
|
+
|
|
77
|
+
def test_zero_unknown_future_spend_control_and_auth_never_paid(self):
|
|
78
|
+
for patch in ({"balance": "0"}, {"balance": None}, {"last_error": "unauthenticated"},
|
|
79
|
+
{"fetched_at": "2099-01-01T00:00:00Z"}, {"spend_control_reached": True},
|
|
80
|
+
{"account_id": None}, {"limit_reached_type": "workspace_owner_credits_depleted"}):
|
|
81
|
+
with self.subTest(patch=patch):
|
|
82
|
+
self.account("acct-01", **patch)
|
|
83
|
+
self.assertIn("tier=hard", self.run_shim().stdout)
|
|
84
|
+
directory = self.account("acct-01")
|
|
85
|
+
(directory / ".expired").write_text(f"{int(time.time())}\nreason=org-blocked\n")
|
|
86
|
+
self.assertIn("tier=none", self.run_shim().stdout)
|
|
87
|
+
|
|
88
|
+
def test_paid_refusal_is_durable_until_new_funds(self):
|
|
89
|
+
directory = self.account("acct-01")
|
|
90
|
+
result = self.run_shim(False, FAKE_ERROR="You've hit your usage limit")
|
|
91
|
+
self.assertEqual(result.returncode, 1, result.stderr)
|
|
92
|
+
self.assertEqual(credit_refusal_view(directory)["credit_blocked_balance"], "100")
|
|
93
|
+
self.assertIn("tier=hard", self.run_shim().stdout)
|
|
94
|
+
self.account("acct-01", balance="100")
|
|
95
|
+
self.assertIn("tier=hard", self.run_shim().stdout)
|
|
96
|
+
self.account("acct-01", balance="101")
|
|
97
|
+
self.assertIn("tier=credits", self.run_shim().stdout)
|
|
98
|
+
|
|
99
|
+
def test_transient_refusal_never_records_depletion(self):
|
|
100
|
+
directory = self.account("acct-01")
|
|
101
|
+
result = self.run_shim(False, FAKE_ERROR="429 Server is temporarily limiting requests (not your usage limit)")
|
|
102
|
+
self.assertEqual(result.returncode, 1)
|
|
103
|
+
self.assertFalse((directory / ".credit-refused.json").exists())
|
|
104
|
+
|
|
105
|
+
def test_holder_adjusted_balance_and_identity_bound_receipt(self):
|
|
106
|
+
directory = self.account("acct-01", balance="100")
|
|
107
|
+
self.account("acct-02", balance="60")
|
|
108
|
+
record_launch(directory, os.getpid(), time.time())
|
|
109
|
+
self.assertIn("pick=acct-02 tier=credits", self.run_shim().stdout)
|
|
110
|
+
record_refusal(directory, os.getpid(), time.time())
|
|
111
|
+
self.assertEqual(credit_refusal_view(directory)["limited_reason"], "credits")
|
|
112
|
+
self.account("acct-01", account_id="different-wallet")
|
|
113
|
+
self.assertEqual(credit_refusal_view(directory), {})
|
|
114
|
+
|
|
115
|
+
def test_credit_tier_autoresume_protocol(self):
|
|
116
|
+
self.assertEqual(parse_probe("pick=acct-02 tier=credits\n"), ("acct-02", "credits"))
|
|
117
|
+
self.assertTrue(probe_allows("quota", "acct-02", "credits", "acct-01"))
|
|
118
|
+
self.assertFalse(probe_allows("quota", "acct-01", "credits", "acct-01"))
|
|
119
|
+
self.assertFalse(probe_allows("auth", "acct-02", "credits", "acct-01"))
|
|
120
|
+
|
|
121
|
+
def test_paid_interactive_refusal_survives_watcher_handoff(self):
|
|
122
|
+
case = watcher_fixtures.CodexWatcherTests()
|
|
123
|
+
case.setUp()
|
|
124
|
+
self.addCleanup(case.tearDown)
|
|
125
|
+
state = case.write_state(["--yolo", "resume", watcher_fixtures.CX_SID], provider="codex")
|
|
126
|
+
with open(state, "a") as target:
|
|
127
|
+
target.write("funding_tier=credits\n")
|
|
128
|
+
case.rollout([watcher_fixtures.cx_meta(case.launched, cwd=str(case.work)),
|
|
129
|
+
watcher_fixtures.cx_error(case.t0, "usage_limit_exceeded")])
|
|
130
|
+
watcher, runner = case.watcher(state)
|
|
131
|
+
runner.probe_out = "pick=acct-02 tier=credits\n"
|
|
132
|
+
self.assertEqual(watcher.run(), 0)
|
|
133
|
+
fields, _ = case.relaunch_of(runner)
|
|
134
|
+
self.assertEqual(fields["funding_tier"], "credits")
|
|
135
|
+
self.assertEqual(fields["funding_pid"], str(case.PID))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
unittest.main()
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Paid fallback policy simulations, independent of credentials, network and quota."""
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
import json
|
|
5
|
+
import subprocess
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
from test_selector import ROOT, NOW, _candidate, _request
|
|
9
|
+
from selector_policy import select
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _credits(**patch):
|
|
13
|
+
value = {"has_credits": True, "unlimited": False, "balance": "61812.4520750000",
|
|
14
|
+
"fetched_at": NOW, "account_id": "wallet-a", "ordinary_usage_allowed": False,
|
|
15
|
+
"spend_control_reached": False, "limit_reached_type": "rate_limit_reached"}
|
|
16
|
+
return dict(value, **patch)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _paid(**patch):
|
|
20
|
+
return _candidate(engine="codex", weekly_pct=100, status="limited", limited_reason="usage",
|
|
21
|
+
limited_until="2026-08-26T09:00:00Z", credits=_credits(), credit_capable=True,
|
|
22
|
+
**patch)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FundingSelectionTests(unittest.TestCase):
|
|
26
|
+
def test_included_then_reserve_then_credits(self):
|
|
27
|
+
paid = _paid(account_id="paid")
|
|
28
|
+
reserve = _candidate(account_id="reserve", weekly_pct=97, session_pct=80)
|
|
29
|
+
included = _candidate(account_id="included", weekly_pct=75, session_pct=85)
|
|
30
|
+
for pool, winner, tier in (([paid, reserve, included], "included", "included"),
|
|
31
|
+
([paid, reserve], "reserve", "reserve"),
|
|
32
|
+
([paid], "paid", "credits")):
|
|
33
|
+
response = select(_request(pool, credit_fallback=True))
|
|
34
|
+
self.assertEqual((response["account_id"], response["funding_tier"]), (winner, tier))
|
|
35
|
+
self.assertEqual(response["score_basis"]["funding_tier"], tier)
|
|
36
|
+
self.assertEqual(select(_request([paid, included], credit_fallback=True,
|
|
37
|
+
required_funding_tier="credits"))["account_id"], "paid")
|
|
38
|
+
paid["session_pct"] = None
|
|
39
|
+
paid["credits"]["ordinary_usage_allowed"] = None
|
|
40
|
+
self.assertEqual(select(_request([paid], credit_fallback=True))["funding_tier"], "credits")
|
|
41
|
+
paid["credits"]["balance"] = "0"
|
|
42
|
+
self.assertFalse(select(_request([paid], credit_fallback=True))["ok"])
|
|
43
|
+
|
|
44
|
+
def test_included_preserves_gate_and_weekly_band(self):
|
|
45
|
+
pool = [_candidate(account_id="a", weekly_pct=10, session_pct=80),
|
|
46
|
+
_candidate(account_id="b", weekly_pct=60, session_pct=20),
|
|
47
|
+
_candidate(account_id="c", weekly_pct=75, session_pct=50), _paid(account_id="paid")]
|
|
48
|
+
winners = {select(_request(pool, credit_fallback=True, reservation_key=f"task-{n}"))["account_id"]
|
|
49
|
+
for n in range(100)}
|
|
50
|
+
self.assertEqual(winners, {"b", "c"})
|
|
51
|
+
|
|
52
|
+
def test_credits_fail_closed_for_all_restrictions(self):
|
|
53
|
+
bad_credits = ({"balance": "0"}, {"balance": None}, {"balance": "NaN"},
|
|
54
|
+
{"balance": "-1"}, {"has_credits": False}, {"account_id": None},
|
|
55
|
+
{"fetched_at": None}, {"fetched_at": "2026-08-25T09:00:31Z"},
|
|
56
|
+
{"last_error": "unauthenticated"}, {"last_error": "refusal_read_failed"},
|
|
57
|
+
{"spend_control_reached": True},
|
|
58
|
+
{"limit_reached_type": "workspace_owner_usage_limit_reached"},
|
|
59
|
+
{"limit_reached_type": "unknown"})
|
|
60
|
+
for patch in bad_credits:
|
|
61
|
+
with self.subTest(patch=patch):
|
|
62
|
+
row = _paid()
|
|
63
|
+
row["credits"].update(patch)
|
|
64
|
+
self.assertEqual(select(_request([row], credit_fallback=True))["error_code"], "no_candidate")
|
|
65
|
+
for patch in ({"credit_capable": False}, {"status": "expired"}, {"status": "blocked"},
|
|
66
|
+
{"limited_reason": "auth"}, {"limited_reason": "disabled"},
|
|
67
|
+
{"seen_at": "2026-08-25T08:54:59Z"}, {"provider_capable": False},
|
|
68
|
+
{"reservation_history": {"last_selected_at": None,
|
|
69
|
+
"active_expires_at": "2026-08-25T09:01:00Z"}}):
|
|
70
|
+
with self.subTest(patch=patch):
|
|
71
|
+
row = dict(_paid(), **patch)
|
|
72
|
+
self.assertEqual(select(_request([row], credit_fallback=True))["error_code"], "no_candidate")
|
|
73
|
+
|
|
74
|
+
def test_unlimited_and_balance_per_holder_rank_without_percentage_clamp(self):
|
|
75
|
+
a, b = _paid(account_id="a"), _paid(account_id="b")
|
|
76
|
+
a["credits"]["balance"], b["credits"]["balance"] = "60000.123", "50000"
|
|
77
|
+
a["credits"]["account_id"], b["credits"]["account_id"] = "wallet-a", "wallet-b"
|
|
78
|
+
a["credit_active_holders"] = 1
|
|
79
|
+
response = select(_request([a, b], credit_fallback=True))
|
|
80
|
+
self.assertEqual(response["account_id"], "b")
|
|
81
|
+
self.assertEqual(response["score_basis"]["credit_balance"], "50000")
|
|
82
|
+
a["credits"].update(unlimited=True, balance=None)
|
|
83
|
+
self.assertEqual(select(_request([a, b], credit_fallback=True))["account_id"], "a")
|
|
84
|
+
|
|
85
|
+
def test_paid_refusal_needs_cooldown_and_new_read_or_increased_balance(self):
|
|
86
|
+
row = _paid()
|
|
87
|
+
row.update(limited_reason="credits", credit_blocked_at="2026-08-25T08:59:00Z",
|
|
88
|
+
credit_blocked_balance="61812.452075")
|
|
89
|
+
self.assertFalse(select(_request([row], credit_fallback=True))["ok"])
|
|
90
|
+
row["credits"]["balance"] = "62000"
|
|
91
|
+
self.assertTrue(select(_request([row], credit_fallback=True))["ok"])
|
|
92
|
+
row["credits"]["balance"] = "61812.452075"
|
|
93
|
+
row["credit_blocked_at"] = "2026-08-25T08:49:00Z"
|
|
94
|
+
self.assertTrue(select(_request([row], credit_fallback=True))["ok"])
|
|
95
|
+
row["credits"]["fetched_at"] = "2026-08-25T08:48:00Z"
|
|
96
|
+
self.assertFalse(select(_request([row], credit_fallback=True))["ok"])
|
|
97
|
+
|
|
98
|
+
def test_funding_proof_covers_inputs_and_repeats(self):
|
|
99
|
+
request = _request([_paid()], credit_fallback=True)
|
|
100
|
+
first = select(request)
|
|
101
|
+
self.assertEqual(first, select(deepcopy(request)))
|
|
102
|
+
self.assertEqual(first["credit_capability"], "codex_credits_v1")
|
|
103
|
+
self.assertEqual(first["candidate_snapshot_digest"],
|
|
104
|
+
"ccf7afd64b788f03e3fd9a1e6f4b2b0741bee957dd18ff028d1f328177dff0d6")
|
|
105
|
+
self.assertEqual(first["selection_digest"],
|
|
106
|
+
"6a3f9b1b7c90dfd68841482d4fec30e18152338d401deab0a384c75f11750f4e")
|
|
107
|
+
request["candidates"][0]["credits"]["balance"] = "1"
|
|
108
|
+
self.assertNotEqual(first["candidate_snapshot_digest"], select(request)["candidate_snapshot_digest"])
|
|
109
|
+
request["credit_max_age_seconds"] = 600
|
|
110
|
+
self.assertNotEqual(first["selection_digest"], select(request)["selection_digest"])
|
|
111
|
+
|
|
112
|
+
def test_replicated_wallet_uses_newest_balance_and_shared_holder_count(self):
|
|
113
|
+
old, latest, other = _paid(account_id="old"), _paid(account_id="latest"), _paid(account_id="other")
|
|
114
|
+
old["credits"].update(balance="100000", fetched_at="2026-08-25T08:59:00Z")
|
|
115
|
+
latest["credits"].update(balance="100")
|
|
116
|
+
other["credits"].update(account_id="different-wallet", balance="75")
|
|
117
|
+
latest["credit_active_holders"] = 1
|
|
118
|
+
self.assertEqual(select(_request([old, latest, other], credit_fallback=True))["account_id"], "other")
|
|
119
|
+
latest["credits"]["balance"] = "0"
|
|
120
|
+
self.assertFalse(select(_request([old, latest], credit_fallback=True))["ok"])
|
|
121
|
+
|
|
122
|
+
def test_opt_in_and_capability_fence(self):
|
|
123
|
+
self.assertEqual(select(_request([_paid()]))["error_code"], "invalid_request")
|
|
124
|
+
for patch in ({"credit_fallback": []}, {"required_funding_tier": []},
|
|
125
|
+
{"credit_max_age_seconds": True}, {"credit_max_age_seconds": 3601}):
|
|
126
|
+
response = select(_request([_candidate()], credit_fallback=True, **patch)) \
|
|
127
|
+
if "credit_fallback" not in patch else select(_request([_candidate()], **patch))
|
|
128
|
+
self.assertEqual(response["error_code"], "invalid_request")
|
|
129
|
+
output = subprocess.check_output([str(ROOT / "bin/multiacc-select"), "--capabilities"], text=True)
|
|
130
|
+
self.assertEqual(json.loads(output)["capabilities"], ["codex_credits_v1"])
|
|
131
|
+
|
|
132
|
+
for key, value in (("account_id", []), ("balance", {}), ("unlimited", 1), ("stale", "false")):
|
|
133
|
+
row = _paid()
|
|
134
|
+
row["credits"][key] = value
|
|
135
|
+
response = select(_request([row], credit_fallback=True))
|
|
136
|
+
self.assertEqual(response["error_code"], "invalid_request")
|
|
137
|
+
self.assertEqual(response["error_detail"]["field"], f"candidates[0].credits.{key}")
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
unittest.main()
|