Four repo gates reported success in states they exist to reject. `check-vale-style-sync.sh` passed while a Kyberforge lint rule was silenced. The check matched a blocklist of severity values, but Vale's semantic is an allowlist: anything that is not exactly YES/error/warning/suggestion disables the rule. So `= false`, `= 0`, `= garbage`, an empty value and — worst — a lowercase `= yes` all killed enforcement while reading as "enabled" to a human. Inverted to an allowlist. Two sibling holes: dropping `KyberforgeCopilot` from `BasedOnStyles` unloaded the Copilot-only check silently, and narrowing a section glob to a location made Vale lint zero files, which is the "0 files, hook Passed" failure the script's own comment says it exists to catch. `sync-marketplace-mirror.sh --check` failed open when its source was missing, while its sibling correctly errored in the same state. `check-scope-walkup-sync.sh` wrote to hardcoded `/tmp/fN.out` paths and read one back, making it non-reentrant — a concurrent instance can flip a verdict, and this branch made the test runner concurrent. Now per-run `mktemp -d`. `check-manifests.sh` had no disk-to-marketplace pass, so a plugin directory absent from `marketplace.json` passed every gate while the `validate-plugins` hook globbed it. The "listed" match is restricted to remote-source entry names; matching any entry name let a genuine orphan through on a name coincidence. `run-bats.sh` reported an empty TAP stream as `0 tests, 0 failures`, exit 0 — a total harness failure reading as a pass. The test-side changes are the larger half, because the guards were the real problem. `test-sync-marketplace-mirror.sh` could overwrite the live tracked mirror under an inherited GIT_DIR, which is precisely the git-hook context it runs in. The bash-3.2 scan hand-maintained its file list, omitting the new shared runner, and had no rule for `wait -n` or `nproc` — the two hazards the previous review round found live. It now derives 43 files across three globs with per-glob floors. Several assertions were decoration: the concurrency checks caught the reentrancy defect 0 times in 10, the leak fix was green either way, and two manifest fixtures passed with the code they claimed to cover deleted. Every assertion now has a revert it provably fails against. Refs: #90 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
399 lines
16 KiB
Bash
399 lines
16 KiB
Bash
#!/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)); }
|
|
|
|
# One trap over a registry rather than a fresh `trap 'rm -rf "$FIXTUREn"' EXIT`
|
|
# per fixture: each such trap REPLACES the previous one, so only the last
|
|
# fixture was ever cleaned and the rest leaked into TMPDIR every run. Same
|
|
# pattern as tests/test-check-vale-style-sync.sh and
|
|
# tests/test-check-scope-walkup-sync.sh; the emptiness guard is there because
|
|
# bash 3.2 treats "${arr[@]}" on an empty array as unbound under `set -u`.
|
|
FIXTURES=()
|
|
cleanup() { [[ ${#FIXTURES[@]} -eq 0 ]] || rm -rf "${FIXTURES[@]}"; }
|
|
trap cleanup EXIT
|
|
|
|
# Helper: make a minimal valid repo fixture with marketplace + plugin structure.
|
|
# Per ADR-0015/ADR-0017, the manifest check-manifests.sh validates is
|
|
# .claude-plugin/plugin.json (compiled output) -- not the root-level plugin.json,
|
|
# which was deleted repo-wide, and not the skills/hooks/mcpServers/agents pointer
|
|
# fields apm's compiler deliberately never populates (see scripts/check-manifests.sh's
|
|
# own header comment). Content-presence drift is scripts/sync-plugin-content.sh's job.
|
|
make_valid_fixture() {
|
|
local dir
|
|
dir="$(mktemp -d)"
|
|
mkdir -p "$dir/.claude-plugin"
|
|
mkdir -p "$dir/plugins/myplugin/.claude-plugin"
|
|
cat > "$dir/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "myplugin", "source": "./plugins/myplugin" }
|
|
]
|
|
}
|
|
JSON
|
|
cat > "$dir/plugins/myplugin/.claude-plugin/plugin.json" <<'JSON'
|
|
{
|
|
"name": "myplugin"
|
|
}
|
|
JSON
|
|
echo "$dir"
|
|
}
|
|
|
|
# --- 1. Exits 0 against valid repo structure ---
|
|
echo ""
|
|
echo "--- exits 0 when all references are valid ---"
|
|
FIXTURE="$(make_valid_fixture)"
|
|
FIXTURES+=("$FIXTURE")
|
|
if bash "$SCRIPT" "$FIXTURE" > /dev/null 2>&1; then
|
|
pass "exits 0 when all manifest references resolve"
|
|
else
|
|
fail "exited non-zero against a valid fixture"
|
|
fi
|
|
|
|
# --- 2. Exits 1 when plugin source dir is missing ---
|
|
echo ""
|
|
echo "--- exits 1 when plugin source directory missing ---"
|
|
FIXTURE2="$(mktemp -d)"
|
|
FIXTURES+=("$FIXTURE2")
|
|
mkdir -p "$FIXTURE2/.claude-plugin"
|
|
cat > "$FIXTURE2/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "ghost", "source": "./plugins/ghost" }
|
|
]
|
|
}
|
|
JSON
|
|
if bash "$SCRIPT" "$FIXTURE2" > /dev/null 2>&1; then
|
|
fail "exited 0 when plugin source dir is missing — expected exit 1"
|
|
else
|
|
pass "exits non-zero when plugin source directory does not exist"
|
|
fi
|
|
|
|
# --- 3. Exits 1 when .claude-plugin/plugin.json is missing from plugin dir ---
|
|
echo ""
|
|
echo "--- exits 1 when .claude-plugin/plugin.json missing from plugin directory ---"
|
|
FIXTURE3="$(mktemp -d)"
|
|
FIXTURES+=("$FIXTURE3")
|
|
mkdir -p "$FIXTURE3/.claude-plugin"
|
|
mkdir -p "$FIXTURE3/plugins/nomanifest"
|
|
cat > "$FIXTURE3/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "nomanifest", "source": "./plugins/nomanifest" }
|
|
]
|
|
}
|
|
JSON
|
|
if bash "$SCRIPT" "$FIXTURE3" > /dev/null 2>&1; then
|
|
fail "exited 0 when .claude-plugin/plugin.json is missing — expected exit 1"
|
|
else
|
|
pass "exits non-zero when .claude-plugin/plugin.json is missing from plugin directory"
|
|
fi
|
|
|
|
# --- 4. Exits 1 when a remote-source plugin entry's local plugin still lacks a manifest ---
|
|
# Remote sources (object-typed `source:`) are skipped entirely; only string (local path)
|
|
# sources are checked. This guards that a mixed marketplace.json still catches a broken
|
|
# local entry alongside a legitimately-skipped remote one.
|
|
echo ""
|
|
echo "--- exits 1 for a broken local entry even when a remote entry is present ---"
|
|
FIXTURE4="$(mktemp -d)"
|
|
FIXTURES+=("$FIXTURE4")
|
|
mkdir -p "$FIXTURE4/.claude-plugin"
|
|
mkdir -p "$FIXTURE4/plugins/broken"
|
|
cat > "$FIXTURE4/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "remote-thing", "source": { "repo": "someorg/somerepo", "source": "github" } },
|
|
{ "name": "broken", "source": "./plugins/broken" }
|
|
]
|
|
}
|
|
JSON
|
|
if bash "$SCRIPT" "$FIXTURE4" > /dev/null 2>&1; then
|
|
fail "exited 0 with a broken local entry present — expected exit 1"
|
|
else
|
|
pass "exits non-zero for a broken local entry even alongside a skipped remote entry"
|
|
fi
|
|
|
|
# --- 5. Non-.apm/ plugin with a broken pointer field is caught by the fallback path ---
|
|
# apm-native plugins (.apm/ present) get their skills/hooks/mcpServers/agents
|
|
# pointer-field validation from sync-plugin-content.sh --check instead (see this
|
|
# script's header comment) -- but that script skips any plugin dir lacking .apm/
|
|
# outright, so a non-apm plugin's hand-authored plugin.json needs this script's own
|
|
# fallback validation to catch a broken pointer field.
|
|
echo ""
|
|
echo "--- catches a broken pointer field in a non-apm plugin's plugin.json ---"
|
|
FIXTURE5="$(mktemp -d)"
|
|
FIXTURES+=("$FIXTURE5")
|
|
mkdir -p "$FIXTURE5/.claude-plugin"
|
|
mkdir -p "$FIXTURE5/plugins/legacy/.claude-plugin"
|
|
cat > "$FIXTURE5/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "legacy", "source": "./plugins/legacy" }
|
|
]
|
|
}
|
|
JSON
|
|
cat > "$FIXTURE5/plugins/legacy/.claude-plugin/plugin.json" <<'JSON'
|
|
{
|
|
"name": "legacy",
|
|
"skills": ["./skills/does-not-exist"]
|
|
}
|
|
JSON
|
|
if bash "$SCRIPT" "$FIXTURE5" > /dev/null 2>&1; then
|
|
fail "exited 0 for a non-apm plugin with a broken skills pointer -- expected exit 1"
|
|
else
|
|
pass "catches a broken skills pointer field in a non-apm (no .apm/) plugin.json"
|
|
fi
|
|
|
|
# --- 6. Non-.apm/ plugin with valid pointer fields still passes (no false positive) ---
|
|
echo ""
|
|
echo "--- a non-apm plugin with valid pointer fields still passes ---"
|
|
FIXTURE6="$(mktemp -d)"
|
|
FIXTURES+=("$FIXTURE6")
|
|
mkdir -p "$FIXTURE6/.claude-plugin"
|
|
mkdir -p "$FIXTURE6/plugins/legacy-ok/.claude-plugin"
|
|
mkdir -p "$FIXTURE6/plugins/legacy-ok/skills/real-skill"
|
|
cat > "$FIXTURE6/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "legacy-ok", "source": "./plugins/legacy-ok" }
|
|
]
|
|
}
|
|
JSON
|
|
cat > "$FIXTURE6/plugins/legacy-ok/.claude-plugin/plugin.json" <<'JSON'
|
|
{
|
|
"name": "legacy-ok",
|
|
"skills": ["./skills/real-skill"]
|
|
}
|
|
JSON
|
|
if bash "$SCRIPT" "$FIXTURE6" > /dev/null 2>&1; then
|
|
pass "a non-apm plugin with a resolving skills pointer passes"
|
|
else
|
|
fail "exited non-zero for a non-apm plugin whose pointer fields all resolve"
|
|
fi
|
|
|
|
# --- 7. An .apm/ plugin with a broken pointer field is NOT caught here (delegated) ---
|
|
# Guards against the fallback path in finding #6 accidentally widening to also
|
|
# validate apm-native plugins, which would duplicate (and could disagree with)
|
|
# sync-plugin-content.sh --check's own drift detection.
|
|
echo ""
|
|
echo "--- an apm-native plugin's pointer fields are left to sync-plugin-content.sh --check ---"
|
|
FIXTURE7="$(mktemp -d)"
|
|
FIXTURES+=("$FIXTURE7")
|
|
mkdir -p "$FIXTURE7/.claude-plugin"
|
|
mkdir -p "$FIXTURE7/plugins/apm-plugin/.claude-plugin"
|
|
mkdir -p "$FIXTURE7/plugins/apm-plugin/.apm"
|
|
cat > "$FIXTURE7/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "apm-plugin", "source": "./plugins/apm-plugin" }
|
|
]
|
|
}
|
|
JSON
|
|
cat > "$FIXTURE7/plugins/apm-plugin/.claude-plugin/plugin.json" <<'JSON'
|
|
{
|
|
"name": "apm-plugin",
|
|
"skills": ["./skills/does-not-exist"]
|
|
}
|
|
JSON
|
|
if bash "$SCRIPT" "$FIXTURE7" > /dev/null 2>&1; then
|
|
pass "an apm-native plugin (has .apm/) is not checked here, even with a broken pointer field"
|
|
else
|
|
fail "check-manifests.sh failed on an apm-native plugin -- pointer-field validation should be delegated, not duplicated"
|
|
fi
|
|
|
|
# --- 8. Disk -> marketplace: an apm package dir with no marketplace entry is caught ---
|
|
# Both this script and sync-plugin-content.sh --all derive their plugin set from
|
|
# marketplace.json, so before this check an unlisted plugins/<name>/ was skipped by
|
|
# every marketplace-derived gate at once while still being globbed by the
|
|
# validate-plugins pre-commit hook -- two different notions of "the plugin set".
|
|
# Per ADR-0015 marketplace.json is compiled from root apm.yml's marketplace.packages[],
|
|
# so an on-disk apm package missing from it is compiled-output drift.
|
|
echo ""
|
|
echo "--- exits 1 for a plugins/<name>/ apm package with no marketplace entry ---"
|
|
FIXTURE8="$(mktemp -d)"
|
|
FIXTURES+=("$FIXTURE8")
|
|
mkdir -p "$FIXTURE8/.claude-plugin"
|
|
mkdir -p "$FIXTURE8/plugins/listed/.claude-plugin"
|
|
mkdir -p "$FIXTURE8/plugins/orphan/.claude-plugin" "$FIXTURE8/plugins/orphan/.apm/skills"
|
|
cat > "$FIXTURE8/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "listed", "source": "./plugins/listed" }
|
|
]
|
|
}
|
|
JSON
|
|
echo '{ "name": "listed" }' > "$FIXTURE8/plugins/listed/.claude-plugin/plugin.json"
|
|
echo '{ "name": "orphan" }' > "$FIXTURE8/plugins/orphan/.claude-plugin/plugin.json"
|
|
printf 'name: orphan\nversion: 0.1.0\ntype: skill\n' > "$FIXTURE8/plugins/orphan/apm.yml"
|
|
if bash "$SCRIPT" "$FIXTURE8" > /dev/null 2>&1; then
|
|
fail "exited 0 for an on-disk apm package absent from marketplace.json -- expected exit 1"
|
|
else
|
|
pass "catches a plugins/<name>/ apm package that produced no marketplace entry"
|
|
fi
|
|
|
|
# --- 9. A plugins/<name>/ dir with none of the three plugin markers is not flagged ---
|
|
# The trigger is apm.yml || .apm/ || .claude-plugin/plugin.json -- broad enough to match
|
|
# the plugins/*/ set the validate-plugins hook globs, which is the disagreement this check
|
|
# closes. A directory carrying none of the three is scratch and stays out of scope.
|
|
echo ""
|
|
echo "--- a plugins/<name>/ directory with none of the three plugin markers is not flagged ---"
|
|
FIXTURE9="$(mktemp -d)"
|
|
FIXTURES+=("$FIXTURE9")
|
|
mkdir -p "$FIXTURE9/.claude-plugin"
|
|
mkdir -p "$FIXTURE9/plugins/listed/.claude-plugin"
|
|
mkdir -p "$FIXTURE9/plugins/scratch/notes"
|
|
cat > "$FIXTURE9/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "listed", "source": "./plugins/listed" }
|
|
]
|
|
}
|
|
JSON
|
|
echo '{ "name": "listed" }' > "$FIXTURE9/plugins/listed/.claude-plugin/plugin.json"
|
|
if bash "$SCRIPT" "$FIXTURE9" > /dev/null 2>&1; then
|
|
pass "a plugins/<name>/ directory with no plugin markers is left alone"
|
|
else
|
|
fail "flagged a non-package directory under plugins/ -- expected exit 0"
|
|
fi
|
|
|
|
# --- 9b. Each of the three markers on its own is enough to trigger the check ---
|
|
# Keying only off apm.yml would leave a plugin dir carrying just .apm/ or just a
|
|
# compiled .claude-plugin/plugin.json invisible -- exactly the class of gap this
|
|
# check exists to close, since validate-plugins would still glob it.
|
|
marker_case() {
|
|
local label="$1" marker_setup="$2" dir
|
|
dir="$(mktemp -d)"
|
|
FIXTURES+=("$dir")
|
|
mkdir -p "$dir/.claude-plugin" "$dir/plugins/listed/.claude-plugin" "$dir/plugins/orphan"
|
|
cat > "$dir/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "listed", "source": "./plugins/listed" }
|
|
]
|
|
}
|
|
JSON
|
|
echo '{ "name": "listed" }' > "$dir/plugins/listed/.claude-plugin/plugin.json"
|
|
case "$marker_setup" in
|
|
apm-dir) mkdir -p "$dir/plugins/orphan/.apm/skills" ;;
|
|
plugin-json)
|
|
mkdir -p "$dir/plugins/orphan/.claude-plugin"
|
|
echo '{ "name": "orphan" }' > "$dir/plugins/orphan/.claude-plugin/plugin.json"
|
|
;;
|
|
esac
|
|
if bash "$SCRIPT" "$dir" > /dev/null 2>&1; then
|
|
fail "an unlisted plugin dir carrying only $label was not flagged"
|
|
else
|
|
pass "an unlisted plugin dir carrying only $label is flagged"
|
|
fi
|
|
}
|
|
echo ""
|
|
echo "--- .apm/ alone and .claude-plugin/plugin.json alone each trigger the check ---"
|
|
marker_case ".apm/" apm-dir
|
|
marker_case ".claude-plugin/plugin.json" plugin-json
|
|
|
|
# --- 9c. A vendored plugin declared with a remote-object source: is already listed ---
|
|
# list_marketplace_local_plugins deliberately skips remote-object entries, so a
|
|
# path-only listed/unlisted match reported a missing entry for a directory whose
|
|
# entry is in fact right there -- telling the author to add what already exists.
|
|
# The name axis of the match closes that.
|
|
echo ""
|
|
echo "--- a vendored plugin whose marketplace entry uses a remote source: is not flagged ---"
|
|
FIXTURE9C="$(mktemp -d)"
|
|
FIXTURES+=("$FIXTURE9C")
|
|
mkdir -p "$FIXTURE9C/.claude-plugin" "$FIXTURE9C/plugins/vendored/.claude-plugin"
|
|
cat > "$FIXTURE9C/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "vendored", "source": { "repo": "someorg/somerepo", "source": "github" } }
|
|
]
|
|
}
|
|
JSON
|
|
echo '{ "name": "vendored" }' > "$FIXTURE9C/plugins/vendored/.claude-plugin/plugin.json"
|
|
printf 'name: vendored\nversion: 1.2.3\ntype: skill\n' > "$FIXTURE9C/plugins/vendored/apm.yml"
|
|
if bash "$SCRIPT" "$FIXTURE9C" > /dev/null 2>&1; then
|
|
pass "a vendored dir matching a remote-source entry's name counts as listed"
|
|
else
|
|
fail "flagged a vendored plugin that already has a remote-source marketplace entry"
|
|
fi
|
|
|
|
# --- 9d. The name axis must NOT rescue an orphan via a LOCAL entry's name ---
|
|
# A local entry's name need not equal the basename of the directory it points at. An
|
|
# entry named "beta" pointing at ./plugins/alpha must not mark an unrelated, entirely
|
|
# unlisted plugins/beta/ as listed -- local entries match on their exact path, so
|
|
# extending the name fallback to them just reopens the gap this check exists to close.
|
|
echo ""
|
|
echo "--- a local entry's name does not rescue a same-named but unlisted directory ---"
|
|
FIXTURE9D="$(mktemp -d)"
|
|
FIXTURES+=("$FIXTURE9D")
|
|
mkdir -p "$FIXTURE9D/.claude-plugin" "$FIXTURE9D/plugins/alpha/.claude-plugin" "$FIXTURE9D/plugins/beta"
|
|
cat > "$FIXTURE9D/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "beta", "source": "./plugins/alpha" }
|
|
]
|
|
}
|
|
JSON
|
|
echo '{ "name": "alpha" }' > "$FIXTURE9D/plugins/alpha/.claude-plugin/plugin.json"
|
|
printf 'name: beta\nversion: 0.1.0\ntype: skill\n' > "$FIXTURE9D/plugins/beta/apm.yml"
|
|
if bash "$SCRIPT" "$FIXTURE9D" > /dev/null 2>&1; then
|
|
fail "an unlisted plugins/beta/ was rescued by an unrelated local entry named beta -- expected exit 1"
|
|
else
|
|
pass "an unlisted directory is not rescued by a local entry that merely shares its name"
|
|
fi
|
|
|
|
# --- 10. Marketplace `source:` spelling variants still count as "listed" ---
|
|
# The disk -> marketplace comparison canonicalizes both sides, so `plugins/x` and
|
|
# `./plugins/x/` must resolve to the same directory as the glob's `plugins/x/`.
|
|
#
|
|
# The entry names deliberately DIFFER from the directory basenames. With names equal to
|
|
# basenames this fixture proved nothing whenever the name axis was permissive: deleting
|
|
# the canonicalization entirely still left it passing, because the name match rescued it.
|
|
# Restricting the name axis to remote entries fixed that, but making the names differ is
|
|
# what keeps this assertion honest independently of that restriction.
|
|
echo ""
|
|
echo "--- a marketplace source without ./ or with a trailing slash still counts as listed ---"
|
|
FIXTURE10="$(mktemp -d)"
|
|
FIXTURES+=("$FIXTURE10")
|
|
mkdir -p "$FIXTURE10/.claude-plugin"
|
|
mkdir -p "$FIXTURE10/plugins/bare/.claude-plugin" "$FIXTURE10/plugins/trailing/.claude-plugin"
|
|
cat > "$FIXTURE10/.claude-plugin/marketplace.json" <<'JSON'
|
|
{
|
|
"name": "test-marketplace",
|
|
"plugins": [
|
|
{ "name": "bare-entry", "source": "plugins/bare" },
|
|
{ "name": "trailing-entry", "source": "./plugins/trailing/" }
|
|
]
|
|
}
|
|
JSON
|
|
echo '{ "name": "bare" }' > "$FIXTURE10/plugins/bare/.claude-plugin/plugin.json"
|
|
echo '{ "name": "trailing" }' > "$FIXTURE10/plugins/trailing/.claude-plugin/plugin.json"
|
|
printf 'name: bare\nversion: 0.1.0\ntype: skill\n' > "$FIXTURE10/plugins/bare/apm.yml"
|
|
printf 'name: trailing\nversion: 0.1.0\ntype: skill\n' > "$FIXTURE10/plugins/trailing/apm.yml"
|
|
if bash "$SCRIPT" "$FIXTURE10" > /dev/null 2>&1; then
|
|
pass "source: spelling variants are canonicalized before the listed/unlisted comparison"
|
|
else
|
|
fail "flagged a listed plugin because its source: string was spelled differently"
|
|
fi
|
|
|
|
echo ""
|
|
echo "Results: $PASS passed, $FAIL failed"
|
|
[[ $FAIL -eq 0 ]]
|