From e341bb2661fda1755fa5a6a015ae0fa68eb818a2 Mon Sep 17 00:00:00 2001 From: chengyongru Date: Thu, 27 Aug 2026 12:05:16 +0800 Subject: [PATCH] perf(tui): skip redundant dependency installs --- nanobot/cli/tui_launcher.py | 48 ++++++++++++++++++ tests/cli/test_tui_launcher.py | 92 ++++++++++++++++++++++++++++++++-- 2 files changed, 136 insertions(+), 4 deletions(-) diff --git a/nanobot/cli/tui_launcher.py b/nanobot/cli/tui_launcher.py index b0ddfb487..8ed6e6f54 100644 --- a/nanobot/cli/tui_launcher.py +++ b/nanobot/cli/tui_launcher.py @@ -67,6 +67,8 @@ _TUI_RELEASE_LIMITS = { _TUI_DETACH_EXIT_CODE = 90 _GATEWAY_READY_TIMEOUT_S = 20.0 _GATEWAY_READY_POLL_S = 0.1 +_TUI_DEPENDENCY_METADATA = ("package.json", "bun.lock") +_TUI_DEPENDENCY_CACHE = ".nanobot-install.sha256" @dataclass(frozen=True) @@ -224,6 +226,21 @@ def _tui_source_dir(project_root: Path) -> Path | None: def _resolve_source_tui_command(source_dir: Path, bun: str) -> list[str]: dependency = source_dir / "node_modules" / "@opentui" / "core" + cache = source_dir / "node_modules" / _TUI_DEPENDENCY_CACHE + fingerprint = _tui_dependency_fingerprint(source_dir) + if dependency.is_dir() and fingerprint is not None: + try: + if cache.read_text(encoding="ascii") == f"{fingerprint}\n": + return _source_tui_command(source_dir, bun) + except (OSError, UnicodeError): + pass + + try: + cache.unlink(missing_ok=True) + except OSError as exc: + raise TuiUnavailableError( + f"could not prepare the TUI dependency install: {exc}" + ) from exc try: install = subprocess.run( [bun, "install", "--frozen-lockfile"], @@ -238,6 +255,37 @@ def _resolve_source_tui_command(source_dir: Path, bun: str) -> list[str]: detail = (install.stderr or install.stdout).strip().splitlines() suffix = f": {detail[-1]}" if detail else "" raise TuiUnavailableError(f"could not install TUI dependencies{suffix}") + + current_fingerprint = _tui_dependency_fingerprint(source_dir) + if fingerprint is not None and current_fingerprint == fingerprint: + pending = cache.with_name(f"{cache.name}.tmp-{os.getpid()}") + try: + pending.write_text(f"{fingerprint}\n", encoding="ascii") + pending.replace(cache) + except OSError: + try: + pending.unlink(missing_ok=True) + except OSError: + pass + + return _source_tui_command(source_dir, bun) + + +def _tui_dependency_fingerprint(source_dir: Path) -> str | None: + digest = hashlib.sha256() + try: + for name in _TUI_DEPENDENCY_METADATA: + content = (source_dir / name).read_bytes() + digest.update(name.encode()) + digest.update(b"\0") + digest.update(len(content).to_bytes(8, "big")) + digest.update(content) + except OSError: + return None + return digest.hexdigest() + + +def _source_tui_command(source_dir: Path, bun: str) -> list[str]: executable = named_executable( bun, name="nanobot-tui", diff --git a/tests/cli/test_tui_launcher.py b/tests/cli/test_tui_launcher.py index 5cc1f4a65..24a5fdd4c 100644 --- a/tests/cli/test_tui_launcher.py +++ b/tests/cli/test_tui_launcher.py @@ -51,6 +51,14 @@ def _release_archive( return payload, checksum +def _tui_source(tmp_path: Path) -> Path: + source_dir = tmp_path / "tui" + source_dir.mkdir() + (source_dir / "package.json").write_text('{"dependencies": {}}\n', encoding="utf-8") + (source_dir / "bun.lock").write_text('lockfileVersion = 1\n', encoding="utf-8") + return source_dir + + @pytest.mark.parametrize( ("session_id", "expected"), [ @@ -525,18 +533,18 @@ def test_classic_options_require_an_explicit_classic_prompt( ) -def test_source_checkout_refreshes_locked_tui_dependencies( +def test_source_checkout_installs_missing_locked_tui_dependencies( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, ) -> None: - source_dir = tmp_path / "tui" - source_dir.mkdir() - (source_dir / "node_modules" / "@opentui" / "core").mkdir(parents=True) + source_dir = _tui_source(tmp_path) + dependency = source_dir / "node_modules" / "@opentui" / "core" bun = str(tmp_path / "bun") def install(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: assert command == [bun, "install", "--frozen-lockfile"] assert kwargs["cwd"] == source_dir + dependency.mkdir(parents=True) return subprocess.CompletedProcess(command, 0, "", "") monkeypatch.setattr("nanobot.cli.tui_launcher.subprocess.run", install) @@ -551,6 +559,82 @@ def test_source_checkout_refreshes_locked_tui_dependencies( ] +def test_source_checkout_skips_install_when_locked_dependencies_are_current( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + source_dir = _tui_source(tmp_path) + dependency = source_dir / "node_modules" / "@opentui" / "core" + installs: list[list[str]] = [] + + def install(command: list[str], **_kwargs: object) -> subprocess.CompletedProcess[str]: + installs.append(command) + dependency.mkdir(parents=True) + return subprocess.CompletedProcess(command, 0, "", "") + + monkeypatch.setattr("nanobot.cli.tui_launcher.subprocess.run", install) + + _resolve_source_tui_command(source_dir, "bun") + _resolve_source_tui_command(source_dir, "bun") + + assert installs == [["bun", "install", "--frozen-lockfile"]] + + +@pytest.mark.parametrize("metadata_name", ["package.json", "bun.lock"]) +def test_source_checkout_refreshes_dependencies_when_metadata_changes( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + metadata_name: str, +) -> None: + source_dir = _tui_source(tmp_path) + dependency = source_dir / "node_modules" / "@opentui" / "core" + installs: list[list[str]] = [] + + def install(command: list[str], **_kwargs: object) -> subprocess.CompletedProcess[str]: + installs.append(command) + dependency.mkdir(parents=True, exist_ok=True) + return subprocess.CompletedProcess(command, 0, "", "") + + monkeypatch.setattr("nanobot.cli.tui_launcher.subprocess.run", install) + + _resolve_source_tui_command(source_dir, "bun") + with (source_dir / metadata_name).open("a", encoding="utf-8") as metadata: + metadata.write("changed\n") + _resolve_source_tui_command(source_dir, "bun") + + assert installs == [ + ["bun", "install", "--frozen-lockfile"], + ["bun", "install", "--frozen-lockfile"], + ] + + +def test_failed_source_dependency_install_does_not_leave_a_valid_cache( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + source_dir = _tui_source(tmp_path) + dependency = source_dir / "node_modules" / "@opentui" / "core" + outcomes = iter((0, 1, 0)) + installs = 0 + + def install(command: list[str], **_kwargs: object) -> subprocess.CompletedProcess[str]: + nonlocal installs + installs += 1 + dependency.mkdir(parents=True, exist_ok=True) + returncode = next(outcomes) + return subprocess.CompletedProcess(command, returncode, "", "partial install") + + monkeypatch.setattr("nanobot.cli.tui_launcher.subprocess.run", install) + + _resolve_source_tui_command(source_dir, "bun") + dependency.rmdir() + with pytest.raises(TuiUnavailableError, match="partial install"): + _resolve_source_tui_command(source_dir, "bun") + _resolve_source_tui_command(source_dir, "bun") + + assert installs == 3 + + def test_source_checkout_fails_when_locked_dependencies_cannot_be_refreshed( monkeypatch: pytest.MonkeyPatch, tmp_path: Path,