Files
holocron/tests/test-adr0020-frontmatter.sh
Defame1297 b6e68e9a2b fix(kyberforge): close the vacuous-pass paths in the ADR-0020 gate scripts
Three ways the gates could report green having measured nothing. All three were
invisible to a passing test suite, because pre-commit prints nothing at all for a
hook that exits 0 — a gate that declines to check and a gate that checked and
passed produce the identical signal.

- A UTF-8 BOM, a leading blank line, a trailing space after a `---` marker or
  CRLF line endings defeated the `^---\n` frontmatter matcher. Every ADR-0020
  check was then skipped and the file passed: measured at the time, a
  550-character description with a 1,000-word body exited 0 behind a BOM.
  All four shapes are now tolerated, and frontmatter that genuinely cannot be
  parsed is a hard ERROR rather than a silent skip.
- An agent file with a valueless `description:` followed by another key let a
  line regex capture the *next* key, which looked non-empty, so the
  missing-or-empty branch never fired and every gate below it early-returned on
  the empty folded value — zero output, exit 0, on a blocking gate. The one
  field this contract is entirely about was the one field a gate could fail to
  notice was absent. Presence is now decided on the YAML-folded value and
  nowhere else, and a missing or empty description is a hard FAIL in all three
  validators.
- The hand-rolled frontmatter fallback disagreed with PyYAML across the FAIL
  boundary on folded scalars, so which reader happened to be available decided
  the verdict. A fallback that mis-parses a scalar shape reports a vacuous pass,
  which is worse than not running, so it is deleted: python3 and PyYAML are hard
  requirements that fail loudly with an install pointer.

Boundary-target resolution no longer derives its universe from its own location.
A `${BASH_SOURCE}`-relative repo root leaked this repo's 39-skill universe into
every consumer repo running the hook through pre-commit, so a consumer skill
routing to `skill-audit` resolved against a plugin it had never installed. The
interim form resolved through `.claude/` and `.agents/`, which are gitignored
`apm install` output — the same commit reported 2 dangling targets on a machine
that had run the install and 6 on a fresh clone. Resolution now walks up from the
file being checked to an authoring root (nearest ancestor holding
`plugins/*/.apm/{skills,agents}`, else the nearest `.git`, in two passes so a
nested `.git` cannot outrank a real monorepo root); the universe is every skill
and agent under `<root>/plugins/*/` plus the file's own apm package and that
package's declared `dependencies.apm`. Deployed trees are consulted only when no
authoring root exists at all — the consumer case. One commit now gets one verdict,
which a gate shipping hot with no baseline file has to.

Narrowed in the same pass: a routing target inferred from the prose boundary form
and corroborated by nothing else reports at SUGGESTION instead of blocking. A
blocking check with no escape hatch is the wrong trade when the inference from
prose is the weak part of it.

New deterministic checks, all previously untested or absent: every
`references/<file>.md` a body names must exist (ERROR — a broken pointer is not a
style opinion); a description with no boundary clause at all, a Gotchas section
over five entries, and a Gotchas section over 25% of the body are SUGGESTIONs.
Where no universe can be determined the target check prints `INFO ... DID NOT
RUN` rather than passing quietly. Each prose-scanning check needed its own
false-positive fix — a fenced example of a Gotchas section was being read as the
section itself — and those fixes are pinned rather than assumed.

The resolver is one block copied verbatim into all three scripts between
BEGIN/END markers, because a cache-installed plugin's scripts cannot read outside
their own plugin directory. Nothing asserted the copies were still identical; a
one-line edit to a single copy passed every constant-agreement assertion, since
constants are not what drifts.

Tests land here rather than in a later commit. The existing suites assert the old
behaviour and go red against these scripts, so splitting them would leave a commit
whose own `run-tests` pre-push gate fails in isolation.

Refs: ADR-0020
2026-08-16 16:39:29 +00:00

287 lines
13 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Regression test for the two ways an ADR-0020 gate can be made to check NOTHING
# while still exiting 0. Both were live defects, both were silent, and both sit
# in the shared resolver block that all three scripts embed verbatim — so every
# case below runs against all three.
#
# 1. THE FRONTMATTER BLOCKER. The frontmatter matcher used to be `^---\n`. A
# UTF-8 BOM, a leading blank line, a trailing space after either marker, or
# CRLF line endings all defeated it, and the miss was not reported: every
# ADR-0020 check was skipped and the file passed. Measured at the time: a
# 550-character description with a 1,000-word body exited 0 behind a BOM.
# So this file asserts two complementary things — that each of those four
# shapes is now TOLERATED (the findings actually fire), and that
# frontmatter which genuinely cannot be parsed is a hard ERROR rather than
# a quiet skip. A file that cannot be measured must never report green.
#
# 2. THE VALUELESS DESCRIPTION. `description:` with no value, followed by
# another key, let a line regex's `\s*` cross the newline and capture the
# NEXT key. The value then looked present (so "missing or empty" never
# fired) and was empty once folded (so every ADR-0020 gate early-returned).
# An agent file with one exited 0 with zero output through a BLOCKING
# pre-push gate. All five spellings of "no value" are pinned here.
#
# Both fixtures carry an over-ceiling description AND an over-ceiling body on
# purpose: asserting a non-zero exit alone would be satisfied by the "cannot
# parse" error itself, so the tolerated shapes are asserted on the CONTENT of
# the findings, not on the exit code.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HOOK="$REPO_ROOT/scripts/skill-size-check.sh"
SKILL_VALIDATE="$REPO_ROOT/plugins/kyberforge/.apm/skills/skill-audit/scripts/validate.sh"
AGENT_VALIDATE="$REPO_ROOT/plugins/kyberforge/.apm/skills/agent-audit/scripts/validate.sh"
PASS=0
FAIL=0
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
TMPDIR_T="$(mktemp -d)"
trap 'rm -rf "$TMPDIR_T"' EXIT
DESC_CHARS=450
BODY_WORDS=1000
# write_fixture <kind> <path> <name> — one generator for both file shapes.
#
# Byte-level control is the point: BOM placement, line endings and trailing
# whitespace are exactly what is under test, so the file is emitted in binary
# mode rather than through a shell heredoc that would normalise them.
write_fixture() {
python3 - "$1" "$2" "$3" "$DESC_CHARS" "$BODY_WORDS" <<'PY'
import sys
kind, path, name, desc_chars, body_words = sys.argv[1:6]
desc = 'x' * int(desc_chars)
body = ' '.join(['word'] * int(body_words))
# The default, well-formed shape. Variants below mutate it.
open_marker = '---'
close_marker = '---'
prefix = ''
newline = '\n'
fm_lines = ['name: ' + name, 'description: ' + desc]
if kind == 'plain':
pass
elif kind == 'bom':
prefix = ''
elif kind == 'leading-blanks':
prefix = '\n\n \n'
elif kind == 'trailing-ws':
open_marker = '--- '
close_marker = '---\t '
elif kind == 'crlf':
newline = '\r\n'
elif kind == 'no-close':
close_marker = None
elif kind == 'yaml-list':
fm_lines = ['- one', '- two']
elif kind == 'yaml-string':
fm_lines = ['just a bare scalar, not a mapping']
elif kind == 'yaml-none':
fm_lines = []
elif kind == 'yaml-malformed':
fm_lines = ['name: ' + name, 'description: "unterminated', 'tabs:\t- a']
elif kind == 'desc-no-value':
# The exact shape that exited 0 with zero output: a line regex's `\s*`
# crosses the newline and captures `model: sonnet` as the description.
fm_lines = ['name: ' + name, 'description:', 'model: sonnet']
elif kind == 'desc-null':
fm_lines = ['name: ' + name, 'description: null']
elif kind == 'desc-single-quoted-empty':
fm_lines = ['name: ' + name, "description: ''"]
elif kind == 'desc-double-quoted-empty':
fm_lines = ['name: ' + name, 'description: ""']
elif kind == 'desc-empty-fold':
fm_lines = ['name: ' + name, 'description: >']
else:
raise SystemExit('unknown fixture kind: %s' % kind)
parts = [prefix, open_marker, newline]
for line in fm_lines:
parts.append(line)
parts.append(newline)
if close_marker is not None:
parts.append(close_marker)
parts.append(newline)
parts.append(newline)
parts.append(body)
parts.append(newline)
with open(path, 'wb') as fh:
fh.write(''.join(parts).encode('utf-8'))
PY
}
# Builds all three subjects for one fixture kind and echoes nothing; the paths
# are fixed by convention so the probes below can find them.
#
# skill-audit takes a DIRECTORY (SKILL.md inside it, name matching the dir);
# agent-audit takes a FILE inside an apm package. The hook takes the SKILL.md
# directly, so it and skill-audit share one file.
build_subjects() {
local kind="$1" base="$TMPDIR_T/$1"
rm -rf "$base"
mkdir -p "$base/skill/my-skill" "$base/agent/.apm/agents"
cat > "$base/agent/apm.yml" <<'EOF'
name: test-package
version: 0.1.0
type: skill
EOF
write_fixture "$kind" "$base/skill/my-skill/SKILL.md" my-skill
write_fixture "$kind" "$base/agent/.apm/agents/my-agent.agent.md" my-agent
}
# probe_all <label> <kind> <needle>... — runs all three scripts over the fixture
# and requires every one of them to exit non-zero AND report every needle. One
# assertion per script would let two of them drift apart while the suite stayed
# green; the whole point of the shared resolver block is that they cannot.
#
# A needle written `@skills:<text>` is asserted for the hook and skill-audit but
# NOT for agent-audit. There is exactly one such needle in this file — the body
# word ceiling — and the exemption is the ADR, not a workaround: ADR-0020 gives
# agents the description gates and deliberately NO body word gate, because an
# agent body becomes the system prompt of a fresh context rather than competing
# with the caller's live conversation. Demanding a body finding from agent-audit
# would be demanding the ADR be contradicted.
probe_all() {
local label="$1" kind="$2"
shift 2
local base="$TMPDIR_T/$kind"
local -a targets=(
"hook|$HOOK|$base/skill/my-skill/SKILL.md"
"skill-audit|$SKILL_VALIDATE|$base/skill/my-skill"
"agent-audit|$AGENT_VALIDATE|$base/agent/.apm/agents/my-agent.agent.md"
)
local problems=""
for target in "${targets[@]}"; do
local who="${target%%|*}" rest="${target#*|}"
local script="${rest%%|*}" arg="${rest#*|}"
local out status=0
set +e
out="$(bash "$script" "$arg" 2>&1)"
status=$?
set -e
if [[ $status -eq 0 ]]; then
problems="$problems [$who exited 0: ${out:-<no output>}]"
continue
fi
for needle in "$@"; do
if [[ "$needle" == @skills:* ]]; then
# Spelled as a full `if`, not `[[ ... ]] && continue`. Under `set -e` the
# short form's exit status is the test's when it is false, and relying on
# the &&-list exemption to keep that from aborting the run is a footgun
# one edit away from biting.
if [[ "$who" == agent-audit ]]; then
continue
fi
needle="${needle#@skills:}"
fi
if [[ "$out" != *"$needle"* ]]; then
problems="$problems [$who never said '$needle': $out]"
fi
done
done
if [[ -z "$problems" ]]; then
pass "$label"
else
fail "$label —$problems"
fi
}
# ---------------------------------------------------------------------------
# 1a. Tolerated frontmatter shapes — the gates must RUN, not merely not-pass
# ---------------------------------------------------------------------------
# The needles are the FINDINGS, not the exit code. A script that rejected the BOM
# outright would exit non-zero too, and would still be skipping every ADR-0020
# measurement — which is the defect, one error message later.
echo ""
echo "--- a BOM, leading blanks, trailing marker whitespace and CRLF are all tolerated, and the gates still fire ---"
for kind in plain bom leading-blanks trailing-ws crlf; do
build_subjects "$kind"
done
probe_all "control: a well-formed over-ceiling file fails on BOTH the description and the body" \
plain "description is $DESC_CHARS char" "@skills:body is $BODY_WORDS words"
probe_all "a UTF-8 BOM does not hide an over-ceiling description or body" \
bom "description is $DESC_CHARS char" "@skills:body is $BODY_WORDS words"
probe_all "leading blank lines before the opening --- do not hide the findings" \
leading-blanks "description is $DESC_CHARS char" "@skills:body is $BODY_WORDS words"
probe_all "trailing whitespace after either --- marker does not hide the findings" \
trailing-ws "description is $DESC_CHARS char" "@skills:body is $BODY_WORDS words"
# Note on what this last one can and cannot detect. read_text() opens the file in
# TEXT mode, so Python's universal-newline translation turns \r\n into \n before
# the frontmatter matcher ever sees it — verified by mutation: reverting
# FRONTMATTER_RE to the old `^---\n(.*?)\n---` breaks the leading-blanks and
# trailing-whitespace cases above but NOT this one. So this case pins the
# end-to-end behaviour (a CRLF file is measured, not skipped) rather than the
# `\r?\n` alternations in the regex, and it would catch a future switch to binary
# reads or to a newline='' open. Kept for that reason, and labelled so nobody
# reads it as covering more than it does.
probe_all "CRLF line endings do not hide the findings" \
crlf "description is $DESC_CHARS char" "@skills:body is $BODY_WORDS words"
# ---------------------------------------------------------------------------
# 1b. Unparseable frontmatter is a hard ERROR, never a quiet skip
# ---------------------------------------------------------------------------
echo ""
echo "--- genuinely unparseable frontmatter exits non-zero with a message, rather than passing quietly ---"
for kind in no-close yaml-list yaml-string yaml-none yaml-malformed; do
build_subjects "$kind"
done
probe_all "frontmatter with no closing --- is reported, not skipped" \
no-close "frontmatter"
probe_all "frontmatter that parses to a LIST is reported, not skipped" \
yaml-list "frontmatter"
probe_all "frontmatter that parses to a STRING is reported, not skipped" \
yaml-string "frontmatter"
probe_all "frontmatter that parses to None (empty block) is reported, not skipped" \
yaml-none "frontmatter"
probe_all "malformed YAML in the frontmatter is reported, not skipped" \
yaml-malformed "frontmatter"
# ---------------------------------------------------------------------------
# 2. A valueless description is a hard FAIL in all three scripts
# ---------------------------------------------------------------------------
# All five spellings mean the same thing to a YAML parser — an empty value — and
# all five have to be decided on the FOLDED value rather than on a line regex.
# `description:` followed by `model: sonnet` is the one that shipped: it made the
# value look present, skipped the "missing or empty" failure, and then
# early-returned out of every ADR-0020 gate on the genuinely empty folded value.
echo ""
echo "--- every spelling of a valueless description hard-FAILs in all three scripts ---"
for kind in desc-no-value desc-null desc-single-quoted-empty desc-double-quoted-empty desc-empty-fold; do
build_subjects "$kind"
done
probe_all "'description:' with no value (next key not captured as the value) FAILs" \
desc-no-value "description field is missing or empty"
probe_all "'description: null' FAILs" \
desc-null "description field is missing or empty"
probe_all "\"description: ''\" FAILs" \
desc-single-quoted-empty "description field is missing or empty"
probe_all "'description: \"\"' FAILs" \
desc-double-quoted-empty "description field is missing or empty"
probe_all "'description: >' with nothing folded under it FAILs" \
desc-empty-fold "description field is missing or empty"
# The specific regression, spelled out: the valueless-description agent file must
# not merely fail — it must not be SILENT. Zero output on a blocking gate is what
# made this un-diagnosable, so the output is asserted non-empty independently.
echo ""
echo "--- the valueless-description agent file produces output, not silence ---"
build_subjects desc-no-value
set +e
SILENT_OUT="$(bash "$AGENT_VALIDATE" "$TMPDIR_T/desc-no-value/agent/.apm/agents/my-agent.agent.md" 2>&1)"
SILENT_RC=$?
set -e
if [[ $SILENT_RC -ne 0 && -n "$SILENT_OUT" ]]; then
pass "agent-audit reports a valueless description rather than exiting 0 with zero output"
else
fail "agent-audit exited $SILENT_RC with output '${SILENT_OUT:-<empty>}' — the original defect was exit 0 and total silence on a blocking pre-push gate"
fi
echo ""
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]]