diff --git a/AGENTS.md b/AGENTS.md index 3c08d01cd..753b76a54 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -135,7 +135,7 @@ cd frontend && pnpm test # Unit tests Rule of thumb: **root `make` = the full application**; **`backend/Makefile` and `frontend/` (`pnpm`) = per-module work.** -Host-side pnpm consumers, including the root/frontend Makefiles and local diagnostic scripts, must run through `scripts/pnpm.py`. The runner preserves direct `pnpm`/`pnpm.cmd` priority, falls back to `corepack pnpm`, and is invoked from `frontend/` so Corepack honors the package-manager version pinned by that project. +Host-side pnpm consumers, including the root/frontend Makefiles and local diagnostic scripts, must run through `scripts/pnpm.py`. Diagnostic scripts resolve the runner and frontend directory to absolute paths before changing the child process working directory, so they remain independent of the caller's current directory. The runner preserves direct `pnpm`/`pnpm.cmd` priority, falls back to `corepack pnpm`, and is invoked from `frontend/` so Corepack honors the package-manager version pinned by that project. ## Where to Go Next diff --git a/README.md b/README.md index d3f9c5395..9fd72aa69 100644 --- a/README.md +++ b/README.md @@ -320,7 +320,7 @@ On Windows, run the local development flow from Git Bash. Native `cmd.exe` and P make check # Verifies Node.js 22+, pnpm, uv, nginx ``` - The local `make check`, `make install`, `make dev`, and `make start` entry points use a direct `pnpm`/`pnpm.cmd` executable when available and otherwise fall back to `corepack pnpm`. Corepack runs from `frontend/`, so it honors the `packageManager` version pinned in `frontend/package.json`; enabling a global pnpm shim is not required. + The local `make check`, `make install`, `make dev`, and `make start` entry points use a direct `pnpm`/`pnpm.cmd` executable when available and otherwise fall back to `corepack pnpm`. The shared runner and diagnostics resolve repository paths absolutely, so these checks work regardless of the caller's current directory. Corepack runs from `frontend/`, so it honors the `packageManager` version pinned in `frontend/package.json`; enabling a global pnpm shim is not required. 2. **Install dependencies**: ```bash diff --git a/backend/tests/test_check_script.py b/backend/tests/test_check_script.py index 342ec5f7a..ea615d738 100644 --- a/backend/tests/test_check_script.py +++ b/backend/tests/test_check_script.py @@ -71,7 +71,20 @@ def test_find_pnpm_command_falls_back_to_corepack_cmd(monkeypatch): def test_check_script_uses_shared_pnpm_runner(): check_script = CHECK_SCRIPT_PATH.read_text(encoding="utf-8") - assert 'Path(__file__).with_name("pnpm.py")' in check_script + assert 'Path(__file__).resolve().with_name("pnpm.py")' in check_script + + +def test_check_script_resolves_runner_paths_independently_of_cwd(monkeypatch): + # Reproduce invoking the entry point with a relative script path from the + # repository root. Before the fix, `__file__` stayed relative and the + # runner became `scripts/pnpm.py`, which later broke after cwd changed. + monkeypatch.chdir(REPO_ROOT) + check_script = _load_script(Path("scripts/check.py"), "deerflow_check_script_paths") + + assert check_script.PNPM_SCRIPT_PATH == PNPM_SCRIPT_PATH + assert check_script.PNPM_SCRIPT_PATH.is_absolute() + assert check_script.FRONTEND_DIR == REPO_ROOT / "frontend" + assert check_script.FRONTEND_DIR.is_absolute() def test_check_script_preserves_runner_failure_diagnostics(monkeypatch): diff --git a/backend/tests/test_doctor.py b/backend/tests/test_doctor.py index 9e7617cc0..4f443d793 100644 --- a/backend/tests/test_doctor.py +++ b/backend/tests/test_doctor.py @@ -6,10 +6,25 @@ Run from repo root: from __future__ import annotations +import importlib.util import sys +from pathlib import Path import doctor +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def _load_script(path: Path, name: str): + assert path.exists(), f"{path} must exist" + spec = importlib.util.spec_from_file_location(name, path) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + # --------------------------------------------------------------------------- # check_python # --------------------------------------------------------------------------- @@ -28,6 +43,18 @@ class TestCheckPython: class TestCheckPnpm: + def test_resolves_shared_runner_from_relative_script_path(self, monkeypatch): + # Load the script as `scripts/doctor.py`, as a user would from the + # repository root. The derived paths must not depend on that relative + # invocation path. + monkeypatch.chdir(REPO_ROOT) + relative_doctor = _load_script(Path("scripts/doctor.py"), "deerflow_doctor_relative") + + assert relative_doctor.PNPM_SCRIPT_PATH == REPO_ROOT / "scripts" / "pnpm.py" + assert relative_doctor.PNPM_SCRIPT_PATH.is_absolute() + assert relative_doctor.FRONTEND_DIR == REPO_ROOT / "frontend" + assert relative_doctor.FRONTEND_DIR.is_absolute() + def test_uses_shared_runner_from_frontend(self, monkeypatch): captured = {} diff --git a/backend/tests/test_pnpm_script.py b/backend/tests/test_pnpm_script.py index 261138b35..bafdb4a0a 100644 --- a/backend/tests/test_pnpm_script.py +++ b/backend/tests/test_pnpm_script.py @@ -122,7 +122,7 @@ def test_official_entrypoints_route_pnpm_through_shared_runner(): assert 'DEERFLOW_PNPM_RUNNER="$REPO_ROOT/scripts/pnpm.py"' in serve_script assert 'FRONTEND_CMD=\'"$DEERFLOW_PNPM_PYTHON" "$DEERFLOW_PNPM_RUNNER" run dev\'' in serve_script assert '"\\$DEERFLOW_PNPM_RUNNER\\" run preview"' in serve_script - assert 'Path(__file__).with_name("pnpm.py")' in doctor_script + assert 'Path(__file__).resolve().with_name("pnpm.py")' in doctor_script assert 'project_root / "scripts" / "pnpm.py"' in support_bundle_script diff --git a/scripts/check.py b/scripts/check.py index 1248a87df..f6bb4a71e 100644 --- a/scripts/check.py +++ b/scripts/check.py @@ -8,7 +8,7 @@ import subprocess import sys from pathlib import Path -PNPM_SCRIPT_PATH = Path(__file__).with_name("pnpm.py") +PNPM_SCRIPT_PATH = Path(__file__).resolve().with_name("pnpm.py") FRONTEND_DIR = PNPM_SCRIPT_PATH.parent.parent / "frontend" COREPACK_NOTICE = "Using pnpm via Corepack." diff --git a/scripts/doctor.py b/scripts/doctor.py index 06009e89b..ad044e3cd 100644 --- a/scripts/doctor.py +++ b/scripts/doctor.py @@ -24,7 +24,7 @@ from typing import Literal # --------------------------------------------------------------------------- Status = Literal["ok", "warn", "fail", "skip"] -PNPM_SCRIPT_PATH = Path(__file__).with_name("pnpm.py") +PNPM_SCRIPT_PATH = Path(__file__).resolve().with_name("pnpm.py") FRONTEND_DIR = PNPM_SCRIPT_PATH.parent.parent / "frontend"