claude-multiacc 1.0.20 → 2.0.0

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.
@@ -0,0 +1,201 @@
1
+ """Strict transport, normalization, and RFC 8785 primitives for pool-selection.v2."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import math
8
+ import re
9
+ import unicodedata
10
+ from datetime import datetime, timezone
11
+ from decimal import Decimal, InvalidOperation
12
+
13
+ SCHEMA = "claude-multiacc/pool-selection.v2"
14
+ SELECTOR_VERSION = "2.0.0"
15
+ TIME_RE = re.compile(
16
+ r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-5][0-9]"
17
+ r"(\.[0-9]{1,6})?(Z|[+-][0-9]{2}:[0-9]{2})$")
18
+ DECIMAL_RE = re.compile(r"^-?(0|[1-9][0-9]*)(\.[0-9]+)?$")
19
+ SAFE_INTEGER = 9_007_199_254_740_991
20
+ MAX_PLAIN_DECIMAL_PLACES = 10_000
21
+ IDENTITY_KEYS = {"runner_id", "runner_generation", "engine", "account_id"}
22
+ CANDIDATE_KEYS = IDENTITY_KEYS | {
23
+ "status", "weekly_pct", "session_pct", "resets_at", "limited_until", "seen_at",
24
+ "provider_capable", "reservation_history"}
25
+ REQUEST_KEYS = {
26
+ "schema", "database_now", "policy", "required_engine", "producer_identity",
27
+ "excluded_identities", "candidates", "reservation_key"}
28
+ POLICIES = {
29
+ "default_claude", "default_codex", "default_both", "explicit", "producer_retry", "reviewer"}
30
+
31
+
32
+ def timestamp(value: object) -> tuple[datetime, str]:
33
+ if not isinstance(value, str) or not TIME_RE.fullmatch(value):
34
+ raise ValueError("invalid timestamp")
35
+ try:
36
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
37
+ utc = parsed.astimezone(timezone.utc).replace(tzinfo=None)
38
+ except (ValueError, OverflowError) as error:
39
+ raise ValueError("invalid timestamp") from error
40
+ return parsed, utc.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
41
+
42
+
43
+ def decimal_value(value: object) -> tuple[Decimal | None, str | None]:
44
+ if isinstance(value, bool) or value is None:
45
+ return None, None
46
+ if isinstance(value, str) and not DECIMAL_RE.fullmatch(value):
47
+ return None, None
48
+ try:
49
+ parsed = Decimal(str(value))
50
+ if not parsed.is_finite() or (isinstance(value, float) and not math.isfinite(value)):
51
+ return None, None
52
+ if parsed <= 0:
53
+ clamped = Decimal(0)
54
+ elif parsed >= 100:
55
+ clamped = Decimal(100)
56
+ elif parsed.as_tuple().exponent < -MAX_PLAIN_DECIMAL_PLACES:
57
+ return None, None
58
+ else:
59
+ clamped = parsed
60
+ result = format(clamped, "f") if clamped else "0"
61
+ except (InvalidOperation, ValueError, OverflowError):
62
+ return None, None
63
+ return clamped, result.rstrip("0").rstrip(".") if "." in result else result
64
+
65
+
66
+ def identity(raw: dict) -> tuple[int | None, int | None, str | None, str | None, bool]:
67
+ runner = raw.get("runner_id")
68
+ generation = raw.get("runner_generation")
69
+ engine_value = raw.get("engine")
70
+ account_value = raw.get("account_id")
71
+ engine = engine_value.strip().lower() if isinstance(engine_value, str) else None
72
+ account = unicodedata.normalize("NFC", account_value).strip() \
73
+ if isinstance(account_value, str) else None
74
+ runner = runner if isinstance(runner, int) and not isinstance(runner, bool) and runner > 0 else None
75
+ generation = generation if isinstance(generation, int) \
76
+ and not isinstance(generation, bool) and generation > 0 else None
77
+ engine = engine if engine in {"claude", "codex"} else None
78
+ account = account or None
79
+ return runner, generation, engine, account, all((runner, generation, engine, account))
80
+
81
+
82
+ def collision_key(parts: tuple) -> tuple:
83
+ runner, generation, engine, account = parts
84
+ folded = unicodedata.normalize("NFC", account.casefold()) if account else account
85
+ return runner, generation, engine, folded
86
+
87
+
88
+ def _encode_rfc8785(value: object) -> str:
89
+ if value is None:
90
+ return "null"
91
+ if value is True:
92
+ return "true"
93
+ if value is False:
94
+ return "false"
95
+ if isinstance(value, int):
96
+ if abs(value) > SAFE_INTEGER:
97
+ raise ValueError("integer outside RFC 8785 interoperable range")
98
+ return str(value)
99
+ if isinstance(value, str):
100
+ value.encode("utf-8")
101
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
102
+ if isinstance(value, list):
103
+ return "[" + ",".join(_encode_rfc8785(item) for item in value) + "]"
104
+ if isinstance(value, dict):
105
+ if any(not isinstance(key, str) for key in value):
106
+ raise ValueError("RFC 8785 object keys must be strings")
107
+ keys = sorted(value, key=lambda key: key.encode("utf-16-be", "surrogatepass"))
108
+ pairs = (_encode_rfc8785(key) + ":" + _encode_rfc8785(value[key]) for key in keys)
109
+ return "{" + ",".join(pairs) + "}"
110
+ raise ValueError(f"value outside frozen RFC 8785 digest domain: {type(value).__name__}")
111
+
112
+
113
+ def canonical_sha256(value: object) -> str:
114
+ return hashlib.sha256(_encode_rfc8785(value).encode("utf-8")).hexdigest()
115
+
116
+
117
+ def _identity_error(value: object, field: str) -> str | None:
118
+ if not isinstance(value, dict) or set(value) != IDENTITY_KEYS:
119
+ return field
120
+ runner, generation, _, account, valid = identity(value)
121
+ if not isinstance(value.get("engine"), str) or not valid:
122
+ return field
123
+ if abs(runner or 0) > SAFE_INTEGER or abs(generation or 0) > SAFE_INTEGER or account is None:
124
+ return field
125
+ return None
126
+
127
+
128
+ def _candidate_error(value: object, index: int) -> str | None:
129
+ field = f"candidates[{index}]"
130
+ if not isinstance(value, dict) or set(value) != CANDIDATE_KEYS:
131
+ return field
132
+ for name in ("runner_id", "runner_generation"):
133
+ item = value[name]
134
+ if not isinstance(item, int) or isinstance(item, bool) or not 0 < item <= SAFE_INTEGER:
135
+ return f"{field}.{name}"
136
+ if not all(isinstance(value[name], str) for name in ("engine", "account_id", "status", "seen_at")):
137
+ return field
138
+ if not isinstance(value["provider_capable"], bool):
139
+ return f"{field}.provider_capable"
140
+ scalars = (str, int, float, Decimal, bool, type(None))
141
+ if any(not isinstance(value[name], scalars) for name in ("weekly_pct", "session_pct")):
142
+ return field
143
+ if any(isinstance(value[name], float) and not math.isfinite(value[name])
144
+ for name in ("weekly_pct", "session_pct")):
145
+ return field
146
+ if any(item is not None and not isinstance(item, str)
147
+ for item in (value["resets_at"], value["limited_until"])):
148
+ return field
149
+ history = value["reservation_history"]
150
+ if not isinstance(history, dict) or set(history) != {"active_expires_at", "last_selected_at"}:
151
+ return f"{field}.reservation_history"
152
+ if any(item is not None and not isinstance(item, str) for item in history.values()):
153
+ return f"{field}.reservation_history"
154
+ if history["active_expires_at"] is not None:
155
+ try:
156
+ timestamp(history["active_expires_at"])
157
+ except ValueError:
158
+ return f"{field}.reservation_history.active_expires_at"
159
+ return None
160
+
161
+
162
+ def request_error(request: object) -> tuple[str, dict] | None:
163
+ if not isinstance(request, dict):
164
+ return "invalid_request", {"field": "$"}
165
+ if "schema" not in request:
166
+ return "invalid_request", {"field": "schema"}
167
+ if request.get("schema") != SCHEMA:
168
+ return "unsupported_schema", {"field": "schema"}
169
+ if set(request) != REQUEST_KEYS:
170
+ return "invalid_request", {"field": "$"}
171
+ try:
172
+ timestamp(request["database_now"])
173
+ except ValueError:
174
+ return "invalid_request", {"field": "database_now"}
175
+ policy, required = request["policy"], request["required_engine"]
176
+ if not isinstance(policy, str) or policy not in POLICIES:
177
+ return "invalid_request", {"field": "policy"}
178
+ if required is not None and (not isinstance(required, str) or required not in {"claude", "codex"}):
179
+ return "invalid_request", {"field": "required_engine"}
180
+ needs_engine = policy in {"explicit", "producer_retry", "reviewer"}
181
+ if (needs_engine and required is None) or (not needs_engine and required is not None):
182
+ return "invalid_request", {"field": "required_engine"}
183
+ producer = request["producer_identity"]
184
+ if (policy == "reviewer") != (producer is not None):
185
+ return "invalid_request", {"field": "producer_identity"}
186
+ if producer is not None and (error := _identity_error(producer, "producer_identity")):
187
+ return "invalid_request", {"field": error}
188
+ excluded = request["excluded_identities"]
189
+ if not isinstance(excluded, list):
190
+ return "invalid_request", {"field": "excluded_identities"}
191
+ for index, value in enumerate(excluded):
192
+ if error := _identity_error(value, f"excluded_identities[{index}]"):
193
+ return "invalid_request", {"field": error}
194
+ if not isinstance(request["reservation_key"], str) or not request["reservation_key"].strip():
195
+ return "invalid_request", {"field": "reservation_key"}
196
+ if not isinstance(request["candidates"], list):
197
+ return "invalid_request", {"field": "candidates"}
198
+ for index, value in enumerate(request["candidates"]):
199
+ if error := _candidate_error(value, index):
200
+ return "invalid_request", {"field": error}
201
+ return None
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "1.0.20",
4
- "description": "Multi-account addon for Claude Code and OpenAI Codex CLI: every claude / claude -p and every codex / codex exec runs under a randomly-picked subscription account with the most usage headroom. Mirrors to a deploy server. No API keys.",
3
+ "version": "2.0.0",
4
+ "description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "claude-multiacc": "bin/cli.mjs"
7
+ "claude-multiacc": "bin/cli.mjs",
8
+ "multiacc-select": "bin/multiacc-select"
8
9
  },
9
10
  "files": [
10
11
  "bin/",
@@ -12,6 +13,7 @@
12
13
  "install.sh",
13
14
  "scripts/postinstall.mjs",
14
15
  "tests/",
16
+ "docs/",
15
17
  "README.md",
16
18
  "CLAUDE_ACCS_TASK.md"
17
19
  ],
@@ -51,7 +53,7 @@
51
53
  },
52
54
  "scripts": {
53
55
  "postinstall": "node scripts/postinstall.mjs",
54
- "test": "bash tests/run-tests.sh"
56
+ "test": "bash tests/run-tests.sh && python3 tests/test_selector.py"
55
57
  },
56
58
  "dependencies": {
57
59
  "update-notifier": "^7.3.1"
@@ -253,11 +253,30 @@ done
253
253
  && t_ok "CLAUDE_SHIM_SELECT=random restores uniform spread" \
254
254
  || t_fail "random opt-out" "acct-01=$hits1 acct-02=$hits2"
255
255
 
256
- # stale telemetry is neutral, never assumed free
256
+ # Unknown telemetry ranks behind every truthful reading, never as neutral or free.
257
257
  printf '{"fetched_at":1,"weekly_percent":1,"session_percent":1,"max_percent":1,"buckets":[]}' > "$ACC/acct-01/limits.json"
258
258
  lj 30 30 30 > "$ACC/acct-02/limits.json"
259
259
  out="$(claude 2>&1)"
260
260
  check "stale 1% loses to fresh 30% (stale is not trusted)" "CFG=acct-02" "$out"
261
+ lj 88 88 88 > "$ACC/acct-01/limits.json"
262
+ rm -f "$ACC/acct-02/limits.json" "$ACC/.last-pick"
263
+ all1=1
264
+ for _ in $(seq 1 15); do
265
+ case "$(claude 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
266
+ done
267
+ [ "$all1" = "1" ] && t_ok "unknown telemetry never beats a known account at 88%" \
268
+ || t_fail "unknown telemetry ranking" "the unknown account beat truthful 88% usage"
269
+ rm -f "$ACC/acct-01/limits.json" "$ACC/.last-pick"
270
+ hits1=0; hits2=0
271
+ for _ in $(seq 1 40); do
272
+ case "$(claude 2>&1)" in
273
+ *CFG=acct-01*) hits1=$((hits1+1)) ;;
274
+ *CFG=acct-02*) hits2=$((hits2+1)) ;;
275
+ esac
276
+ done
277
+ { [ "$hits1" -gt 0 ] && [ "$hits2" -gt 0 ]; } \
278
+ && t_ok "an entirely unknown Claude pool still fails open" \
279
+ || t_fail "unknown Claude pool fail-open" "acct-01=$hits1 acct-02=$hits2"
261
280
  rm -f "$ACC"/acct-*/limits.json
262
281
 
263
282
  # ---- 6. explicit pin -------------------------------------------------------
@@ -2525,11 +2544,30 @@ done
2525
2544
  { [ "$hits1" -gt 0 ] && [ "$hits2" -gt 0 ] && [ $((hits1+hits2)) -eq 40 ]; } \
2526
2545
  && t_ok "codex: equal scores spread randomly (acct-01=$hits1 acct-02=$hits2)" \
2527
2546
  || t_fail "codex tie spreading" "acct-01=$hits1 acct-02=$hits2 (want both >0, total 40)"
2528
- # stale telemetry ranks neutral, never free
2547
+ # Unknown telemetry ranks behind every truthful reading, never as neutral or free.
2529
2548
  printf '{"fetched_at":1,"weekly_percent":1,"session_percent":1,"max_percent":1,"buckets":[]}' > "$CX/acct-01/limits.json"
2530
2549
  cxlj 30 30 30 > "$CX/acct-02/limits.json"
2531
2550
  out="$(codex 2>&1)"
2532
2551
  check "codex: stale 1% loses to fresh 30%" "CFG=acct-02" "$out"
2552
+ cxlj 88 88 88 > "$CX/acct-01/limits.json"
2553
+ rm -f "$CX/acct-02/limits.json" "$CX/.last-pick"
2554
+ all1=1
2555
+ for _ in $(seq 1 15); do
2556
+ case "$(codex 2>&1)" in *CFG=acct-01*) ;; *) all1=0 ;; esac
2557
+ done
2558
+ [ "$all1" = "1" ] && t_ok "codex: unknown telemetry never beats known 88%" \
2559
+ || t_fail "codex unknown telemetry ranking" "the unknown account beat truthful 88% usage"
2560
+ rm -f "$CX/acct-01/limits.json" "$CX/.last-pick"
2561
+ hits1=0; hits2=0
2562
+ for _ in $(seq 1 40); do
2563
+ case "$(codex 2>&1)" in
2564
+ *CFG=acct-01*) hits1=$((hits1+1)) ;;
2565
+ *CFG=acct-02*) hits2=$((hits2+1)) ;;
2566
+ esac
2567
+ done
2568
+ { [ "$hits1" -gt 0 ] && [ "$hits2" -gt 0 ]; } \
2569
+ && t_ok "codex: an entirely unknown pool still fails open" \
2570
+ || t_fail "codex unknown pool fail-open" "acct-01=$hits1 acct-02=$hits2"
2533
2571
  rm -f "$CX"/acct-*/limits.json
2534
2572
 
2535
2573
  # ---- C4. pin -----------------------------------------------------------------------
@@ -0,0 +1,209 @@
1
+ """Golden and transport tests for pool-selection.v2."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ import unittest
9
+ from pathlib import Path
10
+
11
+ ROOT = Path(__file__).resolve().parents[1]
12
+ sys.path.insert(0, str(ROOT / "lib"))
13
+
14
+ from selector_policy import select # noqa: E402
15
+ from selector_primitives import canonical_sha256 # noqa: E402
16
+
17
+ SCHEMA = "claude-multiacc/pool-selection.v2"
18
+ NOW = "2026-08-25T09:00:00.000000Z"
19
+
20
+
21
+ def _candidate(**patch) -> dict:
22
+ row = {
23
+ "runner_id": 3, "runner_generation": 7, "engine": "claude", "account_id": "acct-a",
24
+ "status": "active", "weekly_pct": 20, "session_pct": 40,
25
+ "resets_at": "2026-08-26T09:00:00Z", "limited_until": None,
26
+ "seen_at": "2026-08-25T08:59:00Z", "provider_capable": True,
27
+ "reservation_history": {"active_expires_at": None, "last_selected_at": None}}
28
+ row.update(patch)
29
+ return row
30
+
31
+
32
+ def _request(candidates: list[dict], **patch) -> dict:
33
+ request = {
34
+ "schema": SCHEMA, "database_now": NOW, "policy": "default_both",
35
+ "required_engine": None, "producer_identity": None, "excluded_identities": [],
36
+ "candidates": candidates, "reservation_key": "proof"}
37
+ request.update(patch)
38
+ return request
39
+
40
+
41
+ class SelectorTests(unittest.TestCase):
42
+ def test_union_proof_vector_is_byte_stable(self):
43
+ request = _request([
44
+ _candidate(engine="claude", account_id="claude-a", weekly_pct=10, session_pct=20,
45
+ seen_at="2026-08-25T08:54:59Z"),
46
+ _candidate(engine="codex", account_id="codex-b", weekly_pct=30, session_pct=40),
47
+ _candidate(engine="both", account_id=" ", weekly_pct=0, session_pct=0),
48
+ ], reservation_key="launch-1")
49
+ response = select(request)
50
+ self.assertEqual(response["engine"], "codex")
51
+ self.assertEqual(response["account_id"], "codex-b")
52
+ self.assertEqual(
53
+ response["candidate_snapshot_digest"],
54
+ "e8ef0b4bc7b95e76231ee6da79616d92bd9251c1c991845c8c0524fcbfb6593a")
55
+ self.assertEqual(
56
+ response["selection_digest"],
57
+ "66179083753e967bbe3914692103c1b4a30966a86ecf85cd62e14f3511c78cc3")
58
+
59
+ def test_unicode_and_active_reservation_proof(self):
60
+ busy = {"active_expires_at": "2026-08-25T09:00:00.000001Z", "last_selected_at": None}
61
+ response = select(_request([
62
+ _candidate(engine="claude", account_id="claude-active", reservation_history=busy),
63
+ _candidate(engine="codex", account_id="café-🚀", weekly_pct=25, session_pct=50),
64
+ ], policy="explicit", required_engine="codex", reservation_key="launch-unicode"))
65
+ self.assertEqual((response["engine"], response["account_id"]), ("codex", "café-🚀"))
66
+ self.assertEqual(
67
+ response["candidate_snapshot_digest"],
68
+ "29e8e7d1a611f38ee0be43622102b33875e7ff75ff9024aa43d16151575071d4")
69
+ self.assertEqual(
70
+ response["selection_digest"],
71
+ "eaea620082bb0882a12598b8cc46e79090da07be0b307f34f0a68f555a6c9d3e")
72
+
73
+ def test_both_chooses_each_provider_and_survives_provider_loss(self):
74
+ codex = select(_request([
75
+ _candidate(engine="claude", account_id="claude-a", weekly_pct=70, session_pct=60),
76
+ _candidate(engine="codex", account_id="codex-a", weekly_pct=20, session_pct=30)]))
77
+ claude = select(_request([
78
+ _candidate(engine="claude", account_id="claude-a", weekly_pct=10, session_pct=30),
79
+ _candidate(engine="codex", account_id="codex-a", weekly_pct=20, session_pct=80)]))
80
+ only = select(_request([_candidate(engine="codex", account_id="codex-only")]))
81
+ self.assertEqual((codex["engine"], claude["engine"], only["engine"]),
82
+ ("codex", "claude", "codex"))
83
+
84
+ def test_known_quota_and_least_recent_selection_win(self):
85
+ known = select(_request([
86
+ _candidate(engine="claude", account_id="unknown", weekly_pct=None, session_pct=None),
87
+ _candidate(engine="codex", account_id="known", weekly_pct=99, session_pct=99)]))
88
+ old = {"active_expires_at": None, "last_selected_at": "2026-08-25T08:00:00Z"}
89
+ recent = {"active_expires_at": None, "last_selected_at": "2026-08-25T08:50:00Z"}
90
+ fair = select(_request([
91
+ _candidate(engine="claude", account_id="recent", weekly_pct=20, session_pct=20,
92
+ reservation_history=recent),
93
+ _candidate(engine="codex", account_id="old", weekly_pct=20, session_pct=20,
94
+ reservation_history=old)]))
95
+ self.assertEqual((known["account_id"], fair["account_id"]), ("known", "old"))
96
+ utc = select(_request([_candidate()]))
97
+ offset = select(_request(
98
+ [_candidate()], database_now="2026-08-25T12:00:00+03:00"))
99
+ self.assertEqual(utc["selection_digest"], offset["selection_digest"])
100
+
101
+ def test_explicit_and_reviewer_policies_never_cross_provider(self):
102
+ explicit = select(_request([
103
+ _candidate(engine="claude", account_id="free", weekly_pct=0, session_pct=0),
104
+ _candidate(engine="codex", account_id="busy", weekly_pct=90, session_pct=90),
105
+ ], policy="explicit", required_engine="codex"))
106
+ producer = {"runner_id": 3, "runner_generation": 7, "engine": "claude",
107
+ "account_id": "producer"}
108
+ review = select(_request([
109
+ _candidate(engine="claude", account_id="producer", weekly_pct=0, session_pct=0),
110
+ _candidate(engine="claude", account_id="reviewer", weekly_pct=90, session_pct=90),
111
+ ], policy="reviewer", required_engine="claude", producer_identity=producer,
112
+ excluded_identities=[producer]))
113
+ sole = select(_request([
114
+ _candidate(engine="claude", account_id="producer")], policy="reviewer",
115
+ required_engine="claude", producer_identity=producer))
116
+ alternative = {"runner_id": 4, "runner_generation": 7, "engine": "claude",
117
+ "account_id": "alternative"}
118
+ fallback = select(_request([
119
+ _candidate(account_id="producer"),
120
+ _candidate(runner_id=4, account_id="alternative")], policy="reviewer",
121
+ required_engine="claude", producer_identity=producer,
122
+ excluded_identities=[alternative]))
123
+ self.assertEqual((explicit["engine"], review["account_id"]), ("codex", "reviewer"))
124
+ self.assertEqual(sole["fallback_reason"], "sole_eligible_account")
125
+ self.assertEqual((fallback["account_id"], fallback["fallback_reason"]),
126
+ ("producer", "sole_eligible_account"))
127
+
128
+ def test_stable_errors_fail_closed(self):
129
+ collisions = [(" Acct-A ", "acct-a"), ("CAFÉ", "cafe\u0301")]
130
+ duplicates = [select(_request([
131
+ _candidate(engine=" CLAUDE ", account_id=left),
132
+ _candidate(engine="claude", account_id=right)])) for left, right in collisions]
133
+ no_candidate = select(_request([
134
+ _candidate(engine="claude")], policy="default_codex"))
135
+ invalid = select({**_request([]), "unexpected": True})
136
+ preserved = select(_request([_candidate(account_id=" Acct-A ")]))
137
+ overflow_now = select(_request(
138
+ [], database_now="9999-12-31T23:59:59-01:00"))
139
+ overflow_seen = select(_request([
140
+ _candidate(seen_at="0001-01-01T00:00:00+01:00")]))
141
+ overflow_history = select(_request([_candidate(reservation_history={
142
+ "active_expires_at": "9999-12-31T23:59:59-01:00", "last_selected_at": None})]))
143
+ self.assertTrue(all(item["error_code"] == "duplicate_candidate_identity"
144
+ for item in duplicates))
145
+ self.assertTrue(all(item["error_detail"] == {"input_ordinals": [0, 1]}
146
+ for item in duplicates))
147
+ self.assertEqual(no_candidate["error_code"], "no_candidate")
148
+ self.assertEqual(preserved["account_id"], "Acct-A")
149
+ self.assertEqual(overflow_now["error_detail"], {"field": "database_now"})
150
+ self.assertEqual(overflow_seen["error_code"], "no_candidate")
151
+ self.assertEqual(overflow_history["error_detail"], {
152
+ "field": "candidates[0].reservation_history.active_expires_at"})
153
+ self.assertEqual(invalid, {"schema": SCHEMA, "selector_version": "2.0.0", "ok": False,
154
+ "error_code": "invalid_request", "error_detail": {"field": "$"}})
155
+
156
+ def test_rfc8785_digest_vectors(self):
157
+ self.assertEqual(
158
+ canonical_sha256({"account_id": "café-🚀", "metadata": {"": "bmp", "😀": "astral"}}),
159
+ "2f33747d98d7c2f3cfc0a660eba492a7652a66bc46b147edeccf4cc941154981")
160
+ with self.assertRaises(ValueError):
161
+ canonical_sha256({"unsafe": 9_007_199_254_740_992})
162
+
163
+ def test_cli_transport_and_version_contract(self):
164
+ binary = ROOT / "bin" / "multiacc-select"
165
+ args = [str(binary), "--request-json", "-", "--response-json", "-"]
166
+ good_payload = json.dumps(_request([_candidate(engine="codex")]), separators=(",", ":"))
167
+ good = subprocess.run(args, input=good_payload, text=True, capture_output=True, check=True)
168
+ bad_payloads = [
169
+ good_payload.replace('"weekly_pct":20', '"weekly_pct":NaN', 1),
170
+ good_payload.replace(
171
+ '"weekly_pct":20', '"weekly_pct":1e999999999999999999999999999999', 1),
172
+ good_payload.replace('"policy":"default_both"',
173
+ '"policy":"explicit","policy":"default_both"', 1),
174
+ good_payload.replace('"active_expires_at":null',
175
+ '"active_expires_at":null,"active_expires_at":null', 1),
176
+ good_payload + "{}",
177
+ ]
178
+ bad = [subprocess.run(args, input=payload, text=True, capture_output=True, check=True)
179
+ for payload in bad_payloads]
180
+ decimal_request = _request([
181
+ _candidate(account_id="z-lower", weekly_pct="LOW"),
182
+ _candidate(runner_id=4, account_id="a-higher", weekly_pct="HIGH")])
183
+ decimal_payload = json.dumps(decimal_request, separators=(",", ":"))
184
+ decimal_payload = decimal_payload.replace(
185
+ '"LOW"', "50.00000000000000000000000000001")
186
+ decimal_payload = decimal_payload.replace(
187
+ '"HIGH"', "50.00000000000000000000000000002")
188
+ decimal_response = subprocess.run(
189
+ args, input=decimal_payload, text=True, capture_output=True, check=True)
190
+ version = subprocess.run([str(binary), "--version"], text=True,
191
+ capture_output=True, check=True).stdout.strip()
192
+ version_script = (
193
+ "import {resolveVersion} from './scripts/auto-version.mjs';"
194
+ "console.log(resolveVersion('2.0.0','1.0.21'));"
195
+ "console.log(resolveVersion('2.0.0','2.0.0'));"
196
+ )
197
+ versions = subprocess.run(
198
+ ["node", "--input-type=module", "-e", version_script], cwd=ROOT,
199
+ text=True, capture_output=True, check=True).stdout.splitlines()
200
+ self.assertTrue(json.loads(good.stdout)["ok"])
201
+ self.assertTrue(all(json.loads(item.stdout)["error_code"] == "invalid_request"
202
+ for item in bad))
203
+ self.assertEqual(json.loads(decimal_response.stdout)["account_id"], "z-lower")
204
+ self.assertEqual(version, "2.0.0")
205
+ self.assertEqual(versions, ["2.0.0", "2.0.1"])
206
+
207
+
208
+ if __name__ == "__main__":
209
+ unittest.main()