#!/usr/bin/env bats setup() { REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../../../../../../" && pwd)" load "$REPO_ROOT/tests/test_helper/bats-support/load" load "$REPO_ROOT/tests/test_helper/bats-assert/load" SCRIPT="$(cd "$BATS_TEST_DIRNAME/../scripts" && pwd)/validate-provenance.sh" TMPDIR="$(mktemp -d)" # Helper: create an APM package root at (apm.yml with a top-level # type: line — a real package manifest, not marketplace-only) plus a # single vendor-neutral agent file at /.apm/agents/.agent.md. make_package() { local root="$1" mkdir -p "$root/.apm/agents" cat > "$root/apm.yml" < "$root/.apm/agents/${name}.agent.md" < "$root/.apm/agents/${name}.agent.md" < "$root/sources.md" < "$dir/.apm/agents/my-agent.agent.md" < "$dir/apm.yml" < "$dir/.apm/agents/my-agent.agent.md" < "$dir/plugin.json" cat > "$dir/agents/my-agent.md" < "$dir/apm.yml" < "$dir/sub/.apm/agents/my-agent.agent.md" < "$dir/apm.yml" < "$fake_home/.apm/agents/my-agent.agent.md" < "$dir/apm.yml" < "$dir/sub/.apm/agents/my-agent.agent.md" < "$root/sources.md" <> "$root/sources.md" run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md" assert_success } # --------------------------------------------------------------------------- # Check 2: source_keys slug missing from sources.md → FAIL # --------------------------------------------------------------------------- @test "FAIL: source_keys slug in agent file not present as H2 in sources.md" { local root="$TMPDIR/package" make_package "$root" make_agent_with_source_keys "$root" "my-agent" "my-source" make_sources_md "$root" "different-source" "(none)" "(none)" run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md" assert_failure assert_output --partial "FAIL" } # --------------------------------------------------------------------------- # Check 3: Contributing file path doesn't exist → FAIL # --------------------------------------------------------------------------- @test "FAIL: Contributing file listed in sources.md does not exist" { 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" } @test "pass: (none) in Contributing files is skipped" { 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 } # --------------------------------------------------------------------------- # Check 5: Research doc field missing or placeholder → FAIL # --------------------------------------------------------------------------- @test "FAIL: Research doc field missing from sources.md entry" { local root="$TMPDIR/package" make_package "$root" make_agent_with_source_keys "$root" cat > "$root/sources.md" < "$root/sources.md" < "$root/.apm/agents/my-agent.agent.md" < "$root/sources.md" < "$root/sources.md" < "$root/sources.md" < "$root/sources.md" < "$root/sources.md" < "$root/.apm/agents/my-agent.txt" run bash "$SCRIPT" "$root/.apm/agents/my-agent.txt" [ "$status" -eq 2 ] assert_output --partial "Error: '$root/.apm/agents/my-agent.txt' matches neither a skill directory nor an agent file." } @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" # dirname and basename are deliberately ABSENT, and that absence is load- # bearing. The invariant: nothing external is needed to reach the python3 # preflight. The entry point resolves SCRIPT_DIR and classifies the target # with bash builtins (_kf_dirname, _kf_basename, _kf_parent_name) and the # mode libraries build their Python bodies with `read` heredocs, so a # genuinely empty PATH reaches the preflight too. Widening this list to keep # a test green would silently retire that guarantee: the script would die at # 127 naming `dirname` instead of the dependency it actually needs, the # failure tests/test-adr0020-contract.sh assertion 2 exists to prevent. cat # and sed stay so the stub matches the skill suite's and is a DENY OF python3 # ALONE rather than a test of "no PATH at all"; bash is not among them # because `env -i` below invokes it by absolute path. local cmd for cmd in cat sed; do ln -s "$(command -v "$cmd")" "$emptybin/$cmd" done 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" < "$fake_home/.apm/agents/my-agent.agent.md" < "$root/sources.md" < "$root/sources.md" < "$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" < "$root/sources.md" < "$root/sources.md" < "$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 ] } # --------------------------------------------------------------------------- # Auto-detection — the merged provenance entry point classifies its own target # # NEW with the factory-audit merge, and new behaviour rather than a ported # case: scripts/validate-provenance.sh is now ONE entry point for both artifact # types and works out from the target which rubric to run. This file is the # AGENT half of that contract; validate-provenance-skill.bats holds the skill # half and the neither-shape rejection. Same script in all four suites. # # ADR-0025 states the rule: "A directory containing SKILL.md takes the skill # flow; an .agent.md file or a file under agents/ takes the agent flow." That is # TWO independent rules on the agent side, and each is pinned on its own below, # because either one alone would make the other look like it worked: # # 1. the filename ends in .agent.md, wherever it sits # 2. the file sits under a directory named agents/, whatever it is called # (.apm/agents/, .claude/agents/, .github/agents/ — the same shape the # exported Vale hook matches with `(^|/)agents/[^/]+\.md$`) # # The skill rubric rejects a FILE outright ("not a directory"), so a # misclassified agent file does not produce a wrong finding — it produces a # usage error about a file that was perfectly well-formed, which is why these # assert on a real finding rather than merely on a non-crash. # --------------------------------------------------------------------------- @test "auto-detect: a *.agent.md file is checked in AGENT provenance mode wherever it sits" { # Rule 1 in isolation. The directory is deliberately named `defs/`, not # `agents/`, so rule 2 cannot reach this fixture and the extension is the # only thing that can classify it. local root="$TMPDIR/package" make_package "$root" mkdir -p "$root/defs" cat > "$root/defs/my-agent.agent.md" < "$root/.apm/agents/my-agent.md" < "$root/.apm/agents/my-agent.md" < "$decoy/scripts/$lib" done cd "$(dirname "$SCRIPT")/.." run env CDPATH="$decoy" bash scripts/validate-provenance.sh "$root/.apm/agents/my-agent.agent.md" [ "$status" -eq 0 ] assert_output "" }