diff --git a/codex_session_delete/cli.py b/codex_session_delete/cli.py index 5b305cf..516facb 100644 --- a/codex_session_delete/cli.py +++ b/codex_session_delete/cli.py @@ -20,6 +20,7 @@ def add_launch_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument("--backup-dir", type=Path, default=Path.home() / ".codex-session-delete" / "backups") parser.add_argument("--debug-port", type=int, default=9229) parser.add_argument("--helper-port", type=int, default=57321) + parser.add_argument("--proxy", action="store_true", default=False, help="Auto-detect and inject local proxy into Codex process environment") def build_parser() -> argparse.ArgumentParser: @@ -150,7 +151,7 @@ def run_launch(args: argparse.Namespace) -> int: stop_existing_windows_launchers() maybe_print_update_notice() try: - server, codex_proc = launch_and_inject(args.app_dir, args.db, args.backup_dir, args.debug_port, args.helper_port) + server, codex_proc = launch_and_inject(args.app_dir, args.db, args.backup_dir, args.debug_port, args.helper_port, auto_proxy=args.proxy) except Exception as exc: log_launch_failure(exc) raise diff --git a/codex_session_delete/launcher.py b/codex_session_delete/launcher.py index 4930d3e..e5e9ee2 100644 --- a/codex_session_delete/launcher.py +++ b/codex_session_delete/launcher.py @@ -152,8 +152,10 @@ def local_proxy_url() -> str | None: return None -def codex_process_environment() -> dict[str, str]: +def codex_process_environment(auto_proxy: bool = False) -> dict[str, str]: env = os.environ.copy() + if not auto_proxy: + return env if has_proxy_environment(env): return env proxy = local_proxy_url() @@ -265,9 +267,9 @@ def activate_packaged_app(app_user_model_id: str, arguments: str) -> int: ole32.CoUninitialize() -def launch_codex_app(app_dir: Path, debug_port: int) -> Any: +def launch_codex_app(app_dir: Path, debug_port: int, auto_proxy: bool = False) -> Any: app_user_model_id = packaged_app_user_model_id(app_dir) if sys.platform == "win32" else None - env = codex_process_environment() + env = codex_process_environment(auto_proxy=auto_proxy) if app_user_model_id: proxy_keys = ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY") previous = {key: os.environ.get(key) for key in proxy_keys} @@ -328,7 +330,7 @@ def inject_with_retry( raise RuntimeError("Codex injection failed") -def launch_and_inject(app_dir: Path | None, db_path: Path | None, backup_dir: Path, debug_port: int, helper_port: int) -> tuple[HelperServer, Any]: +def launch_and_inject(app_dir: Path | None, db_path: Path | None, backup_dir: Path, debug_port: int, helper_port: int, auto_proxy: bool = False) -> tuple[HelperServer, Any]: resolved_app_dir = resolve_codex_app_dir(app_dir) if resolved_app_dir is None: raise RuntimeError("Codex App directory not found") @@ -348,7 +350,7 @@ def launch_and_inject(app_dir: Path | None, db_path: Path | None, backup_dir: Pa server = start_helper(service, export_service, port=helper_port) codex_proc = None try: - codex_proc = launch_codex_app(resolved_app_dir, debug_port) + codex_proc = launch_codex_app(resolved_app_dir, debug_port, auto_proxy=auto_proxy) server.bridge_socket = inject_with_retry(debug_port, script_path, server.port, service, export_service, runtime) return server, codex_proc except Exception: diff --git a/tests/test_launcher_cli.py b/tests/test_launcher_cli.py index e9ce8f6..aa50d36 100644 --- a/tests/test_launcher_cli.py +++ b/tests/test_launcher_cli.py @@ -50,7 +50,7 @@ def test_launch_codex_windows_allows_devtools_websocket_origin(monkeypatch): assert "--remote-allow-origins=http://127.0.0.1:9229" in popen_calls[0] -def test_launch_codex_injects_detected_local_proxy(monkeypatch): +def test_launch_codex_injects_detected_local_proxy_with_auto_proxy(monkeypatch): app_dir = Path("C:/Codex/app") popen_calls = [] monkeypatch.delenv("HTTP_PROXY", raising=False) @@ -59,17 +59,32 @@ def test_launch_codex_injects_detected_local_proxy(monkeypatch): monkeypatch.setattr(launcher, "local_proxy_url", lambda: "http://127.0.0.1:7897") monkeypatch.setattr(launcher.subprocess, "Popen", lambda args, **kw: popen_calls.append((args, kw))) - launch_codex_app(app_dir, 9229) + launch_codex_app(app_dir, 9229, auto_proxy=True) assert popen_calls[0][1]["env"]["HTTP_PROXY"] == "http://127.0.0.1:7897" assert popen_calls[0][1]["env"]["HTTPS_PROXY"] == "http://127.0.0.1:7897" -def test_launch_codex_keeps_explicit_proxy(monkeypatch): +def test_default_does_not_inject_proxy(monkeypatch): + app_dir = Path("C:/Codex/app") + popen_calls = [] + monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("HTTPS_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.setattr(launcher, "local_proxy_url", lambda: "http://127.0.0.1:7897") + monkeypatch.setattr(launcher.subprocess, "Popen", lambda args, **kw: popen_calls.append((args, kw))) + + launch_codex_app(app_dir, 9229) + + assert "HTTP_PROXY" not in popen_calls[0][1]["env"] + assert "HTTPS_PROXY" not in popen_calls[0][1]["env"] + + +def test_launch_codex_keeps_explicit_proxy_with_auto_proxy(monkeypatch): monkeypatch.setenv("HTTPS_PROXY", "http://127.0.0.1:9999") monkeypatch.setattr(launcher, "local_proxy_url", lambda: (_ for _ in ()).throw(AssertionError("should not auto-detect"))) - env = launcher.codex_process_environment() + env = launcher.codex_process_environment(auto_proxy=True) assert env["HTTPS_PROXY"] == "http://127.0.0.1:9999" @@ -139,7 +154,7 @@ def test_non_windows_port_selector_keeps_requested_port(monkeypatch): def test_cli_keeps_helper_server_alive_after_injection(monkeypatch): waited = [] - monkeypatch.setattr(cli, "launch_and_inject", lambda *args: (FakeServer(), None)) + monkeypatch.setattr(cli, "launch_and_inject", lambda *args, **kwargs: (FakeServer(), None)) monkeypatch.setattr(cli, "wait_for_shutdown", lambda server, proc: waited.append(server.port)) exit_code = cli.main([]) @@ -151,7 +166,7 @@ def test_cli_keeps_helper_server_alive_after_injection(monkeypatch): def test_cli_launch_subcommand_keeps_helper_server_alive_after_injection(monkeypatch): waited = [] calls = [] - monkeypatch.setattr(cli, "launch_and_inject", lambda *args: calls.append(args) or (FakeServer(), None)) + monkeypatch.setattr(cli, "launch_and_inject", lambda *args, **kwargs: calls.append(args) or (FakeServer(), None)) monkeypatch.setattr(cli, "wait_for_shutdown", lambda server, proc: waited.append(server.port)) exit_code = cli.main(["launch"]) @@ -189,7 +204,7 @@ def test_launch_retries_injection_until_codex_page_is_ready(monkeypatch, tmp_pat attempts = [] monkeypatch.setattr(launcher, "resolve_codex_app_dir", lambda app_dir=None: tmp_path) monkeypatch.setattr(launcher, "start_helper", lambda *args, **kwargs: FakeServer()) - monkeypatch.setattr(launcher, "launch_codex_app", lambda *args: None) + monkeypatch.setattr(launcher, "launch_codex_app", lambda *args, **kwargs: None) def inject_after_retry(*args): attempts.append(args) @@ -210,7 +225,7 @@ def inject_after_retry(*args): def test_launch_and_inject_returns_windows_packaged_process_id(monkeypatch, tmp_path): monkeypatch.setattr(launcher, "resolve_codex_app_dir", lambda app_dir=None: tmp_path) monkeypatch.setattr(launcher, "start_helper", lambda *args, **kwargs: FakeServer()) - monkeypatch.setattr(launcher, "launch_codex_app", lambda *args: 1234) + monkeypatch.setattr(launcher, "launch_codex_app", lambda *args, **kwargs: 1234) monkeypatch.setattr(launcher, "inject_with_retry", lambda *args, **kwargs: {"result": {}}) server, proc = launcher.launch_and_inject(None, None, tmp_path / "backups", 9229, 57321) @@ -226,7 +241,7 @@ def test_launch_and_inject_runs_provider_sync_before_launch_when_enabled(monkeyp monkeypatch.setattr(launcher, "inject_with_retry", lambda *args, **kwargs: {"result": {}}) monkeypatch.setattr(launcher, "backend_settings", lambda: type("Settings", (), {"provider_sync_enabled": True})()) monkeypatch.setattr(launcher, "run_provider_sync", lambda: events.append("sync") or type("Result", (), {"status": "synced", "message": "ok"})()) - monkeypatch.setattr(launcher, "launch_codex_app", lambda *args: events.append("launch") or 1234) + monkeypatch.setattr(launcher, "launch_codex_app", lambda *args, **kwargs: events.append("launch") or 1234) launcher.launch_and_inject(None, None, tmp_path / "backups", 9229, 57321) @@ -240,7 +255,7 @@ def test_launch_and_inject_skips_provider_sync_when_disabled(monkeypatch, tmp_pa monkeypatch.setattr(launcher, "inject_with_retry", lambda *args, **kwargs: {"result": {}}) monkeypatch.setattr(launcher, "backend_settings", lambda: type("Settings", (), {"provider_sync_enabled": False})()) monkeypatch.setattr(launcher, "run_provider_sync", lambda: (_ for _ in ()).throw(AssertionError("sync should not run"))) - monkeypatch.setattr(launcher, "launch_codex_app", lambda *args: events.append("launch") or 1234) + monkeypatch.setattr(launcher, "launch_codex_app", lambda *args, **kwargs: events.append("launch") or 1234) launcher.launch_and_inject(None, None, tmp_path / "backups", 9229, 57321) @@ -251,7 +266,7 @@ def test_launch_and_inject_closes_helper_when_injection_fails(monkeypatch, tmp_p server = FakeServer() monkeypatch.setattr(launcher, "resolve_codex_app_dir", lambda app_dir=None: tmp_path) monkeypatch.setattr(launcher, "start_helper", lambda *args, **kwargs: server) - monkeypatch.setattr(launcher, "launch_codex_app", lambda *args: 1234) + monkeypatch.setattr(launcher, "launch_codex_app", lambda *args, **kwargs: 1234) monkeypatch.setattr(launcher, "inject_with_retry", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("inject failed"))) with pytest.raises(RuntimeError, match="inject failed"): @@ -309,7 +324,7 @@ def test_cli_skips_launcher_cleanup_on_non_windows(monkeypatch): def test_cli_launch_runs_launcher_cleanup_before_injection(monkeypatch): events = [] monkeypatch.setattr(cli, "stop_existing_windows_launchers", lambda: events.append("cleanup")) - monkeypatch.setattr(cli, "launch_and_inject", lambda *args: events.append("launch") or (FakeServer(), None)) + monkeypatch.setattr(cli, "launch_and_inject", lambda *args, **kwargs: events.append("launch") or (FakeServer(), None)) monkeypatch.setattr(cli, "wait_for_shutdown", lambda server, proc: events.append("wait")) exit_code = cli.main(["launch"]) @@ -322,7 +337,7 @@ def test_cli_launch_checks_update_before_injection(monkeypatch): events = [] monkeypatch.setattr(cli, "stop_existing_windows_launchers", lambda: events.append("cleanup")) monkeypatch.setattr(cli, "maybe_print_update_notice", lambda: events.append("check-update")) - monkeypatch.setattr(cli, "launch_and_inject", lambda *args: events.append("launch") or (FakeServer(), None)) + monkeypatch.setattr(cli, "launch_and_inject", lambda *args, **kwargs: events.append("launch") or (FakeServer(), None)) monkeypatch.setattr(cli, "wait_for_shutdown", lambda server, proc: events.append("wait")) exit_code = cli.main(["launch"]) @@ -444,7 +459,7 @@ def test_cli_remove_alias_uninstalls_with_default_options(monkeypatch): def test_cli_logs_launch_failure_for_hidden_pythonw(monkeypatch, tmp_path): log_path = tmp_path / "codex-plus.log" - monkeypatch.setattr(cli, "launch_and_inject", lambda *args: (_ for _ in ()).throw(RuntimeError("inject failed"))) + monkeypatch.setattr(cli, "launch_and_inject", lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("inject failed"))) monkeypatch.setattr(cli, "launch_log_path", lambda: log_path) with pytest.raises(RuntimeError, match="inject failed"): diff --git a/tests/test_windows_installer.py b/tests/test_windows_installer.py index 46b8081..f649b3a 100644 --- a/tests/test_windows_installer.py +++ b/tests/test_windows_installer.py @@ -2,6 +2,7 @@ from codex_session_delete.installers import InstallOptions from codex_session_delete import __version__ +from codex_session_delete import windows_installer from codex_session_delete.windows_installer import build_install_shortcut_script, build_uninstall_shortcut_script @@ -45,7 +46,7 @@ def test_default_windows_launcher_uses_current_python_executable(tmp_path): assert "$Python = " not in script assert "Get-Command python" not in script assert "-m codex_session_delete launch" in script - assert "pythonw.exe" in script or "python.exe" in script + assert windows_installer._default_python_executable() in script def test_build_uninstall_shortcut_script_removes_codex_plus_shortcuts(tmp_path):