From caee5291ff2048e8d1139e803593d91c063cf742 Mon Sep 17 00:00:00 2001 From: Seth Freiberg Date: Wed, 29 Apr 2026 10:22:18 -0400 Subject: [PATCH] feat: add check-no-upstream-edits.sh + bats tests (enforces I1) Co-Authored-By: Claude Sonnet 4.6 --- scripts/check-no-upstream-edits.sh | 34 ++++++ tests-impl/test-check-no-upstream-edits.bats | 108 +++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100755 scripts/check-no-upstream-edits.sh create mode 100644 tests-impl/test-check-no-upstream-edits.bats diff --git a/scripts/check-no-upstream-edits.sh b/scripts/check-no-upstream-edits.sh new file mode 100755 index 0000000..690eb77 --- /dev/null +++ b/scripts/check-no-upstream-edits.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Enforce Invariant I1: no upstream-tracked file is ever edited. +# Exits 0 on clean state, 1 on violation. +# +# Catches BOTH committed drift (commits unique to HEAD vs upstream/master) +# AND working-tree drift (uncommitted local edits to tracked files). +# +# Spec: sethlabels-docs/specs/2026-04-29-packaging-design.md §5.5 (I1, F1) +set -euo pipefail + +# Allowlist: files/dirs sethLabels is permitted to add or modify. +# `.gitignore` is the one upstream-file exception (called out in spec §2). +allowed_pattern='^\.gitignore$|^\.claude/|^scripts/|^packaging/|^sethlabels-docs/|^tests-impl/|^README\.sethlabels\.md$|^CLAUDE\.md$|^IDEA\.md$|^DECISIONS\.md$' + +committed=$(git diff --name-only upstream/master..HEAD 2>/dev/null || true) +working=$(git diff --name-only HEAD 2>/dev/null || true) + +all_changes=$(printf "%s\n%s\n" "$committed" "$working" | sort -u | sed '/^$/d') + +if [ -z "$all_changes" ]; then + exit 0 +fi + +violations=$(echo "$all_changes" | grep -vE "$allowed_pattern" || true) + +if [ -n "$violations" ]; then + echo "ERROR: strict-zero policy violated. The following upstream files have been modified:" + echo "$violations" + echo "" + echo "See sethlabels-docs/specs/2026-04-29-packaging-design.md §I1." + exit 1 +fi + +exit 0 diff --git a/tests-impl/test-check-no-upstream-edits.bats b/tests-impl/test-check-no-upstream-edits.bats new file mode 100644 index 0000000..aa9a68f --- /dev/null +++ b/tests-impl/test-check-no-upstream-edits.bats @@ -0,0 +1,108 @@ +#!/usr/bin/env bats + +# Tests for scripts/check-no-upstream-edits.sh + +setup() { + REPO_ROOT="$(git rev-parse --show-toplevel)" + SCRIPT="$REPO_ROOT/scripts/check-no-upstream-edits.sh" + TMP_REPO="" +} + +teardown() { + if [ -n "$TMP_REPO" ] && [ -d "$TMP_REPO" ]; then + rm -rf "$TMP_REPO" + fi +} + +# --- Helpers --- + +# Build a minimal disposable repo that mimics the sethLabels structure with a +# local "upstream/master" ref. Returns its path via stdout. +make_test_repo() { + local tmp=$(mktemp -d) + cd "$tmp" + git init -q -b master + git config user.email "test@test" + git config user.name "test" + # Pretend-upstream files + echo "upstream content" > UPSTREAM_FILE.md + echo "real source" > glabels-source.cpp + git add UPSTREAM_FILE.md glabels-source.cpp + git commit -m "upstream base" -q + # Create a local "upstream/master" ref pointing here + git update-ref refs/remotes/upstream/master HEAD + # Create a feature branch for sethLabels content + git checkout -q -b main + echo "$tmp" +} + +# --- Tests --- + +@test "script exists and is executable" { + [ -x "$SCRIPT" ] +} + +@test "exits 0 on clean state with only allowlisted committed changes" { + TMP_REPO=$(make_test_repo) + cd "$TMP_REPO" + mkdir -p scripts packaging sethlabels-docs .claude/handoffs + echo "test" > CLAUDE.md + echo "test" > scripts/something.sh + echo "test" > packaging/x.env + echo "test" > sethlabels-docs/spec.md + echo "" >> .gitignore + git add -A + git commit -m "sethLabels additions" -q + + run "$SCRIPT" + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "exits 1 when an upstream file is committed-modified" { + TMP_REPO=$(make_test_repo) + cd "$TMP_REPO" + echo "evil edit" >> glabels-source.cpp + git add glabels-source.cpp + git commit -m "BAD: edit upstream file" -q + + run "$SCRIPT" + [ "$status" -eq 1 ] + [[ "$output" == *"glabels-source.cpp"* ]] + [[ "$output" == *"strict-zero"* ]] +} + +@test "exits 1 when an upstream file has uncommitted working-tree edits" { + TMP_REPO=$(make_test_repo) + cd "$TMP_REPO" + echo "uncommitted evil edit" >> glabels-source.cpp + + run "$SCRIPT" + [ "$status" -eq 1 ] + [[ "$output" == *"glabels-source.cpp"* ]] +} + +@test "exits 0 when only .gitignore is modified (allowlisted)" { + TMP_REPO=$(make_test_repo) + cd "$TMP_REPO" + echo "*.tmp" >> .gitignore + git add .gitignore + git commit -m "extend gitignore" -q + + run "$SCRIPT" + [ "$status" -eq 0 ] +} + +@test "exits 0 when only CLAUDE.md / IDEA.md / DECISIONS.md / README.sethlabels.md are added" { + TMP_REPO=$(make_test_repo) + cd "$TMP_REPO" + echo "x" > CLAUDE.md + echo "x" > IDEA.md + echo "x" > DECISIONS.md + echo "x" > README.sethlabels.md + git add -A + git commit -m "add sethLabels root docs" -q + + run "$SCRIPT" + [ "$status" -eq 0 ] +}