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
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Sharing boundaries and recovery use synthetic machine-local pools only."""
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
import subprocess
|
|
9
|
+
import unittest
|
|
10
|
+
from unittest.mock import patch
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'lib'))
|
|
13
|
+
import codex_mcp_oauth as M
|
|
14
|
+
from codex_mcp_oauth_store import SENTINEL
|
|
15
|
+
|
|
16
|
+
ENTRY = {'server_name': 'demo', 'server_url': 'https://example.invalid/mcp',
|
|
17
|
+
'client_id': 'fake-client', 'access_token': 'fake-access', 'refresh_token': 'fake-refresh'}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SafetyTests(unittest.TestCase):
|
|
21
|
+
def setUp(self):
|
|
22
|
+
self.temp = tempfile.TemporaryDirectory()
|
|
23
|
+
self.addCleanup(self.temp.cleanup)
|
|
24
|
+
self.root = Path(self.temp.name)
|
|
25
|
+
self.account = self.root / 'acct-01'
|
|
26
|
+
self.account.mkdir()
|
|
27
|
+
(self.root / 'accounts.json').write_text(json.dumps({'accounts': [{'id': 'acct-01'}]}))
|
|
28
|
+
self.source = self.account / '.credentials.json'
|
|
29
|
+
self.source.write_text(json.dumps({'demo|1234': ENTRY}))
|
|
30
|
+
self.shared = self.root / M.SHARED_DIR
|
|
31
|
+
|
|
32
|
+
def test_nonmanifest_and_symlink_accounts_are_never_touched(self):
|
|
33
|
+
outside = self.root / 'unmanaged'
|
|
34
|
+
outside.mkdir()
|
|
35
|
+
self.assertFalse(M.ensure(self.root, outside))
|
|
36
|
+
self.assertFalse(self.shared.exists())
|
|
37
|
+
self.source.unlink()
|
|
38
|
+
self.account.rmdir()
|
|
39
|
+
self.account.symlink_to(outside, target_is_directory=True)
|
|
40
|
+
self.assertFalse(M.ensure(self.root, self.account, initialize=True))
|
|
41
|
+
self.assertEqual(list(outside.iterdir()), [])
|
|
42
|
+
|
|
43
|
+
def test_symlink_credentials_and_shared_state_are_refused(self):
|
|
44
|
+
other = self.root / 'private.json'
|
|
45
|
+
self.source.rename(other)
|
|
46
|
+
before = other.read_bytes()
|
|
47
|
+
self.source.symlink_to(other)
|
|
48
|
+
self.assertFalse(M.ensure(self.root))
|
|
49
|
+
self.assertEqual(other.read_bytes(), before)
|
|
50
|
+
self.source.unlink()
|
|
51
|
+
other.rename(self.source)
|
|
52
|
+
outside = self.root / 'outside'
|
|
53
|
+
outside.mkdir()
|
|
54
|
+
self.shared.symlink_to(outside, target_is_directory=True)
|
|
55
|
+
self.assertFalse(M.ensure(self.root))
|
|
56
|
+
self.assertEqual(list(outside.iterdir()), [])
|
|
57
|
+
|
|
58
|
+
def test_shared_canonical_deletion_never_reimports_old_link(self):
|
|
59
|
+
self.assertTrue(M.ensure(self.root))
|
|
60
|
+
(self.shared / 'credentials.json').unlink()
|
|
61
|
+
self.assertTrue(M.ensure(self.root))
|
|
62
|
+
self.assertEqual(set(json.loads(self.source.read_text())), {SENTINEL})
|
|
63
|
+
self.assertTrue(M.ensure(self.root))
|
|
64
|
+
self.assertEqual(set(json.loads(self.source.read_text())), {SENTINEL})
|
|
65
|
+
|
|
66
|
+
def test_removed_account_link_clears_shared_document(self):
|
|
67
|
+
self.assertTrue(M.ensure(self.root))
|
|
68
|
+
self.source.unlink()
|
|
69
|
+
self.assertTrue(M.ensure(self.root))
|
|
70
|
+
self.assertEqual(set(json.loads(self.source.read_text())), {SENTINEL})
|
|
71
|
+
|
|
72
|
+
def test_foreign_locks_and_malformed_documents_are_refused(self):
|
|
73
|
+
foreign = self.root / 'foreign-locks'
|
|
74
|
+
foreign.mkdir()
|
|
75
|
+
locks = self.account / 'mcp-oauth-locks'
|
|
76
|
+
locks.symlink_to(foreign, target_is_directory=True)
|
|
77
|
+
self.assertFalse(M.ensure(self.root))
|
|
78
|
+
self.assertEqual(list(foreign.iterdir()), [])
|
|
79
|
+
locks.unlink()
|
|
80
|
+
for doc in ([], {'bad': {'access_token': 'secret-do-not-print'}}, {'bad': dict(ENTRY, expires_at='bad')}):
|
|
81
|
+
self.source.write_text(json.dumps(doc))
|
|
82
|
+
self.assertFalse(M.ensure(self.root))
|
|
83
|
+
self.assertFalse(self.shared.exists())
|
|
84
|
+
|
|
85
|
+
def test_encrypted_backend_is_refused_unless_explicit_file_store(self):
|
|
86
|
+
config = self.account / 'config.toml'
|
|
87
|
+
config.write_text('[features]\nsecret_auth_storage = true\n')
|
|
88
|
+
self.assertFalse(M.ensure(self.root))
|
|
89
|
+
self.assertFalse(self.shared.exists())
|
|
90
|
+
config.write_text('mcp_oauth_credentials_store = "file"\n[features]\nsecret_auth_storage = true\n')
|
|
91
|
+
self.assertTrue(M.ensure(self.root))
|
|
92
|
+
|
|
93
|
+
def test_cli_refusal_reports_only_static_reason(self):
|
|
94
|
+
binary = self.root / 'codex-fake'
|
|
95
|
+
binary.write_text('#!/bin/sh\necho "codex-cli 0.163.0"\n')
|
|
96
|
+
binary.chmod(0o700)
|
|
97
|
+
self.source.write_text('{"secret-do-not-print": "private-value-do-not-print"}')
|
|
98
|
+
command = [sys.executable, M.__file__, 'ensure', '--root', str(self.root), '--codex-bin', str(binary)]
|
|
99
|
+
result = subprocess.run(command, capture_output=True, text=True, check=False)
|
|
100
|
+
self.assertEqual(result.returncode, 1)
|
|
101
|
+
self.assertIn('invalid credential entry', result.stderr)
|
|
102
|
+
self.assertNotIn('private-value', result.stdout + result.stderr)
|
|
103
|
+
self.assertNotIn('secret-do-not-print', result.stdout + result.stderr)
|
|
104
|
+
quiet = subprocess.run(command + ['--quiet'], capture_output=True, text=True, check=False)
|
|
105
|
+
self.assertEqual(quiet.returncode, 1)
|
|
106
|
+
self.assertEqual(quiet.stdout + quiet.stderr, '')
|
|
107
|
+
|
|
108
|
+
def test_failed_link_staging_preserves_original_grant_and_lock_directory(self):
|
|
109
|
+
other = self.root / 'acct-02'
|
|
110
|
+
other.mkdir()
|
|
111
|
+
(self.root / 'accounts.json').write_text(json.dumps({'accounts': [{'id': 'acct-01'}, {'id': 'acct-02'}]}))
|
|
112
|
+
before = self.source.read_bytes()
|
|
113
|
+
real_link = os.link
|
|
114
|
+
|
|
115
|
+
def reject_destination(source, destination):
|
|
116
|
+
if Path(destination).parent == other.resolve():
|
|
117
|
+
raise OSError('synthetic destination failure')
|
|
118
|
+
return real_link(source, destination)
|
|
119
|
+
|
|
120
|
+
with patch('codex_mcp_oauth_store.os.link', side_effect=reject_destination):
|
|
121
|
+
self.assertFalse(M.ensure(self.root))
|
|
122
|
+
self.assertEqual(self.source.read_bytes(), before)
|
|
123
|
+
self.assertFalse((other / '.credentials.json').exists())
|
|
124
|
+
self.assertFalse((self.account / 'mcp-oauth-locks').is_symlink())
|
|
125
|
+
self.assertEqual(list(other.glob('.codex-mcp-oauth*')), [])
|
|
126
|
+
self.assertTrue(M.ensure(self.root))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if __name__ == '__main__':
|
|
130
|
+
unittest.main()
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Exercise Codex OAuth launch hooks with native-shaped, entirely synthetic state."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import time
|
|
12
|
+
import unittest
|
|
13
|
+
|
|
14
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
15
|
+
SHIM = REPO / 'bin/codex'
|
|
16
|
+
CLI = REPO / 'bin/codex-accounts'
|
|
17
|
+
ENTRY = {'server_name': 'motionsites', 'server_url': 'https://example.invalid/mcp',
|
|
18
|
+
'client_id': 'fake-client', 'access_token': 'fake-mcp-access', 'refresh_token': 'fake-mcp-refresh',
|
|
19
|
+
'expires_at': 2000000000000, 'scopes': []}
|
|
20
|
+
DOCUMENT = {'motionsites|0000000000000001': ENTRY}
|
|
21
|
+
REAL = '''#!/bin/sh
|
|
22
|
+
if [ "${1:-}" = --version ]; then
|
|
23
|
+
echo "codex-cli ${TEST_CODEX_VERSION:-0.163.0}"
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
printf 'exec %s\n' "${CODEX_HOME##*/}" >> "$EVENT_LOG"
|
|
27
|
+
if [ "$CODEX_HOME/.credentials.json" -ef "$CODEX_ACCOUNTS_ROOT/.codex-mcp-oauth/credentials.json" ]; then
|
|
28
|
+
echo shared
|
|
29
|
+
else
|
|
30
|
+
echo private
|
|
31
|
+
fi
|
|
32
|
+
if [ "${TEST_RETRY_ONCE:-0}" = 1 ] && [ ! -e "$RETRY_MARK" ]; then
|
|
33
|
+
: > "$RETRY_MARK"
|
|
34
|
+
echo 'usage limit exceeded' >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
'''
|
|
38
|
+
PYTHON = '''#!/bin/sh
|
|
39
|
+
printf 'python %s\n' "${1##*/}" >> "$EVENT_LOG"
|
|
40
|
+
exec "$REAL_PYTHON" "$@"
|
|
41
|
+
'''
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CodexMcpOauthShimTests(unittest.TestCase):
|
|
45
|
+
def setUp(self):
|
|
46
|
+
work = tempfile.TemporaryDirectory(prefix='codex-mcp-oauth-shim-')
|
|
47
|
+
self.addCleanup(work.cleanup)
|
|
48
|
+
self.work = Path(work.name)
|
|
49
|
+
self.pool, self.home, self.realbin = [self.work / name for name in ('pool', 'home', 'realbin')]
|
|
50
|
+
for directory in (self.pool, self.home, self.realbin):
|
|
51
|
+
directory.mkdir()
|
|
52
|
+
self.accounts = [self.pool / name for name in ('acct-01', 'acct-02')]
|
|
53
|
+
manifest = {'version': 1, 'server': 'none', 'accounts': [{'id': p.name} for p in self.accounts]}
|
|
54
|
+
(self.pool / 'accounts.json').write_text(json.dumps(manifest))
|
|
55
|
+
for account in self.accounts:
|
|
56
|
+
account.mkdir()
|
|
57
|
+
(account / 'auth.json').write_text('{"tokens":{"access_token":"fake-codex-access"}}\n')
|
|
58
|
+
(account / 'config.toml').write_text('# account settings\nmodel = "kept"\n')
|
|
59
|
+
(account / 'limits.json').write_text(json.dumps({
|
|
60
|
+
'fetched_at': int(time.time()), 'weekly_percent': 10, 'session_percent': 10, 'max_percent': 10}))
|
|
61
|
+
self.credential = self.accounts[0] / '.credentials.json'
|
|
62
|
+
self.credential.write_text(json.dumps(DOCUMENT))
|
|
63
|
+
self.events = self.work / 'events.log'
|
|
64
|
+
for name, script in (('codex', REAL), ('python-log', PYTHON)):
|
|
65
|
+
executable = self.realbin / name
|
|
66
|
+
executable.write_text(script)
|
|
67
|
+
executable.chmod(0o755)
|
|
68
|
+
self.env = {
|
|
69
|
+
'PATH': f'{self.realbin}:/usr/bin:/bin:/usr/sbin:/sbin', 'HOME': str(self.home), 'USER': 'tester',
|
|
70
|
+
'LC_ALL': 'C', 'CODEX_ACCOUNTS_ROOT': str(self.pool), 'EVENT_LOG': str(self.events),
|
|
71
|
+
'CLAUDE_MULTIACC_PYTHON': str(self.realbin / 'python-log'), 'REAL_PYTHON': sys.executable,
|
|
72
|
+
'CODEX_MULTIACC_AUTORESUME': '0', 'CODEX_MULTIACC_CLIENT_LIMITS': '0',
|
|
73
|
+
'CODEX_MULTIACC_PATH_PROBE': '0', 'CODEX_MULTIACC_MODEL_CATALOG': '0',
|
|
74
|
+
'CLAUDE_MULTIACC_MODEL_CATALOG': '0', 'CODEX_MULTIACC_MCP': '0',
|
|
75
|
+
'RETRY_MARK': str(self.work / 'retried'), 'PYTHONDONTWRITEBYTECODE': '1',
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
def run_command(self, argv, **environment):
|
|
79
|
+
self.events.write_text('')
|
|
80
|
+
result = subprocess.run([str(item) for item in argv], env={**self.env, **environment},
|
|
81
|
+
stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=60)
|
|
82
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
83
|
+
self.assertNotIn('fake-mcp-access', result.stdout + result.stderr)
|
|
84
|
+
self.assertNotIn('fake-mcp-refresh', result.stdout + result.stderr)
|
|
85
|
+
return result, self.events.read_text().splitlines()
|
|
86
|
+
|
|
87
|
+
def test_pinned_selected_and_nested_clients_receive_shared_state_before_exec(self):
|
|
88
|
+
modes = ({'CODEX_ACCOUNT': 'acct-02'}, {},
|
|
89
|
+
{'CODEX_HOME': str(self.accounts[1]), 'CODEX_SHIM_ACTIVE': '1'})
|
|
90
|
+
original_auth = [(account / 'auth.json').read_bytes() for account in self.accounts]
|
|
91
|
+
for environment in modes:
|
|
92
|
+
with self.subTest(environment=environment):
|
|
93
|
+
result, events = self.run_command([SHIM, '--help'], **environment)
|
|
94
|
+
self.assertEqual(result.stdout.strip(), 'shared')
|
|
95
|
+
self.assertEqual(events[0], 'python codex_mcp_oauth.py')
|
|
96
|
+
self.assertTrue(events[-1].startswith('exec acct-'))
|
|
97
|
+
authority = self.pool / '.codex-mcp-oauth/credentials.json'
|
|
98
|
+
for account, before in zip(self.accounts, original_auth):
|
|
99
|
+
self.assertTrue(os.path.samefile(account / '.credentials.json', authority))
|
|
100
|
+
self.assertEqual((account / 'mcp-oauth-locks').resolve(), (authority.parent / 'locks').resolve())
|
|
101
|
+
self.assertEqual((account / 'auth.json').read_bytes(), before)
|
|
102
|
+
|
|
103
|
+
def test_retry_prepares_the_replacement_account_before_running_it(self):
|
|
104
|
+
result, events = self.run_command([SHIM, 'exec', 'test prompt'], TEST_RETRY_ONCE='1')
|
|
105
|
+
oauth_and_exec = [line for line in events if line == 'python codex_mcp_oauth.py' or line.startswith('exec ')]
|
|
106
|
+
self.assertEqual(len(oauth_and_exec), 4, events)
|
|
107
|
+
self.assertEqual(oauth_and_exec[::2], ['python codex_mcp_oauth.py'] * 2)
|
|
108
|
+
self.assertNotEqual(oauth_and_exec[1], oauth_and_exec[3])
|
|
109
|
+
self.assertEqual(result.stdout.strip(), 'shared')
|
|
110
|
+
|
|
111
|
+
def test_empty_pool_kill_switch_and_missing_python_preserve_launch(self):
|
|
112
|
+
self.credential.unlink()
|
|
113
|
+
result, events = self.run_command([SHIM, '--help'], CODEX_ACCOUNT='acct-02')
|
|
114
|
+
self.assertEqual((result.stdout.strip(), events), ('private', ['exec acct-02']))
|
|
115
|
+
self.assertFalse((self.pool / '.codex-mcp-oauth').exists())
|
|
116
|
+
self.credential.write_text(json.dumps(DOCUMENT))
|
|
117
|
+
for environment in ({'CODEX_MULTIACC_MCP_OAUTH': '0'}, {'CODEX_MULTIACC_DISABLE': '1',
|
|
118
|
+
'CODEX_HOME': str(self.accounts[1])},
|
|
119
|
+
{'CLAUDE_MULTIACC_PYTHON': '/nonexistent/python3'}):
|
|
120
|
+
with self.subTest(environment=environment):
|
|
121
|
+
result, events = self.run_command([SHIM, '--help'], CODEX_ACCOUNT='acct-02', **environment)
|
|
122
|
+
self.assertEqual((result.stdout.strip(), events), ('private', ['exec acct-02']))
|
|
123
|
+
self.assertFalse((self.accounts[1] / '.credentials.json').exists())
|
|
124
|
+
broken = self.work / 'broken-install'
|
|
125
|
+
(broken / 'bin').mkdir(parents=True)
|
|
126
|
+
(broken / 'lib').mkdir()
|
|
127
|
+
shim = broken / 'bin/codex'
|
|
128
|
+
shim.write_bytes(SHIM.read_bytes())
|
|
129
|
+
shim.chmod(0o755)
|
|
130
|
+
(broken / 'lib/codex_mcp_oauth.sh').write_text('if then invalid shell syntax\n')
|
|
131
|
+
result, events = self.run_command([shim, '--help'], CODEX_ACCOUNT='acct-02')
|
|
132
|
+
self.assertEqual((result.stdout.strip(), events), ('private', ['exec acct-02']))
|
|
133
|
+
|
|
134
|
+
def test_nested_foreign_and_retired_homes_cannot_initialize_pool_sharing(self):
|
|
135
|
+
for home in (self.work / 'foreign', self.pool / 'acct-99'):
|
|
136
|
+
with self.subTest(home=home):
|
|
137
|
+
home.mkdir()
|
|
138
|
+
path = home / '.credentials.json'
|
|
139
|
+
path.write_text('{"outside":"untouched"}\n')
|
|
140
|
+
before = path.read_bytes()
|
|
141
|
+
result, _events = self.run_command([SHIM, '--help'], CODEX_HOME=str(home), CODEX_SHIM_ACTIVE='1')
|
|
142
|
+
self.assertEqual(result.stdout.strip(), 'private')
|
|
143
|
+
self.assertEqual(path.read_bytes(), before)
|
|
144
|
+
self.assertFalse((self.pool / '.codex-mcp-oauth').exists())
|
|
145
|
+
self.assertEqual(json.loads(self.credential.read_text()), DOCUMENT)
|
|
146
|
+
|
|
147
|
+
def test_auth_sync_dispatches_and_keeps_cli_arguments(self):
|
|
148
|
+
result, events = self.run_command([CLI, 'mcp', 'auth-sync', '--quiet'])
|
|
149
|
+
self.assertEqual(result.stdout, '')
|
|
150
|
+
self.assertIn('python codex_mcp_oauth.py', events)
|
|
151
|
+
self.assertTrue(os.path.samefile(self.credential, self.accounts[1] / '.credentials.json'))
|
|
152
|
+
self.assertTrue((self.accounts[1] / 'mcp-oauth-locks').is_symlink())
|
|
153
|
+
|
|
154
|
+
def test_account_seeding_joins_existing_oauth_state(self):
|
|
155
|
+
command = ['bash', '-c', '. "$1"; seed_account_dir "$2"', 'seed',
|
|
156
|
+
REPO / 'lib/common.sh', self.accounts[1]]
|
|
157
|
+
_result, events = self.run_command(command, MULTIACC_PROVIDER='codex')
|
|
158
|
+
self.assertIn('python codex_mcp_oauth.py', events)
|
|
159
|
+
self.assertTrue(os.path.samefile(self.credential, self.accounts[1] / '.credentials.json'))
|
|
160
|
+
self.assertEqual((self.accounts[1] / 'config.toml').read_text(), '# account settings\nmodel = "kept"\n')
|
|
161
|
+
|
|
162
|
+
def test_pool_backstop_joins_every_manifest_account(self):
|
|
163
|
+
command = ['bash', '-c', '. "$1"; codex_mcp_oauth_ensure "$2" "$3" "$4" "" "$5"', 'backstop',
|
|
164
|
+
REPO / 'lib/codex_mcp_oauth.sh', self.pool, self.realbin / 'python-log',
|
|
165
|
+
REPO / 'lib/codex_mcp_oauth.py', self.realbin / 'codex']
|
|
166
|
+
_result, events = self.run_command(command)
|
|
167
|
+
self.assertEqual(events, ['python codex_mcp_oauth.py'])
|
|
168
|
+
self.assertTrue(os.path.samefile(self.credential, self.accounts[1] / '.credentials.json'))
|
|
169
|
+
# A subsequent pass must accept already linked lock dirs, including /var aliases on macOS.
|
|
170
|
+
newcomer = self.pool / 'acct-03'
|
|
171
|
+
newcomer.mkdir()
|
|
172
|
+
manifest_path = self.pool / 'accounts.json'
|
|
173
|
+
manifest = json.loads(manifest_path.read_text())
|
|
174
|
+
manifest['accounts'].append({'id': newcomer.name})
|
|
175
|
+
manifest_path.write_text(json.dumps(manifest))
|
|
176
|
+
self.run_command(command)
|
|
177
|
+
self.assertTrue(os.path.samefile(self.credential, newcomer / '.credentials.json'))
|
|
178
|
+
|
|
179
|
+
def test_older_native_client_preserves_private_credentials(self):
|
|
180
|
+
result, events = self.run_command([SHIM, '--help'], CODEX_ACCOUNT='acct-02', TEST_CODEX_VERSION='0.162.0')
|
|
181
|
+
self.assertEqual(result.stdout.strip(), 'private')
|
|
182
|
+
self.assertEqual(events, ['python codex_mcp_oauth.py', 'exec acct-02'])
|
|
183
|
+
self.assertFalse((self.pool / '.codex-mcp-oauth').exists())
|
|
184
|
+
self.assertEqual(json.loads(self.credential.read_text()), DOCUMENT)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == '__main__':
|
|
188
|
+
unittest.main()
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Buffered Codex retry owns and terminates only its active native child."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import signal
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import time
|
|
12
|
+
import unittest
|
|
13
|
+
|
|
14
|
+
import test_codex_exec_stdin as pipe_fixture
|
|
15
|
+
|
|
16
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
17
|
+
HELPER = REPO / 'lib/codex_retry_process.sh'
|
|
18
|
+
RUNNER = '''source "$1"
|
|
19
|
+
exec 3< "$2/input"
|
|
20
|
+
fixture_root="$2"
|
|
21
|
+
trap 'printf cleaned > "$fixture_root/exit-cleanup"' EXIT
|
|
22
|
+
trap 'printf restored > "$fixture_root/restored-trap"' TERM
|
|
23
|
+
CODEX_HOME="$2/fake-profile" codex_retry_run "$3" "$2/native.py" "$2" "$4"
|
|
24
|
+
rc=$?
|
|
25
|
+
[ "$4" != restore ] || kill -TERM $$
|
|
26
|
+
exit "$rc"
|
|
27
|
+
'''
|
|
28
|
+
NATIVE = '''import json, os, pathlib, signal, sys, time
|
|
29
|
+
root = pathlib.Path(sys.argv[1])
|
|
30
|
+
mode = sys.argv[2]
|
|
31
|
+
def stop(number, frame):
|
|
32
|
+
(root / 'stopped').write_text(str(number))
|
|
33
|
+
sys.exit(0)
|
|
34
|
+
for number in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
|
|
35
|
+
signal.signal(number, stop)
|
|
36
|
+
data = sys.stdin.buffer.read()
|
|
37
|
+
(root / 'received').write_bytes(data)
|
|
38
|
+
(root / 'profile').write_text(os.environ.get('CODEX_HOME', ''))
|
|
39
|
+
(root / 'native.pid').write_text(str(os.getpid()))
|
|
40
|
+
if mode == 'wait':
|
|
41
|
+
while True:
|
|
42
|
+
time.sleep(0.05)
|
|
43
|
+
print(json.dumps({'mode': mode}))
|
|
44
|
+
print('native stderr', file=sys.stderr)
|
|
45
|
+
sys.exit(7 if mode == 'fail' else 0)
|
|
46
|
+
'''
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CodexRetryProcessTests(unittest.TestCase):
|
|
50
|
+
def launch(self, mode):
|
|
51
|
+
temporary = tempfile.TemporaryDirectory(prefix='multiacc-retry-process-')
|
|
52
|
+
self.addCleanup(temporary.cleanup)
|
|
53
|
+
root = Path(temporary.name)
|
|
54
|
+
(root / 'input').write_bytes(b'finite stdin\n\x00\xff')
|
|
55
|
+
(root / 'native.py').write_text(NATIVE)
|
|
56
|
+
process = subprocess.Popen(
|
|
57
|
+
['bash', '-c', RUNNER, 'retry-test', str(HELPER), str(root), sys.executable, mode],
|
|
58
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
59
|
+
self.addCleanup(self.cleanup_process, process, root)
|
|
60
|
+
deadline = time.monotonic() + 5
|
|
61
|
+
while not (root / 'native.pid').exists() and process.poll() is None and time.monotonic() < deadline:
|
|
62
|
+
time.sleep(0.02)
|
|
63
|
+
self.assertTrue((root / 'native.pid').exists(), 'owned native child did not launch')
|
|
64
|
+
return process, root
|
|
65
|
+
|
|
66
|
+
def cleanup_process(self, process, root):
|
|
67
|
+
# Only this fixture's owned native PID; ordinary assertions verify it was
|
|
68
|
+
# already reaped. A regression must not leave a sleeping fake behind.
|
|
69
|
+
if process.poll() is None:
|
|
70
|
+
process.terminate()
|
|
71
|
+
pid_file = root / 'native.pid'
|
|
72
|
+
if pid_file.exists():
|
|
73
|
+
try:
|
|
74
|
+
os.kill(int(pid_file.read_text()), signal.SIGTERM)
|
|
75
|
+
except ProcessLookupError:
|
|
76
|
+
pass
|
|
77
|
+
try:
|
|
78
|
+
process.communicate(timeout=3)
|
|
79
|
+
except subprocess.TimeoutExpired:
|
|
80
|
+
process.kill()
|
|
81
|
+
process.communicate(timeout=3)
|
|
82
|
+
|
|
83
|
+
def test_termination_signals_reach_and_reap_only_the_owned_native_child(self):
|
|
84
|
+
for number in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
|
|
85
|
+
with self.subTest(signal=number):
|
|
86
|
+
process, root = self.launch('wait')
|
|
87
|
+
process.send_signal(number)
|
|
88
|
+
process.communicate(timeout=3)
|
|
89
|
+
self.assertEqual(process.returncode, 128 + number)
|
|
90
|
+
self.assertEqual(int((root / 'stopped').read_text()), number)
|
|
91
|
+
with self.assertRaises(ProcessLookupError):
|
|
92
|
+
os.kill(int((root / 'native.pid').read_text()), 0)
|
|
93
|
+
self.assertEqual((root / 'exit-cleanup').read_text(), 'cleaned')
|
|
94
|
+
|
|
95
|
+
def test_success_failure_and_prior_traps_are_preserved(self):
|
|
96
|
+
for mode, expected in [('success', 0), ('fail', 7), ('restore', 0)]:
|
|
97
|
+
with self.subTest(mode=mode):
|
|
98
|
+
process, root = self.launch(mode)
|
|
99
|
+
stdout, stderr = process.communicate(timeout=3)
|
|
100
|
+
self.assertEqual(process.returncode, expected)
|
|
101
|
+
self.assertEqual(json.loads(stdout), {'mode': mode})
|
|
102
|
+
self.assertEqual(stderr, b'native stderr\n')
|
|
103
|
+
self.assertEqual((root / 'received').read_bytes(), (root / 'input').read_bytes())
|
|
104
|
+
self.assertEqual((root / 'profile').read_text(), str(root / 'fake-profile'))
|
|
105
|
+
self.assertEqual((root / 'exit-cleanup').read_text(), 'cleaned')
|
|
106
|
+
if mode == 'restore':
|
|
107
|
+
self.assertEqual((root / 'restored-trap').read_text(), 'restored')
|
|
108
|
+
|
|
109
|
+
def test_actual_shim_pipe_cancellation_reaps_native_without_retry_or_scratch(self):
|
|
110
|
+
fixture = pipe_fixture.CodexExecStdinTests()
|
|
111
|
+
fixture.setUp()
|
|
112
|
+
self.addCleanup(fixture.doCleanups)
|
|
113
|
+
root = fixture.root
|
|
114
|
+
native = NATIVE.replace('root = pathlib.Path(sys.argv[1])',
|
|
115
|
+
"root = pathlib.Path(os.environ['FAKE_ROOT'])\n"
|
|
116
|
+
"with (root / 'attempts').open('a') as handle: handle.write('attempt\\n')")
|
|
117
|
+
native = native.replace('mode = sys.argv[2]', "mode = 'wait'")
|
|
118
|
+
(root / 'bin/codex').write_text(f'#!{sys.executable}\n' + native)
|
|
119
|
+
process = subprocess.Popen([str(REPO / 'bin/codex'), *fixture.args], env=fixture.env,
|
|
120
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
121
|
+
self.addCleanup(self.cleanup_process, process, root)
|
|
122
|
+
process.stdin.write(pipe_fixture.PROMPT)
|
|
123
|
+
process.stdin.close()
|
|
124
|
+
process.stdin = None
|
|
125
|
+
deadline = time.monotonic() + 5
|
|
126
|
+
while not (root / 'native.pid').exists() and process.poll() is None and time.monotonic() < deadline:
|
|
127
|
+
time.sleep(0.02)
|
|
128
|
+
self.assertTrue((root / 'native.pid').exists(), 'shim did not launch native child')
|
|
129
|
+
process.send_signal(signal.SIGTERM)
|
|
130
|
+
process.communicate(timeout=3)
|
|
131
|
+
self.assertEqual(process.returncode, 143)
|
|
132
|
+
self.assertEqual(int((root / 'stopped').read_text()), signal.SIGTERM)
|
|
133
|
+
with self.assertRaises(ProcessLookupError):
|
|
134
|
+
os.kill(int((root / 'native.pid').read_text()), 0)
|
|
135
|
+
self.assertEqual((root / 'attempts').read_text(), 'attempt\n')
|
|
136
|
+
self.assertEqual((root / 'received').read_bytes(), pipe_fixture.PROMPT)
|
|
137
|
+
self.assertEqual(list(fixture.pool.glob('tmp/shim.*')), [])
|
|
138
|
+
self.assertNotIn('retry from=', (fixture.pool / 'selection.log').read_text())
|
|
139
|
+
self.assertFalse((fixture.pool / 'acct-01/.limited').exists())
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == '__main__':
|
|
143
|
+
unittest.main()
|