Skip to content

Commit 59ce386

Browse files
committed
Fix manual termination confirmation override behavior
1 parent b4a161f commit 59ce386

4 files changed

Lines changed: 130 additions & 11 deletions

File tree

src/codexsync/app.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -326,21 +326,34 @@ def _handle_running_codex(
326326
if not cfg.process_detection.allow_terminate_if_running:
327327
raise SafetyPreconditionError("Codex process is running and termination is disabled by config.")
328328

329-
details = ", ".join(f"{proc.name}:{proc.pid}" for proc in snapshot.main_processes)
330-
approved = confirm_process_termination(
331-
"Codex main process is still running, but codex-windows-sandbox is not detected.\n\n"
332-
f"Detected main processes: {details}\n\n"
333-
"Terminate Codex processes now and continue sync?",
334-
mode=cfg.process_detection.terminate_confirmation_mode,
329+
require_manual_confirmation = _resolve_manual_terminate_confirmation(
330+
manual_override=manual_override,
331+
cfg=cfg,
335332
)
336-
if not approved:
337-
raise SafetyPreconditionError("User rejected Codex process termination.")
333+
details = ", ".join(f"{proc.name}:{proc.pid}" for proc in snapshot.main_processes)
334+
if require_manual_confirmation:
335+
approved = confirm_process_termination(
336+
"Codex main process is still running, but codex-windows-sandbox is not detected.\n\n"
337+
f"Detected main processes: {details}\n\n"
338+
"Terminate Codex processes now and continue sync?",
339+
mode=cfg.process_detection.terminate_confirmation_mode,
340+
)
341+
if not approved:
342+
raise SafetyPreconditionError("User rejected Codex process termination.")
343+
else:
344+
LOG.info("Manual termination confirmation disabled; terminating Codex process automatically.")
338345

339346
terminated = detector.terminate(snapshot.main_processes, timeout_seconds=cfg.process_detection.terminate_timeout_seconds)
340347
if not terminated:
341348
raise FailSafeError("Failed to terminate Codex processes before timeout.")
342349

343350

351+
def _resolve_manual_terminate_confirmation(manual_override: bool | None, cfg: AppConfig) -> bool:
352+
if manual_override is not None:
353+
return manual_override
354+
return bool(cfg.process_detection.manual_terminate_confirmation)
355+
356+
344357
def _current_os_background_processes(cfg: AppConfig) -> list[str]:
345358
os_key = _current_os_key()
346359
configured = cfg.process_detection.background_process_names.get(os_key, [])

src/codexsync/cli.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def main(argv: list[str] | None = None) -> int:
9191
if args.command == "plan":
9292
ctx = build_context(
9393
config_path,
94-
manual_terminate_confirmation_override=args.manual_terminate_confirmation,
94+
manual_terminate_confirmation_override=(True if args.manual_terminate_confirmation else None),
9595
enforce_safety=False,
9696
)
9797
print_plan(ctx.plan)
@@ -100,7 +100,7 @@ def main(argv: list[str] | None = None) -> int:
100100
if args.command == "sync":
101101
ctx = build_context(
102102
config_path,
103-
manual_terminate_confirmation_override=args.manual_terminate_confirmation,
103+
manual_terminate_confirmation_override=(True if args.manual_terminate_confirmation else None),
104104
enforce_safety=True,
105105
)
106106
dry_run = ctx.config.sync.dry_run_default
@@ -124,7 +124,7 @@ def main(argv: list[str] | None = None) -> int:
124124
snapshot_name=args.snapshot,
125125
target=args.target,
126126
dry_run=dry_run,
127-
manual_terminate_confirmation_override=args.manual_terminate_confirmation,
127+
manual_terminate_confirmation_override=(True if args.manual_terminate_confirmation else None),
128128
)
129129
mode = "Dry-run" if dry_run else "Restore"
130130
print(
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from __future__ import annotations
2+
3+
from types import SimpleNamespace
4+
from unittest.mock import patch
5+
import unittest
6+
7+
from codexsync.cli import main
8+
from codexsync.exceptions import ConfigError
9+
10+
11+
def _ctx() -> SimpleNamespace:
12+
return SimpleNamespace(
13+
config=SimpleNamespace(sync=SimpleNamespace(dry_run_default=True)),
14+
plan=SimpleNamespace(conflicts=[]),
15+
)
16+
17+
18+
class CliManualConfirmationTests(unittest.TestCase):
19+
@patch("codexsync.cli.run_sync")
20+
@patch("codexsync.cli.build_context", return_value=_ctx())
21+
@patch("codexsync.cli.load_config", side_effect=ConfigError("skip verbose config load"))
22+
def test_sync_without_flag_passes_none_override(self, _load_cfg, build_ctx, _run_sync) -> None:
23+
code = main(["-c", "config.toml", "sync", "--dry-run"])
24+
self.assertEqual(code, 0)
25+
self.assertEqual(build_ctx.call_args.kwargs["manual_terminate_confirmation_override"], None)
26+
27+
@patch("codexsync.cli.run_sync")
28+
@patch("codexsync.cli.build_context", return_value=_ctx())
29+
@patch("codexsync.cli.load_config", side_effect=ConfigError("skip verbose config load"))
30+
def test_sync_with_flag_passes_true_override(self, _load_cfg, build_ctx, _run_sync) -> None:
31+
code = main(["-c", "config.toml", "--manual-terminate-confirmation", "sync", "--dry-run"])
32+
self.assertEqual(code, 0)
33+
self.assertEqual(build_ctx.call_args.kwargs["manual_terminate_confirmation_override"], True)
34+
35+
36+
if __name__ == "__main__":
37+
unittest.main()

tests/test_safety_policy.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,23 @@ def _build_cfg() -> AppConfig:
5757

5858

5959
class SafetyPolicyTests(unittest.TestCase):
60+
@patch("codexsync.app.sys.platform", "win32")
61+
@patch("codexsync.app.confirm_process_termination", return_value=True)
62+
def test_manual_confirmation_enabled_by_default_prompts(self, confirm_mock) -> None:
63+
cfg = _build_cfg()
64+
cfg.process_detection.manual_terminate_confirmation = True
65+
detector = _DetectorStub()
66+
main = [ProcessInfo(pid=10, name="Codex.exe")]
67+
snapshot = ProcessSnapshot(
68+
main_processes=main,
69+
subprocesses=[],
70+
sandbox_detected=False,
71+
)
72+
73+
_handle_running_codex(cfg, detector, snapshot, manual_override=None)
74+
self.assertEqual(detector.terminated, [main])
75+
confirm_mock.assert_called_once()
76+
6077
@patch("codexsync.app.sys.platform", "win32")
6178
def test_sandbox_detected_blocks_without_terminate(self) -> None:
6279
cfg = _build_cfg()
@@ -83,6 +100,58 @@ def test_no_sandbox_prompts_and_terminates(self, _confirm_mock) -> None:
83100
)
84101
_handle_running_codex(cfg, detector, snapshot, manual_override=None)
85102
self.assertEqual(detector.terminated, [main])
103+
_confirm_mock.assert_called_once()
104+
105+
@patch("codexsync.app.sys.platform", "win32")
106+
@patch("codexsync.app.confirm_process_termination", return_value=True)
107+
def test_manual_confirmation_disabled_in_config_skips_prompt(self, confirm_mock) -> None:
108+
cfg = _build_cfg()
109+
cfg.process_detection.manual_terminate_confirmation = False
110+
detector = _DetectorStub()
111+
main = [ProcessInfo(pid=10, name="Codex.exe")]
112+
snapshot = ProcessSnapshot(
113+
main_processes=main,
114+
subprocesses=[],
115+
sandbox_detected=False,
116+
)
117+
118+
_handle_running_codex(cfg, detector, snapshot, manual_override=None)
119+
self.assertEqual(detector.terminated, [main])
120+
confirm_mock.assert_not_called()
121+
122+
@patch("codexsync.app.sys.platform", "win32")
123+
@patch("codexsync.app.confirm_process_termination", return_value=True)
124+
def test_override_true_forces_prompt_even_if_config_disabled(self, confirm_mock) -> None:
125+
cfg = _build_cfg()
126+
cfg.process_detection.manual_terminate_confirmation = False
127+
detector = _DetectorStub()
128+
main = [ProcessInfo(pid=10, name="Codex.exe")]
129+
snapshot = ProcessSnapshot(
130+
main_processes=main,
131+
subprocesses=[],
132+
sandbox_detected=False,
133+
)
134+
135+
_handle_running_codex(cfg, detector, snapshot, manual_override=True)
136+
self.assertEqual(detector.terminated, [main])
137+
confirm_mock.assert_called_once()
138+
139+
@patch("codexsync.app.sys.platform", "win32")
140+
@patch("codexsync.app.confirm_process_termination", return_value=True)
141+
def test_override_false_skips_prompt_even_if_config_enabled(self, confirm_mock) -> None:
142+
cfg = _build_cfg()
143+
cfg.process_detection.manual_terminate_confirmation = True
144+
detector = _DetectorStub()
145+
main = [ProcessInfo(pid=10, name="Codex.exe")]
146+
snapshot = ProcessSnapshot(
147+
main_processes=main,
148+
subprocesses=[],
149+
sandbox_detected=False,
150+
)
151+
152+
_handle_running_codex(cfg, detector, snapshot, manual_override=False)
153+
self.assertEqual(detector.terminated, [main])
154+
confirm_mock.assert_not_called()
86155

87156
@patch("codexsync.app.sys.platform", "win32")
88157
@patch("codexsync.app.confirm_process_termination", return_value=True)

0 commit comments

Comments
 (0)