#!/usr/bin/env bats # Seam: documentation completeness. # Every service defined in a compose.yaml must have a matching documentation # page in serverAppConfigs-docs/ and must be reachable from the index. This is the "docs first" # contract of this repo: no service ships without documentation. # The vault's second law, added 2026-10-18: ONE page per live LLM endpoint. # A payload swap is an Eras row on that endpoint's page, never a page of its # own -- a second file holding the same shape is the next thing to go stale. load helpers @test "every compose service has a docs page" { while IFS= read -r f; do while IFS= read -r svc; do # service name is the docs slug: RomM -> serverAppConfigs-docs/Dankserver/RomM.md # matched case-insensitively (vault uses Title Case) local doc doc=$(find "$REPO_ROOT/serverAppConfigs-docs" -iname "${svc}.md" -not -path '*/.AGENTTRASH/*' 2>/dev/null | head -1) if [ -z "$doc" ]; then echo "service '$svc' (from $f) has no docs page: expected serverAppConfigs-docs/**/${svc}.md" return 1 fi done < <(services_in "$f") done < <(find "$REPO_ROOT" -name 'compose.yaml' -not -path '*/.git/*' | sort) } @test "docs index links every docs page" { local index="$REPO_ROOT/serverAppConfigs-docs/index.md" [ -f "$index" ] || { echo "missing $index"; return 1; } while IFS= read -r page; do local base base=$(basename "$page" .md) grep -qF "[[$base]]" "$index" || { echo "docs page $base is not linked from $index" return 1 } # `.AGENTTRASH` is deleted-but-held state per the repo's deletion policy, not # vault content: trashed pages need no index row, and a trashed page must not # answer as the docs page for a live compose service. done < <(find "$REPO_ROOT/serverAppConfigs-docs" -name '*.md' -not -name 'index.md' -not -path '*/.obsidian/*' -not -path '*/.AGENTTRASH/*' | sort) } @test "no unresolved links in the docs vault" { # A wikilink to a page that does not exist is a promise the repo cannot # keep. Fixing the three that were dangling (2026-09-16) also meant # retiring a reference to dankclaw/compose.yaml, which no longer exists. # The checker models Obsidian's resolution rules; see its docstring for # the three false-positive classes a naive version produces. run "$REPO_ROOT/scripts/check-vault-links.py" "$REPO_ROOT/serverAppConfigs-docs" [ "$status" -eq 0 ] || { echo "$output"; false; } } @test "one page per live LLM endpoint" { # The live set is derived, not hardcoded: an endpoint is live if scripts/ # holds both its env and its unit (frank-tts has an env but no unit, so it # is not an inference endpoint; homepage-status is the reverse). local vault="$REPO_ROOT/serverAppConfigs-docs" derived=0 name extra for envf in "$REPO_ROOT"/scripts/frank-*.env; do name=$(basename "$envf" .env) [ -f "$REPO_ROOT/scripts/$name.service" ] || continue derived=$((derived + 1)) [ -f "$vault/LLM/$name.md" ] || { echo "live endpoint '$name' has no canonical page: serverAppConfigs-docs/LLM/$name.md" return 1 } extra=$(find "$vault" -name "${name}-*.md" -not -path '*/.AGENTTRASH/*' | sort) if [ -n "$extra" ]; then echo "endpoint '$name' owns exactly one page; these files re-create a second owner of its shape:" echo "$extra" echo "Fold them into LLM/$name.md (an Eras row plus, if needed, a Law) and trash the page." return 1 fi done # Guard against the derivation going vacuous -- an empty loop passes silently. [ "$derived" -ge 3 ] || { echo "expected at least 3 live endpoints derived from scripts/*.env + scripts/*.service, got $derived" return 1 } } @test "the vault conforms to OKF v0.1" { # Frontmatter contract (type/description/timestamp), the 500-line split # hint, log.md's date-heading shape and newest-first order, and index rows # that restate a fleet quantity instead of pointing at its owner. The only # length exemptions are the live endpoint pages, derived from scripts/ -- # see scripts/check-okf.py. run "$REPO_ROOT/scripts/check-okf.py" "$REPO_ROOT/serverAppConfigs-docs" [ "$status" -eq 0 ] || { echo "$output"; false; } } @test "vault indices are generated, not hand-carved" { # serverAppConfigs-docs/index.md is regenerated from page frontmatter and # every sub-index is synced against its siblings. A hand-edited row is how # a page silently leaves the index, so regeneration must be a no-op. The # vault is copied first: the seam never mutates the checkout. local work="$BATS_TEST_TMPDIR/vault" cp -r "$REPO_ROOT/serverAppConfigs-docs" "$work" run "$REPO_ROOT/scripts/gen-vault-index.py" "$work" [ "$status" -eq 0 ] || { echo "$output"; false; } local stale=0 f rel while IFS= read -r f; do rel=${f#"$REPO_ROOT"/serverAppConfigs-docs/} if ! diff -q "$f" "$work/$rel" >/dev/null; then echo "stale index row(s) in $f -- run scripts/gen-vault-index.py" diff "$f" "$work/$rel" | head -20 stale=1 fi done < <(find "$REPO_ROOT/serverAppConfigs-docs" -name index.md -not -path '*/.AGENTTRASH/*' | sort) [ "$stale" -eq 0 ] }