Files
prompts/test/update-policy.sh
T
sneak 97da381318
check / check (push) Successful in 20s
update-policy: stop on a .golangci.yml line the canonical file lacks
A repo's own lint setting outside the test-support deny list was
deleted, and the deletion pushed with exit 0. The script now stops
and names each such line.

The check is one function that both merge steps call. An indented #
line now counts as a comment, as YAML needs.

Without --fail the stub curl now writes an error page and exits 0,
and the bad-hash scenario checks for the fetch error.

Model: opus-5-5
2026-11-09 09:03:40 +00:00

520 lines
15 KiB
Bash
Executable File

#!/bin/sh
# test/update-policy.sh: test script/update-policy on throwaway repos.
# Each scenario gets one temp directory holding a repo, its local bare
# origin, the canonical files and a stub curl that serves them, so
# nothing touches the network.
set -eu
UPDATE_POLICY="$(cd "$(dirname "$0")/.." && pwd -P)/script/update-policy"
dir=""
trap 'rm -rf "$dir"' EXIT
fail() {
echo "FAIL: $scenario: $*" >&2
cat "$dir/output" >&2
exit 1
}
write_stub_curl() {
mkdir "$dir/bin"
cat > "$dir/bin/curl" << 'EOF'
#!/bin/sh
previous=""
fail_flag=""
for argument in "$@"; do
[ "$previous" = "--output" ] && output="$argument"
[ "$argument" = "--fail" ] && fail_flag=yes
previous="$argument"
done
url="$previous"
base="https://git.eeqj.de/sneak/prompts/raw/commit/$HASH/"
path="${url#"$base"}"
source="$(dirname "$0")/../canonical/$path"
if [ "$path" = "$url" ] || [ ! -f "$source" ]; then
[ -n "$fail_flag" ] && exit 22
echo "404 page not found" > "$output"
exit 0
fi
cp "$source" "$output"
EOF
chmod +x "$dir/bin/curl"
}
write_canonical_files() {
mkdir -p "$dir/canonical/prompts"
cat > "$dir/canonical/.gitignore" << 'EOF'
*.log
*.tmp
# Own entries go below this comment.
EOF
cat > "$dir/canonical/.dockerignore" << 'EOF'
**/*.log
**/*.tmp
# Own entries go below this comment.
EOF
cat > "$dir/canonical/.editorconfig" << 'EOF'
root = true
[*]
indent_size = 4
# Own sections go below this comment.
EOF
cat > "$dir/canonical/.golangci.yml" << 'EOF'
linters:
settings:
depguard:
rules:
test-support:
deny:
- pkg: net/http/httptest
desc: canonical
- pkg: net/http/cgi
desc: added canonically
other:
deny:
- pkg: example.com/other
EOF
echo '{ "tabWidth": 4 }' > "$dir/canonical/.prettierrc"
echo "new policy" > "$dir/canonical/prompts/REPO_POLICIES.md"
echo "new api policy" > "$dir/canonical/prompts/API_POLICIES.md"
}
# write_outdated_files: this repo's files, one canonical version behind,
# with the repo's own entries in the places the script keeps them. The
# comment above the closing comment in .gitignore is replaced, not kept.
write_outdated_files() {
cat > "$dir/repo/.gitignore" << 'EOF'
*.log
# An outdated comment.
# Own entries go below this comment.
/myapp
EOF
cat > "$dir/repo/.dockerignore" << 'EOF'
**/*.log
# Own entries go below this comment.
/myapp
EOF
cat > "$dir/repo/.editorconfig" << 'EOF'
root = true
[*]
# Own sections go below this comment.
[*.py]
indent_size = 2
EOF
cat > "$dir/repo/.golangci.yml" << 'EOF'
linters:
settings:
depguard:
rules:
test-support:
deny:
- pkg: net/http/httptest
desc: canonical
# Our own test helpers.
- pkg: example.com/app/internal/apptest
desc: own entry
other:
deny:
- pkg: example.com/other
EOF
echo '{ "tabWidth": 2 }' > "$dir/repo/.prettierrc"
echo "old policy" > "$dir/repo/REPO_POLICIES.md"
}
# start_scenario <name>: make a new temp directory holding a repo with
# the outdated files, not yet committed, and an empty origin.
start_scenario() {
scenario="$1"
# The one commit the stub curl serves; every other commit gets a 404.
export HASH=1111111111111111111111111111111111111111
rm -rf "$dir"
dir="$(mktemp -d)"
: > "$dir/output"
write_stub_curl
write_canonical_files
git init -q --bare -b main "$dir/origin.git"
git init -q -b main "$dir/repo"
git -C "$dir/repo" config user.name "Test"
git -C "$dir/repo" config user.email "test@example.com"
git -C "$dir/repo" remote add origin "$dir/origin.git"
mkdir "$dir/repo/script"
cp "$UPDATE_POLICY" "$dir/repo/script/update-policy"
printf '#!/bin/sh\necho example\n' > "$dir/repo/script/projectname"
chmod +x "$dir/repo/script/projectname"
write_outdated_files
}
# commit_and_push_fixture <paths...>: commit those paths, remove the
# rest of the outdated files, and record the commit as $before.
commit_and_push_fixture() {
(
cd "$dir/repo"
git add script "$@"
git commit -q -m "Fixture"
git push -q origin HEAD
git clean -q -f -x
)
before="$(git -C "$dir/repo" rev-parse HEAD)"
}
run_update_policy() {
status=0
(cd "$dir/repo" && PATH="$dir/bin:$PATH" script/update-policy "$@") \
> "$dir/output" 2>&1 || status=$?
}
reject_pushes() {
printf '#!/bin/sh\nexit 1\n' > "$dir/origin.git/hooks/pre-receive"
chmod +x "$dir/origin.git/hooks/pre-receive"
}
accept_pushes() {
rm "$dir/origin.git/hooks/pre-receive"
}
head_of_repo() {
git -C "$dir/repo" rev-parse HEAD
}
head_of_origin() {
git -C "$dir/origin.git" rev-parse refs/heads/main
}
expect_success() {
[ "$status" -eq 0 ] || fail "exited $status"
}
expect_failure() {
[ "$status" -ne 0 ] || fail "exited 0"
}
expect_output() {
grep -q -F -e "$1" "$dir/output" || fail "output lacks: $1"
}
expect_output_without() {
if grep -q -F -e "$1" "$dir/output"; then
fail "output has: $1"
fi
}
# expect_file <path>: the file must equal standard input.
expect_file() {
cat > "$dir/expected"
diff "$dir/expected" "$dir/repo/$1" >&2 || fail "$1 is wrong"
}
expect_absent() {
[ ! -e "$dir/repo/$1" ] || fail "$1 exists"
}
expect_nothing_written() {
[ -z "$(git -C "$dir/repo" status --porcelain)" ] ||
fail "the working tree changed"
[ "$(head_of_repo)" = "$before" ] || fail "the repo has a new commit"
[ "$(head_of_origin)" = "$before" ] || fail "origin has a new commit"
}
test_own_entries_kept() {
start_scenario "own entries kept in all four files"
commit_and_push_fixture .gitignore .dockerignore .editorconfig \
.golangci.yml .prettierrc REPO_POLICIES.md
run_update_policy "$HASH"
expect_success
expect_file .gitignore << 'EOF'
*.log
*.tmp
# Own entries go below this comment.
/myapp
EOF
expect_file .dockerignore << 'EOF'
**/*.log
**/*.tmp
# Own entries go below this comment.
/myapp
EOF
expect_file .editorconfig << 'EOF'
root = true
[*]
indent_size = 4
# Own sections go below this comment.
[*.py]
indent_size = 2
EOF
expect_file .golangci.yml << 'EOF'
linters:
settings:
depguard:
rules:
test-support:
deny:
- pkg: net/http/httptest
desc: canonical
- pkg: net/http/cgi
desc: added canonically
# Our own test helpers.
- pkg: example.com/app/internal/apptest
desc: own entry
other:
deny:
- pkg: example.com/other
EOF
expect_file .prettierrc << 'EOF'
{ "tabWidth": 4 }
EOF
expect_file REPO_POLICIES.md << 'EOF'
new policy
EOF
[ "$(head_of_origin)" = "$(head_of_repo)" ] ||
fail "the commit was not pushed"
[ "$(head_of_repo)" != "$before" ] || fail "no commit was made"
git -C "$dir/repo" log -1 --format=%s | grep -q -F -e "$HASH" ||
fail "the commit message does not name the hash"
}
test_files_not_carried_are_skipped() {
start_scenario "files the repo does not carry skipped"
mkdir "$dir/repo/docs"
mv "$dir/repo/REPO_POLICIES.md" "$dir/repo/docs/REPO_POLICIES.md"
commit_and_push_fixture docs/REPO_POLICIES.md
run_update_policy "$HASH"
expect_success
expect_file docs/REPO_POLICIES.md << 'EOF'
new policy
EOF
expect_absent REPO_POLICIES.md
expect_absent API_POLICIES.md
expect_absent docs/API_POLICIES.md
expect_absent .gitignore
expect_absent .dockerignore
expect_absent .editorconfig
expect_absent .golangci.yml
expect_absent .prettierrc
[ "$(head_of_origin)" = "$(head_of_repo)" ] ||
fail "the commit was not pushed"
}
test_nothing_changed() {
start_scenario "nothing changed"
cp "$dir/canonical/.prettierrc" "$dir/repo/.prettierrc"
commit_and_push_fixture .prettierrc
run_update_policy "$HASH"
expect_success
expect_output "nothing to commit"
expect_nothing_written
}
test_bad_commit_hash() {
for hash in not-a-hash 0000000000000000000000000000000000000000; do
start_scenario "bad commit hash $hash stops with nothing written"
commit_and_push_fixture .gitignore .prettierrc REPO_POLICIES.md
run_update_policy "$hash"
expect_failure
expect_nothing_written
done
# The last hash has the right form, so it is the one that reaches curl.
expect_output "cannot fetch"
}
test_push_failure() {
start_scenario "push failure exits non-zero"
commit_and_push_fixture .prettierrc
reject_pushes
run_update_policy "$HASH"
expect_failure
expect_output "push failed"
[ "$(head_of_origin)" = "$before" ] || fail "origin has a new commit"
}
# Each of these files, with the repo's own entries above the closing
# comment. The comment added with them must not be named.
test_own_entry_above_closing_comment() {
for file in .gitignore .dockerignore .editorconfig; do
start_scenario "own entries above the closing comment of $file stop"
{
printf '/myapp\n/.tool/\n\n# A comment of our own.\n'
cat "$dir/repo/$file"
} > "$dir/merged"
mv "$dir/merged" "$dir/repo/$file"
commit_and_push_fixture .gitignore .dockerignore .editorconfig \
.prettierrc REPO_POLICIES.md
run_update_policy "$HASH"
expect_failure
expect_output "$file has entries"
expect_output "/myapp"
expect_output "/.tool/"
expect_output_without "A comment of our own."
expect_nothing_written
done
}
test_retry_after_failed_push() {
start_scenario "a retry after a failed push delivers the commit"
commit_and_push_fixture .prettierrc
reject_pushes
run_update_policy "$HASH"
expect_failure
committed="$(head_of_repo)"
[ "$committed" != "$before" ] || fail "no commit was made"
[ "$(head_of_origin)" = "$before" ] || fail "origin has a new commit"
accept_pushes
run_update_policy "$HASH"
expect_success
[ "$(head_of_repo)" = "$committed" ] || fail "the retry made a new commit"
[ "$(head_of_origin)" = "$committed" ] ||
fail "the retry did not deliver the commit"
}
test_uncommitted_change_stops() {
start_scenario "a carried file with uncommitted changes stops"
commit_and_push_fixture .prettierrc REPO_POLICIES.md
echo "uncommitted" >> "$dir/repo/REPO_POLICIES.md"
run_update_policy "$HASH"
expect_failure
expect_output "REPO_POLICIES.md has uncommitted changes"
expect_file REPO_POLICIES.md << 'EOF'
old policy
uncommitted
EOF
[ "$(head_of_repo)" = "$before" ] || fail "the repo has a new commit"
[ "$(head_of_origin)" = "$before" ] || fail "origin has a new commit"
}
test_gitignore_lacks_canonical_last_line() {
start_scenario ".gitignore lacking the canonical last line stops"
printf '*.log\n/myapp\n' > "$dir/repo/.gitignore"
commit_and_push_fixture .gitignore .prettierrc
run_update_policy "$HASH"
expect_failure
expect_output ".gitignore lacks this line"
expect_nothing_written
}
test_fetched_golangci_without_deny_list() {
start_scenario "a fetched .golangci.yml with no deny list stops"
cat > "$dir/canonical/.golangci.yml" << 'EOF'
linters:
settings:
depguard:
rules:
test-support:
files:
- "**/*.go"
other:
deny:
- pkg: example.com/other
EOF
commit_and_push_fixture .golangci.yml .prettierrc
run_update_policy "$HASH"
expect_failure
expect_output "no deny list in its test-support rule"
expect_nothing_written
}
test_own_golangci_setting_stops() {
start_scenario "a .golangci.yml setting the canonical file lacks stops"
cat >> "$dir/repo/.golangci.yml" << 'EOF'
# A comment of our own.
forbidigo:
forbid:
- pattern: model.Operator
EOF
commit_and_push_fixture .golangci.yml .prettierrc
run_update_policy "$HASH"
expect_failure
expect_output ".golangci.yml has lines"
expect_output "forbidigo:"
expect_output "- pattern: model.Operator"
expect_output_without "A comment of our own."
expect_nothing_written
}
test_source_repo_stops() {
start_scenario "the repo the policy files come from stops"
printf '#!/bin/sh\necho prompts\n' > "$dir/repo/script/projectname"
commit_and_push_fixture .prettierrc REPO_POLICIES.md
run_update_policy "$HASH"
expect_failure
expect_output "this repo is the source of the policy files"
expect_nothing_written
}
test_staged_file_stays_out_of_commit() {
start_scenario "a file staged before the script starts stays out of its commit"
commit_and_push_fixture .prettierrc
echo "notes" > "$dir/repo/notes.txt"
git -C "$dir/repo" add notes.txt
run_update_policy "$HASH"
expect_success
[ "$(git -C "$dir/repo" show --name-only --format= HEAD)" = ".prettierrc" ] ||
fail "the commit holds more than the policy file"
[ "$(git -C "$dir/repo" diff --cached --name-only)" = "notes.txt" ] ||
fail "notes.txt is no longer staged"
}
test_short_commit_hash_refused() {
start_scenario "a hexadecimal hash shorter than 40 characters is refused"
commit_and_push_fixture .prettierrc
run_update_policy "${HASH%?}"
expect_failure
expect_output "not a full commit hash"
expect_nothing_written
}
# git is pointed at a local repository in place of the prompts remote,
# and the stub curl serves the head of that repository's main.
test_no_hash_uses_head_of_main() {
start_scenario "with no hash the script uses the head of main"
commit_and_push_fixture .prettierrc
git init -q --bare -b main "$dir/prompts.git"
git -C "$dir/repo" push -q "$dir/prompts.git" HEAD:refs/heads/main
git -C "$dir/repo" config "url.$dir/prompts.git.insteadOf" \
"https://git.eeqj.de/sneak/prompts.git"
HASH="$before"
run_update_policy
expect_success
expect_file .prettierrc << 'EOF'
{ "tabWidth": 4 }
EOF
git -C "$dir/repo" log -1 --format=%s | grep -q -F -e "$HASH" ||
fail "the commit message does not name the head of main"
[ "$(head_of_origin)" = "$(head_of_repo)" ] ||
fail "the commit was not pushed"
}
main() {
test_own_entries_kept
test_files_not_carried_are_skipped
test_nothing_changed
test_bad_commit_hash
test_push_failure
test_own_entry_above_closing_comment
test_retry_after_failed_push
test_uncommitted_change_stops
test_gitignore_lacks_canonical_last_line
test_fetched_golangci_without_deny_list
test_own_golangci_setting_stops
test_source_repo_stops
test_staged_file_stays_out_of_commit
test_short_commit_hash_refused
test_no_hash_uses_head_of_main
echo "update-policy tests passed"
}
main "$@"