refactor(skills): retrofit the corpus to the ADR-0020 context contract (#129)
Retrofits all 39 skills to ADR-0020's description/body context contract, then fixes what six rounds of independent review found in that retrofit — including four ways the hot gate itself failed open. Closes #99, #107, #108, #110, #111, #114, #115, #120. ## The retrofit (waves 1-5) | | Start | Now | |---|---|---| | Description FAILs (>400 chars) | 26 | **0** | | Body FAILs (>900 words, body-only) | 9 | **0** | | Dangling routing targets | 2 | **0** | | `Kyberforge.CompositionNote` | 10 | **0** | | Preload tax | 21,005 chars | **~10,500** | Under the 12,000-char success criterion. Per-wave detail is on #99. ## The review fixes **The gate failed open four ways, three of them found after the retrofit shipped.** An unrecognised follower token made a dangling target vanish. A skill directory with no `SKILL.md` resolved as a valid target, so a commit could be green locally and red in a fresh clone — three existing fixtures were relying on that, one of which made the install-leak A/B pass vacuously. Then the free-standing `/name` sweep turned out to be gated on the sentence carrying a boundary marker, so route notation in any other sentence was invisible — not an ERROR, not a SUGGESTION, not an INFO — which left the documented "`/name` always blocks" promise false from a second direction. All four fixed and pinned. **Two checks were silently not running.** `validate-provenance.sh` checks 7-8 were dead across nine skills. Waking them exposed a deeper problem: they assume `Research doc:` names a source index, but 30 of 121 entries point at topic content documents, so every new check-7 INFO was a false positive and check 8 was saved from a false-FAIL flood only by an *unannounced* skip. Checks 7/8 are now scoped to source indexes and every skip announces itself (#121). **The retrofit's own anti-goal, four times.** ADR-0020 warns that a blunt gate gets satisfied by deleting content rather than relocating it. `diagnose` and `skill-audit` relocated prose and then read it unconditionally; `prototype` and `vale-config` deleted rules outright that survived nowhere. All four addressed. ## Verification - `bash tests/run-tests.sh --strict` — 24 suites, 0 skipped, 0 failed - `bash tests/run-bats.sh` — 325 tests, 0 failures - `pre-commit run --all-files` — 17/17 - `pre-commit run --hook-stage pre-push --all-files` — 16/16, with `apm marketplace check` and `apm pack --check-clean` run against the remote, not skipped - `scripts/skill-size-check.sh` over all 39 skills — rc 0, 0 ERROR/FAIL, SUGGESTION-only - Preload tax measured at **10,498 chars**, max description 390 — both inside budget - Every new test proven non-vacuous by a deliberate mutation of the behaviour it covers **Per-commit sync, stated accurately:** the ten commits from the latest review round each pass `check-plugin-content-sync` in isolation, verified by checking each out in a detached worktree with a clean between. The earlier gitea window (`dfacf05..bedbd1d`, nine commits) does **not** — its mirror was regenerated in one batch at `bbc7300`. An earlier revision of this description claimed the property held for every commit; it does not, and a bisect through that window lands on a red commit. **Squash-merge** to collapse it, or accept that this range is not bisectable. ## Version bump Six plugins and the catalog take a **patch**, not a minor. The branch is **89 commits — 40 `fix` / 30 `refactor` / 12 `docs` / 5 `chore` / 2 `test` — zero `feat`, zero `!`, zero `BREAKING CHANGE`** — and adds no skill, agent, command or hook. (Two earlier revisions of this section cited a stale histogram, most recently 78 commits; the figures above are measured at HEAD.) Both rules this repo ships (`forge/references/version-bump.md`, landing in this PR, and `git-commits/references/conventional-commits-spec.md`) make that a patch, and the catalog set is unchanged at 7 entries. Not settled by that: four published files were removed from the installed tree, three moved, and `caveman` gained `disable-model-invocation`, retiring its old triggers. Under a strict reading those are major-class and currently ship under `refactor:` with no marker. Whether the deployed skill surface is a public contract is written down nowhere — worth deciding, but it outlives this PR. ## Deliberately not in scope #112 (cherry-pick ownership, now resolved in favour of `git-commits`), #113 (`rtk git` normalisation), #116 (research fan-out), #101 (audit-skill merge), #122 (non-spec skill-root files), #123 (no PRD producer) stay open. #117 is the one worth reading: the contract's remedy is to move prose into `references/`, which is exactly where neither the size gate nor Vale looks — and the blind spot is wider than #117 currently records, since there is no root `.vale.ini` at all, so every ADR, `CONTEXT.md` and `README.md` is unlinted too. That blind spot let this branch carry two `level: error` `Kyberforge.SentenceOpenerThereIs` violations into `references/` files it created — `provider-adapter-author/references/provider-matrix.md:31` and `agent-audit/references/finding-criteria.md:95`. Both are reworded in `afadaae`, confirmed by routing each file through the audit's own `vale-wrap.sh` (1 error each before, 0 after). Five further occurrences sit in `references/` files already on `main`; those are the pre-existing corpus and stay with #117, which is the real fix. Also unfixed and not this PR's: `apm install` appends a duplicate `SessionStart` entry to `.claude/settings.json`, so a fresh clone cannot get pre-push green without an edit AGENTS.md warns against. Reproduces identically on `main`. Co-authored-by: Defame1297 <gitea@rkdr.net> Reviewed-on: https://git.dev.rkdr.net/Defame1297/holocron/pulls/129 Co-authored-by: Claude Code AI - Gitea MCP <claude@noreply.git.dev.rkdr.net> Co-committed-by: Claude Code AI - Gitea MCP <claude@noreply.git.dev.rkdr.net>
This commit was merged in pull request #129.
This commit is contained in:
@@ -433,3 +433,477 @@ EOF
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_success
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Checks 3 and 4: None ("could not parse") is NOT [] ("explicitly (none)")
|
||||
#
|
||||
# parse_contributing_files returns three distinguishable answers and checks 3
|
||||
# and 4 have to honour all three. `[]` is the author writing "(none)" — the
|
||||
# skip is correct and silent. None is a Contributing files block the parser
|
||||
# cannot read, and skipping THAT silently disables both checks on the one entry
|
||||
# least likely to be right, which is the failure mode the parser's own
|
||||
# docstring warns about. The assertions below are therefore about the INFO
|
||||
# appearing; a silent exit 0 is exactly the bug.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "INFO: an unparsable Contributing files block names the slug instead of skipping checks 3 and 4 silently" {
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
make_agent_with_source_keys "$root"
|
||||
cat > "$root/sources.md" <<EOF
|
||||
# Sources
|
||||
|
||||
## my-source
|
||||
|
||||
- **URL:** https://example.com/my-source
|
||||
- **Description:** A test source.
|
||||
- **Research doc:** (none)
|
||||
**Contributing files:**
|
||||
* .apm/agents/ghost.agent.md (asterisk bullets are not the bullet form)
|
||||
- **Status:** \`extracted\`
|
||||
EOF
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_success
|
||||
assert_output --partial "INFO"
|
||||
assert_output --partial "Contributing-file checks skipped for 'my-source' — the Contributing files block could not be parsed"
|
||||
}
|
||||
|
||||
@test "INFO: an entry with no Contributing files field at all is reported, not skipped silently" {
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
make_agent_with_source_keys "$root"
|
||||
cat > "$root/sources.md" <<EOF
|
||||
# Sources
|
||||
|
||||
## my-source
|
||||
|
||||
- **URL:** https://example.com/my-source
|
||||
- **Description:** A test source.
|
||||
- **Research doc:** (none)
|
||||
- **Status:** \`extracted\`
|
||||
EOF
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_success
|
||||
assert_output --partial "INFO"
|
||||
assert_output --partial "Contributing-file checks skipped for 'my-source' — the Contributing files block could not be parsed"
|
||||
}
|
||||
|
||||
@test "checks 3 and 4 skipped silently: an explicit '(none)' emits no INFO" {
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
make_agent_with_source_keys "$root"
|
||||
make_sources_md "$root" "my-source" "(none — not used directly)"
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_success
|
||||
assert_output ""
|
||||
}
|
||||
|
||||
@test "checks 3 and 4 still run: a parseable Contributing files list is not diverted to the INFO" {
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
make_agent_with_source_keys "$root"
|
||||
make_sources_md "$root" "my-source" ".apm/agents/nonexistent.agent.md"
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_failure
|
||||
assert_output --partial "FAIL"
|
||||
assert_output --partial "Contributing file '.apm/agents/nonexistent.agent.md' does not exist"
|
||||
refute_output --partial "could not be parsed"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The INFO tier itself: kind-aware printing and a kind-aware exit code
|
||||
#
|
||||
# INFO is new here — before it, findings was a 5-tuple and print_findings
|
||||
# stamped every entry FAIL. The two cases below pin the tier rather than any
|
||||
# one check: an INFO must print under the INFO prefix and leave the exit code
|
||||
# at 0, and a real FAIL must keep printing under the FAIL prefix and still exit
|
||||
# non-zero even when an INFO is sitting in the same findings list.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "INFO tier: an INFO alone prints as INFO with a Note and does not set a failing exit code" {
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
make_agent_with_source_keys "$root"
|
||||
cat > "$root/sources.md" <<EOF
|
||||
# Sources
|
||||
|
||||
## my-source
|
||||
|
||||
- **URL:** https://example.com/my-source
|
||||
- **Description:** A test source.
|
||||
- **Research doc:** (none)
|
||||
- **Status:** \`extracted\`
|
||||
EOF
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_success
|
||||
assert_output --partial "INFO Contributing-file checks skipped for 'my-source'"
|
||||
assert_output --partial "Note:"
|
||||
refute_output --partial "FAIL"
|
||||
}
|
||||
|
||||
@test "FAIL tier: a genuine FAIL alongside an INFO still prints as FAIL and exits non-zero" {
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
make_agent_with_source_keys "$root"
|
||||
cat > "$root/sources.md" <<EOF
|
||||
# Sources
|
||||
|
||||
## my-source
|
||||
|
||||
- **URL:** https://example.com/my-source
|
||||
- **Description:** A test source.
|
||||
- **Contributing files:** .apm/agents/nonexistent.agent.md
|
||||
- **Research doc:** (none)
|
||||
- **Status:** \`extracted\`
|
||||
|
||||
## ghost-source
|
||||
|
||||
- **URL:** https://example.com/ghost-source
|
||||
- **Description:** Another test source.
|
||||
- **Research doc:** (none)
|
||||
- **Status:** \`extracted\`
|
||||
EOF
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_failure
|
||||
assert_output --partial "FAIL Contributing file '.apm/agents/nonexistent.agent.md' does not exist"
|
||||
assert_output --partial "Why:"
|
||||
assert_output --partial "INFO Contributing-file checks skipped for 'ghost-source'"
|
||||
assert_output --partial "Note:"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The exit-2 tier, and its boundary with the silent exit 0
|
||||
#
|
||||
# Ported from the skill-audit sibling, which had already split usage and
|
||||
# environment errors (exit 2) away from findings (exit 1). SKILL.md tells the
|
||||
# auditor to surface a non-zero exit, so a usage error leaving exit 1 with
|
||||
# nothing on stdout was indistinguishable from a clean-but-failing run.
|
||||
#
|
||||
# The reconciliation this script needs and the sibling does not: "the walk-up
|
||||
# found no type:-bearing apm.yml" is NOT bad input. It is a verdict about a
|
||||
# real, readable agent file — user or project scope, where plugin-scope
|
||||
# provenance does not apply — and scripts/check-scope-walkup-sync.sh fixture 6
|
||||
# pins it as exit 0 with empty output. Every exit-2 gate is therefore decided
|
||||
# from the ARGUMENT ALONE, before the walk-up runs, so the two can never
|
||||
# collide. The two tests at the end of this block assert both halves.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "exit 2: no arguments is a usage error, not a finding" {
|
||||
run bash "$SCRIPT"
|
||||
[ "$status" -eq 2 ]
|
||||
assert_output --partial "agent-file is required"
|
||||
}
|
||||
|
||||
@test "exit 2: a second positional argument is rejected instead of silently dropped" {
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
make_clean_agent "$root"
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md" --some-typo
|
||||
[ "$status" -eq 2 ]
|
||||
assert_output --partial "expected exactly one argument"
|
||||
}
|
||||
|
||||
@test "exit 2: a nonexistent path is an error, not a silent pass" {
|
||||
run bash "$SCRIPT" "$TMPDIR/no-such-agent.agent.md"
|
||||
[ "$status" -eq 2 ]
|
||||
assert_output --partial "no such file"
|
||||
}
|
||||
|
||||
@test "exit 2: a directory is not an agent file" {
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
run bash "$SCRIPT" "$root/.apm/agents"
|
||||
[ "$status" -eq 2 ]
|
||||
assert_output --partial "not a regular file"
|
||||
}
|
||||
|
||||
@test "exit 2: an unrecognized extension is rejected before the walk-up runs" {
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
echo "not an agent" > "$root/.apm/agents/my-agent.txt"
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.txt"
|
||||
[ "$status" -eq 2 ]
|
||||
assert_output --partial "unrecognized extension"
|
||||
}
|
||||
|
||||
@test "exit 2: a PATH with no python3 names the missing dependency instead of exiting 127" {
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
make_clean_agent "$root"
|
||||
local emptybin="$TMPDIR/emptybin"
|
||||
mkdir -p "$emptybin"
|
||||
local bash_bin
|
||||
bash_bin="$(command -v bash)"
|
||||
run env -i PATH="$emptybin" HOME="$HOME" "$bash_bin" "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
[ "$status" -eq 2 ]
|
||||
# The needle is the DIAGNOSTIC, not the bare word: with no preflight, bash's
|
||||
# own "python3: command not found" would satisfy a bare-word match.
|
||||
assert_output --partial "python3 is required"
|
||||
}
|
||||
|
||||
@test "reconciliation: a REAL agent file at non-plugin scope still exits 0 silently, never 2" {
|
||||
# scripts/check-scope-walkup-sync.sh fixture 6 in miniature. The exit-2 tier
|
||||
# must not widen to cover "find_plugin_root returned None": the file exists,
|
||||
# is readable and is correctly named — it is simply user/project scope.
|
||||
local dir="$TMPDIR/anc"
|
||||
mkdir -p "$dir"
|
||||
cat > "$dir/apm.yml" <<EOF
|
||||
name: outer-package
|
||||
version: 0.1.0
|
||||
type: skill
|
||||
EOF
|
||||
local fake_home="$dir/fakehome"
|
||||
mkdir -p "$fake_home/.apm/agents"
|
||||
cat > "$fake_home/.apm/agents/my-agent.agent.md" <<EOF
|
||||
---
|
||||
name: my-agent
|
||||
description: A valid agent description.
|
||||
source_keys:
|
||||
- my-source
|
||||
---
|
||||
|
||||
You are a test agent.
|
||||
EOF
|
||||
run env HOME="$fake_home" bash "$SCRIPT" "$fake_home/.apm/agents/my-agent.agent.md"
|
||||
[ "$status" -eq 0 ]
|
||||
assert_output ""
|
||||
}
|
||||
|
||||
@test "reconciliation: a nonexistent path inside a non-plugin-scope tree exits 2, not the old silent 0" {
|
||||
# The other half. Before the exit-2 tier, a typo'd path anywhere outside a
|
||||
# package took the not-plugin-scope exit and reported a silent pass, so the
|
||||
# typo and a clean agent produced identical output and identical status.
|
||||
local fake_home="$TMPDIR/plainhome"
|
||||
mkdir -p "$fake_home/.apm/agents"
|
||||
run env HOME="$fake_home" bash "$SCRIPT" "$fake_home/.apm/agents/typo.agent.md"
|
||||
[ "$status" -eq 2 ]
|
||||
assert_output --partial "no such file"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PLACEHOLDER_RE: the trailing character class was CONSUMING
|
||||
#
|
||||
# `(?<!\`)FILL IN:[^\`\n]` required a character after the colon, so a `FILL IN:`
|
||||
# at end of line matched nothing and escaped checks 1 and 5 entirely — and
|
||||
# `- **Description:** FILL IN:` is the most likely spelling of a half-written
|
||||
# entry. The lookahead states the same exclusion without eating a character.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "FAIL: a FILL IN: placeholder at end of line is caught, not skipped" {
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
make_agent_with_source_keys "$root"
|
||||
cat > "$root/sources.md" <<EOF
|
||||
# Sources
|
||||
|
||||
## my-source
|
||||
|
||||
- **URL:** https://example.com/my-source
|
||||
- **Description:** FILL IN:
|
||||
- **Contributing files:** .apm/agents/my-agent.agent.md
|
||||
- **Research doc:** (none)
|
||||
- **Status:** \`extracted\`
|
||||
EOF
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_failure
|
||||
assert_output --partial "Unfilled FILL IN: placeholder"
|
||||
}
|
||||
|
||||
@test "FAIL: a Research doc value that is a bare end-of-line FILL IN: is caught" {
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
make_agent_with_source_keys "$root"
|
||||
cat > "$root/sources.md" <<EOF
|
||||
# Sources
|
||||
|
||||
## my-source
|
||||
|
||||
- **URL:** https://example.com/my-source
|
||||
- **Description:** A test source.
|
||||
- **Contributing files:** .apm/agents/my-agent.agent.md
|
||||
- **Research doc:** FILL IN:
|
||||
- **Status:** \`extracted\`
|
||||
EOF
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_failure
|
||||
assert_output --partial "Research doc field is empty or placeholder"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Encoding, read side: read_text() pins UTF-8 and strips a BOM
|
||||
#
|
||||
# The old code used bare open() calls inheriting locale.getpreferredencoding(),
|
||||
# which is ASCII under LC_ALL=C, and wrapped exactly one of them in
|
||||
# `except Exception: return []` — so an unreadable agent file was reported as
|
||||
# having no source_keys and therefore as CLEAN. The other call sites had no
|
||||
# handler at all and died with a traceback.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "FAIL: an undecodable agent file is reported, not swallowed into a clean pass" {
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
printf '\xff\xfe---\nname: my-agent\n---\n' > "$root/.apm/agents/my-agent.agent.md"
|
||||
make_sources_md "$root" "my-source" "(none)"
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_failure
|
||||
assert_output --partial "not valid UTF-8"
|
||||
refute_output --partial "Traceback"
|
||||
}
|
||||
|
||||
@test "FAIL: an undecodable contributing file is reported, not a traceback" {
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
make_agent_with_source_keys "$root"
|
||||
printf '\xff\xfe---\nname: other\n---\n' > "$root/.apm/agents/other.agent.md"
|
||||
make_sources_md "$root" "my-source" ".apm/agents/other.agent.md"
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_failure
|
||||
assert_output --partial "not valid UTF-8"
|
||||
refute_output --partial "Traceback"
|
||||
}
|
||||
|
||||
@test "exit 2: an undecodable apm.yml names the file instead of dying mid walk-up" {
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
make_clean_agent "$root"
|
||||
printf 'name: t\nversion: 0.1.0\ntype: skill\n# \xff\xfe\n' > "$root/apm.yml"
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
[ "$status" -eq 2 ]
|
||||
assert_output --partial "not valid UTF-8"
|
||||
refute_output --partial "Traceback"
|
||||
}
|
||||
|
||||
@test "a BOM-prefixed agent file still has its source_keys read (check 2 runs)" {
|
||||
# A leading BOM defeats parse_frontmatter()'s ^--- anchor, so no frontmatter
|
||||
# parsed means no source_keys parsed means nothing to validate — check 2
|
||||
# went silently missing on exactly the file it was pointed at.
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
printf '\xef\xbb\xbf---\nname: my-agent\ndescription: A valid agent description.\nsource_keys:\n - ghost-source\n---\n\nYou are a test agent.\n' \
|
||||
> "$root/.apm/agents/my-agent.agent.md"
|
||||
make_sources_md "$root" "my-source" "(none)"
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_failure
|
||||
assert_output --partial "source_keys slug 'ghost-source' not found in sources.md"
|
||||
}
|
||||
|
||||
@test "under LC_ALL=C a sources.md carrying an em dash is read, not a UnicodeDecodeError" {
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
make_agent_with_source_keys "$root"
|
||||
cat > "$root/sources.md" <<EOF
|
||||
# Sources
|
||||
|
||||
## my-source
|
||||
|
||||
- **URL:** https://example.com/my-source
|
||||
- **Description:** A test source — with an em dash.
|
||||
- **Contributing files:** .apm/agents/ghost.agent.md
|
||||
- **Research doc:** (none)
|
||||
- **Status:** \`extracted\`
|
||||
EOF
|
||||
run env LC_ALL=C PYTHONUTF8=0 bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_failure
|
||||
assert_output --partial "Contributing file '.apm/agents/ghost.agent.md' does not exist"
|
||||
refute_output --partial "Traceback"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Encoding, write side: sys.stdout/stderr.reconfigure(encoding='utf-8')
|
||||
#
|
||||
# Pinning only the reads moved the crash from the read to the WRITE. Every
|
||||
# finding this script prints contains an em dash, so under LC_ALL=C
|
||||
# print_findings() died with UnicodeEncodeError after every check had already
|
||||
# run — losing the whole report at the last step.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "under LC_ALL=C the findings report is printed, not lost to a UnicodeEncodeError" {
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
make_agent_with_source_keys "$root"
|
||||
make_sources_md "$root" "my-source" ".apm/agents/ghost.agent.md"
|
||||
run env LC_ALL=C PYTHONUTF8=0 bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_failure
|
||||
assert_output --partial "FAIL Contributing file '.apm/agents/ghost.agent.md' does not exist"
|
||||
assert_output --partial "Why:"
|
||||
refute_output --partial "UnicodeEncodeError"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Half-validated entries announced instead of passing silently
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "INFO: a duplicated '## slug' says only the first block was checked" {
|
||||
# Every per-slug parser locates its block with pattern.search(), so a slug
|
||||
# written twice resolves to the FIRST block every time: the second block's
|
||||
# fields are never validated, and the entry looked fully checked.
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
make_agent_with_source_keys "$root"
|
||||
cat > "$root/sources.md" <<EOF
|
||||
# Sources
|
||||
|
||||
## my-source
|
||||
|
||||
- **URL:** https://example.com/my-source
|
||||
- **Description:** A test source.
|
||||
- **Contributing files:** (none)
|
||||
- **Research doc:** (none)
|
||||
- **Status:** \`extracted\`
|
||||
|
||||
## my-source
|
||||
|
||||
- **URL:** https://example.com/dup
|
||||
- **Description:** A duplicate entry.
|
||||
- **Contributing files:** .apm/agents/ghost.agent.md
|
||||
- **Research doc:** (none)
|
||||
- **Status:** \`extracted\`
|
||||
EOF
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_success
|
||||
assert_output --partial "Duplicate '## my-source' entry in sources.md"
|
||||
# The second block's ghost contributing file is genuinely never checked —
|
||||
# the INFO is what makes that visible rather than a silent half-pass.
|
||||
refute_output --partial "does not exist"
|
||||
}
|
||||
|
||||
@test "INFO: a second '- **Research doc:**' line in one entry is announced, not ignored" {
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
make_agent_with_source_keys "$root"
|
||||
cat > "$root/sources.md" <<EOF
|
||||
# Sources
|
||||
|
||||
## my-source
|
||||
|
||||
- **URL:** https://example.com/my-source
|
||||
- **Description:** A test source.
|
||||
- **Contributing files:** (none)
|
||||
- **Research doc:** (none)
|
||||
- **Research doc:** docs/research/added-later.md
|
||||
- **Status:** \`extracted\`
|
||||
EOF
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_success
|
||||
assert_output --partial "Multiple '- **Research doc:**' lines for 'my-source'"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Finding dedup
|
||||
#
|
||||
# The agent file is read once for its own source_keys and again as a
|
||||
# contributing file, so an unreadable one produced the identical finding twice.
|
||||
# Distinct findings about the same file still both appear.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "the same unreadable file reached by two checks is reported once, not twice" {
|
||||
local root="$TMPDIR/package"
|
||||
make_package "$root"
|
||||
printf '\xff\xfe---\nname: my-agent\n---\n' > "$root/.apm/agents/my-agent.agent.md"
|
||||
make_sources_md "$root" "my-source" ".apm/agents/my-agent.agent.md"
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_failure
|
||||
local count
|
||||
count="$(printf '%s\n' "$output" | grep -c "^FAIL File is not valid UTF-8" || true)"
|
||||
[ "$count" -eq 1 ]
|
||||
}
|
||||
|
||||
@@ -943,3 +943,23 @@ EOF
|
||||
refute_output --partial "Traceback"
|
||||
refute_output --partial "FileNotFoundError"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Encoding, write side: sys.stdout/stderr.reconfigure(encoding='utf-8')
|
||||
#
|
||||
# read_text() in the shared resolver block pins the READS to UTF-8. That moved
|
||||
# the LC_ALL=C crash to the WRITE: this script's own message text carries em
|
||||
# dashes (the ADR-0020 boundary SUGGESTION is one), so the streams' ASCII
|
||||
# default raised UnicodeEncodeError while PRINTING — after every check had
|
||||
# already run. Here it also flipped a clean exit 0 into a traceback and exit 1.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@test "under LC_ALL=C the report is printed, not lost to a UnicodeEncodeError" {
|
||||
local root="$TMPDIR/locale-pkg"
|
||||
make_apm_agent "$root" "locale-agent"
|
||||
run env LC_ALL=C PYTHONUTF8=0 bash "$SCRIPT" "$root/.apm/agents/locale-agent.agent.md"
|
||||
assert_success
|
||||
assert_output --partial "description has no boundary clause"
|
||||
refute_output --partial "UnicodeEncodeError"
|
||||
refute_output --partial "Traceback"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user