Skip to content

Commit 549735e

Browse files
committed
feat(cli): use install_shim for engine bootstrap on first run
Previously cli.main called bootstrap.ensure_engine() which blocked MCP initialize for 30-60s while downloading the ~45 MB engine bundle, leaving BioRouter's UI hung with no progress feedback. Now cli.main checks bootstrap.cached_launcher() (read-only). If cached, we proxy immediately. If not, we serve install_shim which exposes a single codegraphagent_check_engine tool. The agent invokes this tool deliberately; the wait surfaces as a clear 'this will take a moment' message instead of a silent stall. On success the shim returns the launcher and we transition to proxy mode.
1 parent 941d5cb commit 549735e

3 files changed

Lines changed: 71 additions & 24 deletions

File tree

src/codegraphagent/cli.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,43 @@
11
"""CLI entry point for codegraphagent.
22
3-
Orchestrates the three pieces of the shim:
3+
Orchestrates the three (or four) pieces of the shim:
44
1. paths — resolve the project root and ensure the .biorouter/codegraph
55
state dir + .codegraph symlink exist.
6-
2. bootstrap — ensure the vendored engine bundle is downloaded, verified,
7-
and extracted.
8-
3. proxy — spawn the engine and pump MCP traffic.
6+
2. bootstrap.cached_launcher — read-only check: is the engine bundle
7+
already installed?
8+
3. install_shim (only if not cached) — serves a minimal MCP server with
9+
a single tool (codegraphagent_check_engine) that downloads + extracts
10+
the engine on demand. Returns the launcher path when install succeeds.
11+
4. proxy — spawn the engine and pump MCP traffic.
912
10-
If either of the first two fails, hand control to the degraded-mode error
11-
shim so the agent gets a structured error frame rather than an opaque crash.
13+
If the layout step fails (LayoutConflictError), we serve the legacy error_shim
14+
to surface that failure to the agent as a tool error. BootstrapError no longer
15+
happens in this function — it's caught inside install_shim and returned as a
16+
tool result so the user can see it and retry.
1217
"""
1318

1419
from __future__ import annotations
1520

16-
from codegraphagent import bootstrap, error_shim, paths, proxy
21+
from codegraphagent import bootstrap, error_shim, install_shim, paths, proxy
1722
from codegraphagent.errors import CodeGraphAgentError
1823

1924

2025
def main() -> int:
2126
try:
2227
root = paths.resolve_project_root()
2328
paths.ensure_layout(root)
24-
launcher = bootstrap.ensure_engine()
2529
except CodeGraphAgentError as exc:
2630
error_shim.serve(exc)
2731
return 0
2832

33+
cached = bootstrap.cached_launcher()
34+
if cached is not None:
35+
return proxy.run(launcher=cached, cwd=root)
36+
37+
# Engine not cached — serve install-mode until the agent calls
38+
# codegraphagent_check_engine (which triggers the download), or until
39+
# shutdown / EOF.
40+
launcher = install_shim.serve()
41+
if launcher is None:
42+
return 0
2943
return proxy.run(launcher=launcher, cwd=root)

tests/test_cli.py

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""cli.main — orchestrates paths.ensure_layoutbootstrap.ensure_engine → proxy.run."""
1+
"""cli.main — orchestrates paths → cached_launcher(install_shim or proxy)."""
22

33
from __future__ import annotations
44

@@ -8,50 +8,83 @@
88
import pytest
99

1010
from codegraphagent import cli
11-
from codegraphagent.errors import BootstrapError, LayoutConflictError
11+
from codegraphagent.errors import LayoutConflictError
1212

1313

14-
def test_main_happy_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
14+
def test_main_proxies_immediately_when_engine_cached(
15+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
16+
):
17+
"""Cached engine → straight to proxy, no install_shim involvement."""
1518
monkeypatch.setenv("BIOROUTER_WORKING_DIR", str(tmp_path))
1619
(tmp_path / ".git").mkdir()
1720

18-
fake_launcher = tmp_path / "fake-launcher"
19-
fake_launcher.write_text("")
21+
cached_launcher = tmp_path / "cached-launcher"
22+
cached_launcher.write_text("")
2023

2124
proxy_run = MagicMock(return_value=0)
25+
install_shim_serve = MagicMock()
2226

23-
with patch.object(cli.bootstrap, "ensure_engine", return_value=fake_launcher), \
24-
patch.object(cli.proxy, "run", proxy_run):
27+
with patch.object(cli.bootstrap, "cached_launcher", return_value=cached_launcher), \
28+
patch.object(cli.proxy, "run", proxy_run), \
29+
patch.object(cli.install_shim, "serve", install_shim_serve):
2530
rc = cli.main()
2631

2732
assert rc == 0
33+
install_shim_serve.assert_not_called()
2834
proxy_run.assert_called_once()
2935
kwargs = proxy_run.call_args.kwargs
30-
assert kwargs["launcher"] == fake_launcher
36+
assert kwargs["launcher"] == cached_launcher
3137
assert kwargs["cwd"] == tmp_path.resolve()
3238

3339

34-
def test_main_falls_back_to_error_shim_on_bootstrap_error(
40+
def test_main_serves_install_shim_when_not_cached_then_proxies(
3541
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
3642
):
43+
"""Not cached → install_shim runs, returns a launcher, then proxy runs."""
3744
monkeypatch.setenv("BIOROUTER_WORKING_DIR", str(tmp_path))
3845
(tmp_path / ".git").mkdir()
3946

40-
err = BootstrapError("nope", url="https://example.com/x")
41-
error_shim_serve = MagicMock()
47+
installed_launcher = tmp_path / "freshly-installed"
48+
installed_launcher.write_text("")
49+
50+
install_shim_serve = MagicMock(return_value=installed_launcher)
51+
proxy_run = MagicMock(return_value=0)
4252

43-
with patch.object(cli.bootstrap, "ensure_engine", side_effect=err), \
44-
patch.object(cli.error_shim, "serve", error_shim_serve):
53+
with patch.object(cli.bootstrap, "cached_launcher", return_value=None), \
54+
patch.object(cli.install_shim, "serve", install_shim_serve), \
55+
patch.object(cli.proxy, "run", proxy_run):
4556
rc = cli.main()
4657

4758
assert rc == 0
48-
error_shim_serve.assert_called_once()
49-
assert error_shim_serve.call_args.args[0] is err
59+
install_shim_serve.assert_called_once()
60+
proxy_run.assert_called_once()
61+
assert proxy_run.call_args.kwargs["launcher"] == installed_launcher
62+
63+
64+
def test_main_exits_zero_when_install_shim_returns_none(
65+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
66+
):
67+
"""install_shim returns None (shutdown/EOF before install) → exit 0, no proxy."""
68+
monkeypatch.setenv("BIOROUTER_WORKING_DIR", str(tmp_path))
69+
(tmp_path / ".git").mkdir()
70+
71+
install_shim_serve = MagicMock(return_value=None)
72+
proxy_run = MagicMock(return_value=0)
73+
74+
with patch.object(cli.bootstrap, "cached_launcher", return_value=None), \
75+
patch.object(cli.install_shim, "serve", install_shim_serve), \
76+
patch.object(cli.proxy, "run", proxy_run):
77+
rc = cli.main()
78+
79+
assert rc == 0
80+
install_shim_serve.assert_called_once()
81+
proxy_run.assert_not_called()
5082

5183

5284
def test_main_falls_back_to_error_shim_on_layout_conflict(
5385
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
5486
):
87+
"""Real .codegraph directory triggers LayoutConflictError → error_shim."""
5588
monkeypatch.setenv("BIOROUTER_WORKING_DIR", str(tmp_path))
5689
(tmp_path / ".git").mkdir()
5790
(tmp_path / ".codegraph").mkdir()

tests/test_smoke.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def test_cli_entrypoint_runs(tmp_path, monkeypatch):
1919
fake_launcher = tmp_path / "fake-launcher"
2020
fake_launcher.write_text("")
2121

22-
with patch.object(cli.bootstrap, "ensure_engine", return_value=fake_launcher), \
22+
with patch.object(cli.bootstrap, "cached_launcher", return_value=fake_launcher), \
2323
patch.object(cli.proxy, "run", MagicMock(return_value=0)):
2424
rc = cli.main()
2525

0 commit comments

Comments
 (0)