claude-multiacc 2.0.61 → 2.0.65
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/README.md +6 -2
- package/bin/claude-accounts +4 -4
- package/bin/codex +54 -13
- package/bin/codex-accounts +28 -7
- package/docs/ACCOUNT_OPERATIONS.md +2 -2
- package/docs/AUTORESUME.md +16 -2
- package/docs/CODEX.md +58 -2
- package/docs/CODEX_MCP_AUTH.md +58 -0
- package/lib/__pycache__/audit.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_cross.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_dispatch.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_receipt.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_handoff.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_probe.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_receipt.cpython-314.pyc +0 -0
- package/lib/__pycache__/claude_client_error.cpython-314.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-314.pyc +0 -0
- package/lib/__pycache__/claude_token_identity.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_autoresume_mode.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credit_controls.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credit_local_scope.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credit_selection.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credits.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_limits_lock.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_mcp_oauth.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_resume_cwd.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_speed.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_trust.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_usage_fetch.cpython-314.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-314.pyc +0 -0
- package/lib/__pycache__/mcp_mirror.cpython-314.pyc +0 -0
- package/lib/__pycache__/mcp_oauth.cpython-314.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-314.pyc +0 -0
- package/lib/__pycache__/model_catalog.cpython-314.pyc +0 -0
- package/lib/__pycache__/model_match.cpython-314.pyc +0 -0
- package/lib/__pycache__/report.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_credit_scope.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_credits.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_funding.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-314.pyc +0 -0
- package/lib/__pycache__/session_settings.cpython-314.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-314.pyc +0 -0
- package/lib/autoresume.py +34 -13
- package/lib/autoresume_cross.py +2 -1
- package/lib/autoresume_handoff.py +13 -5
- package/lib/codex_exec_stdin.sh +45 -0
- package/lib/codex_mcp_oauth.py +204 -0
- package/lib/codex_mcp_oauth.sh +29 -0
- package/lib/codex_mcp_oauth_locks.py +158 -0
- package/lib/codex_mcp_oauth_native.py +102 -0
- package/lib/codex_mcp_oauth_store.py +149 -0
- package/lib/codex_resume_cwd.py +57 -0
- package/lib/codex_retry_process.sh +37 -0
- package/lib/codex_speed.py +351 -0
- package/lib/codex_speed.sh +39 -0
- package/lib/common.sh +16 -3
- package/lib/model_match.py +4 -1
- package/lib/session_settings.py +25 -6
- package/lib/shim_path.py +6 -0
- package/package.json +10 -5
- package/tests/__pycache__/autoresume_cross_receipt_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_unit_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_tmux_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/packaged_command_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog_cli.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_records.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_trust.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_guards.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_holders.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_origin.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_policy.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_receipt.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tokens.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cursor.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_reader.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_fallback.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_argv.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_history.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_origins.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_mode.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_probe.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_receipt.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_settings.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux_modes.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_trust_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_claude_token_identity.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_exec_stdin.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_mcp_oauth.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_polling.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_report.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_fetch.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror_args.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_oauth.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_registry.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_cached_credits.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_scope.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_shim.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_credits.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_spread.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_ceremony.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_integration.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_retry.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_sync.cpython-314.pyc +0 -0
- package/tests/fake_codex_tui.py +23 -1
- package/tests/run-tests.sh +23 -7
- package/tests/test_autoresume_cwd.py +71 -0
- package/tests/test_autoresume_cwd_handoff.py +92 -0
- package/tests/test_autoresume_cwd_reader.py +105 -0
- package/tests/test_autoresume_cwd_tmux.py +54 -0
- package/tests/test_codex_exec_stdin.py +178 -0
- package/tests/test_codex_mcp_oauth.py +146 -0
- package/tests/test_codex_mcp_oauth_adopt.py +87 -0
- package/tests/test_codex_mcp_oauth_migration.py +141 -0
- package/tests/test_codex_mcp_oauth_native.py +109 -0
- package/tests/test_codex_mcp_oauth_safety.py +130 -0
- package/tests/test_codex_mcp_oauth_shim.py +188 -0
- package/tests/test_codex_retry_process.py +143 -0
- package/tests/test_codex_speed.py +236 -0
- package/tests/test_mutate_lock.py +107 -0
- package/tests/test_shim_path.py +44 -4
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_receipt.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_autoresume_mode.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_trust.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_mirror.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
- package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
- package/lib/__pycache__/model_match.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__/session_settings.cpython-312.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
- 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_catalog.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_trust_tmux.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_mcp_mirror.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror_args.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_oauth.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_registry.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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/tests/fake_codex_tui.py
CHANGED
|
@@ -51,6 +51,27 @@ def initialize_rollout():
|
|
|
51
51
|
return rollout
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def require_resume_cwd():
|
|
55
|
+
if os.environ.get("FAKE_REQUIRE_RESUME_CWD") != "1" or "resume" not in sys.argv:
|
|
56
|
+
return
|
|
57
|
+
path = Path(os.environ["CODEX_HOME"]) / "sessions/2026/10/10"
|
|
58
|
+
path /= "rollout-test-" + os.environ["FAKE_SID"] + ".jsonl"
|
|
59
|
+
saved = None
|
|
60
|
+
for line in path.read_text().splitlines():
|
|
61
|
+
row = json.loads(line)
|
|
62
|
+
payload = row.get("payload", {})
|
|
63
|
+
if row.get("type") in ("session_meta", "turn_context"):
|
|
64
|
+
saved = payload.get("cwd", saved)
|
|
65
|
+
elif row.get("type") == "event_msg" and payload.get("type") == "thread_settings_applied":
|
|
66
|
+
saved = payload.get("thread_settings", {}).get("cwd", saved)
|
|
67
|
+
matches = saved is None or os.path.realpath(saved) == os.getcwd()
|
|
68
|
+
record("resume-cwd", saved=saved, matches=matches)
|
|
69
|
+
if not matches:
|
|
70
|
+
print("Working directory · resume\n› 1. Use session directory\n 2. Use current directory", flush=True)
|
|
71
|
+
while True:
|
|
72
|
+
time.sleep(0.1)
|
|
73
|
+
|
|
74
|
+
|
|
54
75
|
def require_resume_trust():
|
|
55
76
|
if os.environ.get("FAKE_REQUIRE_TRUST") != "1" or "resume" not in sys.argv:
|
|
56
77
|
return
|
|
@@ -67,7 +88,7 @@ def require_resume_trust():
|
|
|
67
88
|
table = re.escape(json.dumps(os.getcwd()))
|
|
68
89
|
trusted = re.search(r'^\s*\[projects\.' + table + r'\]\s*\n(?:(?!\s*\[).*\n)*?\s*trust_level\s*=\s*"trusted"',
|
|
69
90
|
text, re.M) is not None
|
|
70
|
-
record("trust", trusted=trusted)
|
|
91
|
+
record("trust", trusted=trusted, cwd=os.getcwd())
|
|
71
92
|
if not trusted:
|
|
72
93
|
print("Folder access\nTrust this folder?\n› 1. Trust and continue\n 2. Quit", flush=True)
|
|
73
94
|
while True:
|
|
@@ -85,6 +106,7 @@ def main():
|
|
|
85
106
|
settings = termios.tcgetattr(0)
|
|
86
107
|
signal.signal(signal.SIGTERM, stop)
|
|
87
108
|
signal.signal(signal.SIGHUP, stop)
|
|
109
|
+
require_resume_cwd()
|
|
88
110
|
require_resume_trust()
|
|
89
111
|
rollout = initialize_rollout()
|
|
90
112
|
try:
|
package/tests/run-tests.sh
CHANGED
|
@@ -2632,16 +2632,27 @@ regB="$(claude-accounts list 2>&1 | grep par2@test | awk '{print $1}')"
|
|
|
2632
2632
|
[ -n "$regB" ] && claude-accounts remove "$regB" --yes >/dev/null 2>&1
|
|
2633
2633
|
|
|
2634
2634
|
# ---- 15h. two parallel adds signing into the SAME email: exactly one wins ------------
|
|
2635
|
+
if python3 "$REPO_DIR/tests/test_mutate_lock.py"; then
|
|
2636
|
+
t_ok "mutation lock: missing-stat race preserves the replacement owner"
|
|
2637
|
+
else
|
|
2638
|
+
t_fail "mutation lock ownership suite" "see unittest output above"
|
|
2639
|
+
fi
|
|
2635
2640
|
( CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=same@test claude-accounts add >"$WORK/s1.out" 2>&1 ) &
|
|
2636
2641
|
sA=$!
|
|
2637
2642
|
( CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=same@test claude-accounts add >"$WORK/s2.out" 2>&1 ) &
|
|
2638
2643
|
sB=$!
|
|
2639
|
-
|
|
2644
|
+
sArc=0; sBrc=0
|
|
2645
|
+
wait "$sA" || sArc=$?
|
|
2646
|
+
wait "$sB" || sBrc=$?
|
|
2647
|
+
both="$(cat "$WORK/s1.out" "$WORK/s2.out" 2>/dev/null)"
|
|
2648
|
+
[ "$sArc:$sBrc" = "0:0" ] && t_ok "same-email parallel add: both callers succeed" \
|
|
2649
|
+
|| t_fail "same-email parallel add exit status" "first=$sArc second=$sBrc; $both"
|
|
2640
2650
|
n="$(claude-accounts list 2>&1 | grep -c same@test)"
|
|
2641
2651
|
[ "$n" = "1" ] && t_ok "same-email parallel add: exactly one registered (other skipped)" \
|
|
2642
2652
|
|| t_fail "same-email parallel add" "count=$n (expected 1)"
|
|
2643
|
-
|
|
2644
|
-
&& t_ok "same-email parallel add: loser skipped gracefully"
|
|
2653
|
+
grep -qE "already added as|skipping" "$WORK/s1.out" "$WORK/s2.out" \
|
|
2654
|
+
&& t_ok "same-email parallel add: loser skipped gracefully" \
|
|
2655
|
+
|| t_fail "same-email skip msg" "first=$sArc second=$sBrc; no skip message in: $both"
|
|
2645
2656
|
regS="$(claude-accounts list 2>&1 | grep same@test | awk '{print $1}')"
|
|
2646
2657
|
[ -n "$regS" ] && claude-accounts remove "$regS" --yes >/dev/null 2>&1
|
|
2647
2658
|
|
|
@@ -7797,18 +7808,23 @@ fi
|
|
|
7797
7808
|
|
|
7798
7809
|
# two parallel imports get distinct ids and both land in the manifest (lock coverage;
|
|
7799
7810
|
# codex-review finding)
|
|
7800
|
-
codex-accounts import p1@cx --no-sync
|
|
7811
|
+
codex-accounts import p1@cx --no-sync >"$WORK/cx-import1.out" 2>&1 &
|
|
7801
7812
|
imp1=$!
|
|
7802
|
-
codex-accounts import p2@cx --no-sync
|
|
7813
|
+
codex-accounts import p2@cx --no-sync >"$WORK/cx-import2.out" 2>&1 &
|
|
7803
7814
|
imp2=$!
|
|
7804
|
-
|
|
7815
|
+
imp1rc=0; imp2rc=0
|
|
7816
|
+
wait "$imp1" || imp1rc=$?
|
|
7817
|
+
wait "$imp2" || imp2rc=$?
|
|
7818
|
+
both="$(cat "$WORK/cx-import1.out" "$WORK/cx-import2.out" 2>/dev/null)"
|
|
7819
|
+
[ "$imp1rc:$imp2rc" = "0:0" ] && t_ok "codex: both parallel import callers succeed" \
|
|
7820
|
+
|| t_fail "codex parallel import exit status" "first=$imp1rc second=$imp2rc; $both"
|
|
7805
7821
|
n1="$(grep -c '"email": "p1@cx"' "$CX/accounts.json")"
|
|
7806
7822
|
n2="$(grep -c '"email": "p2@cx"' "$CX/accounts.json")"
|
|
7807
7823
|
id1="$(python3 -c "import json,sys; print(next((a['id'] for a in json.load(open('$CX/accounts.json'))['accounts'] if a['email']=='p1@cx'), ''))")"
|
|
7808
7824
|
id2="$(python3 -c "import json,sys; print(next((a['id'] for a in json.load(open('$CX/accounts.json'))['accounts'] if a['email']=='p2@cx'), ''))")"
|
|
7809
7825
|
{ [ "$n1" = "1" ] && [ "$n2" = "1" ] && [ -n "$id1" ] && [ -n "$id2" ] && [ "$id1" != "$id2" ]; } \
|
|
7810
7826
|
&& t_ok "codex: parallel imports get distinct ids ($id1, $id2)" \
|
|
7811
|
-
|| t_fail "codex parallel import" "p1=$n1($id1) p2=$n2($id2)"
|
|
7827
|
+
|| t_fail "codex parallel import" "p1=$n1($id1) p2=$n2($id2); first=$imp1rc second=$imp2rc; $both"
|
|
7812
7828
|
codex-accounts remove "$id1" --yes >/dev/null 2>&1
|
|
7813
7829
|
codex-accounts remove "$id2" --yes >/dev/null 2>&1
|
|
7814
7830
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Account rotation restores the bound Codex session's last working directory."""
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
import test_autoresume as fixtures
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CodexResumeDirectoryTests(unittest.TestCase):
|
|
9
|
+
def setUp(self):
|
|
10
|
+
self.pool = fixtures.CodexWatcherTests(methodName="runTest")
|
|
11
|
+
self.pool.setUp()
|
|
12
|
+
self.addCleanup(self.pool.tearDown)
|
|
13
|
+
self.saved = self.pool.tmp / "saved project ü"
|
|
14
|
+
self.saved.mkdir()
|
|
15
|
+
|
|
16
|
+
def restart(self, records, expected, setup=None):
|
|
17
|
+
pool = self.pool
|
|
18
|
+
records.append(fixtures.cx_error(pool.t0, "usage_limit_exceeded"))
|
|
19
|
+
result, _, runner, _ = pool.run_codex(["resume", fixtures.CX_SID], records, setup=setup)
|
|
20
|
+
self.assertEqual(result, 0)
|
|
21
|
+
fields, argv = pool.relaunch_of(runner)
|
|
22
|
+
self.assertEqual(fields["cwd"], str(expected))
|
|
23
|
+
self.assertEqual(runner.probes[0]["cwd"], str(expected))
|
|
24
|
+
self.assertEqual(argv[-2], fixtures.CX_SID)
|
|
25
|
+
self.assertEqual(pool.events(), ["watch", "detect", "switch"])
|
|
26
|
+
|
|
27
|
+
def test_recorded_session_directory_wins_over_launch_directory(self):
|
|
28
|
+
self.restart([fixtures.cx_meta(self.pool.launched - 100, cwd=str(self.saved))], self.saved)
|
|
29
|
+
|
|
30
|
+
def test_latest_turn_directory_wins_over_initial_session_directory(self):
|
|
31
|
+
self.restart([fixtures.cx_meta(self.pool.launched - 100, cwd=str(self.pool.work)),
|
|
32
|
+
{"type": "turn_context", "payload": {"cwd": str(self.saved)}}], self.saved)
|
|
33
|
+
|
|
34
|
+
def test_latest_own_thread_settings_win_without_a_new_turn(self):
|
|
35
|
+
settings = {"type": "thread_settings_applied", "thread_id": fixtures.CX_SID,
|
|
36
|
+
"thread_settings": {"cwd": str(self.saved)}}
|
|
37
|
+
self.restart([fixtures.cx_meta(self.pool.launched - 100, cwd=str(self.pool.work)),
|
|
38
|
+
{"type": "turn_context", "payload": {"cwd": str(self.pool.work)}},
|
|
39
|
+
fixtures.cx_event(self.pool.t0 - 1, settings)], self.saved)
|
|
40
|
+
|
|
41
|
+
def test_foreign_thread_settings_and_tool_workdirs_do_not_change_directory(self):
|
|
42
|
+
foreign = {"type": "thread_settings_applied", "thread_id": fixtures.CX_SID2,
|
|
43
|
+
"thread_settings": {"cwd": str(self.pool.work)}}
|
|
44
|
+
tool = {"type": "response_item", "payload": {"type": "function_call", "name": "exec_command",
|
|
45
|
+
"arguments": '{"workdir":"/tmp","cmd":"pwd"}'}}
|
|
46
|
+
self.restart([fixtures.cx_meta(self.pool.launched - 100, cwd=str(self.saved)),
|
|
47
|
+
fixtures.cx_event(self.pool.t0 - 1, foreign), tool], self.saved)
|
|
48
|
+
|
|
49
|
+
def test_invalid_saved_values_keep_last_valid_directory(self):
|
|
50
|
+
invalid = [None, 7, {}, "relative/project", "/tmp\nnot-a-directory", "/tmp\0bad"]
|
|
51
|
+
records = [fixtures.cx_meta(self.pool.launched - 100, cwd=str(self.saved))]
|
|
52
|
+
records.extend({"type": "turn_context", "payload": {"cwd": value}} for value in invalid)
|
|
53
|
+
self.restart(records, self.saved)
|
|
54
|
+
|
|
55
|
+
def test_deleted_saved_directory_falls_back_to_existing_launch_directory(self):
|
|
56
|
+
self.restart([fixtures.cx_meta(self.pool.launched - 100, cwd=str(self.saved / "deleted"))],
|
|
57
|
+
self.pool.work)
|
|
58
|
+
|
|
59
|
+
def test_relaunch_cursor_does_not_hide_the_saved_directory(self):
|
|
60
|
+
def setup(watcher, runner, path):
|
|
61
|
+
info = path.stat()
|
|
62
|
+
watcher.st["rollout_cursor"] = "%d:%d" % (info.st_ino, info.st_size)
|
|
63
|
+
runner.at(self.pool.t0 + 1, lambda: fixtures.append(path, [
|
|
64
|
+
fixtures.cx_error(self.pool.t0 + 1, "usage_limit_exceeded")]))
|
|
65
|
+
|
|
66
|
+
self.restart([fixtures.cx_meta(self.pool.launched - 100, cwd=str(self.pool.work)),
|
|
67
|
+
{"type": "turn_context", "payload": {"cwd": str(self.saved)}}], self.saved, setup=setup)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
unittest.main()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""A Codex handoff binds the saved directory and history to one validated snapshot."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
from unittest import mock
|
|
8
|
+
|
|
9
|
+
import test_autoresume as fixtures
|
|
10
|
+
from autoresume_cross_unit_support import scenario
|
|
11
|
+
import autoresume_cross as cross
|
|
12
|
+
import autoresume_handoff as handoff
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CodexDirectoryHandoffTests(unittest.TestCase):
|
|
16
|
+
def setUp(self):
|
|
17
|
+
temporary = tempfile.TemporaryDirectory(prefix="ar-cwd-handoff-")
|
|
18
|
+
self.addCleanup(temporary.cleanup)
|
|
19
|
+
self.home = Path(temporary.name).resolve()
|
|
20
|
+
self.initial = self.home / "initial"
|
|
21
|
+
self.saved = self.home / "saved project"
|
|
22
|
+
self.initial.mkdir()
|
|
23
|
+
self.saved.mkdir()
|
|
24
|
+
self.path = self.home / ("rollout-test-" + fixtures.CX_SID + ".jsonl")
|
|
25
|
+
self.meta = fixtures.cx_meta(1000, cwd=str(self.initial))
|
|
26
|
+
|
|
27
|
+
def write(self, records):
|
|
28
|
+
self.path.write_text("".join(json.dumps(row) + "\n" for row in [self.meta] + records))
|
|
29
|
+
|
|
30
|
+
def validate(self, cwd=None):
|
|
31
|
+
return handoff.validate_source("codex", fixtures.CX_SID, str(self.path), str(cwd or self.saved))
|
|
32
|
+
|
|
33
|
+
def test_changed_directory_from_turn_or_settings_replaces_initial_directory(self):
|
|
34
|
+
turn = {"type": "turn_context", "payload": {"cwd": str(self.saved)}}
|
|
35
|
+
settings = fixtures.cx_event(1001, {"type": "thread_settings_applied", "thread_id": fixtures.CX_SID,
|
|
36
|
+
"thread_settings": {"cwd": str(self.saved)}})
|
|
37
|
+
for record in (turn, settings):
|
|
38
|
+
with self.subTest(kind=record["type"]):
|
|
39
|
+
self.write([record])
|
|
40
|
+
history = self.validate()
|
|
41
|
+
self.assertEqual(history["cwd"], str(self.saved))
|
|
42
|
+
self.assertEqual(history["offset"], self.path.stat().st_size)
|
|
43
|
+
self.assertIsNone(self.validate(self.initial))
|
|
44
|
+
|
|
45
|
+
def test_foreign_settings_cannot_authorize_unrelated_directory(self):
|
|
46
|
+
settings = fixtures.cx_event(1001, {"type": "thread_settings_applied", "thread_id": fixtures.CX_SID2,
|
|
47
|
+
"thread_settings": {"cwd": str(self.saved)}})
|
|
48
|
+
self.write([settings])
|
|
49
|
+
self.assertIsNone(self.validate())
|
|
50
|
+
self.assertIsNotNone(self.validate(self.initial))
|
|
51
|
+
|
|
52
|
+
def test_saved_directory_after_large_history_is_read_beyond_initial_prefix(self):
|
|
53
|
+
self.write([{"type": "response_item", "payload": {"text": "x" * handoff.MAX_SOURCE_BYTES}},
|
|
54
|
+
{"type": "turn_context", "payload": {"cwd": str(self.saved)}}])
|
|
55
|
+
self.assertEqual(self.validate()["cwd"], str(self.saved))
|
|
56
|
+
|
|
57
|
+
def test_appended_settings_after_snapshot_do_not_change_snapshot_directory(self):
|
|
58
|
+
self.write([{ "type": "turn_context", "payload": {"cwd": str(self.saved)}}])
|
|
59
|
+
offset = self.path.stat().st_size
|
|
60
|
+
original = handoff.codex_resume_cwd
|
|
61
|
+
later = {"type": "turn_context", "payload": {"cwd": str(self.initial)}}
|
|
62
|
+
with mock.patch.object(handoff, "codex_resume_cwd", side_effect=lambda *args, **kwargs: (
|
|
63
|
+
fixtures.append(self.path, [later]), original(*args, **kwargs))[1]):
|
|
64
|
+
history = self.validate()
|
|
65
|
+
self.assertEqual((history["cwd"], history["offset"]), (str(self.saved), offset))
|
|
66
|
+
self.assertIsNone(self.validate())
|
|
67
|
+
|
|
68
|
+
def test_replacement_during_directory_read_is_rejected(self):
|
|
69
|
+
self.write([{"type": "turn_context", "payload": {"cwd": str(self.saved)}}])
|
|
70
|
+
replacement = self.path.with_suffix(".replacement")
|
|
71
|
+
replacement.write_bytes(self.path.read_bytes())
|
|
72
|
+
original = handoff.codex_resume_cwd
|
|
73
|
+
with mock.patch.object(handoff, "codex_resume_cwd", side_effect=lambda *args, **kwargs: (
|
|
74
|
+
replacement.replace(self.path), original(*args, **kwargs))[1]):
|
|
75
|
+
self.assertIsNone(self.validate())
|
|
76
|
+
|
|
77
|
+
def test_cross_provider_plan_probes_in_latest_saved_directory(self):
|
|
78
|
+
case, watcher, runner, history, _ = scenario(self, "codex")
|
|
79
|
+
saved = case.tmp / "saved project"
|
|
80
|
+
saved.mkdir()
|
|
81
|
+
fixtures.append(history, [{"type": "turn_context", "payload": {"cwd": str(saved)}}])
|
|
82
|
+
plan = cross.plan_fallback(watcher, watcher.tracker.pending, "")
|
|
83
|
+
self.assertIsNotNone(plan)
|
|
84
|
+
self.assertEqual(plan["history"]["cwd"], str(saved))
|
|
85
|
+
self.assertEqual(runner.target_probes[-1]["cwd"], str(saved))
|
|
86
|
+
self.assertTrue(cross.revalidate(watcher, plan))
|
|
87
|
+
fixtures.append(history, [{"type": "turn_context", "payload": {"cwd": str(case.work)}}])
|
|
88
|
+
self.assertFalse(cross.revalidate(watcher, plan))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
unittest.main()
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Read only a bound Codex thread's saved cwd, even across chunk and restart boundaries."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
from unittest import mock
|
|
9
|
+
|
|
10
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
11
|
+
sys.path.insert(0, str(ROOT / "lib"))
|
|
12
|
+
from codex_resume_cwd import codex_resume_cwd # noqa: E402
|
|
13
|
+
import session_settings # noqa: E402
|
|
14
|
+
|
|
15
|
+
SID = "01a13956-95c9-7d41-9a8b-b3e5ff79d3b2"
|
|
16
|
+
OTHER = "0aaaaaaa-0000-7000-8000-000000000000"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def line(kind, **payload):
|
|
20
|
+
return json.dumps({"type": kind, "payload": payload}, ensure_ascii=False)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CodexCwdReaderTests(unittest.TestCase):
|
|
24
|
+
def setUp(self):
|
|
25
|
+
directory = tempfile.TemporaryDirectory(prefix="codex-cwd-reader-")
|
|
26
|
+
self.addCleanup(directory.cleanup)
|
|
27
|
+
self.path = Path(directory.name) / "rollout.jsonl"
|
|
28
|
+
self.meta = line("session_meta", id=SID, cwd="/initial project")
|
|
29
|
+
|
|
30
|
+
def write(self, records):
|
|
31
|
+
self.path.write_text("\n".join(records) + "\n", encoding="utf-8")
|
|
32
|
+
return self.path
|
|
33
|
+
|
|
34
|
+
def test_bound_id_is_checked_before_any_saved_context(self):
|
|
35
|
+
path = self.write([self.meta, line("turn_context", cwd="/latest")])
|
|
36
|
+
self.assertEqual(codex_resume_cwd(path, SID), "/latest")
|
|
37
|
+
self.assertEqual(codex_resume_cwd(path), "/latest")
|
|
38
|
+
self.assertIsNone(codex_resume_cwd(path, OTHER))
|
|
39
|
+
boundary = path.stat().st_size
|
|
40
|
+
with path.open("a", encoding="utf-8") as output:
|
|
41
|
+
output.write(line("turn_context", cwd="/after-snapshot") + "\n")
|
|
42
|
+
self.assertEqual(codex_resume_cwd(path, SID), "/after-snapshot")
|
|
43
|
+
self.assertEqual(codex_resume_cwd(path, SID, upto=boundary), "/latest")
|
|
44
|
+
|
|
45
|
+
def test_missing_or_unreadable_history_has_no_directory(self):
|
|
46
|
+
for path in (None, self.path, self.path.parent):
|
|
47
|
+
with self.subTest(path=path):
|
|
48
|
+
self.assertIsNone(codex_resume_cwd(path, SID))
|
|
49
|
+
self.write([self.meta])
|
|
50
|
+
with mock.patch("builtins.open", side_effect=PermissionError("denied")):
|
|
51
|
+
self.assertIsNone(codex_resume_cwd(self.path, SID))
|
|
52
|
+
|
|
53
|
+
def test_invalid_metadata_cannot_bind_a_later_context(self):
|
|
54
|
+
bad = ["{", "[]", "null", json.dumps({"type": "session_meta", "payload": []}),
|
|
55
|
+
line("session_meta", id=None, cwd="/initial"),
|
|
56
|
+
line("turn_context", id=SID, cwd="/initial")]
|
|
57
|
+
for first in bad:
|
|
58
|
+
with self.subTest(first=first):
|
|
59
|
+
path = self.write([first, line("turn_context", cwd="/latest")])
|
|
60
|
+
self.assertIsNone(codex_resume_cwd(path, SID))
|
|
61
|
+
|
|
62
|
+
def test_malformed_and_invalid_later_records_preserve_last_valid_cwd(self):
|
|
63
|
+
bad = ["{", '[]', '{"payload":{"cwd":',
|
|
64
|
+
json.dumps({"type": "turn_context", "payload": None}),
|
|
65
|
+
line("event_msg", type="thread_settings_applied", thread_settings=[])]
|
|
66
|
+
bad.extend(line("turn_context", cwd=value)
|
|
67
|
+
for value in (None, 7, {}, "relative", "/bad\npath", "/bad\rpath", "/bad\0path"))
|
|
68
|
+
path = self.write([self.meta, line("turn_context", cwd="/saved ü")] + bad)
|
|
69
|
+
self.assertEqual(codex_resume_cwd(path, SID), "/saved ü")
|
|
70
|
+
|
|
71
|
+
def test_foreign_settings_and_subagent_context_do_not_change_main_cwd(self):
|
|
72
|
+
path = self.write([
|
|
73
|
+
self.meta,
|
|
74
|
+
line("event_msg", type="thread_settings_applied", thread_id=OTHER,
|
|
75
|
+
thread_settings={"cwd": "/foreign"}),
|
|
76
|
+
line("turn_context", thread_id=OTHER, cwd="/foreign-context"),
|
|
77
|
+
line("turn_context", turn_id="child", root_turn_id="parent", cwd="/subagent"),
|
|
78
|
+
line("response_item", type="function_call", cwd="/tool", arguments='{"cwd":"/quoted"}'),
|
|
79
|
+
line("session_meta", id=OTHER, cwd="/copied-parent"),
|
|
80
|
+
])
|
|
81
|
+
self.assertEqual(codex_resume_cwd(path, SID), "/initial project")
|
|
82
|
+
|
|
83
|
+
def test_reverse_chunks_reassemble_long_records_and_keep_latest_settings(self):
|
|
84
|
+
saved = "/saved " + "é" * 200
|
|
85
|
+
path = self.write([
|
|
86
|
+
self.meta, line("turn_context", cwd="/older"),
|
|
87
|
+
line("event_msg", type="thread_settings_applied", thread_id=SID,
|
|
88
|
+
thread_settings={"cwd": saved}),
|
|
89
|
+
line("response_item", type="message", text="padding" * 100),
|
|
90
|
+
])
|
|
91
|
+
with mock.patch.object(session_settings, "CHUNK", 127):
|
|
92
|
+
self.assertEqual(codex_resume_cwd(path, SID), saved)
|
|
93
|
+
|
|
94
|
+
def test_initial_metadata_remains_fallback_beyond_reverse_scan_limit(self):
|
|
95
|
+
large_meta = line("session_meta", id=SID, cwd="/initial project", base_instructions="x" * (1 << 20))
|
|
96
|
+
path = self.write([large_meta, line("session_meta", id=OTHER, cwd="/copied-parent"),
|
|
97
|
+
line("response_item", type="message", text="padding" * 100)])
|
|
98
|
+
self.assertEqual(codex_resume_cwd(path, SID), "/initial project")
|
|
99
|
+
with mock.patch.object(session_settings, "CHUNK", 128), \
|
|
100
|
+
mock.patch.object(session_settings, "SCAN_LIMIT", 128):
|
|
101
|
+
self.assertEqual(codex_resume_cwd(path, SID), "/initial project")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
unittest.main()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""A quota restart restores the saved directory before Codex checks directory and trust."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
from autoresume_tmux_support import SID, TMUX, TmuxScenario
|
|
8
|
+
|
|
9
|
+
QUOTA = {"type": "task_complete", "error": {"message": "You've hit your usage limit.",
|
|
10
|
+
"codex_error_info": "usage_limit_exceeded"}}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@unittest.skipUnless(TMUX, "real tmux is unavailable")
|
|
14
|
+
class CodexCwdTmuxTests(unittest.TestCase):
|
|
15
|
+
def test_quota_restart_restores_latest_directory_before_trust_and_prompt(self):
|
|
16
|
+
self.check_restart("turn_context")
|
|
17
|
+
|
|
18
|
+
def test_settings_change_restores_directory_before_the_next_turn(self):
|
|
19
|
+
self.check_restart("thread_settings_applied")
|
|
20
|
+
|
|
21
|
+
def check_restart(self, record_type):
|
|
22
|
+
case = TmuxScenario(FAKE_REQUIRE_RESUME_CWD="1", FAKE_REQUIRE_TRUST="1")
|
|
23
|
+
self.addCleanup(case.close)
|
|
24
|
+
saved = case.home / "saved project Ω"
|
|
25
|
+
saved.mkdir()
|
|
26
|
+
source = case.pool / "acct-01/config.toml"
|
|
27
|
+
target = case.pool / "acct-02/config.toml"
|
|
28
|
+
source.write_text("# The original account stays unchanged.\n")
|
|
29
|
+
case.start()
|
|
30
|
+
context = {"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
31
|
+
"type": "turn_context", "payload": {"cwd": str(saved)}}
|
|
32
|
+
if record_type == "thread_settings_applied":
|
|
33
|
+
context.update(type="event_msg", payload={"type": record_type, "thread_id": SID,
|
|
34
|
+
"thread_settings": {"cwd": str(saved)}})
|
|
35
|
+
with case.rollout.open("a") as stream:
|
|
36
|
+
stream.write(json.dumps(context) + "\n")
|
|
37
|
+
case.append(QUOTA)
|
|
38
|
+
case.wait(lambda: "autoresume switch" in case.log.read_text(), "restart in saved directory confirmed")
|
|
39
|
+
first, second = case.records("launch")
|
|
40
|
+
self.assertEqual([first["cwd"], second["cwd"]], [str(case.work), str(saved)])
|
|
41
|
+
self.assertEqual([first["account"], second["account"]], ["acct-01", "acct-02"])
|
|
42
|
+
self.assertIn(SID, second["argv"])
|
|
43
|
+
self.assertEqual([row["matches"] for row in case.records("resume-cwd")], [True])
|
|
44
|
+
self.assertEqual([(row["trusted"], row["cwd"]) for row in case.records("trust")], [(True, str(saved))])
|
|
45
|
+
self.assertIn('[projects.' + json.dumps(str(saved), ensure_ascii=False) + ']', target.read_text())
|
|
46
|
+
self.assertNotIn(str(case.work), target.read_text())
|
|
47
|
+
self.assertEqual(source.read_text(), "# The original account stays unchanged.\n")
|
|
48
|
+
self.assertEqual(len(case.records("prompt")), 1)
|
|
49
|
+
self.assertEqual(case.log.read_text().count("autoresume switch"), 1)
|
|
50
|
+
self.assertEqual(case.records("keys"), [], "neither TUI receives a keystroke")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
unittest.main()
|