diff --git a/packaging/curl-installer/install.sh b/packaging/curl-installer/install.sh index 4fc4b42..38a8e40 100755 --- a/packaging/curl-installer/install.sh +++ b/packaging/curl-installer/install.sh @@ -38,7 +38,7 @@ IFS=$'\n\t' # AGENTLINUX_RELEASE_BASE is the test-mode seam consumed by 60-curl-installer.bats; # when set it REPLACES the github.com base URL entirely, including the path. # ------------------------------------------------------------------------------ -: "${ORG:=agentlinux}" +: "${ORG:=Roo4L}" : "${AGENTLINUX_ORG:=$ORG}" # alias for readability in docs : "${AGENTLINUX_RELEASE_BASE:=}" : "${AGENTLINUX_VERSION:=}" diff --git a/plugin/bin/agentlinux-install b/plugin/bin/agentlinux-install index 892915d..947da2b 100755 --- a/plugin/bin/agentlinux-install +++ b/plugin/bin/agentlinux-install @@ -244,7 +244,11 @@ run_purge() { # would resolve to `sudo -u agent -H -E -- -- bash ` and sudo # would try to exec `--` ("command not found"). Pass the command # verbatim without our own terminator. - as_user agent bash "$recipe" || log_warn "uninstall.sh for ${id} failed (continuing)" + # Recipes guard on `${AGENTLINUX_AGENT_HOME:?}` (set by runner.ts on + # normal install/remove). On --purge runner.ts is gone; provide it + # explicitly so the guard does not trip. + AGENTLINUX_AGENT_HOME=/home/agent \ + as_user agent bash "$recipe" || log_warn "uninstall.sh for ${id} failed (continuing)" else log_warn "no uninstall.sh for ${id} at ${recipe}; skipping" fi @@ -261,6 +265,11 @@ run_purge() { rm -f /etc/agentlinux.env rm -f /etc/cron.d/agentlinux + # Step 3.5: sudoers drop-in placed by 20-sudoers.sh (Phase 5.1 / ADR-012). + # Symmetric uninstall must remove the NOPASSWD grant — leaving it behind + # orphans the privilege after the agent user is gone. + rm -f /etc/sudoers.d/agentlinux + # Step 4: NodeSource apt repo files placed by 30-nodejs.sh. # Both filenames (deb822 + legacy) handled — setup_22.x may have created # either depending on Ubuntu version / prior partial migrations. diff --git a/plugin/catalog/agents/gsd/install.sh b/plugin/catalog/agents/gsd/install.sh index f82bbf7..d0e5461 100755 --- a/plugin/catalog/agents/gsd/install.sh +++ b/plugin/catalog/agents/gsd/install.sh @@ -47,4 +47,33 @@ if ! printf '%s' "$banner" | grep -q -F "v${AGENTLINUX_PINNED_VERSION}"; then exit 1 fi -echo "gsd: install complete (resolves at ${bin_path}; banner matches pin)" +## get-shit-done-cc is the BOOTSTRAPPER, not the slash-commands themselves. +## After npm install the binary lives on PATH but Claude Code does not yet +## see any /gsd-* commands or skills. The bootstrapper has to be invoked +## with --global --claude to copy the GSD skill set into ~/.claude/skills/ +## (122+ skill dirs, hooks, statusline, settings) — that is what makes +## /gsd-* commands surface inside Claude Code. +## +## Discovered by dogfood: a fresh AgentLinux + `agentlinux install gsd` +## left ~/.claude/skills/gsd-* empty, so the user ran Claude Code and saw +## zero GSD commands. The recipe was technically correct (npm install +## succeeded, binary on PATH, banner matched pin) but the user-visible +## intent ("install GSD") was not satisfied. +## Wrap the bootstrapper non-fatally so the recipe stays idempotent on +## re-runs / `--force`. Upstream may exit non-zero on "already installed" +## paths or on partial-state recovery; what we actually care about is that +## the skill set ends up under ~/.claude/skills/ — verified below. +echo "gsd: wiring GSD skill set into ~/.claude/ via get-shit-done-cc --global --claude" +get-shit-done-cc --global --claude \ + || echo "gsd install: bootstrapper exited non-zero (re-run / partial-state path); verifying skill dirs anyway" >&2 + +# Sanity-check that at least one gsd-* skill dir landed where Claude Code +# looks. Without this assertion a regression to "binary on PATH but +# bootstrapper never copied skills" would silently slip through. +skill_dir="${AGENTLINUX_AGENT_HOME:-/home/agent}/.claude/skills" +if ! find "$skill_dir" -maxdepth 1 -type d -name 'gsd-*' -print -quit 2>/dev/null | grep -q .; then + printf 'gsd install: no gsd-* skill dirs under %s after bootstrapper run\n' "$skill_dir" >&2 + exit 1 +fi + +echo "gsd: install complete (resolves at ${bin_path}; banner matches pin; skill set wired into ${skill_dir}/gsd-*)" diff --git a/plugin/catalog/agents/gsd/uninstall.sh b/plugin/catalog/agents/gsd/uninstall.sh index 5b62576..a5c5f0a 100755 --- a/plugin/catalog/agents/gsd/uninstall.sh +++ b/plugin/catalog/agents/gsd/uninstall.sh @@ -2,14 +2,41 @@ set -euo pipefail # gsd uninstall.sh — symmetric inverse. npm uninstall -g is idempotent. +: "${AGENTLINUX_AGENT_HOME:?AGENTLINUX_AGENT_HOME not set}" + echo "gsd: removing get-shit-done-cc" -# npm uninstall -g on a missing package exits 0 with "up to date" — idempotent. -# We don't check npm's exit status aggressively; the post-step `command -v` -# check is the real truth. +# Step 1: ask the bootstrapper to undo what install.sh wired into ~/.claude/. +# Mirrors the install path's `--global --claude` invocation. Failure is +# non-fatal — the bootstrapper may be a future version that drops the flag +# or the user may have already removed bits manually; the defensive cleanup +# below catches whatever remains. +if command -v get-shit-done-cc >/dev/null 2>&1; then + get-shit-done-cc --global --claude --uninstall \ + || echo "gsd uninstall: bootstrapper --uninstall returned non-zero (continuing)" >&2 +fi + +# Step 2: defensive cleanup of GSD-installed Claude Code state. The +# bootstrapper's `--uninstall` flag is best-effort (older versions don't +# support it; failure modes leave skill dirs behind). install.sh's comment +# block notes that --global --claude writes "skill dirs, hooks, statusline, +# settings". We sweep the skills (deterministic, dir-naming convention) and +# leave settings.json + hooks alone (user-edited surface; touching it could +# clobber non-GSD config). The user can `rm ~/.claude/settings.json` if they +# want a clean slate. +find "${AGENTLINUX_AGENT_HOME}/.claude/skills" -maxdepth 1 -type d -name 'gsd-*' \ + -exec rm -rf {} + 2>/dev/null \ + || true + +# Step 3: npm uninstall -g on a missing package exits 0 with "up to date" +# — idempotent. Real truth check is `command -v` below. npm uninstall -g get-shit-done-cc --no-fund --no-audit >/dev/null 2>&1 || true -# Verify removal. +# Verify removal. `hash -r` clears bash's command-name cache — without it, +# the prior `get-shit-done-cc --uninstall` invocation hashed the binary's +# path and `command -v` reports it as still-resolvable even after npm +# uninstall -g has deleted the file from disk. +hash -r if command -v get-shit-done-cc >/dev/null 2>&1; then echo "gsd uninstall: get-shit-done-cc still on PATH after npm uninstall -g" >&2 exit 1 diff --git a/plugin/catalog/agents/playwright-cli/install.sh b/plugin/catalog/agents/playwright-cli/install.sh new file mode 100755 index 0000000..a7254aa --- /dev/null +++ b/plugin/catalog/agents/playwright-cli/install.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -euo pipefail +# playwright-cli install.sh — Microsoft's @playwright/cli for coding agents. +# +# Two-part install: +# (1) npm install -g @playwright/cli@$PIN — bootstrapper binary at +# ~agent/.npm-global/bin/playwright-cli +# (2) playwright-cli install --skills — wires the bundled Claude +# Code skill into +# ~/.claude/skills/playwright-cli/ +# +# Discovered by user dogfood: npm-installing the package alone leaves the +# binary on PATH but Claude Code sees no /playwright-cli skills. The +# `--skills` invocation is what makes the user-visible intent ("install +# Playwright CLI for the agent") work end-to-end. +# +# References: +# - https://playwright.dev/agent-cli/installation +# - https://www.npmjs.com/package/@playwright/cli +# - npm view @playwright/cli bin → { 'playwright-cli': 'playwright-cli.js' } + +: "${AGENTLINUX_PINNED_VERSION:?AGENTLINUX_PINNED_VERSION not set}" +: "${AGENTLINUX_AGENT_HOME:?AGENTLINUX_AGENT_HOME not set}" + +echo "playwright-cli: installing @playwright/cli@${AGENTLINUX_PINNED_VERSION}" + +npm install -g \ + --omit=dev \ + --no-fund \ + --no-audit \ + "@playwright/cli@${AGENTLINUX_PINNED_VERSION}" + +bin_path=$(command -v playwright-cli || true) +if [[ -z "$bin_path" ]]; then + echo "playwright-cli install: playwright-cli not on PATH after npm install -g" >&2 + exit 1 +fi + +# Verify CLI version matches pin before invoking the skill bootstrapper. +pw_version=$(playwright-cli --version 2>&1 | head -1 | tr -d '[:space:]') +if [[ "$pw_version" != "${AGENTLINUX_PINNED_VERSION}" ]]; then + printf 'playwright-cli install: pinned=%s but --version: %s\n' \ + "${AGENTLINUX_PINNED_VERSION}" "$pw_version" >&2 + exit 1 +fi + +echo "playwright-cli: CLI at ${bin_path}, version ${pw_version}" +echo "playwright-cli: wiring Claude Code skill via 'playwright-cli install --skills'" + +# Bootstrap the bundled Claude Code skill into ~/.claude/skills/. +# Non-fatal: upstream may exit non-zero on re-runs / "already installed" +# paths; what we actually care about is that the skill landed on disk — +# verified below. +# +# Must run from a writable CWD: `playwright-cli install` calls +# initWorkspace() which mkdirs ./.playwright in the current directory. +# AgentLinux dispatches recipes from /opt/agentlinux-src/ (read-only repo +# copy in Docker / read-only workspace in QEMU), so a bare invocation +# crashes with EACCES on .playwright. Anchor CWD to agent-home (always +# writable, agent-owned) so the workspace dir lives at +# /home/agent/.playwright — a per-user side-effect that purge cleans via +# `userdel -r agent`. +( cd "${AGENTLINUX_AGENT_HOME}" && playwright-cli install --skills ) \ + || echo "playwright-cli install: bootstrapper exited non-zero (re-run / partial-state); verifying skill anyway" >&2 + +# Sanity-check the skill landed where Claude Code looks for it. Anchor +# the match on `playwright-cli` (mirrors install side) — a broader +# `*playwright*` would match unrelated user-installed skills. +skill_dir="${AGENTLINUX_AGENT_HOME}/.claude/skills" +mkdir -p "$skill_dir" +if ! find "$skill_dir" -maxdepth 1 -type d -name 'playwright-cli*' -print -quit 2>/dev/null | grep -q .; then + printf 'playwright-cli install: no playwright-cli skill found under %s after bootstrapper run\n' "$skill_dir" >&2 + exit 1 +fi + +echo "playwright-cli: install complete (binary at ${bin_path}; skill wired into ${skill_dir}/playwright-cli)" diff --git a/plugin/catalog/agents/playwright-cli/uninstall.sh b/plugin/catalog/agents/playwright-cli/uninstall.sh new file mode 100755 index 0000000..eac22a0 --- /dev/null +++ b/plugin/catalog/agents/playwright-cli/uninstall.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail +# playwright-cli uninstall.sh — symmetric inverse of install.sh. +# +# Order matters: +# 1. Tear down the wired Claude Code skill (mirror of `--skills` install) +# 2. npm uninstall -g @playwright/cli +# 3. hash -r so command -v reflects on-disk state, not bash's cache + +: "${AGENTLINUX_AGENT_HOME:?AGENTLINUX_AGENT_HOME not set}" + +echo "playwright-cli: removing @playwright/cli + Claude Code skill" + +# Step 1: best-effort skill teardown via the bootstrapper itself. Some +# upstream versions support a symmetric --uninstall flag; if absent or it +# returns non-zero, we still proceed with the npm uninstall + defensive +# skill directory cleanup below. We do NOT swallow stderr — the tee +# transcript should preserve the actual upstream error if there is one. +if command -v playwright-cli >/dev/null 2>&1; then + playwright-cli install --skills --uninstall \ + || playwright-cli uninstall --skills \ + || echo "playwright-cli uninstall: bootstrapper teardown returned non-zero (continuing)" >&2 +fi + +# Step 2: defensive removal of the playwright-cli skill dirs under +# ~/.claude/skills/. Anchor the match on `playwright-cli` (mirroring the +# install side) so an unrelated user-authored `~/.claude/skills/playwright- +# notes/` is NOT collateral damage. `-name` (not `-iname`) is sufficient +# because upstream's skill dir is conventionally lower-case-kebab. +find "${AGENTLINUX_AGENT_HOME}/.claude/skills" -maxdepth 1 -type d -name 'playwright-cli*' \ + -exec rm -rf {} + \ + || true + +# Step 3: npm uninstall -g. Idempotent on missing package. +npm uninstall -g @playwright/cli --no-fund --no-audit >/dev/null 2>&1 || true + +# Step 4: clear bash's command hash so command -v reflects on-disk state. +hash -r + +if command -v playwright-cli >/dev/null 2>&1; then + echo "playwright-cli uninstall: playwright-cli still on PATH after npm uninstall -g" >&2 + exit 1 +fi + +echo "playwright-cli: uninstall complete" diff --git a/plugin/catalog/agents/playwright/install.sh b/plugin/catalog/agents/playwright/install.sh deleted file mode 100755 index a6a7046..0000000 --- a/plugin/catalog/agents/playwright/install.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -# playwright install.sh — real body (Phase 5 AGT-05). -# -# Three-part install: -# (1) npm install -g playwright@$PIN — CLI + JS bindings, agent-owned -# (2) npx playwright install --with-deps — downloads chromium + apt deps -# chromium in one shot. install-deps -# auto-prepends sudo when -# getuid() != 0 (source: -# playwright-core/src/server/ -# registry/dependencies.ts). -# With ADR-012 sudoers drop-in -# (NOPASSWD: ALL), the -# apt-get install -y ... -# succeeds without prompt. -# -# Why --with-deps instead of separate install + install-deps: -# - Upstream-recommended for CI (cited: playwright.dev/docs/ci) -# - Single command = single exit code; easier error handling -# - install-deps is browser-scoped when a browser arg is given -# -# Browser cache: ~/.cache/ms-playwright/ (agent-owned, ADR-004 compliant). -# Chromium download is ~281 MB (playwright.dev/docs/browsers) — CI time cost -# is accepted per 05-CONTEXT.md; caching is a Phase 6 optimization. - -: "${AGENTLINUX_PINNED_VERSION:?AGENTLINUX_PINNED_VERSION not set}" -: "${AGENTLINUX_AGENT_HOME:?AGENTLINUX_AGENT_HOME not set}" - -echo "playwright: installing playwright@${AGENTLINUX_PINNED_VERSION} (CLI + bindings)" - -npm install -g \ - --omit=dev \ - --no-fund \ - --no-audit \ - "playwright@${AGENTLINUX_PINNED_VERSION}" - -if ! command -v playwright >/dev/null 2>&1; then - echo "playwright install: playwright CLI not on PATH after npm install -g" >&2 - exit 1 -fi - -# Verify CLI version matches pin before downloading browsers — don't waste -# ~281 MB of download on a mispinned install. -pw_version=$(playwright --version 2>&1 | head -1) -if ! printf '%s' "$pw_version" | grep -q -F -- "${AGENTLINUX_PINNED_VERSION}"; then - printf 'playwright install: pinned=%s but --version: %s\n' \ - "${AGENTLINUX_PINNED_VERSION}" "$pw_version" >&2 - exit 1 -fi - -echo "playwright: CLI at $(command -v playwright), ${pw_version}" -echo "playwright: downloading chromium + system deps (~281 MB; uses elevated privileges for apt)" - -# --with-deps triggers the apt-privileged path internally. ADR-012's -# /etc/sudoers.d/agentlinux grant (agent ALL=(ALL) NOPASSWD: ALL) means -# Playwright's internal privileged invocation is non-interactive. If ADR-012 -# regresses, this will fail with "a password is required" — a clear signal. -# -# NPX note: npx needs HOME set for its cache. runner.ts sets HOME=/home/agent. -# If this recipe is ever invoked without HOME (e.g. a raw systemd unit without -# EnvironmentFile), npx falls back to /tmp and still works. -npx --yes playwright install --with-deps chromium - -# Post-install smoke: chromium binary exists in the expected cache location. -cache_dir="${AGENTLINUX_AGENT_HOME}/.cache/ms-playwright" -if [[ ! -d "$cache_dir" ]]; then - printf 'playwright install: browser cache dir %s not created\n' "$cache_dir" >&2 - exit 1 -fi - -# Find at least one chromium-* dir (name is like chromium-1234). -if ! find "$cache_dir" -maxdepth 1 -type d -name 'chromium-*' | head -1 | grep -q .; then - printf 'playwright install: no chromium-* dir in %s\n' "$cache_dir" >&2 - exit 1 -fi - -echo "playwright: install complete (chromium in ${cache_dir})" diff --git a/plugin/catalog/agents/playwright/uninstall.sh b/plugin/catalog/agents/playwright/uninstall.sh deleted file mode 100755 index b3f7138..0000000 --- a/plugin/catalog/agents/playwright/uninstall.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -# playwright uninstall.sh — symmetric inverse. - -: "${AGENTLINUX_AGENT_HOME:?AGENTLINUX_AGENT_HOME not set}" - -echo "playwright: removing playwright CLI and browser cache" - -npm uninstall -g playwright --no-fund --no-audit >/dev/null 2>&1 || true - -# Browser cache is large; removal is part of the uninstall contract. Phase 5 -# uninstall recipes follow the Phase 4 pattern: first-install artifacts -# cleaned; user config (if any) preserved. ms-playwright cache is purely -# a cached download — removing it is pure space reclamation, not data loss. -rm -rf "${AGENTLINUX_AGENT_HOME}/.cache/ms-playwright" - -if command -v playwright >/dev/null 2>&1; then - echo "playwright uninstall: playwright still on PATH after npm uninstall -g" >&2 - exit 1 -fi - -echo "playwright: uninstall complete" diff --git a/plugin/catalog/catalog.json b/plugin/catalog/catalog.json index ef7034a..8dd7f76 100644 --- a/plugin/catalog/catalog.json +++ b/plugin/catalog/catalog.json @@ -30,18 +30,18 @@ "tags": ["workflow", "productivity"] }, { - "id": "playwright", - "display_name": "Playwright", - "description": "Browser automation framework (canonical browser-access tool per ADR; replaces v0.2.0's chrome-devtools-mcp).", - "homepage": "https://playwright.dev/", + "id": "playwright-cli", + "display_name": "Playwright CLI", + "description": "Microsoft's token-efficient Playwright command-line tool for coding agents — exposes browser automation via concise CLI commands and bundles a Claude Code skill.", + "homepage": "https://playwright.dev/agent-cli/installation", "license": "Apache-2.0", "source_kind": "npm", - "npm_package_name": "playwright", - "pinned_version": "1.59.1", + "npm_package_name": "@playwright/cli", + "pinned_version": "0.1.11", "install_recipe_path": "install.sh", "uninstall_recipe_path": "uninstall.sh", - "post_install_verify": "npx playwright --version", - "tags": ["browser", "automation"] + "post_install_verify": "command -v playwright-cli && playwright-cli --version", + "tags": ["browser", "automation", "agent-skill"] }, { "id": "test-dummy", diff --git a/tests/bats/40-registry-cli.bats b/tests/bats/40-registry-cli.bats index 0080215..34ec885 100644 --- a/tests/bats/40-registry-cli.bats +++ b/tests/bats/40-registry-cli.bats @@ -88,8 +88,8 @@ setup() { # ---------- CLI-02: list shows catalog with installed/not-installed ---------- -# CLI-02 + CAT-01: the three real agents (claude-code, gsd, playwright) all -# show up in default `agentlinux list` output; test-dummy MUST be hidden +# CLI-02 + CAT-01: the three real agents (claude-code, gsd, playwright-cli) +# all show up in default `agentlinux list` output; test-dummy MUST be hidden # (test_only:true) without --include-test. Happy-path + filter-negative combo. @test "CLI-02: agentlinux list shows the three real agents by default (test-dummy hidden)" { run sudo -u agent -H bash --login -c 'agentlinux list' @@ -98,8 +98,8 @@ setup() { || __fail "CLI-02" "claude-code in default list" "${output:-}" "$LOG" echo "$output" | grep -q 'gsd' \ || __fail "CLI-02" "gsd in default list" "${output:-}" "$LOG" - echo "$output" | grep -q 'playwright' \ - || __fail "CLI-02" "playwright in default list" "${output:-}" "$LOG" + echo "$output" | grep -q 'playwright-cli' \ + || __fail "CLI-02" "playwright-cli in default list" "${output:-}" "$LOG" # test-dummy MUST NOT appear in default list (CAT-02 related — no test fixtures # leak into user-facing default output). if echo "$output" | grep -q 'test-dummy'; then @@ -316,17 +316,21 @@ setup() { # CAT-01: all three real agents are present in the JSON output (authoritative # machine-readable form; the text-table CLI-02 test is the human-readable twin). -@test "CAT-01: catalog JSON contains claude-code, gsd, playwright" { +@test "CAT-01: catalog JSON contains claude-code, gsd, playwright-cli" { run sudo -u agent -H bash --login -c 'agentlinux list --json' assert_exit_zero "CAT-01" local ids ids=$(echo "$output" | jq -r '.[].id' | sort | tr '\n' ' ') - echo "$ids" | grep -qw 'claude-code' \ + # `grep -qw` would NOT match `playwright-cli` because `-` is a non-word + # boundary; switch to fixed-string `grep -qF` with a leading/trailing + # space so we still get whole-token matching against the space-joined + # id stream above. + echo " $ids" | grep -qF ' claude-code ' \ || __fail "CAT-01" "claude-code in JSON ids" "$ids" "-" - echo "$ids" | grep -qw 'gsd' \ + echo " $ids" | grep -qF ' gsd ' \ || __fail "CAT-01" "gsd in JSON ids" "$ids" "-" - echo "$ids" | grep -qw 'playwright' \ - || __fail "CAT-01" "playwright in JSON ids" "$ids" "-" + echo " $ids" | grep -qF ' playwright-cli ' \ + || __fail "CAT-01" "playwright-cli in JSON ids" "$ids" "-" } # CAT-02: fresh install has empty /opt/agentlinux/state/installed.d/. @@ -359,17 +363,17 @@ setup() { [[ -z "$missing" ]] \ || __fail "CAT-04" "every entry has a pinned_version in .curated" "missing: $missing" "-" # Spot-check the pinned values against what catalog.json declares (Plan 04-02). - local claude_ver gsd_ver playwright_ver dummy_ver + local claude_ver gsd_ver playwright_cli_ver dummy_ver claude_ver=$(echo "$output" | jq -r '.[] | select(.id=="claude-code") | .curated') gsd_ver=$(echo "$output" | jq -r '.[] | select(.id=="gsd") | .curated') - playwright_ver=$(echo "$output" | jq -r '.[] | select(.id=="playwright") | .curated') + playwright_cli_ver=$(echo "$output" | jq -r '.[] | select(.id=="playwright-cli") | .curated') dummy_ver=$(echo "$output" | jq -r '.[] | select(.id=="test-dummy") | .curated') [[ "$claude_ver" == "2.1.98" ]] \ || __fail "CAT-04" "claude-code pinned_version=2.1.98" "$claude_ver" "plugin/catalog/catalog.json" [[ "$gsd_ver" == "1.37.1" ]] \ || __fail "CAT-04" "gsd pinned_version=1.37.1" "$gsd_ver" "plugin/catalog/catalog.json" - [[ "$playwright_ver" == "1.59.1" ]] \ - || __fail "CAT-04" "playwright pinned_version=1.59.1" "$playwright_ver" "plugin/catalog/catalog.json" + [[ "$playwright_cli_ver" == "0.1.11" ]] \ + || __fail "CAT-04" "playwright-cli pinned_version=0.1.11" "$playwright_cli_ver" "plugin/catalog/catalog.json" [[ "$dummy_ver" == "0.0.1" ]] \ || __fail "CAT-04" "test-dummy pinned_version=0.0.1" "$dummy_ver" "plugin/catalog/catalog.json" } @@ -478,6 +482,12 @@ SH || __fail "INST-04" "/etc/agentlinux.env removed" "still present" "-" [[ ! -f /etc/cron.d/agentlinux ]] \ || __fail "INST-04" "/etc/cron.d/agentlinux removed" "still present" "-" + # Step 3.5: Phase 5.1 sudoers drop-in (ADR-012 / BHV-07) gone. + # Without this check, run_purge could regress and leave a NOPASSWD + # grant orphaned after the agent user is removed — the regression + # actually shipped in v0.3.0-rc12 and v0.4.0; caught by dogfood. + [[ ! -f /etc/sudoers.d/agentlinux ]] \ + || __fail "INST-04" "/etc/sudoers.d/agentlinux removed (BHV-07 + INST-04 symmetry)" "still present" "-" # Step 4: NodeSource apt files gone. [[ ! -f /etc/apt/sources.list.d/nodesource.sources ]] \ || __fail "INST-04" "nodesource.sources removed" "still present" "-" diff --git a/tests/bats/50-agents.bats b/tests/bats/50-agents.bats index d7cd4a1..b8e95cd 100644 --- a/tests/bats/50-agents.bats +++ b/tests/bats/50-agents.bats @@ -60,12 +60,22 @@ setup_file() { done fi + # Defensive: scrub any stale ~/.claude/skills/ state from a prior run BEFORE + # the per-agent installs. Without this scrub the AGT-04 / AGT-05 skill-wired + # @tests below could pass on stale state alone — exactly the regression those + # tests are supposed to catch (npm install succeeds but skills don't get + # wired). Bounded to the ids this file installs: gsd-* and *playwright*. + sudo -u agent -H bash --login -c ' + rm -rf ~/.claude/skills/gsd-* 2>/dev/null + find ~/.claude/skills -maxdepth 1 -type d -iname "*playwright*" -exec rm -rf {} + 2>/dev/null + ' >/dev/null 2>&1 || true + # Install all three agents once for the file. Each @test assumes the install # has already happened; we trade setup-file time for test-case simplicity. # Serial installs keep sentinel writes unambiguous (no flock dance). sudo -u agent -H bash --login -c 'agentlinux install claude-code' >/dev/null 2>&1 sudo -u agent -H bash --login -c 'agentlinux install gsd' >/dev/null 2>&1 - sudo -u agent -H bash --login -c 'agentlinux install playwright' >/dev/null 2>&1 + sudo -u agent -H bash --login -c 'agentlinux install playwright-cli' >/dev/null 2>&1 } teardown_file() { @@ -75,7 +85,7 @@ teardown_file() { if [[ -L /home/agent/.npm-global/bin/agentlinux ]]; then sudo -u agent -H bash --login -c 'agentlinux remove --force claude-code' >/dev/null 2>&1 || true sudo -u agent -H bash --login -c 'agentlinux remove --force gsd' >/dev/null 2>&1 || true - sudo -u agent -H bash --login -c 'agentlinux remove --force playwright' >/dev/null 2>&1 || true + sudo -u agent -H bash --login -c 'agentlinux remove --force playwright-cli' >/dev/null 2>&1 || true fi } @@ -121,20 +131,27 @@ teardown_file() { done } -# AGT-01 (playwright): `npx playwright --version` exits 0 in all six modes. -# Using `npx --yes` forces non-interactive (no "install this package?" prompt) -# even though the CLI is already globally installed by setup_file — matches -# how cron/systemd units would invoke it. The setup_file install lands the -# playwright binary at /home/agent/.npm-global/bin/playwright; npx resolves -# to that same path but exercises the full CLI surface (bindings + browsers). -@test "AGT-01: npx playwright --version exits 0 in every invocation mode" { +# AGT-01 (playwright-cli): `playwright-cli --version` exits 0 in all six +# invocation modes AND emits a semver-shaped string. setup_file installed +# @playwright/cli globally; the binary lives at +# /home/agent/.npm-global/bin/playwright-cli. The semver-shape grep is +# parity with the claude --version mode loop above; an exit-0 with empty +# output (e.g. an upstream regression in --version under non-TTY stdin in +# cron mode) would silently pass without it. +@test "AGT-01: playwright-cli --version exits 0 in every invocation mode" { local mode for mode in "${INVOKE_MODES[@]}"; do - invoke_mode "$mode" 'npx --yes playwright --version' + invoke_mode "$mode" 'playwright-cli --version' if [[ "${output:-}" == *SKIP_SYSTEMD_UNAVAILABLE* ]]; then skip "AGT-01 (${mode}): systemd PID 1 not running" fi - assert_exit_zero "AGT-01/Playwright (${mode})" + assert_exit_zero "AGT-01/Playwright-CLI (${mode})" + if ! printf '%s' "${output}" | grep -Eq '[0-9]+\.[0-9]+\.[0-9]+'; then + __fail "AGT-01/Playwright-CLI (${mode})" \ + "playwright-cli --version output contains semver" \ + "${output:-}" \ + "$LOG" + fi done } @@ -211,60 +228,62 @@ teardown_file() { fi } -# ---------- AGT-05: playwright + chromium ---------- +# AGT-04: install.sh must actually wire the GSD skill set into ~/.claude/skills/ +# — without this, npm-installing get-shit-done-cc is a no-op from the user's +# perspective ("agentlinux install gsd" succeeds but Claude Code shows zero +# /gsd-* commands). Discovered by dogfood. Without this @test, install.sh +# could regress to "npm install only" and the bats suite would still go green +# while the user-visible intent silently breaks. +@test "AGT-04: agentlinux install gsd wires ~/.claude/skills/gsd-* (>=10 skills present)" { + local count + count=$(sudo -u agent -H bash --login -c 'ls -1d ~/.claude/skills/gsd-* 2>/dev/null | wc -l') + if [[ "${count:-0}" -lt 10 ]]; then + __fail "AGT-04" \ + '/home/agent/.claude/skills/gsd-* count >= 10 (bootstrapper ran during install)' \ + "found ${count}" \ + "$LOG" + fi +} + +# ---------- AGT-05: playwright-cli (Microsoft @playwright/cli for agents) ---------- -# AGT-05 (version): `npx playwright --version` output must contain the pinned -# version substring. Catalog-driven pin lookup mirrors AGT-02b/AGT-04. -@test "AGT-05: npx playwright --version exits 0 with pinned version string" { +# AGT-05 (version): `playwright-cli --version` matches the catalog pin. +# Catalog-driven pin lookup mirrors AGT-02b/AGT-04. +@test "AGT-05: playwright-cli --version reports pinned version" { local pinned - pinned=$(jq -r '.agents[] | select(.id=="playwright") | .pinned_version' "$CATALOG") - run sudo -u agent -H bash --login -c 'npx --yes playwright --version' + pinned=$(jq -r '.agents[] | select(.id=="playwright-cli") | .pinned_version' "$CATALOG") + run sudo -u agent -H bash --login -c 'playwright-cli --version' assert_exit_zero "AGT-05" if ! printf '%s' "${output}" | grep -q -F -- "$pinned"; then __fail "AGT-05" \ - "playwright --version contains pinned=${pinned}" \ + "playwright-cli --version contains pinned=${pinned}" \ "${output:-}" \ "$LOG" fi } -# AGT-05 (chromium cache): install.sh's third step already downloaded chromium -# into ~agent/.cache/ms-playwright/chromium-. Re-verify the dir exists AND -# is owned by `agent` (NOT root — the ADR-004 keystone: no wrapper shim + no -# root-owned agent-runtime). `stat -c '%U'` prints the owner username; owner -# mismatch flags a sudo-path bug in the Playwright install-deps hook. -@test "AGT-05: chromium cached under ~agent/.cache/ms-playwright (no sudo/EACCES)" { - # Install.sh already downloaded chromium. Re-verify cache exists and is - # agent-owned (ADR-004 keystone). - run sudo -u agent -H bash --login -c 'find /home/agent/.cache/ms-playwright -maxdepth 1 -type d -name "chromium-*" | head -1' - assert_exit_zero "AGT-05" - if [[ -z "${output}" ]]; then - __fail "AGT-05" \ - "at least one chromium-* dir under ~agent/.cache/ms-playwright" \ - "none" \ - "$LOG" - fi - # Ownership check: chromium dir must be agent:agent (not root-owned via - # a sudo-path bug). stat -c '%U' prints owner username. - local owner - owner=$(stat -c '%U' "${output}") - if [[ "$owner" != "agent" ]]; then +# AGT-05 (skill wired): install.sh ran `playwright-cli install --skills`, +# which copies the bundled Claude Code skill set into +# ~/.claude/skills/playwright-cli/. Without this @test the recipe could +# regress to "npm install only" (binary on PATH but Claude Code sees no +# /playwright skills) — the same class of dogfood bug AGT-04's gsd +# coverage closed. +@test "AGT-05: agentlinux install playwright-cli wires ~/.claude/skills/playwright-cli" { + local count + count=$(sudo -u agent -H bash --login -c 'find ~/.claude/skills -maxdepth 2 -iname "*playwright*" 2>/dev/null | wc -l') + if [[ "${count:-0}" -lt 1 ]]; then __fail "AGT-05" \ - "chromium cache owned by agent" \ - "owner=${owner} (path: ${output})" \ + '/home/agent/.claude/skills/*playwright* count >= 1 (bootstrapper ran during install)' \ + "found ${count}" \ "$LOG" fi } # AGT-05 (idempotency): CLI-03's "already installed" short-circuit must hold # on a real (non-test-dummy) agent. setup_file already installed; a second -# invocation with the same pin must print "already installed" and NOT re-download -# chromium (~281 MB). This is the real-agent twin of 40-*.bats's test-dummy -# CLI-03 idempotency @test. -@test "AGT-05: re-install playwright is idempotent (CLI-03 invariant on real agent)" { - # setup_file already installed; a second install with the same pin should - # print "already installed" and not re-download chromium. - run sudo -u agent -H bash --login -c 'agentlinux install playwright' +# invocation with the same pin must print "already installed". +@test "AGT-05: re-install playwright-cli is idempotent (CLI-03 invariant on real agent)" { + run sudo -u agent -H bash --login -c 'agentlinux install playwright-cli' assert_exit_zero "AGT-05 re-install" echo "$output" | grep -q 'already installed' \ || __fail "AGT-05" "idempotent re-install prints 'already installed'" "${output:-}" "$LOG"