#!/usr/bin/env bats # tests/kb-lint.bats — black-box tests for scripts/kb-lint.sh # # Contract: exit 0 when a knowledge bundle is OKF v0.1 conformant; # one line per violation otherwise. Observable surface: stdout lines # and exit status only. setup() { LINT="$BATS_TEST_DIRNAME/../scripts/kb-lint.sh" KB="$(mktemp -d)/kb" mkdir -p "$KB/sub" cat > "$KB/index.md" <<'EOF' # Index * [Good](good.md) — fine. EOF cat > "$KB/good.md" <<'EOF' --- type: reference title: Good --- Body. EOF } teardown() { rm -rf "$(dirname "$KB")"; } @test "conformant bundle passes" { run bash "$LINT" "$KB" [ "$status" -eq 0 ] } @test "missing frontmatter is reported" { printf 'no frontmatter here\n' > "$KB/bad.md" run bash "$LINT" "$KB" [ "$status" -ne 0 ] [[ "$output" == *"bad.md: missing frontmatter"* ]] } @test "frontmatter without type is reported" { printf -- '---\ntitle: x\n---\nbody\n' > "$KB/notype.md" run bash "$LINT" "$KB" [ "$status" -ne 0 ] [[ "$output" == *"notype.md"*"type"* ]] } @test "unclosed frontmatter is reported, not silently passed" { # regression: a `---` in the body (fenced code, rule) must not be # accepted as the closing delimiter of the frontmatter block cat > "$KB/unclosed.md" <<'EOF' --- type: reference title: Unclosed # Heading after a forgotten closing fence ```markdown --- name: example --- ``` EOF run bash "$LINT" "$KB" [ "$status" -ne 0 ] [[ "$output" == *"unclosed.md"* ]] } @test "frontmatter in index.md is reported" { printf -- '---\ntype: x\n---\n# I\n' > "$KB/sub/index.md" run bash "$LINT" "$KB" [ "$status" -ne 0 ] [[ "$output" == *"index.md must not have frontmatter"* ]] } @test "broken relative link is reported" { printf -- '---\ntype: reference\n---\nsee [x](gone.md)\n' > "$KB/links.md" run bash "$LINT" "$KB" [ "$status" -ne 0 ] [[ "$output" == *"links.md: broken relative link"* ]] } @test "broken wikilink is reported" { printf -- '---\ntype: reference\n---\nsee [[nowhere]]\n' > "$KB/wiki.md" run bash "$LINT" "$KB" [ "$status" -ne 0 ] [[ "$output" == *"wiki.md: broken wikilink"* ]] } @test "missing kb directory exits 2" { run bash "$LINT" "$KB/no-such-dir" [ "$status" -eq 2 ] } @test "a second work log in a subdirectory is reported" { # One bundle, one work log. Per-directory logs are how a bundle ends up with fifty # of them: the entry is never where the next agent looks, and each copy re-dates the # same event on its own. printf -- '---\ntype: changelog\n---\n## 2026-01-01\n- created a log\n' > "$KB/log.md" printf -- '---\ntype: changelog\n---\n## 2026-01-02\n- also a log\n' > "$KB/sub/log.md" run bash "$LINT" "$KB" [ "$status" -ne 0 ] [[ "$output" == *"one work log"*"at the bundle root"* ]] [[ "$output" == *"sub/log.md"* ]] } @test "a work log that is not at the bundle root is reported" { printf -- '---\ntype: changelog\n---\n## 2026-01-02\n- misplaced log\n' > "$KB/sub/log.md" run bash "$LINT" "$KB" [ "$status" -ne 0 ] [[ "$output" == *"one work log"*"at the bundle root"* ]] } @test "the quarantine trash directory is not part of the bundle" { # Deleted pages wait in .AGENTTRASH/ for a pruning session with Frank. Holding them # to frontmatter and one-log rules would push an agent to delete history to go green. mkdir -p "$KB/.AGENTTRASH" printf 'no frontmatter, and a second log besides\n' > "$KB/.AGENTTRASH/log.md" run bash "$LINT" "$KB" [ "$status" -eq 0 ] [[ "$output" == *"0 violation(s)"* ]] } @test "a wikilink must not resolve into the quarantine" { # Obsidian resolves [[name]] by filename anywhere under the vault, and the resolver copied # that with a raw rglob — so [[log]] stayed "valid" by landing on the copy waiting in # .AGENTTRASH/. That is #57's failure wearing a green check: the page is out of circulation # until a pruning session with Frank, and it is not even tracked, so on another machine the # link points at nothing while the gate here says it is fine. mkdir -p "$KB/.AGENTTRASH" printf 'no frontmatter, quarantined\n' > "$KB/.AGENTTRASH/log.md" printf -- '---\ntype: reference\n---\nSee [[log]] for the history.\n' > "$KB/good.md" run bash "$LINT" "$KB" [ "$status" -ne 0 ] [[ "$output" == *"broken wikilink -> [[log]]"* ]] } @test "a markdown link into the quarantine is broken" { # #57's failure mode wearing markdown instead of brackets: the relative-link check was a # bare is_file(), so a link that lands on a page waiting in .AGENTTRASH/ passes this gate # and points at nothing on every machine that pulls — the quarantine is gitignored. Both # spellings count: relative, and bundle-absolute with a leading slash. mkdir -p "$KB/.AGENTTRASH" printf 'no frontmatter, quarantined\n' > "$KB/.AGENTTRASH/gone.md" printf -- '---\ntype: reference\n---\nRelative: [gone](.AGENTTRASH/gone.md)\n' > "$KB/rel.md" printf -- '---\ntype: reference\n---\nAbsolute: [gone](/.AGENTTRASH/gone.md)\n' > "$KB/abs.md" run bash "$LINT" "$KB" [ "$status" -ne 0 ] [[ "$output" == *"rel.md: broken relative link -> .AGENTTRASH/gone.md"* ]] [[ "$output" == *"abs.md: broken relative link -> /.AGENTTRASH/gone.md"* ]] } @test "a wikilink to a live page still resolves, in any directory" { # The narrowing above must cost nothing: filename-anywhere semantics is the point. printf -- '---\ntype: reference\n---\nSee [[good]].\n' > "$KB/sub/other.md" run bash "$LINT" "$KB" [ "$status" -eq 0 ] [[ "$output" == *"0 violation(s)"* ]] } @test "the shipped knowledge base itself is conformant" { # The tests above exercise the linter against fixtures; nothing else in the # suite ever points it at the real bundle, so a page added without frontmatter # or an index that stops listing a file passes CI silently. This is the gate. run bash "$BATS_TEST_DIRNAME/../scripts/kb-lint.sh" "$BATS_TEST_DIRNAME/../omp-docs" [ "$status" -eq 0 ] [[ "$output" == *"0 violation(s)"* ]] } @test "every page in the shipped knowledge base is listed by an index" { run python3 "$BATS_TEST_DIRNAME/../scripts/kb-coverage.py" "$BATS_TEST_DIRNAME/../omp-docs" [ "$status" -eq 0 ] [[ "$output" == *"0 issue(s)"* ]] }