Skip to content

Commit 255043e

Browse files
committed
fix: handle deferred tunnel import failures
1 parent c2bee4f commit 255043e

2 files changed

Lines changed: 34 additions & 21 deletions

File tree

packages/prime/src/prime_cli/commands/tunnel.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,11 @@
1818
app = PlainTyper(help="Manage tunnels for exposing local services", no_args_is_help=True)
1919
console = get_console()
2020

21-
TunnelClient: Any = None
2221

22+
def _create_tunnel_client() -> Any:
23+
from prime_tunnel.core.client import TunnelClient
2324

24-
def _tunnel_client_class() -> Any:
25-
if TunnelClient is not None:
26-
return TunnelClient
27-
from prime_tunnel.core.client import TunnelClient as loaded_tunnel_client
28-
29-
return loaded_tunnel_client
25+
return TunnelClient()
3026

3127

3228
def _format_tunnel_for_output(tunnel) -> dict:
@@ -133,6 +129,11 @@ def signal_handler():
133129
asyncio.run(run_tunnel())
134130
except KeyboardInterrupt:
135131
pass
132+
except typer.Exit:
133+
raise
134+
except Exception as e:
135+
console.print(f"[red]Error:[/red] {e}", style="bold")
136+
raise typer.Exit(1)
136137

137138

138139
@app.command("list")
@@ -167,7 +168,7 @@ def list_tunnels(
167168
validate_output_format(output, console)
168169

169170
async def fetch_tunnels():
170-
client = _tunnel_client_class()()
171+
client = _create_tunnel_client()
171172
try:
172173
return await client.list_tunnels_page(
173174
team_id=team_id,
@@ -256,7 +257,7 @@ def tunnel_status(
256257
"""Get status of a specific tunnel."""
257258

258259
async def fetch_status():
259-
client = _tunnel_client_class()()
260+
client = _create_tunnel_client()
260261
try:
261262
return await client.get_tunnel(tunnel_id)
262263
finally:
@@ -342,7 +343,7 @@ def stop_tunnel(
342343
if all:
343344

344345
async def fetch_tunnel_ids() -> List[str]:
345-
client = _tunnel_client_class()()
346+
client = _create_tunnel_client()
346347
try:
347348
scoped_team_id = team_id
348349
if scoped_team_id is None:
@@ -404,7 +405,7 @@ async def fetch_tunnel_ids() -> List[str]:
404405
if labels:
405406

406407
async def validate_label_scope() -> None:
407-
client = _tunnel_client_class()()
408+
client = _create_tunnel_client()
408409
try:
409410
scoped_user_id = client.config.user_id if only_mine else None
410411
scoped_team_id = team_id if team_id is not None else client.config.team_id
@@ -456,7 +457,7 @@ async def validate_label_scope() -> None:
456457
return
457458

458459
async def delete_tunnels() -> tuple[List[str], List[dict], List[dict]]:
459-
client = _tunnel_client_class()()
460+
client = _create_tunnel_client()
460461
succeeded: List[str] = []
461462
not_found: List[dict] = []
462463
failed: List[dict] = []

packages/prime/tests/test_tunnel_cli.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import sys
23
from datetime import datetime, timezone
34
from types import SimpleNamespace
45
from typing import Any
@@ -33,6 +34,17 @@ def test_format_tunnel_does_not_derive_created_from_expiration() -> None:
3334
assert tunnel_data["expires_at"] is not None
3435

3536

37+
def test_tunnel_start_import_failure_exits_cleanly(monkeypatch: pytest.MonkeyPatch) -> None:
38+
monkeypatch.setenv("PRIME_DISABLE_VERSION_CHECK", "1")
39+
monkeypatch.setitem(sys.modules, "prime_tunnel", None)
40+
41+
result = runner.invoke(app, ["tunnel", "start"])
42+
43+
assert result.exit_code == 1
44+
assert "Error:" in result.output
45+
assert "Traceback" not in result.output
46+
47+
3648
def test_tunnel_list_passes_label_filters(monkeypatch: pytest.MonkeyPatch) -> None:
3749
monkeypatch.setenv("PRIME_DISABLE_VERSION_CHECK", "1")
3850
captured: dict[str, Any] = {}
@@ -65,7 +77,7 @@ async def list_tunnels_page(self, **kwargs: Any) -> Any:
6577
async def close(self) -> None:
6678
return None
6779

68-
monkeypatch.setattr("prime_cli.commands.tunnel.TunnelClient", FakeTunnelClient)
80+
monkeypatch.setattr("prime_cli.commands.tunnel._create_tunnel_client", FakeTunnelClient)
6981

7082
result = runner.invoke(
7183
app,
@@ -89,7 +101,7 @@ async def list_tunnels_page(self, **kwargs: Any) -> Any:
89101
async def close(self) -> None:
90102
return None
91103

92-
monkeypatch.setattr("prime_cli.commands.tunnel.TunnelClient", FakeTunnelClient)
104+
monkeypatch.setattr("prime_cli.commands.tunnel._create_tunnel_client", FakeTunnelClient)
93105

94106
result = runner.invoke(app, ["tunnel", "list", "--output", "json"])
95107

@@ -117,7 +129,7 @@ async def bulk_delete_tunnels(self, **kwargs: Any) -> dict[str, Any]:
117129
async def close(self) -> None:
118130
return None
119131

120-
monkeypatch.setattr("prime_cli.commands.tunnel.TunnelClient", FakeTunnelClient)
132+
monkeypatch.setattr("prime_cli.commands.tunnel._create_tunnel_client", FakeTunnelClient)
121133

122134
result = runner.invoke(app, ["tunnel", "stop", "--label", "dev", "--yes"])
123135

@@ -141,7 +153,7 @@ async def bulk_delete_tunnels(self, **kwargs: Any) -> dict[str, Any]:
141153
async def close(self) -> None:
142154
return None
143155

144-
monkeypatch.setattr("prime_cli.commands.tunnel.TunnelClient", FakeTunnelClient)
156+
monkeypatch.setattr("prime_cli.commands.tunnel._create_tunnel_client", FakeTunnelClient)
145157

146158
# No --yes: if the scope check ran after the prompt we'd see the confirmation
147159
# text; instead it must fail fast with a clean error and never prompt.
@@ -179,7 +191,7 @@ async def bulk_delete_tunnels(self, tunnel_ids: list[str]) -> dict[str, Any]:
179191
async def close(self) -> None:
180192
return None
181193

182-
monkeypatch.setattr("prime_cli.commands.tunnel.TunnelClient", FakeTunnelClient)
194+
monkeypatch.setattr("prime_cli.commands.tunnel._create_tunnel_client", FakeTunnelClient)
183195

184196
result = runner.invoke(app, ["tunnel", "stop", "--all", "--yes"])
185197

@@ -216,7 +228,7 @@ async def bulk_delete_tunnels(self, tunnel_ids: list[str]) -> dict[str, Any]:
216228
async def close(self) -> None:
217229
return None
218230

219-
monkeypatch.setattr("prime_cli.commands.tunnel.TunnelClient", FakeTunnelClient)
231+
monkeypatch.setattr("prime_cli.commands.tunnel._create_tunnel_client", FakeTunnelClient)
220232

221233
result = runner.invoke(app, ["tunnel", "stop", "--all", "--all-users", "--yes"])
222234

@@ -252,7 +264,7 @@ async def bulk_delete_tunnels(self, tunnel_ids: list[str]) -> dict[str, Any]:
252264
async def close(self) -> None:
253265
return None
254266

255-
monkeypatch.setattr("prime_cli.commands.tunnel.TunnelClient", FakeTunnelClient)
267+
monkeypatch.setattr("prime_cli.commands.tunnel._create_tunnel_client", FakeTunnelClient)
256268

257269
result = runner.invoke(app, ["tunnel", "stop", "--all", "--yes"])
258270

@@ -278,7 +290,7 @@ async def bulk_delete_tunnels(self, tunnel_ids: list[str]) -> dict[str, Any]:
278290
async def close(self) -> None:
279291
return None
280292

281-
monkeypatch.setattr("prime_cli.commands.tunnel.TunnelClient", FakeTunnelClient)
293+
monkeypatch.setattr("prime_cli.commands.tunnel._create_tunnel_client", FakeTunnelClient)
282294

283295
result = runner.invoke(app, ["tunnel", "stop", "--all", "--yes"])
284296

@@ -301,7 +313,7 @@ async def bulk_delete_tunnels(self, **kwargs: Any) -> dict[str, Any]:
301313
async def close(self) -> None:
302314
return None
303315

304-
monkeypatch.setattr("prime_cli.commands.tunnel.TunnelClient", FakeTunnelClient)
316+
monkeypatch.setattr("prime_cli.commands.tunnel._create_tunnel_client", FakeTunnelClient)
305317

306318
result = runner.invoke(app, ["tunnel", "stop", "--all", "--yes"])
307319

0 commit comments

Comments
 (0)