Files
cozystack/hack/downstream-trigger-map.bats
Myasnikov Daniil 9f875e7874 test(hack): restore set -u and refresh the cozytest-era headers
The layer below moved the unit suite to bats(1), which enforces `set -e`
but not `set -u`. cozytest.sh ran every test body under `set -eu -x`, so
without this a test that reads an unset variable silently sees an empty
string and passes.

There is no runner-level way to inject it, which is why this needs a
per-file load rather than a Makefile flag. Two mechanisms were measured and
rejected: a setup() defined in --setup-suite-file does not reach test files
at all, and SHELLOPTS=nounset does reach them but is exported, so it also
applies to the hack/*.sh scripts the tests exercise as subprocesses --
changing the behaviour of the code under test rather than the tests, and
dropping two tests outright. `set -u` inside setup() is a shell option
rather than an exported one, so it stays scoped to the test body.

hack/test_helper.bash therefore defines the shared setup(), and all 32 unit
files load it. The restoration is mutation-checked, not assumed: a canary
test reading an unset variable aborts with "unbound variable" with the load
in place and passes vacuously without it. Suite is 328/328 green either
way, so nothing in the tree was relying on the laxer behaviour.

The headers are the other half. 27 of the 32 files documented the cozytest
contract in prose -- "there is no bats `run` or `$status`",
"setup()/teardown() are not honored", "Run with: hack/cozytest.sh ...",
title-sanitization rules from the awk parser, and instructions to keep `}`
off column 0 so the parser would not truncate a test. Every one of those
statements is now false or vacuous, and a false comment about the harness
is worse than none: the next author reads "setup() is not honored" and
hand-rolls per-test cleanup that the runner would have done.

Where a constraint merely stopped binding rather than reversing, the
comment says so instead of deleting the line -- the column-0 heredoc
indentation and the manual status capture in migration-seaweedfs-db-adopt
are both kept, now marked belt-and-braces. Claims that survived the move
are left alone: `! cmd` really is still vacuous under `set -e`, since bash
suppresses errexit for a negated command exactly as dash did.

hack/seaweedfs-naming-audit.bats gains a note that its subject is POSIX sh
executed by /bin/sh but sourced into bats' bash, so shellcheck's shell=sh
directive is now the only thing guarding that gap.

Refs: #3453
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-07-31 12:09:12 +05:00

200 lines
10 KiB
Bash

#!/usr/bin/env bats
# Guards the "Downstream Repositories" trigger map in docs/agents/contributing.md.
#
# The map tells a contributor which change in this repo forces a follow-up in a
# satellite repository. Each trigger names a file on this side: hack/update-crd.sh,
# packages/core/platform/values.yaml, and so on. Rename one of those and the map
# keeps confidently pointing at a path that no longer exists, from inside the PR
# template, to every reader. The repository list is deliberately not restated here
# — it would be one more copy to drift; the map and the PR template are the two
# sources, and the tests below hold them to each other.
#
# The three tests pin, in order: every in-repo path the map cites to a file that
# exists; the workflow carve-out that keeps this suite running on a docs-only PR
# to the map's own location; and the map and the PR-template checklist to the same
# repository list, so a repository cannot be listed in one and forgotten in the
# other.
#
# WHAT THIS DOES NOT CHECK, so nobody mistakes a green tick for a correct map:
#
# * Whether a trigger is TRUE. "Change X here and Y breaks over there" is a
# claim about a satellite repo. Verifying it needs that repo checked out at
# a known ref, which this suite deliberately does not do: a unit test must
# not depend on the network or on six sibling clones. A trigger can cite a
# path that exists here and still describe a coupling that is nonsense.
# * Anything on the satellite side. A file named there (its Makefile, its
# scripts/, its playbooks) is not reachable from this repo and is not
# validated. Those names are exactly where the map is most likely to rot.
# * Whether the map is COMPLETE. Deleting a whole trigger, or a whole block of
# them, passes: the repository-list test compares section headings, not bodies.
# * A path written without backticks. Extraction keys off `code spans`, which
# is the section's convention; a lone unquoted path slips through. Stripping
# the backticks wholesale does fail, via the anti-vacuum guard below.
#
# In short: this catches a rename on THIS side, which is the cheap half. The
# expensive half — is the coupling real, and is it still real over there — stays
# a human's job, and reviewers should not assume CI did it for them.
#
# A docs-only PR normally skips the unit tests, which would have exempted the map
# from its own guard; the plan step in .github/workflows/pull-requests.yaml carves
# out docs/agents/contributing.md so that editing the map alone still runs these.
load test_helper
REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
MAP_FILE="$REPO_ROOT/docs/agents/contributing.md"
TEMPLATE_FILE="$REPO_ROOT/.github/PULL_REQUEST_TEMPLATE.md"
# Body of the "## Downstream Repositories" section, up to the next h2.
map_section() {
awk '/^## Downstream Repositories$/ { inside = 1; next }
/^## / { inside = 0 }
inside' "$MAP_FILE"
}
@test "every in-repo path cited by the downstream trigger map exists" {
[ -f "$MAP_FILE" ] || { echo "missing $MAP_FILE" >&2; exit 1; }
section="$(map_section)"
[ -n "$section" ] || { echo "Could not locate the '## Downstream Repositories' section in $MAP_FILE" >&2; exit 1; }
# Backticked tokens rooted at a top-level directory of THIS repo. Satellite
# paths cited by the map (scripts/package.mk, content/en/docs/...) do not match
# these roots and so fall out. Note this is a heuristic, not a guarantee: a
# satellite repo has its own packages/ tree, so a backticked path from over
# there that happens to share a root would be checked against this tree.
# A trailing slash is prose, not a distinct path: `packages/apps/` and
# `packages/apps` are the same anchor and would otherwise both inflate the count.
paths="$(printf '%s\n' "$section" \
| grep -o '`[^`]*`' \
| tr -d '`' \
| grep -E '^(hack|packages|cmd|api|internal|pkg|\.github)/' \
| sed 's#/$##' \
| sort -u)"
# Guard against a vacuous pass: if the extraction breaks, or the section is
# reworded so it no longer cites paths, this test must fail loudly rather than
# silently verify nothing. The floor sits well under the real count (28 as of
# writing) because the map legitimately shrinks too, and a red build should not
# send the reader hunting for a bug that is really a deleted section.
count="$(printf '%s\n' "$paths" | grep -c . || true)"
[ "$count" -ge 20 ] || {
echo "Extracted only $count in-repo path(s) from the trigger map; expected at least 20." >&2
echo "Either the extraction broke and this test is verifying nothing, or the map genuinely" >&2
echo "shrank - if a downstream repository went away, lower this floor deliberately." >&2
exit 1
}
missing=""
for path in $paths; do
# A placeholder segment (<name>) stands for any package, so match it as a glob.
pattern="$(printf '%s' "$path" | sed 's/<[^>]*>/*/g')"
# Unquoted on purpose: the shell must expand the glob. No match => ls fails.
if ! (cd "$REPO_ROOT" && ls -d $pattern) >/dev/null 2>&1; then
missing="$missing $path"
fi
done
if [ -n "$missing" ]; then
echo "The downstream trigger map in docs/agents/contributing.md cites paths that do not exist:" >&2
for path in $missing; do echo " - $path" >&2; done
echo "Fix: update the map to the new path, or drop the trigger if it no longer applies." >&2
echo "The map is read by contributors from inside the PR template; a stale path there sends them to a dead end." >&2
exit 1
fi
echo "All $count in-repo path(s) cited by the trigger map exist"
}
@test "the workflow opts the trigger map back into the unit tests" {
WORKFLOW="$REPO_ROOT/.github/workflows/pull-requests.yaml"
[ -f "$WORKFLOW" ] || { echo "missing $WORKFLOW" >&2; exit 1; }
# Every check below reads the workflow with comments stripped. A commented-out
# line still contains its own text, so grepping the raw file would accept a
# carve-out that had been disabled and left behind as a TODO — the most likely
# way this actually rots.
code_only="$(sed 's/#.*//' "$WORKFLOW")"
# The block of the job that runs the unit tests, from its header to the next
# job's. The condition has to sit on THAT job: pinning it file-wide would accept
# the exact expression pasted into some other job while the unit-test job quietly
# loses it.
unit_job="$(printf '%s\n' "$code_only" | awk '
/^ [a-zA-Z0-9_-]+:[[:space:]]*$/ { inside = ($0 == " checks:") }
inside')"
printf '%s\n' "$unit_job" | grep -q 'make unit-tests' || {
echo "The 'checks' job in .github/workflows/pull-requests.yaml no longer runs 'make unit-tests'." >&2
echo "This suite pins the trigger-map carve-out to that job. If the unit tests moved, point" >&2
echo "the checks below at their new job." >&2
exit 1
}
# Three links carry the carve-out: the plan step names the map, the plan job
# exports the flag, and the unit-test job gates on it. Cutting any one leaves the
# other two looking perfectly wired, and none of them fails loudly — an unset or
# unexported output dereferences to an empty string, so the condition is merely
# false and the suite is skipped in silence, on exactly the PRs it guards, with
# every test in this file still green. Pin all three.
rel="${MAP_FILE#"$REPO_ROOT"/}"
printf '%s\n' "$code_only" | grep -qF "'$rel'" || {
echo "The plan step in .github/workflows/pull-requests.yaml does not name '$rel'." >&2
echo "Without it, a PR that only edits the trigger map is treated as docs-only, the unit" >&2
echo "tests are skipped, and this file never runs — exactly when it is needed most." >&2
echo "Fix: point the trigger_map detection at the map's new path." >&2
exit 1
}
printf '%s\n' "$code_only" | grep -qF 'trigger_map: ${{ steps.p.outputs.trigger_map }}' || {
echo "The plan job in .github/workflows/pull-requests.yaml does not export trigger_map." >&2
echo "The step still computes it and the unit-test job still reads it, so this looks wired" >&2
echo "up, but an unexported output dereferences to an empty string: the job is skipped in" >&2
echo "silence on exactly the PRs this suite guards." >&2
echo "Fix: restore 'trigger_map: \${{ steps.p.outputs.trigger_map }}' to the plan job's outputs." >&2
exit 1
}
condition="(needs.plan.outputs.code == 'true' || needs.plan.outputs.trigger_map == 'true')"
printf '%s\n' "$unit_job" | grep -qF "$condition" || {
echo "The job that runs 'make unit-tests' is not gated on:" >&2
echo " $condition" >&2
echo "Mentioning trigger_map in another job, or in a comment, does not count. Without that" >&2
echo "exact condition on that job, a PR which only edits the trigger map stays docs-only," >&2
echo "skips the unit tests, and never runs this file — while every test here still passes." >&2
exit 1
}
echo "The workflow names '$rel', exports the flag, and gates the unit tests on it"
}
@test "the trigger map and the PR-template checklist list the same repositories" {
[ -f "$TEMPLATE_FILE" ] || { echo "missing $TEMPLATE_FILE" >&2; exit 1; }
# Checklist lines look like: - [ ] [cozystack/website](https://...) - follow-up:
template_repos="$(grep -oE '^- \[ \] \[cozystack/[a-z0-9.-]+\]' "$TEMPLATE_FILE" \
| grep -oE 'cozystack/[a-z0-9.-]+' \
| sort -u)"
# Map headings look like: ### cozystack/website
map_repos="$(map_section \
| grep -oE '^### cozystack/[a-z0-9.-]+' \
| grep -oE 'cozystack/[a-z0-9.-]+' \
| sort -u)"
template_count="$(printf '%s\n' "$template_repos" | grep -c . || true)"
[ "$template_count" -ge 1 ] || { echo "Found no cozystack/* checklist entries in $TEMPLATE_FILE - extraction is broken" >&2; exit 1; }
if [ "$template_repos" != "$map_repos" ]; then
echo "The PR-template checklist and the trigger map disagree on which repositories are downstream." >&2
echo "In the checklist (.github/PULL_REQUEST_TEMPLATE.md):" >&2
printf '%s\n' "$template_repos" | sed 's/^/ /' >&2
echo "In the map (docs/agents/contributing.md, '### cozystack/<repo>' headings):" >&2
printf '%s\n' "$map_repos" | sed 's/^/ /' >&2
echo "Fix: add the repository to both, or remove it from both." >&2
echo "A repository listed in only one of them gets ticked with no guidance, or documented with no box to tick." >&2
exit 1
fi
echo "Checklist and trigger map agree on $template_count repositories"
}