Files
Ignacy Łątka c821a463b0 fix(e2e): pin the absence of an npm postinstall hook (#556)
Found while running the full E2E harness against the packed 0.17.0
bundle.

## Problem

The `install` phase asserted that `scripts/postinstall.cjs` ships in the
package:

```bash
if [ -f "$pkg/scripts/postinstall.cjs" ]; then
  ...
else
  fail "$P" postinstall init-hint "postinstall.cjs not shipped"
fi
```

But the postinstall was deliberately dropped in #510 ("drop the npm
postinstall - its jobs move to runtime"), and neither the file nor a
`postinstall` entry in `package.json` exists any more. So this check
failed on every run, and since the harness exits non-zero on any
failure, the release gate was red for a non-issue - which also buries
any real failure in the noise.

## Change

Pin the intended state instead of the old one: assert the package ships
neither `scripts/postinstall.cjs` nor a `postinstall` entry in the
shipped `package.json`, so the hook can't creep back in. Installs that
pass `--ignore-scripts` (pnpm, hardened CI) skip postinstall silently,
which is why #510 removed it in the first place.

## Verification

- The assertion discriminates across every input it has to separate: no
`postinstall` key and no `scripts` key at all pass; an injected
`.scripts.postinstall`, a present `scripts/postinstall.cjs`, a missing
`package.json` and a malformed one each fail, with the manifest cases
reporting that rather than "no hook". `jq -e` exits non-zero for an
unreadable file just as it does for an absent key, so the manifest is
parsed first — otherwise this absence check would pass whenever it could
not look.
- Full `install` phase re-run against `swmansion-argent-0.17.0.tgz`: no
failures, harness exit code 0 (it was one failing check before).


## Not covered here

The deleted check was the only assertion touching #510's postinstall.
Its init-hint banner went away with the hook, so nothing is left
uncovered there; its other job — killing a stale tool-server so a
freshly installed binary takes effect — moved to runtime in #510 and
this phase does not exercise that. Worth its own check, but it is a new
test rather than a repair of this one.
2026-08-17 13:23:38 +02:00

186 lines
9.0 KiB
Bash

#!/usr/bin/env bash
# Phase 0 — Install from the tgz (the whole point of "given ONLY a tgz").
#
# run-e2e.sh has already done the primary `npm install -g <tgz>` into the
# sandbox prefix (that's what ARGENT_BIN drives). This phase asserts that
# install produced a working CLI + bundled runtime, then exercises the
# configuration lifecycle in throwaway workspaces: global init, local init,
# uninstall, and a best-effort update.
#
# Everything is confined to the sandbox: npm_config_prefix -> $E2E_PREFIX,
# HOME -> $E2E_HOME. Nothing touches the real machine.
# Find a workspace config file that registers the argent MCP server. Editor
# adapter selection is environment-dependent (which editors are "detected"), so
# we assert the semantic outcome across all known config files rather than
# pinning one path. Prints the first matching file (empty if none).
# The default pattern is the quoted server name, not the npm package name: a
# global-mode init registers the MCP command as plain `argent` (the global bin),
# so `@swmansion/argent` never appears in the written config.
_argent_mcp_in_ws() { # ws [grep-pattern]
local pat="${2:-\"argent\"}"
grep -rl "$pat" "$1" \
--include='*.json' --include='*.jsonc' --include='*.toml' --include='*.yaml' 2>/dev/null \
| grep -v -e '/node_modules/' -e 'skills-lock.json' -e 'package-lock.json' -e 'package.json' \
| head -1
}
# Path to the installed package inside the sandbox prefix.
_pkg_dir() {
local d="$E2E_PREFIX/lib/node_modules/@swmansion/argent"
[ -d "$d" ] && { printf '%s\n' "$d"; return; }
# npm on some platforms nests bins differently; fall back to a find.
find "$E2E_PREFIX" -maxdepth 4 -type d -name argent -path '*@swmansion*' 2>/dev/null | head -1
}
run_phase() {
local P=install
local pkg; pkg="$(_pkg_dir)"
# --- the sandbox global install produced a working CLI --------------------
if argent_cli --version && [ "$CLI_OUT" = "$TGZ_VERSION" ]; then
pass "$P" npm-global "argent v$CLI_OUT on PATH"
else
fail "$P" npm-global "version '$CLI_OUT' != '$TGZ_VERSION' (install broken?)"
fi
if [ -n "$pkg" ] && [ -d "$pkg" ]; then
pass "$P" npm-global package-dir "$pkg"
else
fail "$P" npm-global package-dir "installed package dir not found under $E2E_PREFIX"
return 0
fi
# --- bundled native runtime is present + executable for THIS os ----------
local plat="linux"
case "$E2E_OS" in darwin) plat="darwin";; esac
# arm64 linux ships in a separate dir; pick whichever exists.
local simsrv=""
for cand in "$pkg/bin/$plat/simulator-server" "$pkg/bin/${plat}-arm64/simulator-server"; do
[ -f "$cand" ] && simsrv="$cand" && break
done
if [ -n "$simsrv" ] && [ -x "$simsrv" ]; then
pass "$P" bundle simulator-server "$(basename "$(dirname "$simsrv")")"
else
fail "$P" bundle simulator-server "missing/again-executable: $simsrv"
fi
# trace-processor assets (profiler) always shipped
[ -d "$pkg/assets/trace-processor" ] && pass "$P" bundle trace-processor || fail "$P" bundle trace-processor "assets/trace-processor missing"
# dylibs are macOS-only
if [ "$E2E_OS" = "darwin" ]; then
ls "$pkg"/dylibs/*.dylib >/dev/null 2>&1 && pass "$P" bundle dylibs || fail "$P" bundle dylibs "no dylibs in package"
else
skip "$P" bundle dylibs "macOS-only"
fi
# --- no npm postinstall hook ---------------------------------------------
# The postinstall was deliberately dropped (#510) and its jobs moved to
# runtime, because installs that pass --ignore-scripts (pnpm, hardened CI)
# skip it silently — anything it did was unreliable by construction. Pin the
# absence so the hook cannot creep back in: assert neither the script file
# nor a `postinstall` entry in the shipped package.json.
# Read the manifest first: `jq -e` exits non-zero for an unreadable file just
# as it does for an absent key, so without this an install missing its
# package.json would report "no hook" — an absence check passing because it
# could not look, which is the one way this assertion is worth nothing.
if ! jq -e . "$pkg/package.json" >/dev/null 2>&1; then
fail "$P" postinstall no-hook "package.json missing or unparseable at $pkg"
elif [ -f "$pkg/scripts/postinstall.cjs" ] || jq -e '.scripts.postinstall' "$pkg/package.json" >/dev/null 2>&1; then
fail "$P" postinstall no-hook "package ships a postinstall hook again (dropped in #510)"
else
pass "$P" postinstall no-hook
fi
# --- global init in a throwaway workspace --------------------------------
local gws="$E2E_WORK/ws/global"
mkdir -p "$gws"; ( cd "$gws" && git init -q >/dev/null 2>&1 || true )
printf '{"name":"e2e-global-probe","private":true}\n' > "$gws/package.json"
pushd "$gws" >/dev/null
if argent_cli init --yes --global --no-telemetry --from "$E2E_TGZ"; then
pass "$P" init-global exit0
else
fail "$P" init-global exit0 "$(printf '%s' "$CLI_OUT" | tail -3 | tr '\n' ' ')"
fi
popd >/dev/null
# an argent MCP server registered in some editor config?
local gcfg; gcfg="$(_argent_mcp_in_ws "$gws")"
if [ -n "$gcfg" ]; then
pass "$P" init-global mcp-config "$(basename "$(dirname "$gcfg")")/$(basename "$gcfg")"
else
fail "$P" init-global mcp-config "no argent MCP config written under $gws"
fi
# skills synced?
[ -f "$gws/skills-lock.json" ] && pass "$P" init-global skills-lock || skip "$P" init-global skills-lock "no skills-lock.json"
# install record in sandbox HOME
[ -f "$E2E_HOME/.argent/config.json" ] && pass "$P" init-global home-config || skip "$P" init-global home-config "no ~/.argent/config.json"
# --- local (committable) init in a fresh npm project ---------------------
local lws="$E2E_WORK/ws/local"
mkdir -p "$lws"
printf '{"name":"e2e-local-probe","private":true,"version":"0.0.0"}\n' > "$lws/package.json"
pushd "$lws" >/dev/null
if argent_cli init --yes --local --no-telemetry --from "$E2E_TGZ"; then
pass "$P" init-local exit0
else
fail "$P" init-local exit0 "$(printf '%s' "$CLI_OUT" | tail -3 | tr '\n' ' ')"
fi
popd >/dev/null
# devDependency + a node-path MCP command (not the global `argent`)
if [ -d "$lws/node_modules/@swmansion/argent" ]; then
pass "$P" init-local devDependency
else
fail "$P" init-local devDependency "package not in $lws/node_modules"
fi
# local mode records mode:local and points MCP at the in-repo copy
if [ -f "$lws/.argent/install.json" ] && [ "$(jq -r '.mode' "$lws/.argent/install.json" 2>/dev/null)" = "local" ]; then
pass "$P" init-local install-record "mode=local"
else
fail "$P" init-local install-record "no local .argent/install.json"
fi
local lcfg; lcfg="$(_argent_mcp_in_ws "$lws" 'node_modules/@swmansion/argent')"
if [ -n "$lcfg" ]; then
pass "$P" init-local mcp-config "$(basename "$(dirname "$lcfg")")/$(basename "$lcfg") -> node_modules path"
else
# fall back: any argent MCP entry that launches via yarn/npx local resolution
lcfg="$(_argent_mcp_in_ws "$lws")"
[ -n "$lcfg" ] && pass "$P" init-local mcp-config "$(basename "$lcfg")" || fail "$P" init-local mcp-config "no local argent MCP config under $lws"
fi
# --- update: best-effort (needs network to reach the registry) -----------
# BEFORE any uninstall, while the driver is still installed. Acts on the local
# workspace install.
pushd "$lws" >/dev/null
if argent_cli update --yes; then
pass "$P" update "completed (rc=0)"
else
skip "$P" update "non-zero (likely offline/registry): $(printf '%s' "$CLI_OUT" | head -1)"
fi
popd >/dev/null
# --- uninstall LOCAL (safe: removes the devDependency + workspace config,
# never the global driver the rest of the run depends on) --------------
pushd "$lws" >/dev/null
if argent_cli uninstall --yes --local; then pass "$P" uninstall-local exit0; else skip "$P" uninstall-local exit0 "$(printf '%s' "$CLI_OUT" | tail -2 | tr '\n' ' ')"; fi
popd >/dev/null
if [ ! -d "$lws/node_modules/@swmansion/argent" ] || [ ! -f "$lws/.argent/install.json" ]; then
pass "$P" uninstall-local config-removed
else
fail "$P" uninstall-local config-removed "local install still present under $lws"
fi
# --- uninstall GLOBAL, then restore the driver so downstream phases run ---
pushd "$gws" >/dev/null
if argent_cli uninstall --yes --global; then pass "$P" uninstall-global exit0; else skip "$P" uninstall-global exit0 "$(printf '%s' "$CLI_OUT" | tail -2 | tr '\n' ' ')"; fi
popd >/dev/null
local gcfg2; gcfg2="$(_argent_mcp_in_ws "$gws")"
if [ -z "$gcfg2" ]; then pass "$P" uninstall-global config-removed; else fail "$P" uninstall-global config-removed "argent MCP config remains: $gcfg2"; fi
# Restore the global driver (uninstall --global removes the sandbox bin that
# ARGENT_BIN points at). Fast: the tarball is local and npm caches it.
if [ ! -x "${ARGENT_BIN%% *}" ]; then
log "restoring sandbox driver after uninstall test"
npm install -g "$E2E_TGZ" --prefix "$E2E_PREFIX" --omit=optional >/dev/null 2>&1 || true
fi
if argent_cli --version >/dev/null 2>&1; then pass "$P" driver-restored ok; else fail "$P" driver-restored ok "argent not runnable after restore"; fi
}