claude-multiacc 2.0.56 → 2.0.58
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 +7 -2
- package/bin/claude +71 -8
- package/bin/codex +67 -14
- package/docs/AUTORESUME-HANDOFF.md +84 -0
- package/docs/AUTORESUME.md +16 -5
- 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_usage_fetch.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
- package/lib/__pycache__/report.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
- package/lib/autoresume.py +58 -14
- package/lib/autoresume_cross.py +143 -0
- package/lib/autoresume_cross_dispatch.py +114 -0
- package/lib/autoresume_cross_receipt.py +138 -0
- package/lib/autoresume_handoff.py +191 -0
- package/lib/autoresume_probe.py +36 -0
- package/package.json +1 -1
- 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_codex_records.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_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_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
- package/tests/autoresume_cross_receipt_support.py +58 -0
- package/tests/autoresume_cross_tmux_environment.py +59 -0
- package/tests/autoresume_cross_tmux_support.py +112 -0
- package/tests/autoresume_cross_unit_support.py +92 -0
- package/tests/fixtures/cross_provider_cli.py +122 -0
- package/tests/test_autoresume.py +3 -1
- package/tests/test_autoresume_cross_claude_holders.py +81 -0
- package/tests/test_autoresume_cross_guards.py +133 -0
- package/tests/test_autoresume_cross_holders.py +89 -0
- package/tests/test_autoresume_cross_origin.py +70 -0
- package/tests/test_autoresume_cross_policy.py +123 -0
- package/tests/test_autoresume_cross_receipt.py +141 -0
- package/tests/test_autoresume_cross_tmux.py +140 -0
- package/tests/test_autoresume_cross_tmux_races.py +45 -0
- package/tests/test_autoresume_cross_tokens.py +102 -0
- package/tests/test_autoresume_handoff_argv.py +127 -0
- package/tests/test_autoresume_handoff_history.py +115 -0
- package/tests/test_autoresume_handoff_origins.py +53 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Fresh provider conversations retain source history and conservative permission intent."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
import test_autoresume as fixtures
|
|
9
|
+
import autoresume_handoff as handoff
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HandoffArgvTests(unittest.TestCase):
|
|
13
|
+
def setUp(self):
|
|
14
|
+
temporary = tempfile.TemporaryDirectory(prefix="ar-handoff-argv-")
|
|
15
|
+
self.addCleanup(temporary.cleanup)
|
|
16
|
+
self.home = Path(temporary.name).resolve()
|
|
17
|
+
self.cwd = self.home / 'workspace "quoted" $literal'
|
|
18
|
+
self.cwd.mkdir()
|
|
19
|
+
|
|
20
|
+
def history(self, provider):
|
|
21
|
+
sid = fixtures.SID if provider == "claude" else fixtures.CX_SID
|
|
22
|
+
name = sid + ".jsonl" if provider == "claude" else "rollout-fixture-" + sid + ".jsonl"
|
|
23
|
+
path = self.home / name
|
|
24
|
+
record = fixtures.cc_user(1000, "PRIVATE_SOURCE_TEXT_SENTINEL", cwd=str(self.cwd)) \
|
|
25
|
+
if provider == "claude" else fixtures.cx_meta(1000, cwd=str(self.cwd))
|
|
26
|
+
path.write_text(json.dumps(record) + "\n")
|
|
27
|
+
return handoff.validate_source(provider, sid, str(path), str(self.cwd))
|
|
28
|
+
|
|
29
|
+
def test_fresh_target_drops_source_session_model_effort_and_original_prompt(self):
|
|
30
|
+
cases = (("claude", "codex", ["--model", "source-only-model", "--effort", "high",
|
|
31
|
+
"--resume", fixtures.SID, "Original user request"]),
|
|
32
|
+
("codex", "claude", ["resume", "--no-daemon", "--model", "source-only-model",
|
|
33
|
+
fixtures.CX_SID, "Original user request"]))
|
|
34
|
+
for source, target, original in cases:
|
|
35
|
+
with self.subTest(source=source):
|
|
36
|
+
history = self.history(source)
|
|
37
|
+
argv = handoff.handoff_argv(source, target, original, history)
|
|
38
|
+
self.assertIsNotNone(argv)
|
|
39
|
+
for value in ("resume", "--resume", "--last", "--continue", "--model", "--effort",
|
|
40
|
+
"source-only-model", "Original user request", history["sid"]):
|
|
41
|
+
self.assertNotIn(value, argv[:-1])
|
|
42
|
+
self.assertIn(history["sid"], argv[-1])
|
|
43
|
+
self.assertNotIn("Original user request", argv[-1])
|
|
44
|
+
|
|
45
|
+
def test_default_and_allow_bypass_only_keep_conservative_target_permissions(self):
|
|
46
|
+
cases = (("claude", "codex", [], ["--sandbox", "read-only", "--ask-for-approval", "on-request"]),
|
|
47
|
+
("claude", "codex", ["--allow-dangerously-skip-permissions"],
|
|
48
|
+
["--sandbox", "read-only", "--ask-for-approval", "on-request"]),
|
|
49
|
+
("claude", "codex", ["--permission-mode", "default"],
|
|
50
|
+
["--sandbox", "read-only", "--ask-for-approval", "on-request"]),
|
|
51
|
+
("codex", "claude", [], ["--permission-mode", "default"]))
|
|
52
|
+
for source, target, original, expected in cases:
|
|
53
|
+
with self.subTest(source=source, original=original):
|
|
54
|
+
argv = handoff.handoff_argv(source, target, original, self.history(source))
|
|
55
|
+
self.assertIsNotNone(argv)
|
|
56
|
+
self.assertEqual(argv[:-1], expected)
|
|
57
|
+
|
|
58
|
+
def test_explicit_bypass_maps_only_to_equivalent_target_bypass(self):
|
|
59
|
+
cases = (("claude", "codex", "--dangerously-skip-permissions",
|
|
60
|
+
"--dangerously-bypass-approvals-and-sandbox"),
|
|
61
|
+
("codex", "claude", "--yolo", "--dangerously-skip-permissions"),
|
|
62
|
+
("codex", "claude", "--dangerously-bypass-approvals-and-sandbox",
|
|
63
|
+
"--dangerously-skip-permissions"))
|
|
64
|
+
for source, target, option, expected in cases:
|
|
65
|
+
with self.subTest(source=source, option=option):
|
|
66
|
+
argv = handoff.handoff_argv(source, target, [option], self.history(source))
|
|
67
|
+
self.assertIsNotNone(argv)
|
|
68
|
+
self.assertEqual(argv[:-1], [expected])
|
|
69
|
+
argv = handoff.handoff_argv("claude", "codex", ["--permission-mode", "bypassPermissions"],
|
|
70
|
+
self.history("claude"))
|
|
71
|
+
self.assertEqual(argv[:-1], ["--dangerously-bypass-approvals-and-sandbox"])
|
|
72
|
+
|
|
73
|
+
def test_unsupported_restrictive_modes_and_ambiguous_permissions_hold(self):
|
|
74
|
+
for mode in ("plan", "acceptEdits", "dontAsk", "auto", "unknown"):
|
|
75
|
+
with self.subTest(mode=mode):
|
|
76
|
+
self.assertIsNone(handoff.handoff_argv("claude", "codex", ["--permission-mode", mode],
|
|
77
|
+
self.history("claude")))
|
|
78
|
+
conflicting = ["--dangerously-skip-permissions", "--permission-mode", "default"]
|
|
79
|
+
self.assertIsNone(handoff.handoff_argv("claude", "codex", conflicting, self.history("claude")))
|
|
80
|
+
for original in (["--sandbox", "read-only", "--ask-for-approval", "never"],
|
|
81
|
+
["--yolo", "--sandbox", "read-only", "--ask-for-approval", "on-request"]):
|
|
82
|
+
self.assertIsNone(handoff.handoff_argv("codex", "claude", original, self.history("codex")))
|
|
83
|
+
|
|
84
|
+
def test_custom_prompt_cannot_remove_exact_history_and_continuation_instructions(self):
|
|
85
|
+
for source, target in (("claude", "codex"), ("codex", "claude")):
|
|
86
|
+
with self.subTest(source=source):
|
|
87
|
+
history = self.history(source)
|
|
88
|
+
original = Path(history["path"]).read_bytes()
|
|
89
|
+
argv = handoff.handoff_argv(source, target, [], history,
|
|
90
|
+
prompt_override="Preserve the operator's extra instruction.")
|
|
91
|
+
prompt = argv[-1]
|
|
92
|
+
for value in (source, history["sid"], json.dumps(history["path"]), json.dumps(history["cwd"]),
|
|
93
|
+
"Preserve the operator's extra instruction."):
|
|
94
|
+
self.assertIn(value, prompt)
|
|
95
|
+
self.assertRegex(prompt.lower(), r"read.*history|history.*read")
|
|
96
|
+
self.assertRegex(prompt.lower(), r"background|subagent")
|
|
97
|
+
self.assertRegex(prompt.lower(), r"inspect|check")
|
|
98
|
+
self.assertRegex(prompt.lower(), r"inode[^0-9]*" + str(history["inode"]))
|
|
99
|
+
self.assertIn(str(history["offset"]), prompt)
|
|
100
|
+
self.assertNotIn("PRIVATE_SOURCE_TEXT_SENTINEL", prompt)
|
|
101
|
+
self.assertEqual(Path(history["path"]).read_bytes(), original)
|
|
102
|
+
|
|
103
|
+
def test_build_validates_history_before_constructing_any_target_argv(self):
|
|
104
|
+
for source, target in (("claude", "codex"), ("codex", "claude")):
|
|
105
|
+
with self.subTest(source=source):
|
|
106
|
+
history = self.history(source)
|
|
107
|
+
result = handoff.build_handoff(source, target, [], history["sid"], history["path"],
|
|
108
|
+
history["cwd"], inode=history["inode"])
|
|
109
|
+
self.assertIsNotNone(result)
|
|
110
|
+
self.assertEqual(result["history"], history)
|
|
111
|
+
self.assertIn(history["sid"], result["argv"][-1])
|
|
112
|
+
self.assertIsNone(handoff.build_handoff(source, target, [], fixtures.SID2, history["path"],
|
|
113
|
+
history["cwd"]))
|
|
114
|
+
|
|
115
|
+
def test_same_provider_unknown_provider_and_unsupported_cli_modes_are_rejected(self):
|
|
116
|
+
history = self.history("claude")
|
|
117
|
+
for source, target, original in (("claude", "claude", []), ("claude", "other", []),
|
|
118
|
+
("other", "codex", []), ("codex", "claude", []),
|
|
119
|
+
("claude", "codex", ["--fork-session"]),
|
|
120
|
+
("claude", "codex", ["-p", "Noninteractive task"]),
|
|
121
|
+
("claude", "codex", None), ("claude", "codex", [None])):
|
|
122
|
+
with self.subTest(source=source, target=target, original=original):
|
|
123
|
+
self.assertIsNone(handoff.handoff_argv(source, target, original, history))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
unittest.main()
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Cross-provider history must identify one real main conversation before any restart."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
|
|
10
|
+
import test_autoresume as fixtures
|
|
11
|
+
import autoresume_handoff as handoff
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HandoffHistoryTests(unittest.TestCase):
|
|
15
|
+
def setUp(self):
|
|
16
|
+
temporary = tempfile.TemporaryDirectory(prefix="ar-handoff-history-")
|
|
17
|
+
self.addCleanup(temporary.cleanup)
|
|
18
|
+
self.home = Path(temporary.name).resolve()
|
|
19
|
+
self.cwd = self.home / "project with 'quotes'"
|
|
20
|
+
self.cwd.mkdir()
|
|
21
|
+
|
|
22
|
+
def source(self, provider, records=None):
|
|
23
|
+
sid = fixtures.SID if provider == "claude" else fixtures.CX_SID
|
|
24
|
+
directory = self.home / provider / ("projects/project" if provider == "claude" else "sessions/2026/10/11")
|
|
25
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
filename = sid + ".jsonl" if provider == "claude" else "rollout-test-" + sid + ".jsonl"
|
|
27
|
+
path = directory / filename
|
|
28
|
+
if records is None:
|
|
29
|
+
records = [fixtures.cc_user(1000, "Source task content.", cwd=str(self.cwd))] if provider == "claude" else [
|
|
30
|
+
fixtures.cx_meta(1000, cwd=str(self.cwd)),
|
|
31
|
+
fixtures.cx_event(1001, {"type": "user_message", "message": "Source task content."})]
|
|
32
|
+
path.write_text("".join(json.dumps(record) + "\n" for record in records))
|
|
33
|
+
return sid, path
|
|
34
|
+
|
|
35
|
+
def test_both_main_history_formats_return_exact_file_identity(self):
|
|
36
|
+
for provider in ("claude", "codex"):
|
|
37
|
+
with self.subTest(provider=provider):
|
|
38
|
+
sid, path = self.source(provider)
|
|
39
|
+
result = handoff.validate_source(provider, sid, str(path), str(self.cwd))
|
|
40
|
+
self.assertIsNotNone(result)
|
|
41
|
+
self.assertEqual((result["provider"], result["sid"], result["cwd"]),
|
|
42
|
+
(provider, sid, str(self.cwd)))
|
|
43
|
+
self.assertEqual((result["path"], result["inode"], result["offset"]),
|
|
44
|
+
(str(path.resolve()), path.stat().st_ino, path.stat().st_size))
|
|
45
|
+
|
|
46
|
+
def test_wrong_session_and_replaced_inode_are_rejected(self):
|
|
47
|
+
for provider in ("claude", "codex"):
|
|
48
|
+
with self.subTest(provider=provider):
|
|
49
|
+
sid, path = self.source(provider)
|
|
50
|
+
self.assertIsNone(handoff.validate_source(provider, fixtures.SID2, str(path), str(self.cwd)))
|
|
51
|
+
inode = path.stat().st_ino
|
|
52
|
+
replacement = path.with_suffix(".replacement")
|
|
53
|
+
replacement.write_bytes(path.read_bytes())
|
|
54
|
+
replacement.replace(path)
|
|
55
|
+
self.assertNotEqual(path.stat().st_ino, inode)
|
|
56
|
+
self.assertIsNone(handoff.validate_source(provider, sid, str(path), str(self.cwd), inode=inode))
|
|
57
|
+
|
|
58
|
+
def test_codex_subagent_and_noninteractive_histories_are_rejected(self):
|
|
59
|
+
original = fixtures.cx_meta(1000, cwd=str(self.cwd))
|
|
60
|
+
alterations = ({"session_id": fixtures.CX_SID2}, {"source": {"subagent": "spawn"}},
|
|
61
|
+
{"originator": "codex_exec"}, {"thread_source": "subagent"})
|
|
62
|
+
for change in alterations:
|
|
63
|
+
with self.subTest(change=change):
|
|
64
|
+
record = copy.deepcopy(original)
|
|
65
|
+
record["payload"].update(change)
|
|
66
|
+
sid, path = self.source("codex", [record])
|
|
67
|
+
self.assertIsNone(handoff.validate_source("codex", sid, str(path), str(self.cwd)))
|
|
68
|
+
|
|
69
|
+
def test_claude_sidechain_only_and_foreign_conversation_are_rejected(self):
|
|
70
|
+
records = [fixtures.cc_user(1000, "Subagent task.", cwd=str(self.cwd), isSidechain=True),
|
|
71
|
+
fixtures.cc_user(1000, "Other conversation.", cwd=str(self.cwd), sessionId=fixtures.SID2)]
|
|
72
|
+
for record in records:
|
|
73
|
+
with self.subTest(sidechain=record["isSidechain"]):
|
|
74
|
+
sid, path = self.source("claude", [record])
|
|
75
|
+
self.assertIsNone(handoff.validate_source("claude", sid, str(path), str(self.cwd)))
|
|
76
|
+
|
|
77
|
+
def test_missing_directory_empty_and_malformed_histories_are_rejected(self):
|
|
78
|
+
for provider in ("claude", "codex"):
|
|
79
|
+
sid, path = self.source(provider)
|
|
80
|
+
fifo = path.parent / "not-a-history-fifo"
|
|
81
|
+
os.mkfifo(fifo)
|
|
82
|
+
for candidate in (path.parent / "absent.jsonl", path.parent, fifo):
|
|
83
|
+
with self.subTest(provider=provider, candidate=candidate.name):
|
|
84
|
+
self.assertIsNone(handoff.validate_source(provider, sid, str(candidate), str(self.cwd)))
|
|
85
|
+
for text in ("", "not json\n", "[]\n", '{"type":"session_meta","payload":null}\n'):
|
|
86
|
+
with self.subTest(provider=provider, malformed=text):
|
|
87
|
+
path.write_text(text)
|
|
88
|
+
self.assertIsNone(handoff.validate_source(provider, sid, str(path), str(self.cwd)))
|
|
89
|
+
|
|
90
|
+
def test_shared_history_symlink_resolves_to_the_exact_source(self):
|
|
91
|
+
for provider in ("claude", "codex"):
|
|
92
|
+
with self.subTest(provider=provider):
|
|
93
|
+
sid, path = self.source(provider)
|
|
94
|
+
link = self.home / (provider + "-account-history.jsonl")
|
|
95
|
+
link.symlink_to(path)
|
|
96
|
+
result = handoff.validate_source(provider, sid, str(link), str(self.cwd))
|
|
97
|
+
self.assertIsNotNone(result)
|
|
98
|
+
self.assertEqual(result["path"], str(path.resolve()))
|
|
99
|
+
|
|
100
|
+
def test_invalid_provider_id_and_missing_working_directory_are_rejected(self):
|
|
101
|
+
sid, path = self.source("claude")
|
|
102
|
+
for provider, target_sid, cwd in (("other", sid, str(self.cwd)), ("claude", "--last", str(self.cwd)),
|
|
103
|
+
("claude", sid, str(self.home / "absent"))):
|
|
104
|
+
with self.subTest(provider=provider, sid=target_sid, cwd=cwd):
|
|
105
|
+
self.assertIsNone(handoff.validate_source(provider, target_sid, str(path), cwd))
|
|
106
|
+
|
|
107
|
+
def test_claude_leading_progress_does_not_hide_valid_main_history(self):
|
|
108
|
+
records = [{"type": "progress", "data": {"type": "hook_progress"}},
|
|
109
|
+
fixtures.cc_user(1000, "Main task.", cwd=str(self.cwd))]
|
|
110
|
+
sid, path = self.source("claude", records)
|
|
111
|
+
self.assertIsNotNone(handoff.validate_source("claude", sid, str(path), str(self.cwd)))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
unittest.main()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Main Codex TUI histories retain their original transport when resumed elsewhere."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
import test_autoresume as fixtures
|
|
9
|
+
import autoresume_handoff as handoff
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HandoffOriginTests(unittest.TestCase):
|
|
13
|
+
def setUp(self):
|
|
14
|
+
temporary = tempfile.TemporaryDirectory(prefix="ar-handoff-origins-")
|
|
15
|
+
self.addCleanup(temporary.cleanup)
|
|
16
|
+
self.cwd = Path(temporary.name).resolve()
|
|
17
|
+
self.path = self.cwd / ("rollout-origin-" + fixtures.CX_SID + ".jsonl")
|
|
18
|
+
|
|
19
|
+
def history(self, **changes):
|
|
20
|
+
record = fixtures.cx_meta(1000, cwd=str(self.cwd))
|
|
21
|
+
record["payload"].update(source="vscode", thread_source="user", originator="codex-tui")
|
|
22
|
+
record["payload"].update(changes)
|
|
23
|
+
self.path.write_text(json.dumps(record) + "\n")
|
|
24
|
+
return handoff.validate_source("codex", fixtures.CX_SID, str(self.path), str(self.cwd))
|
|
25
|
+
|
|
26
|
+
def test_user_tui_history_accepts_cli_and_vscode_transports(self):
|
|
27
|
+
for source in (None, "cli", "vscode"):
|
|
28
|
+
with self.subTest(source=source):
|
|
29
|
+
history = self.history(source=source)
|
|
30
|
+
self.assertIsNotNone(history)
|
|
31
|
+
self.assertEqual(history["sid"], fixtures.CX_SID)
|
|
32
|
+
self.assertEqual(history["path"], str(self.path))
|
|
33
|
+
self.assertEqual(history["inode"], self.path.stat().st_ino)
|
|
34
|
+
argv = handoff.handoff_argv("codex", "claude", ["--yolo"], history)
|
|
35
|
+
self.assertEqual(argv[:-1], ["--dangerously-skip-permissions"])
|
|
36
|
+
self.assertIn(fixtures.CX_SID, argv[-1])
|
|
37
|
+
|
|
38
|
+
def test_exec_unknown_and_subagent_transports_remain_rejected(self):
|
|
39
|
+
for source in ("exec", "unknown", "app-server", {"subagent": "spawn"}):
|
|
40
|
+
with self.subTest(source=source):
|
|
41
|
+
self.assertIsNone(self.history(source=source))
|
|
42
|
+
|
|
43
|
+
def test_vscode_transport_does_not_bypass_main_conversation_checks(self):
|
|
44
|
+
changes = ({"thread_source": "subagent"}, {"originator": "codex_exec"},
|
|
45
|
+
{"originator": "unknown"}, {"session_id": fixtures.CX_SID2},
|
|
46
|
+
{"id": fixtures.CX_SID2}, {"cwd": str(self.cwd / "unrelated")})
|
|
47
|
+
for fields in changes:
|
|
48
|
+
with self.subTest(fields=fields):
|
|
49
|
+
self.assertIsNone(self.history(**fields))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
unittest.main()
|