From e647f1453593976290ded884f564c61c78451e73 Mon Sep 17 00:00:00 2001 From: Defame1297 Date: Sat, 12 Sep 2026 19:59:18 +0000 Subject: [PATCH] chore: delete the check-manifests pre-commit hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six pre-push hooks were validating overlapping sets of the same manifests. check-manifests (marketplace.json/plugin.json path checks) is redundant with validate-plugins (claude plugin validate) and apm-pack-check-clean, which already cover the same ground. Deletes the check-manifests hook entry, scripts/check-manifests.sh (282 lines), and tests/test-check-manifests.sh (771 lines). scripts/lib/marketplace-plugins.sh is kept — it is still sourced by sync-plugin-content.sh. Updates the now-stale check-manifests.sh mentions and hook counts in README.md and docs/spec/gates.md. The apm-audit-ci and apm-marketplace-check hooks named in the same finding are left untouched — the audit flags them as needing a separate decision. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01YR2CjVumUbEGWcMikcoXBD --- .pre-commit-config.yaml | 9 - README.md | 2 +- SIMPLIFICATION-AUDIT.md | 3 +- docs/spec/gates.md | 15 +- scripts/check-manifests.sh | 282 ------------- tests/test-check-manifests.sh | 771 ---------------------------------- 6 files changed, 10 insertions(+), 1072 deletions(-) delete mode 100755 scripts/check-manifests.sh delete mode 100644 tests/test-check-manifests.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 71b9b2a..31283f0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -75,15 +75,6 @@ repos: pass_filenames: false always_run: true - - id: check-manifests - name: Check plugin manifests - description: Validate marketplace.json and plugin.json paths - entry: bash scripts/check-manifests.sh - language: system - stages: [pre-push] - pass_filenames: false - always_run: true - - id: check-plugin-content-sync name: Check plugin content sync description: Verify each plugin's flat skills/agents/commands/hooks/hooks.json mirror is in sync with .apm/ -- Claude Code has no .apm/ awareness so this compiled mirror must stay current (see issue #90) diff --git a/README.md b/README.md index 80ebf16..92054f4 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Install all of these before setting up. Each one is a hard dependency of a git h | Tool | Why | Install | | --- | --- | --- | | `apm` CLI | Four pre-push hooks shell out to it (`apm-marketplace-check`, `apm-audit-ci`, `apm-pack-check-clean`, and `check-plugin-content-sync` via `scripts/sync-plugin-content.sh`) | The `apm-install` skill, or `curl -sSL https://aka.ms/apm-unix \| sh`. Verify with `apm --version` | -| `jq` | Required by `scripts/check-manifests.sh` and `scripts/sync-plugin-content.sh`, both pre-push | Your package manager | +| `jq` | Required by `scripts/sync-plugin-content.sh`, pre-push | Your package manager | | `python3` + PyYAML | Required by `scripts/skill-size-check.sh` (the `skill-size-check` pre-commit hook), which reads folded YAML frontmatter | `python3` is usually present — pre-commit is itself a Python application. `pip install pyyaml` if the hook reports PyYAML missing | | `vale` | Required by the `vale-audit-prefilter-skill` / `-agent` pre-commit hooks and the `check-vale-style-sync` pre-push hook | `brew install vale` (macOS), `snap install vale` (Linux), `choco install vale` (Windows), or https://vale.sh/docs/vale-cli/installation/ | | `claude` CLI | Required by the `validate-plugins` and `validate-marketplace` pre-push hooks | Claude Code | diff --git a/SIMPLIFICATION-AUDIT.md b/SIMPLIFICATION-AUDIT.md index 83b8af7..98aa181 100644 --- a/SIMPLIFICATION-AUDIT.md +++ b/SIMPLIFICATION-AUDIT.md @@ -59,7 +59,8 @@ Five suites account for 215 s of 276 s. Three of those five (sync-plugin-content This is the area you named as hardest to understand and slowest. Root cause: most pre-push hooks exist to keep two copies of something in sync, or to re-validate what another hook already validates. -1. **Six hooks validate overlapping sets of the same manifests.** `check-manifests`, `validate-plugins`, `validate-marketplace`, `apm-pack-check-clean`, `apm-marketplace-check`, `apm-audit-ci`. Keep the two `claude plugin validate` hooks plus `apm-pack-check-clean`. Delete `check-manifests` (282 lines + 771 test lines; its `lib/marketplace-plugins.sh` stays because `sync-plugin-content.sh` sources it). `apm-audit-ci` spends 12 s confirming that manifests `apm pack` already parses do parse; drop or keep on that basis. Move the network-dependent `apm-marketplace-check` to a release checklist. Effort S. +1. **Six hooks validate overlapping sets of the same manifests.** `check-manifests`, `validate-plugins`, `validate-marketplace`, `apm-pack-check-clean`, `apm-marketplace-check`, `apm-audit-ci`. Keep the two `claude plugin validate` hooks plus `apm-pack-check-clean`. ~~Delete `check-manifests` (282 lines + 771 test lines; its `lib/marketplace-plugins.sh` stays because `sync-plugin-content.sh` sources it).~~ `apm-audit-ci` spends 12 s confirming that manifests `apm pack` already parses do parse; drop or keep on that basis. Move the network-dependent `apm-marketplace-check` to a release checklist. Effort S. + > **Done (2026-09-12):** see commit `9051d14` on `docs/simplification-audit`. Deleted the `check-manifests` pre-commit hook entry, `scripts/check-manifests.sh` (282 lines), and `tests/test-check-manifests.sh` (771 lines); kept `scripts/lib/marketplace-plugins.sh`, still sourced by `sync-plugin-content.sh`. Updated the now-stale `check-manifests.sh` mentions in `README.md` and `docs/spec/gates.md` (hook table row and hook counts). The `apm-audit-ci` and `apm-marketplace-check` decisions in this finding remain open — out of scope for this change. 2. **Four "keep two copies in sync" gates: 1,100 script lines + 1,600 test lines.** Each one is a symptom of duplication that could be removed instead of guarded: - `check-vale-style-sync`: 413 lines + 798 test lines guarding a byte-identical 526-line `vale-wrap.sh` and style directory copied between skill-audit and agent-audit. About 350 of its lines run Vale glob probes against the hook file patterns. Disappears if the two audit skills merge (finding 14); the probes belong in `test-vale-wrap.sh`. diff --git a/docs/spec/gates.md b/docs/spec/gates.md index f16c0f8..a66f591 100644 --- a/docs/spec/gates.md +++ b/docs/spec/gates.md @@ -21,31 +21,30 @@ Install hooks via `pc-run`, wiring **all three stages**. This repo's `.pre-commi `default_install_hook_types`, so a plain install silently skips `commit-msg` (Conventional Commits) and `pre-push` (everything below). -The pre-push command reports **16** hooks, not 14. The extra two are pre-commit's own `meta` hooks, +The pre-push command reports **15** hooks, not 13. The extra two are pre-commit's own `meta` hooks, `check-hooks-apply` and `check-useless-excludes`: they declare no `stages:`, so they run at every stage including this one. Both are declared in this repo's `.pre-commit-config.yaml` like everything -else — what separates them is `repo: meta` (pre-commit's own built-ins) from `repo: local`. Fourteen +else — what separates them is `repo: meta` (pre-commit's own built-ins) from `repo: local`. Thirteen is the count of hooks this repo authors itself. -**The caveat: one of those 14 is a silent no-op under that invocation.** +**The caveat: one of those 13 is a silent no-op under that invocation.** `check-release-needed` exits 0 immediately unless `PRE_COMMIT_REMOTE_BRANCH` equals `refs/heads/main`, and pre-commit exports that variable only from the real pre-push git hook during an actual `git push`. Running the stage by hand — or from a CI runner — therefore reports it `Passed` having checked nothing. That is by design for feature branches — pushing WIP must not be blocked on cutting a premature tag — but it means `--hook-stage pre-push --all-files` is a full -rehearsal of 13 hooks and a skip of the fourteenth. The script's own header records the same gap for +rehearsal of 12 hooks and a skip of the thirteenth. The script's own header records the same gap for a PR merged through Gitea's merge button, where no local push happens at all. ## The pre-push gate -Fourteen hooks, grouped below by what they guard rather than by the order `.pre-commit-config.yaml` declares them in. +Thirteen hooks, grouped below by what they guard rather than by the order `.pre-commit-config.yaml` declares them in. **Core checks** | Hook | Guards | |---|---| | `run-tests` | `bash tests/run-tests.sh --strict` — the whole suite, skips fatal (see [Tests](#tests)) | -| `check-manifests` | `marketplace.json` and `plugin.json` paths resolve (needs `jq`) | **Generated-content drift gates** @@ -92,8 +91,8 @@ and `check-plugin-content-sync` (via `scripts/sync-plugin-content.sh`, which wra first and third are bare `apm …` entries and the second is a `bash -c` loop calling `apm` once per package, so without the CLI the push dies with an unhelpful "command not found". Install with `apm-install`, or `curl -sSL https://aka.ms/apm-unix | sh`; verify with `apm --version`. `jq` is -needed by `scripts/check-manifests.sh` and `scripts/sync-plugin-content.sh` — those at least fail -loudly (`Error: jq is required but not installed`). +needed by `scripts/sync-plugin-content.sh` — it at least fails loudly (`Error: jq is required but +not installed`). ## Skill and agent context gates (ADR-0020) diff --git a/scripts/check-manifests.sh b/scripts/check-manifests.sh deleted file mode 100755 index 957883a..0000000 --- a/scripts/check-manifests.sh +++ /dev/null @@ -1,282 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Validates that marketplace.json's local plugin entries resolve to a real directory -# containing a .claude-plugin/plugin.json. Run from repo root or pass REPO_ROOT as arg. -# -# Per ADR-0015, apm.yml is the authoring source and .claude-plugin/plugin.json is -# compiled output with no skills/hooks/mcpServers/agents pointer fields (apm's plugin.json -# builder deliberately omits them -- Claude Code auto-discovers those convention -# directories, so listing them would be redundant/invalid). For a plugin with an .apm/ -# directory, this script no longer checks those pointer fields itself; that's -# scripts/sync-plugin-content.sh --check's job (drift between .apm/ and the flat -# plugin-root mirror), wired as its own pre-push hook. -# -# sync-plugin-content.sh --check explicitly skips any plugin directory lacking .apm/ -# (an apm-native package it has nothing to compile), so that delegation leaves a real -# gap for a non-apm plugin whose hand-authored plugin.json still uses the old -# skills/hooks/mcpServers/agents pointer-field convention: nothing would check whether -# those paths resolve. The fallback block below restores that check, but only for -# plugins without .apm/ -- apm-native plugins keep relying on the delegation above so -# the two checks don't duplicate (and disagree) on the same manifest. -# -# Both of the above walk marketplace.json -> disk. Nothing walked disk -> marketplace, -# so a plugins// directory that never made it into marketplace.json was invisible -# to every marketplace-derived gate at once (this script and sync-plugin-content.sh -# --all both derive their plugin set from marketplace.json). The final block below -# closes that direction: per ADR-0015 marketplace.json is compiled output of root -# apm.yml's marketplace.packages[], so an on-disk apm package with no entry is -# compiled-output drift of exactly the kind ADR-0017 wires pre-push gates for -- and it -# is the same plugin set the validate-plugins pre-commit hook already globs as -# plugins/*/. -# -# Every pass above reads its plugin set out of marketplace.json, so anything that makes -# that file yield nothing -- absent, unparseable, a non-object root, or an entry whose -# `source` is neither a path string nor a remote object -- used to read as "clean" -# rather than "unchecked". The same is true one level down, of a per-plugin -# .claude-plugin/plugin.json that does not parse: it aborted the walk mid-loop and left -# every later plugin silently unchecked. The guards below turn each of those into an -# explicit, attributable failure instead, because a vacuous pass is the one result a gate -# must never produce. - -# Hard error, not a `|| pwd` fallback, for the reason spelled out in -# scripts/sync-marketplace-mirror.sh: every path below hangs off REPO_ROOT, and the -# exit-0 path is "nothing on disk and no manifest", so a REPO_ROOT pointing somewhere -# that is not this repo reports "clean" over a tree it never looked at. Run this from -# an empty directory outside any worktree and the fallback made that the literal -# outcome -- rev-parse failed, REPO_ROOT became $PWD, no plugins/ and no -# marketplace.json were found, exit 0, silent. -if [[ -n "${1:-}" ]]; then - REPO_ROOT="$1" -elif ! REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || [[ -z "$REPO_ROOT" ]]; then - echo "Error: not inside a git worktree -- cannot locate the repository root, and guessing \$PWD would let this check report \"clean\" over a tree it never inspected. Run it from within the repository, or pass the repo root as an argument." >&2 - exit 1 -fi -FAIL=0 - -err() { echo " FAIL: $1" >&2; FAIL=$((FAIL + 1)); } - -if ! command -v jq &>/dev/null; then - echo "Error: jq is required but not installed" >&2 - exit 1 -fi - -MARKETPLACE="$REPO_ROOT/.claude-plugin/marketplace.json" - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# Repo-root-relative, not script-dir-relative -- see tests/run-tests.sh for why. -# shellcheck source=scripts/lib/marketplace-plugins.sh -source "$SCRIPT_DIR/lib/marketplace-plugins.sh" - -# Candidate plugin directories on disk. The trigger is any of the three markers that -# make a directory a plugin rather than scratch -- apm.yml (the ADR-0015 authoring -# source), .apm/ (its content tree), or a compiled .claude-plugin/plugin.json. Matching -# all three keeps this set aligned with the plugins/*/ glob the validate-plugins -# pre-commit hook uses, which is the disagreement the disk -> marketplace pass below -# exists to close; a directory with none of them is scratch and stays out of scope. -# -# It is collected before the marketplace is read because a missing marketplace.json is -# only "nothing to check" when there is also nothing on disk to check against it. -PLUGIN_DIRS=() -for candidate in "$REPO_ROOT"/plugins/*/; do - candidate="${candidate%/}" - [[ -d "$candidate" ]] || continue - if [[ ! -f "$candidate/apm.yml" && ! -d "$candidate/.apm" && ! -f "$candidate/.claude-plugin/plugin.json" ]]; then - continue - fi - PLUGIN_DIRS+=("$candidate") -done - -# An absent marketplace.json used to exit 0 unconditionally -- the same empty-set-reads- -# as-pass shape this script's other passes were fixed for. Per ADR-0015 the manifest is -# compiled output of root apm.yml's marketplace.packages[], so its absence alongside -# on-disk packages is drift, not an opt-out: it leaves every marketplace-derived gate -# (this one and sync-plugin-content.sh --all) walking an empty plugin set in silence. -if [[ ! -f "$MARKETPLACE" ]]; then - if [[ ${#PLUGIN_DIRS[@]} -eq 0 ]]; then - exit 0 - fi - listing="" - # Guarded expansion even though the check above makes an empty array unreachable - # here: bash 3.2 under `set -u` aborts on a bare expansion of an empty array, and - # tests/test-vale-wrap.sh's bash32_glob scan is line-based, so a guard two lines up - # cannot clear it. Same form as the disk -> marketplace loop below. - for candidate in ${PLUGIN_DIRS[@]+"${PLUGIN_DIRS[@]}"}; do - listing+="${listing:+, }${candidate#"$REPO_ROOT"/}" - done - err ".claude-plugin/marketplace.json does not exist, but plugins/ holds ${#PLUGIN_DIRS[@]} plugin directory/ies ($listing) — every marketplace-derived check (this one, and sync-plugin-content.sh --all) silently walks an empty plugin set without it. Recompile the manifests from root apm.yml with \`apm pack\`." - echo "Manifest check failed: $FAIL error(s)" >&2 - exit 1 -fi - -# Preconditions the marketplace walk below cannot report on itself: it runs inside a -# process substitution, so an abort in there is swallowed (see the helper's comment). -assert_marketplace_manifest_usable "$MARKETPLACE" - -# Validates one plugin.json pointer field against disk, for the non-apm fallback below. -# -# check_pointer_field -# -# test_flag is `test`'s: -d where only a directory is meaningful, -e otherwise. -# -# Per the vendored host docs (plugins/kyberforge/docs/research/docs/ -# claude-code-plugins/configuration.md and .../github-copilot-plugins/configuration.md) -# these fields are legally `string | string[] | object`. Reading them with -# `jq -r ".$field // empty"` collapsed the array and object shapes to their -# pretty-printed JSON text, which then matched no path on disk -- a manifest that -# resolves fine reported as broken. Reading `.skills | length` was worse than wrong: on -# a (legal) string value it returned the character count, and the `.skills[$i]` that -# followed aborted the whole script mid-loop under `set -e` with no summary line, so -# every plugin later in the marketplace went unchecked. -# -# The bare `$(jq ...)` assignments below are safe only because the caller has already -# established that $manifest parses AND that its root is an object (see the -# precondition in the marketplace walk). Do not call this without that check: `set -e` -# turns any jq failure in here into the same silent mid-loop abort described above. -check_pointer_field() { - local name="$1" plugin_dir="$2" field="$3" test_flag="$4" - local manifest="$plugin_dir/.claude-plugin/plugin.json" - local field_type count i elem_type - - field_type="$(jq -r ".${field} | type" "$manifest")" - case "$field_type" in - null) ;; - # An inline definition (a hooks or mcpServers object written straight into the - # manifest) declares no path, so there is nothing on disk to resolve. - object) ;; - string) - check_pointer_path "$name" "$plugin_dir" "$field" "$(jq -r ".${field}" "$manifest")" "$test_flag" - ;; - array) - count="$(jq ".${field} | length" "$manifest")" - for ((i = 0; i < count; i++)); do - elem_type="$(jq -r ".${field}[$i] | type" "$manifest")" - if [[ "$elem_type" != "string" ]]; then - err "plugin '$name': ${field}[$i] must be a path string, got $elem_type" - continue - fi - check_pointer_path "$name" "$plugin_dir" "$field" "$(jq -r ".${field}[$i]" "$manifest")" "$test_flag" - done - ;; - *) - err "plugin '$name': $field must be a path string, an array of path strings, or an inline object, got $field_type" - ;; - esac -} - -check_pointer_path() { - local name="$1" plugin_dir="$2" field="$3" ref="$4" test_flag="$5" - local full_path="$plugin_dir/$ref" - full_path="${full_path%/}" - if ! test "$test_flag" "$full_path"; then - err "plugin '$name': $field path not found: $ref" - fi -} - -# Every local plugin directory marketplace.json claimed, canonicalized, so the -# disk -> marketplace pass below can tell "listed" from "unlisted" regardless of how -# the `source:` string was spelled (./plugins/x, plugins/x, plugins/x/). -SEEN_PLUGIN_DIRS=() - -while IFS=$'\t' read -r name plugin_dir; do - source_rel="${plugin_dir#"$REPO_ROOT"/}" - - if [[ ! -d "$plugin_dir" ]]; then - err "plugin '$name': source directory not found: $source_rel" - continue - fi - # -P so a plugin directory reached through a symlink compares equal to the same - # directory reached directly; the disk-side walk below resolves the same way. - SEEN_PLUGIN_DIRS+=("$(cd "$plugin_dir" && pwd -P)") - - manifest="$plugin_dir/.claude-plugin/plugin.json" - if [[ ! -f "$manifest" ]]; then - err "plugin '$name': .claude-plugin/plugin.json not found in $source_rel" - continue - fi - - # apm-native plugin: pointer-field validation is sync-plugin-content.sh --check's - # job (see header comment above). - [[ -d "$plugin_dir/.apm" ]] && continue - - # Precondition for check_pointer_field, which reads the manifest with bare - # `field_type="$(jq ... )"` assignments. Under `set -e` a jq failure in one of - # those aborts the whole script mid-loop: rc=5, a raw `jq: parse error` and no - # `Manifest check failed:` summary, with every later plugin left unchecked -- - # the same failure class the marketplace's own `jq empty` precondition closes, - # for a file that is equally generated output. Both shapes have to be caught - # here: `jq empty` passes on a valid non-object document like `[]` or `123`, and - # it is the `.skills` lookup on such a root ("Cannot index array with string") - # that aborts, not the parse. - if ! jq empty "$manifest" >/dev/null 2>&1; then - err "plugin '$name': .claude-plugin/plugin.json is not valid JSON — it is compiled output, so recompile it with \`apm pack\`." - continue - fi - manifest_type="$(jq -r 'type' "$manifest")" - if [[ "$manifest_type" != "object" ]]; then - err "plugin '$name': .claude-plugin/plugin.json is a JSON $manifest_type at its top level; expected an object." - continue - fi - - # Fallback for a non-apm plugin: validate that any skills/hooks/mcpServers/agents - # pointer fields in its hand-authored plugin.json still resolve to real paths. - # skills/agents point at directories; hooks/mcpServers may point at a file. - check_pointer_field "$name" "$plugin_dir" skills -d - check_pointer_field "$name" "$plugin_dir" agents -e - check_pointer_field "$name" "$plugin_dir" hooks -e - check_pointer_field "$name" "$plugin_dir" mcpServers -e -done < <(list_marketplace_local_plugins "$REPO_ROOT" "$MARKETPLACE") - -# Disk -> marketplace, over the PLUGIN_DIRS candidate set collected above. -# -# A candidate counts as listed if it is either a directory some local entry pointed at -# (path match, canonicalized above) or a directory whose name matches a REMOTE entry's -# name. The name axis exists only for a plugin vendored on disk but declared with the -# remote-object `source:` shape: list_marketplace_local_plugins deliberately skips those, -# so a path-only match would report a missing entry that is in fact already there. -# -# It is restricted to non-string sources on purpose. Applied to local entries too, the -# name axis silently rescues genuine orphans, because a local entry's name need not equal -# the basename of the directory it points at: an entry named "beta" pointing at -# ./plugins/alpha would mark an unrelated, entirely unlisted plugins/beta/ as listed. -# Local entries already have an exact path to match on, so they need no name fallback. -# -# The select is an allowlist of the object shape, not a denylist of the string one -- -# see list_marketplace_remote_plugin_names in scripts/lib/marketplace-plugins.sh, which -# owns it, and tests/test-check-manifests.sh, which exercises it directly against -# malformed entries rather than through this caller (where -# assert_marketplace_manifest_usable rejects them first, and so would mask a regression -# in the select itself). -MARKETPLACE_NAMES=() -while IFS= read -r entry_name; do - [[ -n "$entry_name" ]] && MARKETPLACE_NAMES+=("$entry_name") -done < <(list_marketplace_remote_plugin_names "$MARKETPLACE") - -for candidate in ${PLUGIN_DIRS[@]+"${PLUGIN_DIRS[@]}"}; do - candidate_abs="$(cd "$candidate" && pwd -P)" - candidate_name="$(basename "$candidate")" - listed=0 - for seen in ${SEEN_PLUGIN_DIRS[@]+"${SEEN_PLUGIN_DIRS[@]}"}; do - if [[ "$seen" == "$candidate_abs" ]]; then - listed=1 - break - fi - done - if [[ $listed -eq 0 ]]; then - for entry_name in ${MARKETPLACE_NAMES[@]+"${MARKETPLACE_NAMES[@]}"}; do - if [[ "$entry_name" == "$candidate_name" ]]; then - listed=1 - break - fi - done - fi - - if [[ $listed -eq 0 ]]; then - err "plugin directory '${candidate#"$REPO_ROOT"/}' has no entry in .claude-plugin/marketplace.json — it is skipped by every marketplace-derived check (this one, and sync-plugin-content.sh --all) while still being globbed by the validate-plugins hook. Add it to root apm.yml's marketplace.packages[] and recompile the manifests." - fi -done - -if [[ $FAIL -gt 0 ]]; then - echo "Manifest check failed: $FAIL error(s)" >&2 - exit 1 -fi diff --git a/tests/test-check-manifests.sh b/tests/test-check-manifests.sh deleted file mode 100644 index 2c76648..0000000 --- a/tests/test-check-manifests.sh +++ /dev/null @@ -1,771 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -SCRIPT="$REPO_ROOT/scripts/check-manifests.sh" -PASS=0 -FAIL=0 - -pass() { echo " PASS: $1"; PASS=$((PASS + 1)); } -fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); } - -# Several distinct faults all end in exit 1, and the bugs fixed below were precisely -# about the WRONG one being reported (a corrupt manifest blamed on six unlisted plugin -# directories, a legal manifest blamed for unresolvable paths). Exit-code-only -# assertions cannot see that, so these cases assert on the message text. -RUN_OUT="" -RUN_RC=0 -run_script() { RUN_OUT="$(bash "$SCRIPT" "$1" 2>&1)" && RUN_RC=0 || RUN_RC=$?; } - -# assert_fails_with