#!/usr/bin/env bash # ci/tests/decisions-log.test.sh — proves ci/decisions-log.sh actually fails closed. # # The stage exists because appending to DECISIONS-LOG.md silently re-creates a merge conflict # for every other open branch. A guard against a silent regression is itself worth nothing # unless somebody has watched it reject the thing it claims to reject, so each case below # CONSTRUCTS the violation in a throwaway tree and asserts a non-zero exit — the same reasoning # as ci/tests/gate-decision.test.sh, one layer down. set -uo pipefail REAL_CI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PASS=0 FAIL=0 ok() { printf ' ok %s\n' "$*"; PASS=$((PASS+1)); } bad() { printf ' FAIL %s\n' "$*"; FAIL=$((FAIL+1)); } TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT # A minimal well-formed tree: the frozen archive, its pin, and one valid entry. mk_tree() { local d="$1"; rm -rf "$d"; mkdir -p "$d/ci" "$d/decisions" cp "${REAL_CI_DIR}/decisions-log.sh" "${REAL_CI_DIR}/lib.sh" "${REAL_CI_DIR}/versions.env" "$d/ci/" printf 'frozen history\n' > "$d/DECISIONS-LOG.md" ( cd "$d" && sha256sum DECISIONS-LOG.md > decisions/ARCHIVE.sha256 ) mk_entry "$d/decisions/2026-09-29-a-good-entry.md" 2026-09-29 # REPO_ROOT in ci/lib.sh is derived from git; give the tree a repo so it resolves here. ( cd "$d" && git init -q . && git add -A && git -c user.email=t@t -c user.name=t commit -qm init ) } mk_entry() { cat > "$1" < -> sets RC and OUT run() { OUT="$( cd "$1" && bash ci/decisions-log.sh 2>&1 )"; RC=$?; } D="${TMP}/t" echo "== 1. a well-formed tree passes" mk_tree "$D"; run "$D" [ "$RC" -eq 0 ] && ok "clean tree -> exit 0" || bad "clean tree -> exit ${RC}: ${OUT}" echo echo "== 2. THE REGRESSION THIS STAGE EXISTS FOR: appending to the frozen archive" mk_tree "$D"; printf '\n## my new handoff\n' >> "$D/DECISIONS-LOG.md"; run "$D" [ "$RC" -ne 0 ] && ok "append to DECISIONS-LOG.md -> non-zero (${RC})" \ || bad "append to DECISIONS-LOG.md was ACCEPTED — the guard is inert" grep -qi 'frozen archive' <<<"$OUT" && ok "the message names the archive as frozen" \ || bad "the message does not say the archive is frozen" grep -q 'decisions/\$(date +%F)' <<<"$OUT" && ok "the message names the file to write instead" \ || bad "the message does not name the remedy" echo echo "== 3. a re-pin is a deliberate, accepted act" mk_tree "$D"; printf '\nredacted\n' >> "$D/DECISIONS-LOG.md" ( cd "$D" && sha256sum DECISIONS-LOG.md > decisions/ARCHIVE.sha256 ); run "$D" [ "$RC" -eq 0 ] && ok "archive changed AND re-pinned -> exit 0" || bad "re-pin rejected: ${OUT}" echo echo "== 4. a missing pin is unverifiable, not green" mk_tree "$D"; rm "$D/decisions/ARCHIVE.sha256"; run "$D" [ "$RC" -ne 0 ] && ok "no ARCHIVE.sha256 -> non-zero (${RC})" || bad "missing pin was accepted" echo echo "== 5. entry naming" for badname in 2026-9-29-x.md 26-09-29-x.md 2026-09-29-Upper.md 2026-09-29-.md 2026-02-30-x.md; do mk_tree "$D"; mk_entry "$D/decisions/${badname}" 2026-09-29; run "$D" [ "$RC" -ne 0 ] && ok "rejects decisions/${badname}" || bad "ACCEPTED decisions/${badname}" done mk_tree "$D"; mk_entry "$D/decisions/2026-10-01-fix-thing-2.md" 2026-10-01; run "$D" [ "$RC" -eq 0 ] && ok "accepts a digits-and-dashes slug" || bad "rejected a valid slug: ${OUT}" echo echo "== 6. frontmatter" mk_tree "$D"; sed -i '1d' "$D/decisions/2026-09-29-a-good-entry.md"; run "$D" [ "$RC" -ne 0 ] && ok "rejects an entry with no frontmatter" || bad "accepted a bare entry" for key in date branch title; do mk_tree "$D"; sed -i "/^${key}:/d" "$D/decisions/2026-09-29-a-good-entry.md"; run "$D" [ "$RC" -ne 0 ] && ok "rejects frontmatter missing '${key}'" || bad "accepted missing '${key}'" done # The copied-template failure: the body was rewritten, the date never was. mk_tree "$D"; mk_entry "$D/decisions/2026-09-30-copied.md" 2026-09-29; run "$D" [ "$RC" -ne 0 ] && ok "rejects a frontmatter date that disagrees with the filename" \ || bad "accepted a date/filename disagreement" echo echo "== 7. the three required sections, and their bodies" for sec in 'What changed' 'What I verified' 'What I did NOT verify'; do mk_tree "$D"; sed -i "/^## ${sec}\$/d" "$D/decisions/2026-09-29-a-good-entry.md"; run "$D" [ "$RC" -ne 0 ] && ok "rejects an entry missing '## ${sec}'" || bad "accepted missing '## ${sec}'" done # A heading with nothing under it is the way this floor gets defeated in practice. mk_tree "$D" python3 - "$D/decisions/2026-09-29-a-good-entry.md" <<'PY' import sys,re p=sys.argv[1]; s=open(p).read() s=re.sub(r'## What I did NOT verify\n\n.*', '## What I did NOT verify\n\nn/a\n', s, flags=re.S) open(p,'w').write(s) PY run "$D" [ "$RC" -ne 0 ] && ok "rejects a stub body under a required section" || bad "accepted an 'n/a' section" echo echo "== 8. README.md in decisions/ is not an entry and is not policed as one" mk_tree "$D"; printf 'not an entry\n' > "$D/decisions/README.md"; run "$D" [ "$RC" -eq 0 ] && ok "decisions/README.md ignored by the entry rules" || bad "README policed: ${OUT}" echo if [ "$FAIL" -eq 0 ]; then echo "decisions-log tests: PASS ($PASS assertions)"; exit 0 else echo "decisions-log tests: FAIL ($FAIL failed, $PASS passed)"; exit 1; fi