claude-multiacc 2.0.64 → 2.0.66

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.
Files changed (233) hide show
  1. package/bin/claude-accounts +15 -201
  2. package/bin/codex +25 -7
  3. package/bin/codex-accounts +16 -3
  4. package/docs/AUTORESUME.md +10 -1
  5. package/docs/CODEX.md +39 -0
  6. package/lib/__pycache__/audit.cpython-314.pyc +0 -0
  7. package/lib/__pycache__/autoresume.cpython-314.pyc +0 -0
  8. package/lib/__pycache__/autoresume_cross.cpython-314.pyc +0 -0
  9. package/lib/__pycache__/autoresume_cross_dispatch.cpython-314.pyc +0 -0
  10. package/lib/__pycache__/autoresume_cross_receipt.cpython-314.pyc +0 -0
  11. package/lib/__pycache__/autoresume_handoff.cpython-314.pyc +0 -0
  12. package/lib/__pycache__/autoresume_probe.cpython-314.pyc +0 -0
  13. package/lib/__pycache__/autoresume_receipt.cpython-314.pyc +0 -0
  14. package/lib/__pycache__/claude_client_error.cpython-314.pyc +0 -0
  15. package/lib/__pycache__/claude_reset.cpython-314.pyc +0 -0
  16. package/lib/__pycache__/claude_token_identity.cpython-314.pyc +0 -0
  17. package/lib/__pycache__/codex_autoresume_mode.cpython-314.pyc +0 -0
  18. package/lib/__pycache__/codex_config_edit.cpython-314.pyc +0 -0
  19. package/lib/__pycache__/codex_credit_controls.cpython-314.pyc +0 -0
  20. package/lib/__pycache__/codex_credit_local_scope.cpython-314.pyc +0 -0
  21. package/lib/__pycache__/codex_credit_selection.cpython-314.pyc +0 -0
  22. package/lib/__pycache__/codex_credits.cpython-314.pyc +0 -0
  23. package/lib/__pycache__/codex_limits_lock.cpython-314.pyc +0 -0
  24. package/lib/__pycache__/codex_mcp_oauth.cpython-314.pyc +0 -0
  25. package/lib/__pycache__/codex_python.cpython-314.pyc +0 -0
  26. package/lib/__pycache__/codex_resume_cwd.cpython-314.pyc +0 -0
  27. package/lib/__pycache__/codex_speed.cpython-314.pyc +0 -0
  28. package/lib/__pycache__/codex_trust.cpython-314.pyc +0 -0
  29. package/lib/__pycache__/codex_usage_fetch.cpython-314.pyc +0 -0
  30. package/lib/__pycache__/keychain.cpython-314.pyc +0 -0
  31. package/lib/__pycache__/mcp_mirror.cpython-314.pyc +0 -0
  32. package/lib/__pycache__/mcp_oauth.cpython-314.pyc +0 -0
  33. package/lib/__pycache__/mcp_registry.cpython-314.pyc +0 -0
  34. package/lib/__pycache__/model_catalog.cpython-314.pyc +0 -0
  35. package/lib/__pycache__/model_match.cpython-314.pyc +0 -0
  36. package/lib/__pycache__/report.cpython-314.pyc +0 -0
  37. package/lib/__pycache__/selector_credit_scope.cpython-314.pyc +0 -0
  38. package/lib/__pycache__/selector_credits.cpython-314.pyc +0 -0
  39. package/lib/__pycache__/selector_funding.cpython-314.pyc +0 -0
  40. package/lib/__pycache__/selector_policy.cpython-314.pyc +0 -0
  41. package/lib/__pycache__/selector_primitives.cpython-314.pyc +0 -0
  42. package/lib/__pycache__/session_settings.cpython-314.pyc +0 -0
  43. package/lib/__pycache__/shim_path.cpython-314.pyc +0 -0
  44. package/lib/autoresume.py +28 -8
  45. package/lib/claude_oauth_lock.py +172 -0
  46. package/lib/claude_oauth_refresh.py +225 -0
  47. package/lib/codex_speed.py +351 -0
  48. package/lib/codex_speed.sh +39 -0
  49. package/lib/common.sh +13 -3
  50. package/lib/model_match.py +4 -1
  51. package/lib/session_settings.py +25 -6
  52. package/lib/shim_path.py +6 -0
  53. package/package.json +4 -3
  54. package/tests/__pycache__/autoresume_cross_receipt_support.cpython-314.pyc +0 -0
  55. package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-314.pyc +0 -0
  56. package/tests/__pycache__/autoresume_cross_tmux_support.cpython-314.pyc +0 -0
  57. package/tests/__pycache__/autoresume_cross_unit_support.cpython-314.pyc +0 -0
  58. package/tests/__pycache__/autoresume_tmux_support.cpython-314.pyc +0 -0
  59. package/tests/__pycache__/packaged_command_support.cpython-314.pyc +0 -0
  60. package/tests/__pycache__/test_autoresume.cpython-314.pyc +0 -0
  61. package/tests/__pycache__/test_autoresume_catalog.cpython-314.pyc +0 -0
  62. package/tests/__pycache__/test_autoresume_catalog_cli.cpython-314.pyc +0 -0
  63. package/tests/__pycache__/test_autoresume_codex_records.cpython-314.pyc +0 -0
  64. package/tests/__pycache__/test_autoresume_codex_trust.cpython-314.pyc +0 -0
  65. package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-314.pyc +0 -0
  66. package/tests/__pycache__/test_autoresume_cross_guards.cpython-314.pyc +0 -0
  67. package/tests/__pycache__/test_autoresume_cross_holders.cpython-314.pyc +0 -0
  68. package/tests/__pycache__/test_autoresume_cross_origin.cpython-314.pyc +0 -0
  69. package/tests/__pycache__/test_autoresume_cross_policy.cpython-314.pyc +0 -0
  70. package/tests/__pycache__/test_autoresume_cross_receipt.cpython-314.pyc +0 -0
  71. package/tests/__pycache__/test_autoresume_cross_tmux.cpython-314.pyc +0 -0
  72. package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-314.pyc +0 -0
  73. package/tests/__pycache__/test_autoresume_cross_tokens.cpython-314.pyc +0 -0
  74. package/tests/__pycache__/test_autoresume_cursor.cpython-314.pyc +0 -0
  75. package/tests/__pycache__/test_autoresume_cwd.cpython-314.pyc +0 -0
  76. package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-314.pyc +0 -0
  77. package/tests/__pycache__/test_autoresume_cwd_reader.cpython-314.pyc +0 -0
  78. package/tests/__pycache__/test_autoresume_cwd_tmux.cpython-314.pyc +0 -0
  79. package/tests/__pycache__/test_autoresume_fallback.cpython-314.pyc +0 -0
  80. package/tests/__pycache__/test_autoresume_handoff_argv.cpython-314.pyc +0 -0
  81. package/tests/__pycache__/test_autoresume_handoff_history.cpython-314.pyc +0 -0
  82. package/tests/__pycache__/test_autoresume_handoff_origins.cpython-314.pyc +0 -0
  83. package/tests/__pycache__/test_autoresume_memory.cpython-314.pyc +0 -0
  84. package/tests/__pycache__/test_autoresume_memory_tmux.cpython-314.pyc +0 -0
  85. package/tests/__pycache__/test_autoresume_mode.cpython-314.pyc +0 -0
  86. package/tests/__pycache__/test_autoresume_probe.cpython-314.pyc +0 -0
  87. package/tests/__pycache__/test_autoresume_receipt.cpython-314.pyc +0 -0
  88. package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-314.pyc +0 -0
  89. package/tests/__pycache__/test_autoresume_settings.cpython-314.pyc +0 -0
  90. package/tests/__pycache__/test_autoresume_sidechain.cpython-314.pyc +0 -0
  91. package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-314.pyc +0 -0
  92. package/tests/__pycache__/test_autoresume_tmux.cpython-314.pyc +0 -0
  93. package/tests/__pycache__/test_autoresume_tmux_modes.cpython-314.pyc +0 -0
  94. package/tests/__pycache__/test_autoresume_trust_tmux.cpython-314.pyc +0 -0
  95. package/tests/__pycache__/test_claude_oauth_lock.cpython-314.pyc +0 -0
  96. package/tests/__pycache__/test_claude_reset.cpython-314.pyc +0 -0
  97. package/tests/__pycache__/test_claude_token_identity.cpython-314.pyc +0 -0
  98. package/tests/__pycache__/test_codex_exec_stdin.cpython-314.pyc +0 -0
  99. package/tests/__pycache__/test_codex_mcp_oauth.cpython-314.pyc +0 -0
  100. package/tests/__pycache__/test_codex_reset.cpython-314.pyc +0 -0
  101. package/tests/__pycache__/test_codex_reset_credits.cpython-314.pyc +0 -0
  102. package/tests/__pycache__/test_codex_reset_credits_polling.cpython-314.pyc +0 -0
  103. package/tests/__pycache__/test_codex_reset_credits_report.cpython-314.pyc +0 -0
  104. package/tests/__pycache__/test_codex_reset_fetch.cpython-314.pyc +0 -0
  105. package/tests/__pycache__/test_codex_reset_polling.cpython-314.pyc +0 -0
  106. package/tests/__pycache__/test_codex_reset_reporting.cpython-314.pyc +0 -0
  107. package/tests/__pycache__/test_codex_reset_windows.cpython-314.pyc +0 -0
  108. package/tests/__pycache__/test_mcp_mirror.cpython-314.pyc +0 -0
  109. package/tests/__pycache__/test_mcp_mirror_args.cpython-314.pyc +0 -0
  110. package/tests/__pycache__/test_mcp_oauth.cpython-314.pyc +0 -0
  111. package/tests/__pycache__/test_mcp_registry.cpython-314.pyc +0 -0
  112. package/tests/__pycache__/test_selector.cpython-314.pyc +0 -0
  113. package/tests/__pycache__/test_selector_cached_credits.cpython-314.pyc +0 -0
  114. package/tests/__pycache__/test_selector_credit_scope.cpython-314.pyc +0 -0
  115. package/tests/__pycache__/test_selector_credit_shim.cpython-314.pyc +0 -0
  116. package/tests/__pycache__/test_selector_credits.cpython-314.pyc +0 -0
  117. package/tests/__pycache__/test_selector_spread.cpython-314.pyc +0 -0
  118. package/tests/__pycache__/test_token_identity_ceremony.cpython-314.pyc +0 -0
  119. package/tests/__pycache__/test_token_identity_integration.cpython-314.pyc +0 -0
  120. package/tests/__pycache__/test_token_identity_retry.cpython-314.pyc +0 -0
  121. package/tests/__pycache__/test_token_identity_sync.cpython-314.pyc +0 -0
  122. package/tests/claude_oauth_fixture.py +100 -0
  123. package/tests/run-tests.sh +23 -7
  124. package/tests/test_claude_oauth_lock.py +138 -0
  125. package/tests/test_claude_oauth_refresh.py +106 -0
  126. package/tests/test_claude_oauth_refresh_failures.py +112 -0
  127. package/tests/test_codex_speed.py +236 -0
  128. package/tests/test_mutate_lock.py +107 -0
  129. package/tests/test_shim_path.py +44 -4
  130. package/lib/__pycache__/audit.cpython-312.pyc +0 -0
  131. package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
  132. package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
  133. package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
  134. package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
  135. package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
  136. package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
  137. package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
  138. package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
  139. package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
  140. package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
  141. package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
  142. package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
  143. package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
  144. package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
  145. package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
  146. package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
  147. package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
  148. package/lib/__pycache__/codex_mcp_oauth.cpython-312.pyc +0 -0
  149. package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
  150. package/lib/__pycache__/codex_resume_cwd.cpython-312.pyc +0 -0
  151. package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
  152. package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
  153. package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
  154. package/lib/__pycache__/mcp_mirror.cpython-312.pyc +0 -0
  155. package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
  156. package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
  157. package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
  158. package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
  159. package/lib/__pycache__/report.cpython-312.pyc +0 -0
  160. package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
  161. package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
  162. package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
  163. package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
  164. package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
  165. package/lib/__pycache__/session_settings.cpython-312.pyc +0 -0
  166. package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
  167. package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
  168. package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
  169. package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
  170. package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
  171. package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
  172. package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
  173. package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
  174. package/tests/__pycache__/test_autoresume_catalog.cpython-312.pyc +0 -0
  175. package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
  176. package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
  177. package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
  178. package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
  179. package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
  180. package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
  181. package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
  182. package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
  183. package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
  184. package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
  185. package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
  186. package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
  187. package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
  188. package/tests/__pycache__/test_autoresume_cwd.cpython-312.pyc +0 -0
  189. package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-312.pyc +0 -0
  190. package/tests/__pycache__/test_autoresume_cwd_reader.cpython-312.pyc +0 -0
  191. package/tests/__pycache__/test_autoresume_cwd_tmux.cpython-312.pyc +0 -0
  192. package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
  193. package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
  194. package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
  195. package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
  196. package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
  197. package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
  198. package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
  199. package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
  200. package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
  201. package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
  202. package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
  203. package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
  204. package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
  205. package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
  206. package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
  207. package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
  208. package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
  209. package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
  210. package/tests/__pycache__/test_codex_exec_stdin.cpython-312.pyc +0 -0
  211. package/tests/__pycache__/test_codex_mcp_oauth.cpython-312.pyc +0 -0
  212. package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
  213. package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
  214. package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
  215. package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
  216. package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
  217. package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
  218. package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
  219. package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
  220. package/tests/__pycache__/test_mcp_mirror.cpython-312.pyc +0 -0
  221. package/tests/__pycache__/test_mcp_mirror_args.cpython-312.pyc +0 -0
  222. package/tests/__pycache__/test_mcp_oauth.cpython-312.pyc +0 -0
  223. package/tests/__pycache__/test_mcp_registry.cpython-312.pyc +0 -0
  224. package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
  225. package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
  226. package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
  227. package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
  228. package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
  229. package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
  230. package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
  231. package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
  232. package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
  233. package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env python3
2
+ """Account-specific speed, native argv, and quota-resume regression coverage."""
3
+
4
+ from datetime import datetime, timezone
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import subprocess
9
+ import sys
10
+ import tempfile
11
+ import unittest
12
+
13
+ ROOT = Path(__file__).resolve().parents[1]
14
+ sys.path.insert(0, str(ROOT / "lib"))
15
+ sys.path.insert(0, str(ROOT / "tests"))
16
+ import codex_speed as cs
17
+ import session_settings
18
+ import autoresume as ar
19
+ import test_codex_exec_stdin as stdin_fixtures
20
+ from test_autoresume_relaunch_settings import shell_gate
21
+
22
+ MODEL = "gpt-6.1-sol"
23
+ NODE = stdin_fixtures.NODE
24
+ SID = "01a135db-f14d-7a60-b656-d5ff72abd71d"
25
+
26
+
27
+ def catalog(directory, ultra=False, model=MODEL, **changes):
28
+ document = {"fetched_at": datetime.now(timezone.utc).isoformat(), "identity": "native-account-identity",
29
+ "models": [{"slug": model, "visibility": "list", "priority": 0,
30
+ "service_tiers": [{"id": "priority"}] + ([{"id": "ultrafast"}] if ultra else [])}]}
31
+ document.update(changes)
32
+ (Path(directory) / "models_cache.json").write_text(json.dumps(document))
33
+ return document
34
+
35
+
36
+ class SpeedResolutionTests(unittest.TestCase):
37
+ def setUp(self):
38
+ self.tmp = tempfile.TemporaryDirectory()
39
+ self.addCleanup(self.tmp.cleanup)
40
+ self.home = Path(self.tmp.name)
41
+
42
+ def test_capability_is_per_account_and_per_model(self):
43
+ catalog(self.home, ultra=True)
44
+ self.assertEqual(cs.resolve(self.home, "ultrafast", MODEL)["effective"], "ultrafast")
45
+ self.assertEqual(cs.resolve(self.home, "ultrafast", "other-model")["effective"], "fast")
46
+ catalog(self.home)
47
+ result = cs.resolve(self.home, "ultrafast", MODEL)
48
+ self.assertEqual((result["effective"], result["service_tier"], result["fallback"]),
49
+ ("fast", "priority", True))
50
+
51
+ def test_stale_malformed_or_reauthenticated_catalog_cannot_grant_ultrafast(self):
52
+ for changes in ({"fetched_at": "2000-01-01T00:00:00Z"}, {"fetched_at": None}, {"models": None}):
53
+ catalog(self.home, ultra=True, **changes)
54
+ self.assertEqual(cs.resolve(self.home, "ultrafast", MODEL)["effective"], "fast")
55
+ catalog(self.home, ultra=True)
56
+ (self.home / "auth.json").write_text("{}")
57
+ future = (self.home / "models_cache.json").stat().st_mtime + 2
58
+ os.utime(self.home / "auth.json", (future, future))
59
+ self.assertEqual(cs.resolve(self.home, "ultrafast", MODEL)["effective"], "fast")
60
+
61
+ def test_service_tiers_are_authoritative_and_legacy_cache_is_supported(self):
62
+ doc = catalog(self.home)
63
+ row = doc["models"][0]
64
+ row.update(service_tiers=[], additional_speed_tiers=["fast", "ultrafast"])
65
+ self.assertEqual(cs.resolve(self.home, "ultrafast", MODEL, doc)["effective"], "fast")
66
+ del row["service_tiers"]
67
+ self.assertEqual(cs.resolve(self.home, "ultrafast", MODEL, doc)["effective"], "ultrafast")
68
+
69
+ def test_env_override_cli_precedence_and_prompt_fidelity(self):
70
+ catalog(self.home, ultra=True)
71
+ (self.home / "config.toml").write_text('model = "gpt-6.1-sol"\nservice_tier = "ultrafast"\n')
72
+ result, args = cs.launch_argv(self.home, ["exec", "--", "-c service_tier=ultrafast"],
73
+ {"CODEX_SPEED": "normal"})
74
+ self.assertEqual(result["effective"], "normal")
75
+ self.assertEqual(args, ["-c", "service_tier=default", "exec", "--", "-c service_tier=ultrafast"])
76
+ result, args = cs.launch_argv(self.home, ["-c", 'service_tier="fast"', "continue"],
77
+ {"CODEX_SPEED": "ultrafast"})
78
+ self.assertEqual(result["effective"], "fast")
79
+ self.assertNotIn('service_tier="fast"', args)
80
+ self.assertEqual(args[-1], "continue")
81
+
82
+ def test_app_server_model_hint_and_profile_defaults(self):
83
+ catalog(self.home, ultra=True)
84
+ (self.home / "config.toml").write_text('model="other-model"\n')
85
+ result, _ = cs.launch_argv(self.home, ["app-server"], {"CODEX_SPEED": "ultrafast", "CODEX_SPEED_MODEL": MODEL})
86
+ self.assertEqual(result["effective"], "ultrafast")
87
+ (self.home / "quick.config.toml").write_text('model="gpt-6.1-sol"\nservice_tier="fast"\n')
88
+ result, _ = cs.launch_argv(self.home, ["--profile", "quick", "continue"], {})
89
+ self.assertEqual(result["effective"], "fast")
90
+
91
+ def test_ignore_user_config_does_not_reintroduce_account_speed(self):
92
+ (self.home / "config.toml").write_text('service_tier="ultrafast"\n')
93
+ argv = ["exec", "--ignore-user-config", "do the work"]
94
+ result, args = cs.launch_argv(self.home, argv, {})
95
+ self.assertIsNone(result)
96
+ self.assertEqual(args, argv)
97
+ result, _ = cs.launch_argv(self.home, argv, {"CODEX_SPEED": "normal"})
98
+ self.assertEqual(result["effective"], "normal")
99
+
100
+ def test_normal_and_fast_never_request_catalog_refresh(self):
101
+ for choice in ("normal", "fast"):
102
+ result, args = cs.launch_argv(self.home, ["app-server"], {"CODEX_SPEED": choice}, executable="/missing")
103
+ self.assertEqual(result["effective"], choice)
104
+ self.assertIn("service_tier=" + ("default" if choice == "normal" else "fast"), args)
105
+
106
+ def test_cold_native_catalog_refresh_and_failure_fallback(self):
107
+ native = self.home / "native"
108
+ native.write_text(f'#!{sys.executable}\n' + '''import json,sys
109
+ for line in sys.stdin:
110
+ request=json.loads(line)
111
+ if 'id' not in request: continue
112
+ result={'data':[{'id':'gpt-6.1-sol','serviceTiers':[{'id':'priority'},{'id':'ultrafast'}]}]} if request['method']=='model/list' else {}
113
+ print(json.dumps({'id':request['id'],'result':result}),flush=True)
114
+ ''')
115
+ native.chmod(0o755)
116
+ result, _ = cs.launch_argv(self.home, ["app-server"], {"CODEX_SPEED": "ultrafast", "CODEX_SPEED_MODEL": MODEL}, str(native))
117
+ self.assertEqual(result["effective"], "ultrafast")
118
+ catalog(self.home, ultra=True, fetched_at="2000-01-01T00:00:00Z")
119
+ result, _ = cs.launch_argv(self.home, ["app-server"], {"CODEX_SPEED": "ultrafast", "CODEX_SPEED_MODEL": MODEL}, "/missing")
120
+ self.assertEqual(result["effective"], "fast")
121
+
122
+ def test_native_resume_preparation_saves_only_speed_and_refuses_failed_writes(self):
123
+ native, requests = self.home / "native", self.home / "requests.jsonl"
124
+ native.write_text(f'#!{sys.executable}\n' + '''import json,sys,pathlib
125
+ for line in sys.stdin:
126
+ request=json.loads(line)
127
+ if 'id' not in request: continue
128
+ with pathlib.Path(__file__).with_name('requests.jsonl').open('a') as log: log.write(json.dumps(request)+'\\n')
129
+ result={'serviceTier':request['params']['serviceTier']} if request['method']=='thread/resume' else {}
130
+ print(json.dumps({'id':request['id'],'result':result}),flush=True)
131
+ ''')
132
+ native.chmod(0o755)
133
+ result, args = cs.launch_argv(self.home, ["resume", SID, "continue now"], {"CODEX_SPEED": "normal"}, str(native))
134
+ calls = [json.loads(line) for line in requests.read_text().splitlines()]
135
+ self.assertEqual([call["method"] for call in calls], ["initialize", "thread/resume"])
136
+ self.assertEqual(calls[-1]["params"], {"threadId": SID, "serviceTier": "default"})
137
+ self.assertIn("service_tier=default", args)
138
+ with self.assertRaisesRegex(ValueError, "could not preserve speed"):
139
+ cs.launch_argv(self.home, ["resume", SID], {"CODEX_SPEED": "normal"}, "/missing")
140
+
141
+
142
+ @unittest.skipUnless(NODE, "Node is required for the actual child-process transport")
143
+ class SpeedShimTests(unittest.TestCase):
144
+ setUp = stdin_fixtures.CodexExecStdinTests.setUp
145
+ run_node = stdin_fixtures.CodexExecStdinTests.run_node
146
+
147
+ def prepare(self):
148
+ self.env["CLAUDE_MULTIACC_PYTHON"] = sys.executable
149
+ for number in (1, 2):
150
+ catalog(self.pool / f"acct-{number:02d}", ultra=number == 1)
151
+ native = self.root / "bin" / "codex"
152
+ source = native.read_text().split("\n", 1)[1]
153
+ native.write_text(f'#!{sys.executable}\n' + '''import sys,os,json,pathlib
154
+ if sys.argv[1:2] == ['app-server']:
155
+ for line in sys.stdin:
156
+ request=json.loads(line)
157
+ if 'id' not in request: continue
158
+ result=json.loads((pathlib.Path(os.environ['CODEX_HOME'])/'models_cache.json').read_text()) if request['method']=='model/list' else {}
159
+ print(json.dumps({'id':request['id'],'result':result}),flush=True)
160
+ sys.exit(0)
161
+ ''' + source)
162
+
163
+ def test_ultrafast_quota_rotation_recomputes_target_capability(self):
164
+ self.prepare()
165
+ result, calls = self.run_node(CODEX_SPEED="ultrafast", CODEX_SPEED_MODEL=MODEL)
166
+ self.assertEqual(result.returncode, 0, result.stderr)
167
+ self.assertEqual([row["account"] for row in calls], ["acct-01", "acct-02"])
168
+ self.assertIn("service_tier=ultrafast", calls[0]["argv"])
169
+ self.assertIn("service_tier=priority", calls[1]["argv"])
170
+ self.assertNotIn("service_tier=ultrafast", calls[1]["argv"])
171
+
172
+ def test_normal_overrides_pinned_home_fast_and_single_word_prompts(self):
173
+ self.prepare()
174
+ home = self.pool / "acct-01"
175
+ (home / "config.toml").write_text('service_tier="fast"\n')
176
+ for pin in ({"CODEX_HOME": str(home)}, {"CODEX_ACCOUNT": "acct-01"}):
177
+ (self.root / "calls.jsonl").unlink(missing_ok=True)
178
+ result, calls = self.run_node(["continue"], FAKE_MODE="success", CODEX_SPEED="normal", **pin)
179
+ self.assertEqual(result.returncode, 0, result.stderr)
180
+ self.assertIn("service_tier=default", calls[0]["argv"])
181
+
182
+ def test_administrative_commands_and_disabled_shim_remain_unchanged(self):
183
+ self.prepare()
184
+ for args, extra in [(["login"], {}), (["exec", "--help"], {}), (["continue"], {"CODEX_MULTIACC_DISABLE": "1"})]:
185
+ (self.root / "calls.jsonl").unlink(missing_ok=True)
186
+ result, calls = self.run_node(args, CODEX_HOME=str(self.pool / "acct-01"),
187
+ FAKE_MODE="success", CODEX_SPEED="ultrafast", **extra)
188
+ self.assertEqual(result.returncode, 0, result.stderr)
189
+ self.assertEqual(calls[0]["argv"], args)
190
+
191
+ def test_invalid_explicit_speed_or_unavailable_helper_never_silently_launches(self):
192
+ self.prepare()
193
+ for extra in ({"CODEX_SPEED": "turbo"}, {"CODEX_SPEED": "normal", "CLAUDE_MULTIACC_PYTHON": "/missing"}):
194
+ result, calls = self.run_node(["continue"], CODEX_HOME=str(self.pool / "acct-01"), **extra)
195
+ self.assertEqual(result.returncode, 2, result.stderr)
196
+ self.assertFalse(calls)
197
+
198
+
199
+ class ResumeSpeedTests(unittest.TestCase):
200
+ def test_history_restores_latest_own_tier_even_when_turn_context_omits_it(self):
201
+ with tempfile.TemporaryDirectory() as directory:
202
+ path = Path(directory) / "history.jsonl"
203
+ records = [
204
+ {"type": "session_meta", "payload": {"id": SID}},
205
+ {"type": "event_msg", "payload": {"type": "thread_settings_applied", "thread_id": SID,
206
+ "thread_settings": {"model": MODEL, "reasoning_effort": "high", "service_tier": "ultrafast"}}},
207
+ {"type": "turn_context", "payload": {"model": MODEL, "effort": "low"}},
208
+ {"type": "event_msg", "payload": {"type": "thread_settings_applied", "thread_id": "other",
209
+ "thread_settings": {"model": MODEL, "service_tier": "default"}}},
210
+ ]
211
+ path.write_text("".join(json.dumps(row) + "\n" for row in records))
212
+ self.assertEqual(session_settings.session_settings("codex", path),
213
+ {"model": MODEL, "effort": "low", "speed": "ultrafast"})
214
+ isolated = subprocess.run([sys.executable, "-I", str(ROOT / "lib/session_settings.py"), "codex", str(path)],
215
+ capture_output=True, text=True)
216
+ self.assertEqual(isolated.returncode, 0, isolated.stderr)
217
+ self.assertEqual(json.loads(isolated.stdout), {"model": MODEL, "effort": "low", "speed": "ultrafast"})
218
+ records.append({"type": "turn_context", "payload": {"model": MODEL, "service_tier": None}})
219
+ path.write_text("".join(json.dumps(row) + "\n" for row in records))
220
+ self.assertEqual(session_settings.session_settings("codex", path)["speed"], "normal")
221
+
222
+ def test_relaunch_replaces_speed_without_losing_effort_and_both_gates_agree(self):
223
+ original = ["-m", MODEL, "-c", "model_reasoning_effort=low", "-c", 'service_tier="ultrafast"']
224
+ resumed = ar.relaunch_argv("codex", original, SID, "quota", settings={"speed": "normal"})
225
+ self.assertIn("model_reasoning_effort=low", resumed)
226
+ self.assertIn("service_tier=default", resumed)
227
+ self.assertNotIn('service_tier="ultrafast"', resumed)
228
+ for args in (original, resumed):
229
+ self.assertTrue(ar.argv_ok("codex", args))
230
+ gate = subprocess.run(["bash", "-c", shell_gate(), "gate", *args], capture_output=True)
231
+ self.assertEqual(gate.returncode, 0, gate.stderr)
232
+ self.assertIsNone(ar.relaunch_argv("claude", original, SID, "quota"))
233
+
234
+
235
+ if __name__ == "__main__":
236
+ unittest.main()
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env python3
2
+ """Exercise real mutation-lock ownership when a failed mkdir races with release."""
3
+
4
+ import os
5
+ from pathlib import Path
6
+ import subprocess
7
+ import tempfile
8
+ import unittest
9
+
10
+ ROOT = Path(__file__).resolve().parents[1]
11
+
12
+
13
+ class MutationLockTests(unittest.TestCase):
14
+ def run_shell(self, body):
15
+ with tempfile.TemporaryDirectory(prefix="multiacc-mutation-lock-") as directory:
16
+ root = Path(directory)
17
+ env = {"PATH": os.environ.get("PATH", "/usr/bin:/bin"), "HOME": str(root),
18
+ "CLAUDE_ACCOUNTS_ROOT": str(root / "pool"), "MULTIACC_PROVIDER": "claude"}
19
+ script = '''set -u
20
+ . "$1/lib/common.sh"
21
+ lock="$ACC_ROOT/.locks/mutate"
22
+ mkdir -p "$lock"
23
+ ''' + body
24
+ result = subprocess.run(["bash", "-c", script, "mutation-lock-test", str(ROOT)],
25
+ env=env, capture_output=True, text=True, timeout=10)
26
+ self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
27
+
28
+ def test_acquisition_does_not_trust_nonexclusive_system_mkdir(self):
29
+ self.run_shell('''
30
+ : > "$lock/competing-owner"
31
+ mkdir() {
32
+ # uutils 0.10 can report success when a competing creator won. Model that
33
+ # behavior deterministically: mkdir -p accepts the other holder's directory.
34
+ command mkdir -p "$@"
35
+ }
36
+ sleep() {
37
+ [ -f "$lock/competing-owner" ] || exit 10
38
+ : > "$ACC_ROOT/waited-for-owner"
39
+ rm "$lock/competing-owner"
40
+ rmdir "$lock"
41
+ }
42
+ mutate_lock || exit 11
43
+ [ -f "$ACC_ROOT/waited-for-owner" ] || exit 12
44
+ [ -d "$lock" ] || exit 13
45
+ mutate_unlock
46
+ ''')
47
+
48
+ def test_missing_stat_never_removes_a_replacement_owner(self):
49
+ self.run_shell('''
50
+ file_mtime() {
51
+ # The first holder releases after our failed mkdir, before stat. A second
52
+ # holder acquires after that failed stat and before its zero reaches us.
53
+ rmdir "$1" || return 1
54
+ local observed
55
+ if [ "$(uname -s)" = Darwin ]; then
56
+ observed="$(stat -f %m "$1" 2>/dev/null || echo 0)"
57
+ else
58
+ observed="$(stat -c %Y "$1" 2>/dev/null || echo 0)"
59
+ fi
60
+ mkdir "$1" || return 1
61
+ : > "$1/replacement-owner"
62
+ printf '%s\\n' "$observed"
63
+ }
64
+ sleep() {
65
+ # Only the replacement owner's own release permits the waiter to acquire.
66
+ [ -f "$lock/replacement-owner" ] || exit 20
67
+ : > "$ACC_ROOT/owner-preserved"
68
+ rm "$lock/replacement-owner"
69
+ rmdir "$lock"
70
+ }
71
+ mutate_lock || exit 21
72
+ [ -f "$ACC_ROOT/owner-preserved" ] || exit 22
73
+ [ "$MUTATE_LOCK_HELD" = 1 ] || exit 23
74
+ mutate_unlock
75
+ [ ! -e "$lock" ]
76
+ ''')
77
+
78
+ def test_unreadable_mtime_uses_the_existing_backoff(self):
79
+ self.run_shell('''
80
+ : > "$lock/owner"
81
+ file_mtime() { printf 'unreadable\\n'; }
82
+ waits=0
83
+ sleep() {
84
+ [ -f "$lock/owner" ] || exit 30
85
+ waits=$((waits + 1))
86
+ if [ "$waits" = 2 ]; then rm "$lock/owner"; rmdir "$lock"; fi
87
+ }
88
+ mutate_lock || exit 31
89
+ [ "$waits" = 2 ] || exit 32
90
+ mutate_unlock
91
+ ''')
92
+
93
+ def test_observed_old_lock_remains_reclaimable(self):
94
+ self.run_shell('''
95
+ : > "$lock/abandoned"
96
+ file_mtime() { printf '1\\n'; }
97
+ epoch_now() { printf '100\\n'; }
98
+ sleep() { exit 40; }
99
+ mutate_lock || exit 41
100
+ [ "$MUTATE_LOCK_HELD" = 1 ] || exit 42
101
+ [ ! -e "$lock/abandoned" ] || exit 43
102
+ mutate_unlock
103
+ ''')
104
+
105
+
106
+ if __name__ == "__main__":
107
+ unittest.main()
@@ -40,6 +40,39 @@ def legacy_block(repo_dir):
40
40
  return '\n'.join(lines[:3] + [MARK_END]) + '\n'
41
41
 
42
42
 
43
+ class ShimPathParserTests(unittest.TestCase):
44
+ def test_complete_osc_shell_integration_wraps_answers(self):
45
+ marker = shim_path.SENTINEL
46
+ for terminator in ('\x07', '\x1b\\'):
47
+ with self.subTest(terminator=repr(terminator)):
48
+ start = '\x1b]3008;start=fixture' + terminator
49
+ end = '\x1b]3008;end=fixture' + terminator
50
+ text = (f'{start}{start}{marker} claude=/pool/claude{end}\n'
51
+ f'{start}{marker} codex=/pool/codex{end}{end}\n')
52
+ self.assertEqual(shim_path._parse(text, shim_path.NAMES),
53
+ {'claude': '/pool/claude', 'codex': '/pool/codex'})
54
+
55
+ def test_osc_removal_keeps_strict_framing_and_first_answer(self):
56
+ marker = shim_path.SENTINEL
57
+ text = (f'\x1b]3008;{marker} claude=/inside-control\x07\n'
58
+ f'echo "{marker} claude=/echoed"\n'
59
+ f'prompt {marker} claude=/prompt\n'
60
+ f'\x1b]3008;start=x\x07{marker} claude=/first\n'
61
+ f'{marker} claude=/later\n'
62
+ f'{marker} codex=/plain\n')
63
+ self.assertEqual(shim_path._parse(text, shim_path.NAMES),
64
+ {'claude': '/first', 'codex': '/plain'})
65
+
66
+ def test_incomplete_osc_cannot_consume_another_line(self):
67
+ marker = shim_path.SENTINEL
68
+ text = (f'\x1b]3008;partial {marker} claude=/unframed\n'
69
+ f'{marker} claude=/complete\n'
70
+ f'\x1b]broken\x1b[31m{marker} codex=/malformed\n'
71
+ f'\x1b]3008;start=x\x1b\\{marker} codex=/complete\n')
72
+ self.assertEqual(shim_path._parse(text, shim_path.NAMES),
73
+ {'claude': '/complete', 'codex': '/complete'})
74
+
75
+
43
76
  class ShimPathTests(unittest.TestCase):
44
77
  def setUp(self):
45
78
  self.tmp = Path(tempfile.mkdtemp(prefix='multiacc-shim-path-'))
@@ -212,12 +245,19 @@ class ShimPathTests(unittest.TestCase):
212
245
  block = installed_block(self.repo)
213
246
  for rc in ('.zshenv', '.zprofile', '.zshrc', '.profile'):
214
247
  self.write(rc, block)
215
- before = sorted(p.name for p in self.home.iterdir())
216
248
  self.probe()
217
- after = sorted(p.name for p in self.home.iterdir())
218
- self.assertEqual(before, after)
219
- for hist in ('.zsh_history', '.bash_history', '.sh_history'):
249
+ histories = ('.zsh_history', '.bash_history', '.sh_history')
250
+ for hist in histories:
220
251
  self.assertFalse((self.home / hist).exists(), hist)
252
+ # System rc files may create unrelated completion caches such as
253
+ # .zcompdump. The guarantee is that probe commands neither create history
254
+ # nor change history the operator already has.
255
+ prior = b'echo existing-user-history\n'
256
+ for hist in histories:
257
+ (self.home / hist).write_bytes(prior)
258
+ self.probe()
259
+ for hist in histories:
260
+ self.assertEqual((self.home / hist).read_bytes(), prior, hist)
221
261
 
222
262
  def test_rc_that_reads_stdin_costs_an_answer_not_a_verdict(self):
223
263
  """A prompt or `read` in an rc file eats one line of the fed script. The lost