Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion codex_session_delete/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
12 changes: 7 additions & 5 deletions codex_session_delete/launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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")
Expand All @@ -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:
Expand Down
43 changes: 29 additions & 14 deletions tests/test_launcher_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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"

Expand Down Expand Up @@ -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([])
Expand All @@ -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"])
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -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"):
Expand Down Expand Up @@ -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"])
Expand All @@ -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"])
Expand Down Expand Up @@ -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"):
Expand Down
3 changes: 2 additions & 1 deletion tests/test_windows_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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):
Expand Down
Loading