test(19.7-05): gate stages plugins, consumers, docker and docs

- plugins: vet, short tests and the additive-export check of the user
  (GOWORK=off, against 930b158 and v0.1.9), OAuth server (d31f8e7) and blog
  (ffa2c8f) plugins, the blog plugin's MCP tests by name
- consumers: the three applications build and pass at their pins, the demo
  also with the MCP tag; the pin check accepts a plugin submodule ahead of
  its pin only by test-only commits
- docker: the cabana, user plugin, OAuth server and demo Postgres suites by
  name
- docs: decision note and STACK rows, docs checks, no consuming application
  named in sources, docs or the phase's tests
- self-test proves additive_check on a planted repository
This commit is contained in:
Jakub Zych
2026-10-23 14:24:13 +02:00
parent f14eae96dd
commit a94bdc199d

View File

@@ -65,7 +65,7 @@ JOURNAL_PLUGIN="${JOURNAL_PLUGIN:-$ROOT/../sm-journal-plugin}"
FONOTEKA="${FONOTEKA:-$ROOT/../fono""teka.go}"
BM_APP="${BM_APP:-$ROOT/../sm-bm-app}"
# The plugins' last commits before the phase (the additive check's bases).
USER_BASE="${PHASE197_USER_BASE:-930b158}"
USER_BASE="${PHASE197_USER_BASE:-930b158 v0.1.9}"
OAS_BASE="${PHASE197_OAS_BASE:-d31f8e7}"
JOURNAL_BASE="${PHASE197_JOURNAL_BASE:-ffa2c8f}"
@@ -126,6 +126,39 @@ ADMIN_FILES=(
# (D-04: existing payloads unchanged).
ACCOUNT_BASE="51d088a"
# plugins stage: Docker-free tests by required name. The user plugin has
# none (its suites need Postgres and run in the docker stage).
OAS_SHORT_TESTS=()
JOURNAL_SHORT_TESTS=(
.:TestJournalMCPSmoke .:TestJournalMCP
./controllers/api:TestOptionalBackendPrincipalDispatch
)
# docker stage: the plugins' and the demo's Postgres suites.
USER_TESTS=(
TestBackendLinkSmoke TestBackendLinkAdminSmoke TestUserMCPGuardSmoke TestFrontendPermissionCatalogSmoke
TestBackendLink TestFrontendPermissionCatalog TestUserMCPGuard
)
OAS_TESTS=(TestMCPTokenVerifierSmoke TestMCPConsentSmoke TestMCPTokenVerifier TestMCPConsent)
DEMO_TESTS=(TestMCPEndToEnd)
# docs stage: the phase's framework test files, checked for consuming
# application names next to every non-test file under modules/ and docs/.
PHASE_TEST_DIRS=(modules/pier)
PHASE_TEST_FILES=(
modules/bouncer/dispatch_test.go modules/bouncer/dispatch_smoke_test.go
modules/cabana/dispatch_marker_test.go modules/cabana/dispatch_smoke_test.go
modules/cabana/account_contributions_test.go modules/cabana/account_contributions_smoke_test.go
modules/cabana/record_action_prompt_test.go modules/cabana/record_action_prompt_smoke_test.go
modules/cabana/record_action_prompt_postgres_test.go modules/cabana/scope_limit_for_test.go
modules/cabana/subject_events_test.go
modules/surf/streaming_test.go modules/surf/streaming_smoke_test.go
modules/surf/pier_mount_test.go modules/surf/pier_mount_smoke_test.go modules/surf/dispatch_smoke_test.go
modules/wristband/mcp_scopes_test.go modules/wristband/mcp_scopes_smoke_test.go
admin/tests/account/AccountContributions.test.ts admin/tests/form/RecordActionsPrompt.test.ts
)
STACK_NAMES=("github.com/modelcontextprotocol/go-sdk" "github.com/google/jsonschema-go")
# docs stage: names of consuming applications (split so this file does not
# match itself), the decision note and the dependency names it must carry.
APP_NAMES=("p[lł]yta""rium" "fono""teka" "wave""path" "horo""skopia" "bm\\.jakub""zych" "grzyby""funkcjonalne" "stu""dia")
@@ -450,6 +483,34 @@ run_self_test() {
fi
done
# additive_check end to end on a planted repository: an added export
# passes, a removed one fails, an unknown base fails.
local repo base
repo="$(mktemp -d)"
git -C "$repo" init -q
printf 'package p\n\nfunc Keep() {}\n\nfunc Gone() {}\n' >"$repo/p.go"
git -C "$repo" add p.go
git -C "$repo" -c user.name=t -c user.email=t@example.test commit -q -m base
base="$(git -C "$repo" rev-parse HEAD)"
printf 'package p\n\nfunc Keep() {}\n\nfunc Gone() {}\n\nfunc Added() {}\n' >"$repo/p.go"
if ! additive_check planted "$repo" "$base" >/dev/null 2>&1; then
rm -rf "$repo"
echo "refuse: self-test additive_check refused an addition" >&2
return 1
fi
printf 'package p\n\nfunc Keep() {}\n' >"$repo/p.go"
if additive_check planted "$repo" "$base" >/dev/null 2>&1; then
rm -rf "$repo"
echo "refuse: self-test additive_check accepted a removed export" >&2
return 1
fi
if additive_check planted "$repo" "no-such-base" >/dev/null 2>&1; then
rm -rf "$repo"
echo "refuse: self-test additive_check accepted an unknown base" >&2
return 1
fi
rm -rf "$repo"
local mode
for mode in self-test go plugins consumers docker admin docs marker all; do
grep -q -- "^ --$mode" "${BASH_SOURCE[0]}" || {
@@ -509,14 +570,107 @@ run_marker() {
echo "phase19.7 marker passed"
}
# additive_check NAME DIR BASES reports the exported declarations removed
# from DIR's non-test .go files since each of BASES (D-31: the core
# plugins stay additive). git's status is captured before any filtering,
# so an unknown base fails instead of reading as an empty diff.
additive_check() {
local name="$1" dir="$2" bases="$3" base diff rc removed added
for base in $bases; do
rc=0
diff="$(git -C "$dir" diff "$base" -- '*.go' ':(exclude)*_test.go' 2>&1)" || rc=$?
if [[ "$rc" -ne 0 ]]; then
printf '%s\n' "$diff" | tail -n 5 >&2
echo "refuse: git diff $base in $dir failed (exit $rc)" >&2
return 1
fi
removed="$(removed_exports <<<"$diff")"
if [[ -n "$removed" ]]; then
printf '%s\n' "$removed" >&2
echo "refuse: the $name plugin removed or changed exported declarations since $base (additive only)" >&2
return 1
fi
added="$(grep -cE '^\+(func|type) ' <<<"$diff" || true)"
echo "additive: $name: no exported func, method or type removed since $base ($added declarations added)"
done
}
run_plugins() {
echo "TODO stage plugins" >&2
return 1
require_dir "$USER_PLUGIN" "user plugin"
require_dir "$OAUTHSERVER_PLUGIN" "OAuth server plugin"
require_dir "$JOURNAL_PLUGIN" "blog plugin"
# The user plugin is checked against its own go.mod (GOWORK=off), the
# way a consuming application resolves it.
GO_ENV="GOWORK=off" go_vet "$USER_PLUGIN" ./...
GO_ENV="GOWORK=off" go_short "$USER_PLUGIN" ./...
additive_check user "$USER_PLUGIN" "$USER_BASE"
go_vet "$OAUTHSERVER_PLUGIN" ./...
go_short "$OAUTHSERVER_PLUGIN" ./...
if [[ "${#OAS_SHORT_TESTS[@]}" -gt 0 ]]; then
go_pkg_named "$OAUTHSERVER_PLUGIN" "-short" "${OAS_SHORT_TESTS[@]}"
fi
additive_check oauthserver "$OAUTHSERVER_PLUGIN" "$OAS_BASE"
go_vet "$JOURNAL_PLUGIN" ./...
go_short "$JOURNAL_PLUGIN" ./...
go_pkg_named "$JOURNAL_PLUGIN" "-short" "${JOURNAL_SHORT_TESTS[@]}"
additive_check journal "$JOURNAL_PLUGIN" "$JOURNAL_BASE"
echo "phase19.7 plugins passed"
}
run_consumers() {
echo "TODO stage consumers" >&2
return 1
local app
for app in "$FONOTEKA" "$BM_APP" "$DEMO_APP"; do
require_dir "$app" "application"
(cd "$app" && go build ./...)
echo "build: $app"
go_short "$app" ./...
done
(cd "$DEMO_APP" && go build -tags "$MCP_TAG" ./...)
echo "build: $DEMO_APP (-tags $MCP_TAG)"
GO_FLAGS="-tags $MCP_TAG" go_short "$DEMO_APP" ./...
demo_pins
echo "phase19.7 consumers passed"
}
# demo_pins runs the demo's pin check (every plugin submodule on the commit
# go.mod requires). A submodule ahead of its pin only by commits that touch
# *_test.go files (this phase's plugin tests, committed but not released)
# ships the same code and is reported, not refused; any other mismatch
# fails.
demo_pins() {
local out rc=0 line name want head changed
[[ -f "$DEMO_APP/scripts/check-pins.sh" ]] || {
echo "refuse: $DEMO_APP/scripts/check-pins.sh not found" >&2
return 1
}
out="$(cd "$DEMO_APP" && bash scripts/check-pins.sh 2>&1)" || rc=$?
if [[ "$rc" -eq 0 ]]; then
printf '%s\n' "$out"
return 0
fi
while IFS= read -r line; do
[[ -z "$line" ]] && continue
if [[ "$line" =~ ^check-pins:\ (plugins/[^ ]+)\ is\ at\ ([0-9a-f]+),\ go\.mod\ requires\ .*\(([0-9a-f]+)\)$ ]]; then
name="${BASH_REMATCH[1]}"
head="${BASH_REMATCH[2]}"
want="${BASH_REMATCH[3]}"
if ! git -C "$DEMO_APP/$name" merge-base --is-ancestor "$want" HEAD 2>/dev/null; then
echo "refuse: $name at $head is not a descendant of its pin $want" >&2
return 1
fi
changed="$(git -C "$DEMO_APP/$name" diff --name-only "$want" HEAD | grep -vE '_test\.go$' || true)"
if [[ -n "$changed" ]]; then
printf '%s\n' "$changed" >&2
echo "refuse: $name is ahead of its pin $want with non-test changes" >&2
return 1
fi
echo "pins: $name is ahead of its pin $want by test-only commits (release not needed for shipped code)"
continue
fi
printf '%s\n' "$out" >&2
echo "refuse: the demo's pin check failed" >&2
return 1
done <<<"$out"
}
docker_up() {
@@ -528,8 +682,14 @@ run_docker() {
echo "refuse: the docker stage needs a reachable Docker daemon (testcontainers); run it on a Docker host" >&2
return 1
fi
echo "TODO stage docker" >&2
return 1
require_dir "$USER_PLUGIN" "user plugin"
require_dir "$OAUTHSERVER_PLUGIN" "OAuth server plugin"
require_dir "$DEMO_APP" "demo application"
go_pkg_named "$ROOT" "-timeout 20m" "${DOCKER_TESTS[@]}"
GO_ENV="GOWORK=off" go_named "$USER_PLUGIN" . "-timeout 20m" "${USER_TESTS[@]}"
go_named "$OAUTHSERVER_PLUGIN" . "-timeout 20m" "${OAS_TESTS[@]}"
go_named "$DEMO_APP" . "-timeout 20m -tags $MCP_TAG" "${DEMO_TESTS[@]}"
echo "phase19.7 docker passed"
}
# docker_gate is the docker step of --all: run the stage when a daemon
@@ -599,8 +759,48 @@ run_admin() {
}
run_docs() {
echo "TODO stage docs" >&2
return 1
local name
for name in "${DECISION_NAMES[@]}"; do
if ! grep -qF -- "$name" "$ROOT/$DECISION_NOTE"; then
echo "refuse: the decision note $DECISION_NOTE does not name $name" >&2
return 1
fi
done
echo "note: $DECISION_NOTE names ${DECISION_NAMES[*]}"
for name in "${STACK_NAMES[@]}"; do
if ! grep -qF -- "| \`$name\`" "$ROOT/.planning/research/STACK.md"; then
echo "refuse: .planning/research/STACK.md has no row for $name" >&2
return 1
fi
done
echo "stack: rows for ${STACK_NAMES[*]}"
go_named "$ROOT" ./cmd/summer "" TestDocsTree
(cd "$ROOT" && go run ./cmd/summer docs:build --check)
local pattern hits f files=()
pattern="$(IFS='|'; echo "${APP_NAMES[*]}")"
while IFS= read -r -d '' f; do
files+=("$f")
done < <(find "$ROOT/modules" "$ROOT/docs" -type f ! -name '*_test.go' ! -path '*/node_modules/*' -print0)
for f in "${PHASE_TEST_DIRS[@]}"; do
while IFS= read -r -d '' g; do
files+=("$g")
done < <(find "$ROOT/$f" -type f -name '*_test.go' -print0)
done
for f in "${PHASE_TEST_FILES[@]}"; do
if [[ ! -f "$ROOT/$f" ]]; then
echo "refuse: phase test file $f is missing" >&2
return 1
fi
files+=("$ROOT/$f")
done
hits="$(grep -inHIE "$pattern" "${files[@]}" || true)"
if [[ -n "$hits" ]]; then
printf '%s\n' "$hits" | head -n 20 >&2
echo "refuse: a framework source, README, docs page or phase test names a consuming application" >&2
return 1
fi
echo "names: ${#files[@]} files (modules/ and docs/ sources, the phase's tests) checked, no consuming application named"
echo "phase19.7 docs passed"
}
run_all() {