mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 08:13:11 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e341bb2661 |
@@ -67,6 +67,8 @@ _TUI_RELEASE_LIMITS = {
|
|||||||
_TUI_DETACH_EXIT_CODE = 90
|
_TUI_DETACH_EXIT_CODE = 90
|
||||||
_GATEWAY_READY_TIMEOUT_S = 20.0
|
_GATEWAY_READY_TIMEOUT_S = 20.0
|
||||||
_GATEWAY_READY_POLL_S = 0.1
|
_GATEWAY_READY_POLL_S = 0.1
|
||||||
|
_TUI_DEPENDENCY_METADATA = ("package.json", "bun.lock")
|
||||||
|
_TUI_DEPENDENCY_CACHE = ".nanobot-install.sha256"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@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]:
|
def _resolve_source_tui_command(source_dir: Path, bun: str) -> list[str]:
|
||||||
dependency = source_dir / "node_modules" / "@opentui" / "core"
|
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:
|
try:
|
||||||
install = subprocess.run(
|
install = subprocess.run(
|
||||||
[bun, "install", "--frozen-lockfile"],
|
[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()
|
detail = (install.stderr or install.stdout).strip().splitlines()
|
||||||
suffix = f": {detail[-1]}" if detail else ""
|
suffix = f": {detail[-1]}" if detail else ""
|
||||||
raise TuiUnavailableError(f"could not install TUI dependencies{suffix}")
|
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(
|
executable = named_executable(
|
||||||
bun,
|
bun,
|
||||||
name="nanobot-tui",
|
name="nanobot-tui",
|
||||||
|
|||||||
@@ -51,6 +51,14 @@ def _release_archive(
|
|||||||
return payload, checksum
|
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(
|
@pytest.mark.parametrize(
|
||||||
("session_id", "expected"),
|
("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,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
source_dir = tmp_path / "tui"
|
source_dir = _tui_source(tmp_path)
|
||||||
source_dir.mkdir()
|
dependency = source_dir / "node_modules" / "@opentui" / "core"
|
||||||
(source_dir / "node_modules" / "@opentui" / "core").mkdir(parents=True)
|
|
||||||
bun = str(tmp_path / "bun")
|
bun = str(tmp_path / "bun")
|
||||||
|
|
||||||
def install(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
def install(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
||||||
assert command == [bun, "install", "--frozen-lockfile"]
|
assert command == [bun, "install", "--frozen-lockfile"]
|
||||||
assert kwargs["cwd"] == source_dir
|
assert kwargs["cwd"] == source_dir
|
||||||
|
dependency.mkdir(parents=True)
|
||||||
return subprocess.CompletedProcess(command, 0, "", "")
|
return subprocess.CompletedProcess(command, 0, "", "")
|
||||||
|
|
||||||
monkeypatch.setattr("nanobot.cli.tui_launcher.subprocess.run", install)
|
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(
|
def test_source_checkout_fails_when_locked_dependencies_cannot_be_refreshed(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
|
|||||||
Reference in New Issue
Block a user