Skip to content
Merged
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
16 changes: 9 additions & 7 deletions src/entrypoints/web_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,24 +103,26 @@ def _build_parser() -> argparse.ArgumentParser:
parser.add_argument("--no-open", action="store_true", dest="no_open",
help="Print the URL without launching a browser.")
parser.add_argument("--build", action="store_true",
help="Build ui-web before serving (source checkouts).")
help="Install locked dependencies and build ui-web before serving.")
parser.add_argument("--allow-remote", action="store_true", dest="allow_remote",
help="Permit a non-loopback --host. The page hands out the "
"session token, so only do this behind your own auth.")
return parser


def build_web_app(app_dir: Path) -> int:
"""Run ``npm install`` (when needed) then ``npm run build`` in ``ui-web``."""
"""Sync locked dependencies with ``npm ci``, then build ``ui-web``."""
if not (app_dir / "package.json").is_file():
print(f"web: no web app at {app_dir}", file=sys.stderr)
return 1

npm = "npm.cmd" if os.name == "nt" else "npm"
plan: list[list[str]] = []
if not (app_dir / "node_modules").is_dir():
plan.append([npm, "ci", "--no-audit", "--no-fund"])
plan.append([npm, "run", "build"])
# Installer upgrades update the lockfile but can leave old node_modules.
# Always sync before an explicit build, including incomplete installs.
plan = [
[npm, "ci", "--no-audit", "--no-fund"],
[npm, "run", "build"],
]

for command in plan:
print(f"web: {' '.join(command)}", file=sys.stderr)
Expand All @@ -140,7 +142,7 @@ def _missing_bundle_message(app_dir: Path) -> str:
return (
"web: the browser client is not built yet.\n"
f" Run `clawcodex web --build`, or build it directly:\n"
f" cd {app_dir} && npm install && npm run build"
f" cd {app_dir} && npm ci && npm run build"
)
return (
"web: no web client bundle found. This install does not ship one; "
Expand Down
48 changes: 48 additions & 0 deletions tests/entrypoints/test_web_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,61 @@
from __future__ import annotations

import argparse
import subprocess
from pathlib import Path

import pytest

from src.entrypoints import web_cli


# ── web build ────────────────────────────────────────────────────────────────


@pytest.mark.parametrize("has_node_modules", [False, True], ids=["fresh", "upgrade"])
def test_web_build_syncs_locked_dependencies(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, has_node_modules: bool,
) -> None:
(tmp_path / "package.json").write_text("{}", encoding="utf-8")
if has_node_modules:
# An installer upgrade can leave dependencies from the previous version.
(tmp_path / "node_modules").mkdir()
calls: list[list[str]] = []

def fake_run(command: list[str], *, cwd: str, check: bool):
assert cwd == str(tmp_path)
assert check is False
calls.append(command)
return subprocess.CompletedProcess(command, 0)

monkeypatch.setattr(web_cli.subprocess, "run", fake_run)

assert web_cli.build_web_app(tmp_path) == 0
npm = "npm.cmd" if web_cli.os.name == "nt" else "npm"
assert calls == [
[npm, "ci", "--no-audit", "--no-fund"],
[npm, "run", "build"],
]


@pytest.mark.parametrize("failure_step", ["ci", "run"])
def test_web_build_stops_and_propagates_command_failure(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, failure_step: str,
) -> None:
(tmp_path / "package.json").write_text("{}", encoding="utf-8")
(tmp_path / "node_modules").mkdir()
calls: list[str] = []

def fake_run(command: list[str], *, cwd: str, check: bool):
calls.append(command[1])
return subprocess.CompletedProcess(command, 23 if command[1] == failure_step else 0)

monkeypatch.setattr(web_cli.subprocess, "run", fake_run)

assert web_cli.build_web_app(tmp_path) == 23
assert calls == (["ci"] if failure_step == "ci" else ["ci", "run"])


# ── loopback classification ──────────────────────────────────────────────────


Expand Down
Loading