From 2963104f65c44fe5912bba2df3e88beeba797a6c Mon Sep 17 00:00:00 2001 From: lostmartian Date: Mon, 19 Oct 2026 07:22:56 +0530 Subject: [PATCH 1/2] feat(dist): add npx agentdiff and make every install path work Add an npm launcher (npm/) so `npx agentdiff` runs the Python CLI at the same version via uv, pipx, or a private virtualenv built once with any Python 3.10+. Publish it from the tag workflow after PyPI and test it in CI. Give the Claude Code plugin's report script the same pipx and plain-Python fallbacks, and add an install table to the README. --- .github/workflows/ci.yml | 13 +- .github/workflows/publish.yml | 41 +++++++ AGENTS.md | 7 +- CHANGELOG.md | 11 ++ README.md | 28 ++++- npm/README.md | 26 ++++ npm/bin/agentdiff.js | 113 ++++++++++++++++++ npm/package.json | 34 ++++++ npm/test/agentdiff.test.js | 103 ++++++++++++++++ .../agentdiff/skills/report/scripts/report.sh | 48 ++++++-- ..._claude_plugin.py => test_distribution.py} | 24 +++- 11 files changed, 426 insertions(+), 22 deletions(-) create mode 100644 npm/README.md create mode 100755 npm/bin/agentdiff.js create mode 100644 npm/package.json create mode 100644 npm/test/agentdiff.test.js rename tests/{test_claude_plugin.py => test_distribution.py} (77%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69461d73..adc8fdd9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,4 +49,15 @@ jobs: - uses: actions/upload-artifact@v4 with: name: dist - path: dist/ \ No newline at end of file + path: dist/ + + npm-launcher: + name: npm launcher + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + - run: npm test + working-directory: npm diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 610905a5..ae65f873 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,6 +2,8 @@ name: Publish to PyPI # Build and publish a release to PyPI whenever a version tag (`v*`) is pushed. # Uses OpenID Connect trusted publishing — no PyPI API token is stored. +# After PyPI, publishes the `agentdiff` npm launcher (npm/) at the same +# version, so `npx agentdiff` resolves to the release just uploaded. on: push: tags: @@ -51,6 +53,45 @@ jobs: with: packages-dir: dist/ + publish-npm: + name: Publish npm launcher + needs: publish + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # npm provenance + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + registry-url: "https://registry.npmjs.org" + + - name: Check versions match the tag + run: | + TAG="${GITHUB_REF_NAME#v}" + NPM_VERSION=$(node -p "require('./npm/package.json').version") + if [ "$TAG" != "$NPM_VERSION" ]; then + echo "::error::npm/package.json is $NPM_VERSION but the tag is $TAG" + exit 1 + fi + + - name: Test launcher + working-directory: npm + run: npm test + + - name: Publish to npm + working-directory: npm + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + if [ -z "$NODE_AUTH_TOKEN" ]; then + echo "::warning::NPM_TOKEN secret is not set; skipping npm publish." + exit 0 + fi + npm publish --provenance --access public + release: name: Create GitHub Release needs: publish diff --git a/AGENTS.md b/AGENTS.md index 9181faa5..3a8fb090 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,8 +38,11 @@ Any behavior change ships with tests; coverage must not drop. - Every user-facing change adds a `CHANGELOG.md` entry under `[Unreleased]`. - Version bumps + release notes happen in their own `chore/release-*` branch. - PyPI package: `agent-trajectory-diff`. SemVer + Keep a Changelog. -- Release bumps also update `plugins/agentdiff/.claude-plugin/plugin.json` `version` - (Claude Code pins users to it; `tests/test_claude_plugin.py` enforces the match). +- Release bumps also update `plugins/agentdiff/.claude-plugin/plugin.json` and + `npm/package.json` `version` (Claude Code pins users to the plugin version; the + npm launcher runs the PyPI release of the same version). `tests/test_claude_plugin.py` + enforces both. The tag workflow publishes PyPI first, then npm (needs the + `NPM_TOKEN` repository secret). ## Project conventions diff --git a/CHANGELOG.md b/CHANGELOG.md index ef16e87c..f9f41b43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 (`--since`, `--project`, `--top`) and prints install help instead of failing when neither `agentdiff` nor `uv` is available. The plugin version tracks the package version. +- **`npx agentdiff`**: an npm package (`npm/`) that launches the Python CLI at + the same version using uv, else pipx, else a private virtualenv built once + with any Python 3.10+. Zero dependencies, about 2 kB. The tag workflow + publishes it to npm right after PyPI (needs the `NPM_TOKEN` secret), and CI + runs its tests. + +### Changed +- The `/agentdiff:report` plugin skill now also falls back to pipx and to plain + Python 3.10+ (a private virtualenv under `~/.cache/agentdiff`), so it works + without uv. +- README: install table for npx, uv, pipx, pip and Claude Code. ## [0.6.0] - 2026-10-19 diff --git a/README.md b/README.md index f5d0c6de..8cf3af6e 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,11 @@

```bash -uvx --from agent-trajectory-diff agentdiff +npx agentdiff ``` +or `uvx --from agent-trajectory-diff agentdiff` · or `/agentdiff:report` in [Claude Code](#inside-claude-code) + AgentDiff reads the session logs Claude Code already keeps on your machine and @@ -83,8 +85,21 @@ agentdiff --json > spend.json # machine-readable agentdiff --path /other/.claude # a different Claude Code config dir ``` -Install it permanently with `pip install agent-trajectory-diff` (the command is -`agentdiff`), or run it once with `uvx` as shown above. +## Install + +Pick whatever your machine already has. Every option runs the same CLI. + +| You have | Run once | Install | +| :--- | :--- | :--- | +| Node 18+ | `npx agentdiff` | `npm i -g agentdiff` | +| [uv](https://docs.astral.sh/uv/) | `uvx --from agent-trajectory-diff agentdiff` | `uv tool install agent-trajectory-diff` | +| pipx | `pipx run --spec agent-trajectory-diff agentdiff` | `pipx install agent-trajectory-diff` | +| Python 3.10+ | | `pip install agent-trajectory-diff` | +| Claude Code | `/agentdiff:report` | see below | + +The installed command is always `agentdiff`. The npm package is a small +launcher for the same Python CLI at the same version. It uses uv if present, +else pipx, else builds a private virtualenv once with your Python 3.10+. ### Inside Claude Code @@ -99,7 +114,8 @@ On Claude Code versions before 2.1.275, add the marketplace first: `/plugin install agentdiff@agentdiff`. Then run the report in any session. Claude shows it and explains the biggest -avoidable items: +avoidable items. The plugin finds AgentDiff the same way as the npm launcher: +an installed `agentdiff`, else uv, pipx, or Python 3.10+. ```text /agentdiff:report @@ -107,8 +123,8 @@ avoidable items: ``` Claude can also pick it up on its own when you ask things like "why is my -Claude Code usage so high?". The plugin needs either `uv` or an installed -`agentdiff` (0.6.0+). Without one, it tells you what to install. +Claude Code usage so high?". With no Python or uv at all, it tells you what to +install. Without the plugin, prefix the command with `!` to run it in a session: `!uvx --from agent-trajectory-diff agentdiff`. diff --git a/npm/README.md b/npm/README.md new file mode 100644 index 00000000..70d6b0ea --- /dev/null +++ b/npm/README.md @@ -0,0 +1,26 @@ +# agentdiff + +**See what your AI coding agent actually did, and what it cost.** + +```bash +npx agentdiff +``` + +Reads the Claude Code session logs already on your machine and shows where the +money went and what was avoidable: long context re-sent every turn, prompt +caches rebuilt after breaks, stuck retries. No config, no API key, nothing +uploaded. + +```bash +npx agentdiff --since 30d # 24h, 7d, 30d, 12w … or all +npx agentdiff --project api # only projects whose name contains "api" +npx agentdiff --json # machine-readable +``` + +This npm package is a thin launcher for the Python CLI +([`agent-trajectory-diff`](https://pypi.org/project/agent-trajectory-diff/)) +at the same version. It uses `uvx` if you have uv, else `pipx`, else builds a +private virtualenv once with any Python 3.10+. + +Docs and the Claude Code plugin: [agentdiff.app](https://agentdiff.app) · +[GitHub](https://github.com/kerrshift/agentdiff). MIT licensed. diff --git a/npm/bin/agentdiff.js b/npm/bin/agentdiff.js new file mode 100755 index 00000000..ac78e938 --- /dev/null +++ b/npm/bin/agentdiff.js @@ -0,0 +1,113 @@ +#!/usr/bin/env node +// `npx agentdiff` — runs the AgentDiff Python CLI (PyPI: agent-trajectory-diff) +// at the same version as this npm package, using whichever Python runner the +// machine already has: uvx, then pipx, then a private virtualenv built once +// with any Python 3.10+. No runtime dependencies. +"use strict"; + +const { spawnSync } = require("node:child_process"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); + +const { version } = require("../package.json"); + +const SPEC = `agent-trajectory-diff==${version}`; +const IS_WIN = process.platform === "win32"; + +function which(cmd) { + const exts = IS_WIN ? (process.env.PATHEXT || ".EXE;.CMD;.BAT").split(";") : [""]; + for (const dir of (process.env.PATH || "").split(path.delimiter)) { + if (!dir) continue; + for (const ext of exts) { + const candidate = path.join(dir, cmd + ext); + try { + if (fs.statSync(candidate).isFile()) { + fs.accessSync(candidate, fs.constants.X_OK); + return candidate; + } + } catch { + // not here + } + } + } + return null; +} + +function run(cmd, args) { + const result = spawnSync(cmd, args, { stdio: "inherit" }); + if (result.error) { + console.error(`agentdiff: could not start ${cmd}: ${result.error.message}`); + return 1; + } + if (result.signal) process.kill(process.pid, result.signal); + return result.status ?? 1; +} + +function quiet(cmd, args) { + const result = spawnSync(cmd, args, { stdio: "ignore" }); + return result.error ? 1 : result.status ?? 1; +} + +function findPython() { + for (const name of ["python3", "python"]) { + const exe = which(name); + if (exe && quiet(exe, ["-c", "import sys; sys.exit(sys.version_info < (3, 10))"]) === 0) { + return exe; + } + } + return null; +} + +function cacheDir() { + if (process.env.AGENTDIFF_CACHE_DIR) return process.env.AGENTDIFF_CACHE_DIR; + if (IS_WIN) { + return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "agentdiff"); + } + return path.join(process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache"), "agentdiff"); +} + +function viaVirtualenv(python, args) { + const venv = path.join(cacheDir(), `venv-${version}`); + const binDir = path.join(venv, IS_WIN ? "Scripts" : "bin"); + const agentdiff = path.join(binDir, IS_WIN ? "agentdiff.exe" : "agentdiff"); + if (!fs.existsSync(agentdiff)) { + console.error(`Setting up AgentDiff ${version} in ${venv} (first run only)...`); + const venvPython = path.join(binDir, IS_WIN ? "python.exe" : "python"); + if ( + quiet(python, ["-m", "venv", venv]) !== 0 || + quiet(venvPython, ["-m", "pip", "install", "--quiet", SPEC]) !== 0 + ) { + console.error(`agentdiff: could not install ${SPEC} with ${python}.`); + console.error(`Try: pip install "${SPEC}"`); + return 1; + } + } + return run(agentdiff, args); +} + +function main(args) { + const uvx = which("uvx"); + if (uvx) return run(uvx, ["-q", "--from", SPEC, "agentdiff", ...args]); + + const pipx = which("pipx"); + if (pipx) { + // Warm pipx's cache silently so its setup chatter stays out of the output. + quiet(pipx, ["run", "--spec", SPEC, "agentdiff", "--help"]); + return run(pipx, ["run", "--spec", SPEC, "agentdiff", ...args]); + } + + const python = findPython(); + if (python) return viaVirtualenv(python, args); + + console.error( + [ + "agentdiff needs Python 3.10+ (or uv) and none was found.", + " uv: https://docs.astral.sh/uv/getting-started/installation/", + " Python: https://www.python.org/downloads/", + ].join("\n"), + ); + return 1; +} + +process.exitCode = main(process.argv.slice(2)); diff --git a/npm/package.json b/npm/package.json new file mode 100644 index 00000000..bb8dd321 --- /dev/null +++ b/npm/package.json @@ -0,0 +1,34 @@ +{ + "name": "agentdiff", + "version": "0.6.0", + "description": "See what your AI coding agent actually did, and what it cost. Local spend report for Claude Code sessions.", + "keywords": [ + "claude-code", + "cost", + "usage", + "tokens", + "prompt-caching", + "ai-agents", + "cli" + ], + "homepage": "https://agentdiff.app", + "repository": { + "type": "git", + "url": "git+https://github.com/kerrshift/agentdiff.git", + "directory": "npm" + }, + "bugs": "https://github.com/kerrshift/agentdiff/issues", + "license": "MIT", + "bin": { + "agentdiff": "bin/agentdiff.js" + }, + "files": [ + "bin" + ], + "engines": { + "node": ">=18" + }, + "scripts": { + "test": "node --test test/*.test.js" + } +} diff --git a/npm/test/agentdiff.test.js b/npm/test/agentdiff.test.js new file mode 100644 index 00000000..3b37f043 --- /dev/null +++ b/npm/test/agentdiff.test.js @@ -0,0 +1,103 @@ +"use strict"; +// Exercises the launcher's runner selection with stub executables on PATH. +// Each stub appends its argv to a log file so the test can see what ran. + +const { test } = require("node:test"); +const assert = require("node:assert/strict"); +const { spawnSync } = require("node:child_process"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); + +const BIN = path.join(__dirname, "..", "bin", "agentdiff.js"); +const { version } = require("../package.json"); +const SPEC = `agent-trajectory-diff==${version}`; + +const skipOnWindows = { skip: process.platform === "win32" }; + +function sandbox() { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agentdiff-npm-")); + const bin = path.join(dir, "bin"); + fs.mkdirSync(bin); + return { dir, bin, log: path.join(dir, "calls.log") }; +} + +function stub(binDir, name, body) { + const file = path.join(binDir, name); + fs.writeFileSync(file, `#!/bin/sh\n${body}\n`, { mode: 0o755 }); +} + +function logArgs(log, name) { + return `echo "${name} $*" >> "${log}"`; +} + +function launch(sb, args) { + return spawnSync(process.execPath, [BIN, ...args], { + env: { PATH: `${sb.bin}:/bin`, HOME: sb.dir, AGENTDIFF_CACHE_DIR: path.join(sb.dir, "cache") }, + encoding: "utf8", + }); +} + +function calls(sb) { + return fs.existsSync(sb.log) ? fs.readFileSync(sb.log, "utf8").trim().split("\n") : []; +} + +test("prefers uvx and pins the matching PyPI version", skipOnWindows, () => { + const sb = sandbox(); + stub(sb.bin, "uvx", logArgs(sb.log, "uvx")); + stub(sb.bin, "pipx", logArgs(sb.log, "pipx")); + const result = launch(sb, ["--since", "30d"]); + assert.equal(result.status, 0); + assert.deepEqual(calls(sb), [`uvx -q --from ${SPEC} agentdiff --since 30d`]); +}); + +test("falls back to pipx when uv is missing", skipOnWindows, () => { + const sb = sandbox(); + stub(sb.bin, "pipx", logArgs(sb.log, "pipx")); + const result = launch(sb, ["--json"]); + assert.equal(result.status, 0); + assert.deepEqual(calls(sb), [ + `pipx run --spec ${SPEC} agentdiff --help`, + `pipx run --spec ${SPEC} agentdiff --json`, + ]); +}); + +test("builds a private virtualenv with plain Python, once", skipOnWindows, () => { + const sb = sandbox(); + // Fake python3: passes the version check, and `-m venv DIR` creates a venv + // whose python (pip install) and agentdiff log their calls. + stub( + sb.bin, + "python3", + `${logArgs(sb.log, "python3")} +if [ "$1" = "-m" ] && [ "$2" = "venv" ]; then + mkdir -p "$3/bin" + printf '#!/bin/sh\\necho "venv-python $*" >> "${sb.log}"\\n' > "$3/bin/python" + printf '#!/bin/sh\\necho "venv-agentdiff $*" >> "${sb.log}"\\nexit 3\\n' > "$3/bin/agentdiff" + chmod +x "$3/bin/python" "$3/bin/agentdiff" +fi +exit 0`, + ); + const first = launch(sb, ["--top", "2"]); + assert.equal(first.status, 3, "propagates the CLI's exit code"); + assert.match(first.stderr, /first run only/); + const venv = path.join(sb.dir, "cache", `venv-${version}`); + assert.deepEqual(calls(sb), [ + "python3 -c import sys; sys.exit(sys.version_info < (3, 10))", + `python3 -m venv ${venv}`, + `venv-python -m pip install --quiet ${SPEC}`, + "venv-agentdiff --top 2", + ]); + + fs.writeFileSync(sb.log, ""); + launch(sb, []); + assert.deepEqual(calls(sb).filter(Boolean).slice(-1), ["venv-agentdiff"]); + assert.ok(!calls(sb).some((c) => c.includes("pip install")), "reuses the venv"); +}); + +test("explains what to install when nothing is available", skipOnWindows, () => { + const sb = sandbox(); + const result = launch(sb, []); + assert.equal(result.status, 1); + assert.match(result.stderr, /needs Python 3\.10\+ \(or uv\)/); +}); diff --git a/plugins/agentdiff/skills/report/scripts/report.sh b/plugins/agentdiff/skills/report/scripts/report.sh index f609b59d..c8bb2fda 100755 --- a/plugins/agentdiff/skills/report/scripts/report.sh +++ b/plugins/agentdiff/skills/report/scripts/report.sh @@ -1,26 +1,56 @@ #!/usr/bin/env bash # Runs the AgentDiff session report for the /agentdiff:report skill. # -# Prefers an installed `agentdiff` that has the `sessions` command, then falls -# back to uvx. Always exits 0: a failed skill shell command aborts the whole -# skill, so problems are printed as text for Claude to relay instead. +# Tries, in order: an installed `agentdiff` that has the `sessions` command, +# uvx, pipx, then a private virtualenv built with any Python 3.10+ (created +# once under ~/.cache/agentdiff). Always exits 0: a failed skill shell command +# aborts the whole skill, so problems are printed as text for Claude to relay. set -u +SPEC='agent-trajectory-diff>=0.6.0' export COLUMNS="${COLUMNS:-100}" export NO_COLOR=1 -if command -v agentdiff >/dev/null 2>&1 && agentdiff sessions --help >/dev/null 2>&1; then - agentdiff sessions "$@" 2>&1 || echo "AgentDiff exited with an error (see above)." +fail() { echo "AgentDiff exited with an error (see above)."; } + +has_sessions() { "$1" sessions --help >/dev/null 2>&1; } + +find_python() { + for py in python3 python; do + if command -v "$py" >/dev/null 2>&1 \ + && "$py" -c 'import sys; sys.exit(sys.version_info < (3, 10))' 2>/dev/null; then + echo "$py" + return 0 + fi + done + return 1 +} + +if command -v agentdiff >/dev/null 2>&1 && has_sessions agentdiff; then + agentdiff sessions "$@" 2>&1 || fail elif command -v uvx >/dev/null 2>&1; then - uvx -q --from 'agent-trajectory-diff>=0.6.0' agentdiff sessions "$@" 2>&1 \ - || echo "AgentDiff exited with an error (see above)." + uvx -q --from "$SPEC" agentdiff sessions "$@" 2>&1 || fail +elif command -v pipx >/dev/null 2>&1; then + # Warm pipx's cache silently so its setup chatter stays out of the report. + pipx run --spec "$SPEC" agentdiff --help >/dev/null 2>&1 + pipx run --spec "$SPEC" agentdiff sessions "$@" 2>&1 || fail +elif PY=$(find_python); then + VENV="${XDG_CACHE_HOME:-$HOME/.cache}/agentdiff/venv" + BIN="$VENV/bin/agentdiff" + if [ ! -x "$BIN" ] || ! has_sessions "$BIN"; then + echo "Setting up AgentDiff in $VENV (first run only)..." + "$PY" -m venv "$VENV" >/dev/null 2>&1 \ + && "$VENV/bin/python" -m pip install --quiet --upgrade "$SPEC" >/dev/null 2>&1 \ + || { echo "Could not install AgentDiff with $PY. Try: pip install \"$SPEC\""; exit 0; } + fi + "$BIN" sessions "$@" 2>&1 || fail else cat <<'EOF' -AgentDiff could not run: neither `agentdiff` (0.6.0 or later) nor `uvx` was found. +AgentDiff could not run: it needs Python 3.10+ (or uv) and none was found. Install one of these, then run /agentdiff:report again: - uv: https://docs.astral.sh/uv/getting-started/installation/ - - pip: pip install "agent-trajectory-diff>=0.6.0" + - Python: https://www.python.org/downloads/ EOF fi exit 0 diff --git a/tests/test_claude_plugin.py b/tests/test_distribution.py similarity index 77% rename from tests/test_claude_plugin.py rename to tests/test_distribution.py index dbd88cd1..fed6c76b 100644 --- a/tests/test_claude_plugin.py +++ b/tests/test_distribution.py @@ -1,4 +1,4 @@ -"""Claude Code plugin: marketplace/manifest consistency and the report script.""" +"""Distribution: Claude Code plugin, its report script, and the npm launcher.""" import json import os @@ -45,6 +45,16 @@ def test_plugin_version_tracks_package_version(): assert manifest["version"] == __version__ +def test_npm_launcher_version_tracks_package_version(): + from agentdiff import __version__ + + package = json.loads((ROOT / "npm" / "package.json").read_text()) + assert package["name"] == "agentdiff" + assert package["version"] == __version__ + assert package["bin"] == {"agentdiff": "bin/agentdiff.js"} + assert os.access(ROOT / "npm" / "bin" / "agentdiff.js", os.X_OK) + + def test_skill_preapproves_the_command_it_injects(): text = SKILL.read_text() fields = _frontmatter(text) @@ -82,8 +92,14 @@ def test_report_script_runs_and_never_fails(tmp_path): @pytest.mark.skipif(shutil.which("bash") is None, reason="needs bash") def test_report_script_explains_install_when_nothing_is_available(tmp_path): - env = {"PATH": "/usr/bin:/bin", "HOME": str(tmp_path)} - result = subprocess.run([str(SCRIPT)], env=env, capture_output=True, text=True) + # PATH holds only `cat`: no agentdiff, uvx, pipx or Python to fall back on. + empty = tmp_path / "bin" + empty.mkdir() + (empty / "cat").symlink_to(shutil.which("cat")) + env = {"PATH": str(empty), "HOME": str(tmp_path)} + result = subprocess.run( + [shutil.which("bash"), str(SCRIPT)], env=env, capture_output=True, text=True + ) assert result.returncode == 0 assert "AgentDiff could not run" in result.stdout - assert "pip install" in result.stdout + assert "Python 3.10+" in result.stdout From eed25c3e26be66d221fdc16a125ece479f8c5cff Mon Sep 17 00:00:00 2001 From: lostmartian Date: Mon, 19 Oct 2026 07:36:45 +0530 Subject: [PATCH 2/2] test(npm): hide system bin in the nothing-installed case On Ubuntu /bin is /usr/bin and holds python3, so the launcher correctly found Python and the test's expectation of 'nothing available' failed in CI. Run that case with only the stub directory on PATH. --- npm/test/agentdiff.test.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/npm/test/agentdiff.test.js b/npm/test/agentdiff.test.js index 3b37f043..3aebe760 100644 --- a/npm/test/agentdiff.test.js +++ b/npm/test/agentdiff.test.js @@ -31,9 +31,16 @@ function logArgs(log, name) { return `echo "${name} $*" >> "${log}"`; } -function launch(sb, args) { +// Stubs are POSIX sh scripts that need coreutils (mkdir, chmod) from /bin. +// Pass systemBin=false to hide everything but the stubs, since /bin may hold +// a real python3 (it does on Ubuntu, where /bin is /usr/bin). +function launch(sb, args, { systemBin = true } = {}) { return spawnSync(process.execPath, [BIN, ...args], { - env: { PATH: `${sb.bin}:/bin`, HOME: sb.dir, AGENTDIFF_CACHE_DIR: path.join(sb.dir, "cache") }, + env: { + PATH: systemBin ? `${sb.bin}:/bin` : sb.bin, + HOME: sb.dir, + AGENTDIFF_CACHE_DIR: path.join(sb.dir, "cache"), + }, encoding: "utf8", }); } @@ -97,7 +104,7 @@ exit 0`, test("explains what to install when nothing is available", skipOnWindows, () => { const sb = sandbox(); - const result = launch(sb, []); + const result = launch(sb, [], { systemBin: false }); assert.equal(result.status, 1); assert.match(result.stderr, /needs Python 3\.10\+ \(or uv\)/); });