relay-codex 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -7
- package/docs/commands.md +123 -39
- package/docs/publishing.md +29 -13
- package/docs/troubleshooting.md +15 -3
- package/package.json +1 -1
- package/python/Menu.swift +169 -51
- package/python/commands.py +219 -20
- package/python/compatibility.py +3 -1
- package/python/desktop.py +131 -12
- package/python/execution.py +551 -0
- package/python/launch.py +375 -75
- package/python/pool.py +7 -0
- package/python/router.py +305 -70
- package/python/service.py +216 -142
- package/python/settings.py +1 -0
- package/python/tools.py +233 -0
- package/python/watch.py +74 -0
- package/tests/fake_native.py +4 -2
- package/tests/test_audit.py +320 -0
- package/tests/test_controller.py +3 -5
- package/tests/test_features.py +350 -0
- package/tests/test_launch.py +150 -0
- package/tests/test_services.py +6 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""Local management and native-option behavior, with isolated state."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextlib
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import plistlib
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import time
|
|
14
|
+
import unittest
|
|
15
|
+
from unittest.mock import patch
|
|
16
|
+
|
|
17
|
+
from launch import backend_arguments, native_mode, prepare_worktree, translate
|
|
18
|
+
from pool import Pool
|
|
19
|
+
from router import parse_arguments
|
|
20
|
+
from settings import atomic_json
|
|
21
|
+
import commands
|
|
22
|
+
import desktop
|
|
23
|
+
import execution
|
|
24
|
+
import tools
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ManagementTests(unittest.TestCase):
|
|
28
|
+
def test_profile_layer_and_explicit_config_precedence(self):
|
|
29
|
+
with tempfile.TemporaryDirectory() as folder:
|
|
30
|
+
home = Path(folder)
|
|
31
|
+
(home / "work.config.toml").write_text(
|
|
32
|
+
'model="profile-model"\n[features]\nexample=true\n'
|
|
33
|
+
)
|
|
34
|
+
with patch.dict(os.environ, {"CODEX_HOME": folder}):
|
|
35
|
+
args = backend_arguments(["-pwork", "-c", 'model="explicit-model"'])
|
|
36
|
+
self.assertEqual(args[-2:], ["-c", 'model="explicit-model"'])
|
|
37
|
+
self.assertIn('model="profile-model"', args)
|
|
38
|
+
self.assertIn("features.example=true", args)
|
|
39
|
+
self.assertEqual(
|
|
40
|
+
translate(["resume", "-pwork", "--add-dir", "/tmp"])[0], ["resume"]
|
|
41
|
+
)
|
|
42
|
+
with self.assertRaises(ValueError):
|
|
43
|
+
backend_arguments(["-p", "../outside"])
|
|
44
|
+
self.assertFalse(native_mode(["resume", "-pwork", "--add-dir=/tmp"]))
|
|
45
|
+
|
|
46
|
+
def test_background_cd_resolves_against_supplied_directory(self):
|
|
47
|
+
with tempfile.TemporaryDirectory() as folder:
|
|
48
|
+
root = Path(folder).resolve()
|
|
49
|
+
(root / "child").mkdir()
|
|
50
|
+
cwd, settings, backend = __import__("service").headless_options(
|
|
51
|
+
["-Cchild", "-mtest", "-c", "features.x=true"], str(root)
|
|
52
|
+
)
|
|
53
|
+
self.assertEqual(cwd, str(root / "child"))
|
|
54
|
+
self.assertEqual(settings["model"], "test")
|
|
55
|
+
self.assertEqual(backend[-2:], ["-c", "features.x=true"])
|
|
56
|
+
|
|
57
|
+
def test_real_git_worktree_is_separate_and_resume_combination_is_rejected(self):
|
|
58
|
+
with tempfile.TemporaryDirectory() as folder:
|
|
59
|
+
root = Path(folder).resolve()
|
|
60
|
+
repo = root / "repo"
|
|
61
|
+
state = root / "state"
|
|
62
|
+
repo.mkdir()
|
|
63
|
+
state.mkdir()
|
|
64
|
+
subprocess.run(["git", "init", "-q", str(repo)], check=True)
|
|
65
|
+
subprocess.run(
|
|
66
|
+
[
|
|
67
|
+
"git",
|
|
68
|
+
"-C",
|
|
69
|
+
str(repo),
|
|
70
|
+
"-c",
|
|
71
|
+
"user.name=Test",
|
|
72
|
+
"-c",
|
|
73
|
+
"user.email=test@example.invalid",
|
|
74
|
+
"commit",
|
|
75
|
+
"--allow-empty",
|
|
76
|
+
"-qm",
|
|
77
|
+
"test",
|
|
78
|
+
],
|
|
79
|
+
check=True,
|
|
80
|
+
)
|
|
81
|
+
args, cwd = prepare_worktree(["--worktree", "hello"], state, str(repo))
|
|
82
|
+
self.assertNotEqual(cwd, str(repo))
|
|
83
|
+
self.assertTrue((Path(cwd) / ".git").exists())
|
|
84
|
+
self.assertNotIn("--worktree", args)
|
|
85
|
+
with self.assertRaises(ValueError):
|
|
86
|
+
prepare_worktree(["resume", "--worktree"], state, str(repo))
|
|
87
|
+
|
|
88
|
+
def test_account_emails_are_displayed_and_resolve_without_reading_tokens(self):
|
|
89
|
+
with tempfile.TemporaryDirectory() as folder:
|
|
90
|
+
state = Path(folder)
|
|
91
|
+
home = state / "accounts" / "account1"
|
|
92
|
+
home.mkdir(parents=True)
|
|
93
|
+
atomic_json(home / "enrolled", {"identity": "synthetic"})
|
|
94
|
+
atomic_json(home / "identity.json", {"email": "person@example.invalid"})
|
|
95
|
+
with patch(
|
|
96
|
+
"router.read_keychain",
|
|
97
|
+
side_effect=AssertionError("Status must not read Keychain"),
|
|
98
|
+
):
|
|
99
|
+
result = commands.snapshot(state)
|
|
100
|
+
self.assertEqual(
|
|
101
|
+
result["accounts"][0]["name"], "person@example.invalid"
|
|
102
|
+
)
|
|
103
|
+
self.assertEqual(
|
|
104
|
+
commands.resolve_label(state, "person@example.invalid"), "account1"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def test_redacted_diagnostics_omit_paths_names_and_prompts(self):
|
|
108
|
+
with tempfile.TemporaryDirectory() as folder:
|
|
109
|
+
state = Path(folder)
|
|
110
|
+
pool = Pool(state)
|
|
111
|
+
pool.assign("private-label")
|
|
112
|
+
pool.touch(
|
|
113
|
+
cwd="/secret/project",
|
|
114
|
+
name="secret-task",
|
|
115
|
+
state="working",
|
|
116
|
+
prompt="PRIVATE_PROMPT",
|
|
117
|
+
)
|
|
118
|
+
pool.record("working", project="/secret/project", email="PRIVATE_EMAIL")
|
|
119
|
+
target = state / "diagnostics.json"
|
|
120
|
+
tools.diagnostics(state, target)
|
|
121
|
+
output = target.read_text()
|
|
122
|
+
for secret in [
|
|
123
|
+
"/secret/project",
|
|
124
|
+
"private-label",
|
|
125
|
+
"secret-task",
|
|
126
|
+
"PRIVATE_PROMPT",
|
|
127
|
+
"PRIVATE_EMAIL",
|
|
128
|
+
folder,
|
|
129
|
+
]:
|
|
130
|
+
self.assertNotIn(secret, output)
|
|
131
|
+
self.assertEqual(target.stat().st_mode & 0o777, 0o600)
|
|
132
|
+
pool.close()
|
|
133
|
+
|
|
134
|
+
def test_cleanup_dry_run_retains_workspace_and_runtime_references(self):
|
|
135
|
+
with tempfile.TemporaryDirectory() as folder:
|
|
136
|
+
state = Path(folder)
|
|
137
|
+
old = state / "native-runtimes" / "old"
|
|
138
|
+
old.mkdir(parents=True)
|
|
139
|
+
(old / "binary").write_text("test")
|
|
140
|
+
unused = state / "relay-runtimes" / "unused"
|
|
141
|
+
unused.mkdir(parents=True)
|
|
142
|
+
job = state / "services" / "saved.job.json"
|
|
143
|
+
atomic_json(job, {"state": "stopped", "binary": str(old / "binary")})
|
|
144
|
+
atomic_json(state / "workspaces.json", {"daily": ["saved"]})
|
|
145
|
+
before = time.time() - 90 * 86400
|
|
146
|
+
for p in [old, unused, job]:
|
|
147
|
+
os.utime(p, (before, before))
|
|
148
|
+
value = tools.cleanup(state, days=30, jobs=True)
|
|
149
|
+
self.assertTrue(value["dry_run"])
|
|
150
|
+
self.assertEqual(value["paths"], [str(unused)])
|
|
151
|
+
self.assertTrue(unused.exists())
|
|
152
|
+
self.assertTrue(job.exists())
|
|
153
|
+
|
|
154
|
+
def test_launch_agent_uses_stable_app_and_has_no_auth_token(self):
|
|
155
|
+
with tempfile.TemporaryDirectory() as folder:
|
|
156
|
+
state = Path(folder) / "state"
|
|
157
|
+
with patch(
|
|
158
|
+
"subprocess.run", return_value=subprocess.CompletedProcess([], 0)
|
|
159
|
+
):
|
|
160
|
+
path = desktop.login_item(state, enabled=True, home=folder)
|
|
161
|
+
data = plistlib.loads(path.read_bytes())
|
|
162
|
+
self.assertIn("RelayMenu", data["ProgramArguments"][0])
|
|
163
|
+
self.assertTrue(data["RunAtLoad"])
|
|
164
|
+
self.assertNotIn("token", path.read_text().lower())
|
|
165
|
+
desktop.login_item(state, enabled=False, home=folder)
|
|
166
|
+
self.assertFalse(path.exists())
|
|
167
|
+
|
|
168
|
+
@unittest.skipUnless(sys.platform == "darwin", "Native app bundle test")
|
|
169
|
+
def test_native_menu_bundle_signs_and_symbols_exist(self):
|
|
170
|
+
with tempfile.TemporaryDirectory() as folder:
|
|
171
|
+
app = desktop.menubar(
|
|
172
|
+
Path(folder), launcher="/usr/bin/false", build_only=True
|
|
173
|
+
)
|
|
174
|
+
verify = subprocess.run(
|
|
175
|
+
["/usr/bin/codesign", "--verify", "--strict", str(app)],
|
|
176
|
+
capture_output=True,
|
|
177
|
+
)
|
|
178
|
+
self.assertEqual(verify.returncode, 0, verify.stderr.decode())
|
|
179
|
+
result = subprocess.run(
|
|
180
|
+
[str(app / "Contents/MacOS/RelayMenu"), "--self-check"],
|
|
181
|
+
capture_output=True,
|
|
182
|
+
text=True,
|
|
183
|
+
)
|
|
184
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
185
|
+
|
|
186
|
+
def test_secret_profile_fields_do_not_enter_process_arguments_or_pool_settings(
|
|
187
|
+
self,
|
|
188
|
+
):
|
|
189
|
+
from launch import profile_config
|
|
190
|
+
from router import Controller
|
|
191
|
+
from test_router import FakeAccount, FakeBackend
|
|
192
|
+
|
|
193
|
+
with tempfile.TemporaryDirectory() as folder:
|
|
194
|
+
home = Path(folder)
|
|
195
|
+
(home / "private.config.toml").write_text(
|
|
196
|
+
'model="normal"\n[mcp_servers.example.http_headers]\nAuthorization="PRIVATE_TEST_SECRET"\n'
|
|
197
|
+
)
|
|
198
|
+
with patch.dict(os.environ, {"CODEX_HOME": folder}):
|
|
199
|
+
arguments = backend_arguments(["-p", "private"])
|
|
200
|
+
self.assertNotIn("PRIVATE_TEST_SECRET", " ".join(arguments))
|
|
201
|
+
overrides = profile_config(["-p", "private"])
|
|
202
|
+
self.assertIn("PRIVATE_TEST_SECRET", json.dumps(overrides))
|
|
203
|
+
c = Controller(
|
|
204
|
+
[FakeAccount("one")], FakeBackend(), report=lambda _: None
|
|
205
|
+
)
|
|
206
|
+
c.merge_settings("thread", {"config": overrides})
|
|
207
|
+
self.assertNotIn("PRIVATE_TEST_SECRET", json.dumps(c.thread_settings))
|
|
208
|
+
|
|
209
|
+
def test_help_command_and_combined_run_help(self):
|
|
210
|
+
import router
|
|
211
|
+
|
|
212
|
+
for args in [
|
|
213
|
+
[],
|
|
214
|
+
["help"],
|
|
215
|
+
["help", "workspace"],
|
|
216
|
+
["help", "run"],
|
|
217
|
+
["run", "--help"],
|
|
218
|
+
]:
|
|
219
|
+
with (
|
|
220
|
+
self.subTest(args=args),
|
|
221
|
+
patch.object(sys, "argv", ["relay", *args]),
|
|
222
|
+
patch("router.codex_path", return_value="/codex"),
|
|
223
|
+
patch("subprocess.call", return_value=0),
|
|
224
|
+
contextlib.redirect_stdout(io.StringIO()) as out,
|
|
225
|
+
):
|
|
226
|
+
if args in (["help", "run"], ["run", "--help"]):
|
|
227
|
+
self.assertEqual(router.main(), 0)
|
|
228
|
+
self.assertIn("Relay launch options", out.getvalue())
|
|
229
|
+
else:
|
|
230
|
+
with self.assertRaises(SystemExit) as end:
|
|
231
|
+
router.main()
|
|
232
|
+
self.assertEqual(end.exception.code, 0)
|
|
233
|
+
self.assertNotIn("router.py", out.getvalue())
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class ExecutionTests(unittest.IsolatedAsyncioTestCase):
|
|
237
|
+
async def test_managed_exec_json_and_last_message_over_real_jsonl(self):
|
|
238
|
+
from test_services import fake_executable
|
|
239
|
+
from test_router import FakeAccount
|
|
240
|
+
|
|
241
|
+
with tempfile.TemporaryDirectory() as folder:
|
|
242
|
+
root = Path(folder)
|
|
243
|
+
home = root / "home"
|
|
244
|
+
home.mkdir()
|
|
245
|
+
state = root / "state"
|
|
246
|
+
state.mkdir()
|
|
247
|
+
for label in ("one", "two"):
|
|
248
|
+
account = state / "accounts" / label
|
|
249
|
+
account.mkdir(parents=True)
|
|
250
|
+
(account / "enrolled").write_text("{}")
|
|
251
|
+
binary = fake_executable(root)
|
|
252
|
+
output_file = root / "answer.txt"
|
|
253
|
+
args = parse_arguments(
|
|
254
|
+
[
|
|
255
|
+
"run",
|
|
256
|
+
"--",
|
|
257
|
+
"exec",
|
|
258
|
+
"--skip-git-repo-check",
|
|
259
|
+
"--json",
|
|
260
|
+
"-o",
|
|
261
|
+
str(output_file),
|
|
262
|
+
"test only",
|
|
263
|
+
]
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
class Account(FakeAccount):
|
|
267
|
+
def __init__(self, label, *args):
|
|
268
|
+
super().__init__(label)
|
|
269
|
+
|
|
270
|
+
output = io.StringIO()
|
|
271
|
+
with (
|
|
272
|
+
patch.dict(os.environ, {"CODEX_HOME": str(home)}),
|
|
273
|
+
patch("broker.ClientAccount", Account),
|
|
274
|
+
patch("sys.stdin", io.StringIO("")),
|
|
275
|
+
contextlib.redirect_stdout(output),
|
|
276
|
+
):
|
|
277
|
+
self.assertEqual(
|
|
278
|
+
await asyncio.wait_for(execution.run(args, state, binary), 8), 0
|
|
279
|
+
)
|
|
280
|
+
events = [json.loads(line) for line in output.getvalue().splitlines()]
|
|
281
|
+
self.assertEqual(events[0]["type"], "thread.started")
|
|
282
|
+
self.assertEqual(events[-1]["type"], "turn.completed")
|
|
283
|
+
self.assertEqual(output_file.read_text(), "FAKE_OK\n")
|
|
284
|
+
history = json.loads((home / "fake-history.json").read_text())
|
|
285
|
+
self.assertEqual(
|
|
286
|
+
sum(call["method"] == "turn/start" for call in history["calls"]), 1
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
async def test_noninteractive_approval_is_not_automatically_accepted(self):
|
|
290
|
+
output = execution.Output(True)
|
|
291
|
+
with contextlib.redirect_stderr(io.StringIO()):
|
|
292
|
+
await output.send(
|
|
293
|
+
json.dumps(
|
|
294
|
+
{
|
|
295
|
+
"id": 1,
|
|
296
|
+
"method": "item/commandExecution/requestApproval",
|
|
297
|
+
"params": {},
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
self.assertTrue(output.done.is_set())
|
|
302
|
+
self.assertEqual(output.status, 1)
|
|
303
|
+
|
|
304
|
+
async def test_exec_json_includes_tool_records(self):
|
|
305
|
+
output = execution.Output(True)
|
|
306
|
+
output.thread = "parent"
|
|
307
|
+
stream = io.StringIO()
|
|
308
|
+
with contextlib.redirect_stdout(stream):
|
|
309
|
+
for kind in ("mcpToolCall", "webSearch", "collabAgentToolCall"):
|
|
310
|
+
await output.send(
|
|
311
|
+
json.dumps(
|
|
312
|
+
{
|
|
313
|
+
"method": "item/completed",
|
|
314
|
+
"params": {
|
|
315
|
+
"threadId": "parent",
|
|
316
|
+
"item": {"id": kind, "type": kind},
|
|
317
|
+
},
|
|
318
|
+
}
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
self.assertEqual(
|
|
322
|
+
[
|
|
323
|
+
json.loads(line)["item"]["type"]
|
|
324
|
+
for line in stream.getvalue().splitlines()
|
|
325
|
+
],
|
|
326
|
+
["mcp_tool_call", "web_search", "collab_tool_call"],
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
async def test_only_foreground_thread_can_finish_exec(self):
|
|
330
|
+
output = execution.Output(True)
|
|
331
|
+
output.thread = "parent"
|
|
332
|
+
await output.send(
|
|
333
|
+
json.dumps(
|
|
334
|
+
{
|
|
335
|
+
"method": "turn/completed",
|
|
336
|
+
"params": {"threadId": "child", "turn": {"status": "completed"}},
|
|
337
|
+
}
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
self.assertFalse(output.done.is_set())
|
|
341
|
+
|
|
342
|
+
def test_native_exec_subcommands_and_prompt_separator(self):
|
|
343
|
+
value = execution.options(
|
|
344
|
+
["exec", "resume", "saved", "--json", "--", "--yolo"], "/tmp"
|
|
345
|
+
)
|
|
346
|
+
self.assertEqual(value["mode"], "resume")
|
|
347
|
+
self.assertEqual(value["target"], "saved")
|
|
348
|
+
self.assertEqual(value["prompt"], "--yolo")
|
|
349
|
+
self.assertNotIn("--yolo", value["flags"])
|
|
350
|
+
self.assertTrue(native_mode(["exec", "help"]))
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Native argv and working-directory regressions."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
from unittest.mock import AsyncMock, patch
|
|
8
|
+
|
|
9
|
+
from launch import (
|
|
10
|
+
backend_arguments,
|
|
11
|
+
frontend_arguments,
|
|
12
|
+
native_mode,
|
|
13
|
+
translate,
|
|
14
|
+
working_directory,
|
|
15
|
+
)
|
|
16
|
+
from router import dispatch, resume_target, run_pool
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ArgumentTests(unittest.TestCase):
|
|
20
|
+
def test_values_that_look_like_flags_are_not_reinterpreted(self):
|
|
21
|
+
argv = ["resume", "-c", "--yolo", "-i", "--sandbox", "-m", "--search"]
|
|
22
|
+
self.assertEqual(translate(argv), (argv, {}))
|
|
23
|
+
self.assertEqual(
|
|
24
|
+
backend_arguments(argv), ["-c", "--yolo", "-c", 'model="--search"']
|
|
25
|
+
)
|
|
26
|
+
self.assertFalse(native_mode(["resume", "-i", "--oss"]))
|
|
27
|
+
|
|
28
|
+
def test_attached_options_and_config_boundaries(self):
|
|
29
|
+
argv, overrides = translate(
|
|
30
|
+
[
|
|
31
|
+
"resume",
|
|
32
|
+
"-sworkspace-write",
|
|
33
|
+
"-anever",
|
|
34
|
+
"-mcustom",
|
|
35
|
+
"--",
|
|
36
|
+
"--yolo",
|
|
37
|
+
"--search",
|
|
38
|
+
]
|
|
39
|
+
)
|
|
40
|
+
self.assertEqual(
|
|
41
|
+
overrides, {"sandbox": "workspace-write", "approvalPolicy": "never"}
|
|
42
|
+
)
|
|
43
|
+
self.assertEqual(backend_arguments(argv), ["-c", 'model="custom"'])
|
|
44
|
+
|
|
45
|
+
def test_approve_for_me_and_private_backend(self):
|
|
46
|
+
argv, overrides = translate(["resume", "--approve-for-me", "--no-daemon"])
|
|
47
|
+
self.assertEqual(argv, ["resume"])
|
|
48
|
+
self.assertEqual(
|
|
49
|
+
overrides,
|
|
50
|
+
{
|
|
51
|
+
"approvalPolicy": "on-request",
|
|
52
|
+
"sandbox": "workspace-write",
|
|
53
|
+
"approvalsReviewer": "auto_review",
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def test_native_modes_keep_command_and_option_values_distinct(self):
|
|
58
|
+
for argv in (
|
|
59
|
+
["resume", "--oss"],
|
|
60
|
+
["resume", "--help"],
|
|
61
|
+
["--remote=ws://localhost:1", "resume"],
|
|
62
|
+
):
|
|
63
|
+
with self.subTest(argv=argv):
|
|
64
|
+
self.assertTrue(native_mode(argv))
|
|
65
|
+
for argv in (
|
|
66
|
+
["resume", "--last"],
|
|
67
|
+
["exec", "--json", "hello"],
|
|
68
|
+
["-m", "custom", "review", "--uncommitted"],
|
|
69
|
+
["-c", "exec", "resume"],
|
|
70
|
+
["fork", "--", "--oss"],
|
|
71
|
+
["a prompt", "exec"],
|
|
72
|
+
):
|
|
73
|
+
with self.subTest(argv=argv):
|
|
74
|
+
self.assertFalse(native_mode(argv))
|
|
75
|
+
|
|
76
|
+
def test_cwd_defaults_and_relative_explicit_options(self):
|
|
77
|
+
with tempfile.TemporaryDirectory() as folder:
|
|
78
|
+
cwd = str(Path(folder).resolve())
|
|
79
|
+
self.assertEqual(frontend_arguments(["resume"], cwd), ["-C", cwd, "resume"])
|
|
80
|
+
for option in (["-C", "child"], ["--cd=child"], ["-Cchild"]):
|
|
81
|
+
argv = ["resume", *option, "--all", "-i", "picture.png"]
|
|
82
|
+
target = str(Path(cwd) / "child")
|
|
83
|
+
self.assertEqual(working_directory(argv, cwd), target)
|
|
84
|
+
self.assertEqual(
|
|
85
|
+
frontend_arguments(argv, cwd),
|
|
86
|
+
["-C", target, "resume", "--all", "-i", "picture.png"],
|
|
87
|
+
)
|
|
88
|
+
self.assertEqual(
|
|
89
|
+
working_directory(["-c", "--cd=wrong", "--", "-Cwrong"], cwd), cwd
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def test_uuid_in_an_option_is_not_a_resume_target(self):
|
|
93
|
+
ident = "01a1aa71-5cb7-7bc0-8c23-bcb26f693cf7"
|
|
94
|
+
self.assertIsNone(
|
|
95
|
+
resume_target(["resume", "-m", ident], Path("/missing"), "/tmp")
|
|
96
|
+
)
|
|
97
|
+
self.assertEqual(
|
|
98
|
+
resume_target(["resume", "-m", "custom", ident], Path("/missing"), "/tmp"),
|
|
99
|
+
ident,
|
|
100
|
+
)
|
|
101
|
+
self.assertIsNone(
|
|
102
|
+
resume_target(["resume", "named-session", ident], Path("/missing"), "/tmp")
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class LaunchFlowTests(unittest.IsolatedAsyncioTestCase):
|
|
107
|
+
async def test_effective_project_and_frontend_invocation_cwd_are_separate(self):
|
|
108
|
+
with tempfile.TemporaryDirectory() as folder:
|
|
109
|
+
root = Path(folder).resolve()
|
|
110
|
+
project = root / "project"
|
|
111
|
+
project.mkdir()
|
|
112
|
+
state = root / "state"
|
|
113
|
+
state.mkdir()
|
|
114
|
+
args = argparse.Namespace(
|
|
115
|
+
codex_args=["--", "resume", "--cd", "project", "--all", "--yolo"],
|
|
116
|
+
profile=None,
|
|
117
|
+
first=None,
|
|
118
|
+
name=None,
|
|
119
|
+
)
|
|
120
|
+
with (
|
|
121
|
+
patch("os.getcwd", return_value=str(root)),
|
|
122
|
+
patch(
|
|
123
|
+
"settings.load", return_value={"background_on_disconnect": True}
|
|
124
|
+
) as prefs,
|
|
125
|
+
patch("service.start", return_value="test") as start,
|
|
126
|
+
patch("service.attach", new=AsyncMock(return_value=0)) as attach,
|
|
127
|
+
):
|
|
128
|
+
self.assertEqual(await run_pool(args, state, "/codex"), 0)
|
|
129
|
+
self.assertEqual(start.call_args.kwargs["cwd"], str(project))
|
|
130
|
+
self.assertEqual(
|
|
131
|
+
start.call_args.kwargs["arguments"],
|
|
132
|
+
["-C", str(project), "resume", "--all", "--yolo"],
|
|
133
|
+
)
|
|
134
|
+
self.assertEqual(attach.call_args.kwargs["cwd"], str(root))
|
|
135
|
+
self.assertTrue(
|
|
136
|
+
all(call.args[1] == str(project) for call in prefs.call_args_list)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def test_native_utilities_need_no_account_pool_or_protocol_adaptation(self):
|
|
140
|
+
argv = ["--", "mcp", "list", "--", "literal value"]
|
|
141
|
+
args = argparse.Namespace(command="run", codex_args=argv)
|
|
142
|
+
process = type("Process", (), {"wait": AsyncMock(return_value=7)})()
|
|
143
|
+
with (
|
|
144
|
+
patch("launch.native_commands", return_value={"mcp"}),
|
|
145
|
+
patch(
|
|
146
|
+
"asyncio.create_subprocess_exec", new=AsyncMock(return_value=process)
|
|
147
|
+
) as spawn,
|
|
148
|
+
):
|
|
149
|
+
self.assertEqual(await dispatch(args, Path("/unused"), "/codex"), 7)
|
|
150
|
+
self.assertEqual(spawn.call_args.args, ("/codex", *argv[1:]))
|
package/tests/test_services.py
CHANGED
|
@@ -396,6 +396,12 @@ class ServiceTests(unittest.IsolatedAsyncioTestCase):
|
|
|
396
396
|
await service.attach(self.state, self.ident, "/new/global/codex"), 0
|
|
397
397
|
)
|
|
398
398
|
self.assertEqual(spawn.call_args.args[0], self.binary)
|
|
399
|
+
expected_cwd = json.loads(self.meta.read_text())["cwd"]
|
|
400
|
+
argv = spawn.call_args.args
|
|
401
|
+
self.assertEqual(
|
|
402
|
+
argv[argv.index("-C") + 1], str(Path(expected_cwd).resolve())
|
|
403
|
+
)
|
|
404
|
+
self.assertEqual(spawn.call_args.kwargs["cwd"], expected_cwd)
|
|
399
405
|
|
|
400
406
|
async def test_takeover_rotates_capability_and_old_frontend_cannot_reconnect(self):
|
|
401
407
|
from websockets.asyncio.client import connect
|