diff --git a/scripts/check-phase19.7.sh b/scripts/check-phase19.7.sh index 0dc956d6..dcad87eb 100755 --- a/scripts/check-phase19.7.sh +++ b/scripts/check-phase19.7.sh @@ -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() {