test(19.7-05): phase gate with the go and marker stages, pier test helpers, TestToolsList and TestDispatchMarker

- scripts/check-phase19.7.sh: fail-closed multi-repo gate (self-test, go,
  marker; plugins, consumers, docker, admin and docs are stubs until the
  later tasks fill them)
- modules/pier/helpers_test.go: the plan 02 fixtures moved out of the smokes
  plus fakeGuard, fakeVerifier, fakeLinkResolver, fakePersonalResolver,
  routePlugin, newApp, twoInstances, sdkClient and fireSubjectChanged
- TestToolsList (pier), TestDispatchMarker (bouncer, cabana with a Postgres
  part through the screens harness, which now keeps its app)
This commit is contained in:
Jakub Zych
2026-10-23 13:31:41 +02:00
parent c49983d3f8
commit 3bbbe6c11e
9 changed files with 1941 additions and 415 deletions

571
scripts/check-phase19.7.sh Executable file
View File

@@ -0,0 +1,571 @@
#!/usr/bin/env bash
# check-phase19.7: fail-closed gate for Phase 19.7, the generic MCP module
# (pier) with its framework seams, the user plugin's account link and MCP
# guard, the OAuth server plugin's MCP verifier and consent, the blog plugin's
# MCP tools and the demo application that wires them end to end. It proves
# the phase across six repositories.
#
# Stages:
# self-test the detectors on planted output, the docker NOT RUN rule on a
# faked host, the marker rule on planted files and the additive
# check on planted diffs.
# go go vet ./... (untagged and with -tags summercms_mcp), go test
# -short ./..., go -C examples/hello test ./..., go build -tags
# summercms_mcp ./..., the hello binary without the MCP SDK, then
# the Docker-free framework tests by required name (GO_TESTS,
# and GO_TAGGED_TESTS with -tags summercms_mcp).
# plugins in the user plugin (GOWORK=off), the OAuth server plugin and
# the blog plugin: go vet, go test -short, the Docker-free tests
# by required name and the additive check (no exported func,
# method or type removed from a non-test .go file since the
# plugin's last commit before the phase).
# consumers the applications at their bumped pins (D-31): go build ./...
# and go test -short ./... in each, the demo also with -tags
# summercms_mcp and its pin check.
# docker Docker host only: the Postgres suites by required name in the
# framework (cabana), the three plugins and the demo (tagged).
# admin npm --prefix admin run typecheck, the vitest run (the phase's
# files must pass), scripts/check-admin-openapi.sh --check and
# scripts/check-admin-dist.sh.
# docs TestDocsTree, summer docs:build --check, the decision note's
# dependency names, the STACK rows, and no framework source,
# README or docs page and no test file of this phase names a
# consuming application.
# marker D-17: no non-test .go file under modules/ outside modules/pier
# and modules/bouncer calls bouncer.WithDispatch, and
# modules/pact depends on neither the MCP SDK, jsonschema-go nor
# modules/bouncer.
#
# A Go stage exits non-zero on a failing command, or on a go test -json run
# that fails, skips a test (a subtest included), matches zero tests, prints
# "no tests to run" or misses a required name (a subtest pass never counts
# for its parent's name). The vitest detector refuses a run whose summary is
# not all-passing, that reports a failed, skipped or todo test, or that has
# no passing line for a required file.
#
# --all runs self-test, go, plugins, consumers, admin, docs and marker, then
# docker when a Docker daemon answers (NOT RUN docker otherwise, a failure
# with PHASE197_REQUIRE_DOCKER=1).
#
# Environment overrides (default: the sibling checkouts next to this
# repository): PHASE197_ROOT (this repository), USER_PLUGIN,
# OAUTHSERVER_PLUGIN, JOURNAL_PLUGIN, DEMO_APP, FONOTEKA, BM_APP, the
# additive bases PHASE197_USER_BASE, PHASE197_OAS_BASE,
# PHASE197_JOURNAL_BASE, and PHASE197_REQUIRE_DOCKER.
set -euo pipefail
unset FORCE_COLOR
export NO_COLOR=1
ROOT="${PHASE197_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
DEMO_APP="${DEMO_APP:-$ROOT/../sm-demo-app}"
USER_PLUGIN="${USER_PLUGIN:-$DEMO_APP/plugins/golem15/user}"
OAUTHSERVER_PLUGIN="${OAUTHSERVER_PLUGIN:-$ROOT/../sm-oauthserver-plugin}"
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}"
OAS_BASE="${PHASE197_OAS_BASE:-d31f8e7}"
JOURNAL_BASE="${PHASE197_JOURNAL_BASE:-ffa2c8f}"
MCP_TAG="summercms_mcp"
# ---------------------------------------------------------------------------
# Required test names. Each entry is <package>:<TestName>; the go stage runs
# every package's names with -run '^(a|b|...)$' and every name must pass.
# go stage: Docker-free framework tests (-short).
GO_TESTS=(
# Plan 01 smokes.
./modules/surf:TestDispatchSmoke ./modules/surf:TestStreamingRouteSmoke
./modules/surf:TestPierMountSmoke ./modules/bouncer:TestDispatchedSmoke
./modules/cabana:TestDispatchMarkerSmoke ./modules/wristband:TestMCPScopesSmoke
./modules/cabana:TestAccountContributionsSmoke ./modules/cabana:TestRecordActionPromptSmoke
./modules/pact:TestPhase197ContractsOnSamplePlugin ./modules/pact:TestMCPGuardsRegistry
./modules/pact:TestPactStaysSDKFree
# Plan 02 smokes.
./modules/pier:TestPierTracerSmoke ./modules/pier:TestPierSessionsSmoke
./modules/pier:TestPierFanOutSmoke ./modules/pier:TestPierStatelessSmoke
./modules/pier:TestExampleBlogPlugin
# Plan 05: the validation map (SC-3, D-17, D-20, D-23).
./modules/pier:TestToolsList
./modules/bouncer:TestDispatchMarker ./modules/cabana:TestDispatchMarker
)
# go stage: framework tests that run in a binary built with the MCP tag.
GO_TAGGED_TESTS=(
./modules/pier:TestToolsList
)
# docker stage: Postgres tests in the framework.
DOCKER_TESTS=(
./modules/cabana:TestDispatchMarker ./modules/cabana:TestRecordActionPromptRun
)
# 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")
DECISION_NOTE=".planning/notes/2026-10-22-mcp-go-sdk.md"
DECISION_NAMES=("modelcontextprotocol/go-sdk" "google/jsonschema-go" "govulncheck")
usage() {
cat >&2 <<'EOF'
usage:
check-phase19.7.sh --self-test
check-phase19.7.sh go | plugins | consumers | docker | admin | docs | marker
check-phase19.7.sh --go | --plugins | --consumers | --docker | --admin | --docs | --marker
check-phase19.7.sh --all
EOF
exit 2
}
# detect_go reads go test -json. Exit 1 fail/build, 2 skip, 3 zero/no-tests,
# 4 non-JSON, 5 missing required name.
detect_go() {
python3 - "$1" <<'PY'
import json, os, sys
path = sys.argv[1]
require = [n for n in os.environ.get("REQUIRE_TESTS", "").split() if n]
passed = set()
failed = []
with open(path, encoding="utf-8", errors="replace") as fh:
for raw in fh:
line = raw.strip()
if not line.startswith("{"):
continue
try:
ev = json.loads(line)
except json.JSONDecodeError:
print("refuse: non-json test output", file=sys.stderr)
sys.exit(4)
action = ev.get("Action")
test = ev.get("Test") or ""
pkg = ev.get("Package") or ev.get("ImportPath") or ""
if action == "build-fail" or (action == "fail" and ev.get("FailedBuild")):
print(f"refuse: build failed {pkg}", file=sys.stderr)
sys.exit(1)
text = ev.get("Output") or ""
if action == "output" and "no tests to run" in text:
print(f"refuse: no tests to run in {pkg}", file=sys.stderr)
sys.exit(3)
if action == "skip" and test:
print(f"refuse: skipped {pkg} {test}", file=sys.stderr)
sys.exit(2)
if action == "fail":
failed.append(f"{pkg} {test}".strip())
if action == "pass" and test:
passed.add(test)
if failed:
print("refuse: failed " + ", ".join(failed), file=sys.stderr)
sys.exit(1)
if not passed:
print("refuse: zero tests", file=sys.stderr)
sys.exit(3)
missing = [n for n in require if n not in passed]
if missing:
print("refuse: required tests did not pass: " + ", ".join(missing), file=sys.stderr)
sys.exit(5)
PY
}
# detect_vitest reads a vitest run's output. Exit 1 failed, skipped or todo
# tests, 3 no all-passing summary, 5 a required file did not pass.
# REQUIRE_FILES is a space-separated list. Only the summary and the file
# lines are read: a slow test's title may contain any word.
detect_vitest() {
python3 - "$1" <<'PY'
import os, re, sys
text = open(sys.argv[1], encoding="utf-8", errors="replace").read()
text = re.sub(r"\x1b\[[0-9;]*m", "", text)
need = os.environ.get("REQUIRE_FILES", "").split()
summary = re.findall(r"^\s*(Test Files|Tests)\s+(.*)$", text, re.M)
if any(re.search(r"\b(failed|skipped|todo)\b", rest) for _, rest in summary) or re.search(r"^\s*(FAIL|×)\s", text, re.M):
print("refuse: vitest reports failed, skipped or todo tests", file=sys.stderr)
sys.exit(1)
m = re.search(r"^\s*Tests\s+(\d+) passed \((\d+)\)\s*$", text, re.M)
if not m or m.group(1) != m.group(2) or m.group(1) == "0":
print("refuse: no all-passing vitest summary", file=sys.stderr)
sys.exit(3)
missing = [f for f in need if not re.search(r"^\s*✓ (?:\|\w+\| )?" + re.escape(f) + r" \(\d+ tests?\)", text, re.M)]
if missing:
print("refuse: did not pass: " + ", ".join(missing), file=sys.stderr)
sys.exit(5)
PY
}
# go_json DIR ARGS... runs go test -json ARGS in DIR and checks the stream
# with detect_go. GO_ENV (word-split) is prepended to the go command's
# environment, for GOWORK=off.
go_json() {
local dir="$1"
shift
local log err rc=0 dc=0
log="$(mktemp)"
err="$(mktemp)"
# shellcheck disable=SC2086
(cd "$dir" && env ${GO_ENV:-} go test -json "$@") >"$log" 2>"$err" || rc=$?
detect_go "$log" || dc=$?
if [[ "$rc" -ne 0 || "$dc" -ne 0 ]]; then
cat "$err" >&2 || true
grep -v '^{' "$log" | tail -n 40 >&2 || true
python3 -c 'import json,sys
for raw in open(sys.argv[1], errors="replace"):
try: ev = json.loads(raw)
except ValueError: continue
if ev.get("Action") == "output" and ("FAIL" in ev.get("Output", "") or "SKIP" in ev.get("Output", "") or "_test.go:" in ev.get("Output", "")):
sys.stderr.write(ev["Output"])' "$log" 2>&1 | tail -n 40 >&2 || true
rm -f "$log" "$err"
echo "refuse: go test $* in $dir (test=$rc detect=$dc)" >&2
return 1
fi
echo "go: $(grep -c '"Action":"pass","Package":"[^"]*","Test":"[^"/]*","Elapsed"' "$log") top-level tests passed in $dir ($*)"
rm -f "$log" "$err"
}
# go_named DIR PACKAGES FLAGS NAME... runs exactly the named top-level tests
# and requires every one of them to pass. PACKAGES and FLAGS are word-split.
go_named() {
local dir="$1" pkgs="$2" flags="$3"
shift 3
if [[ "$#" -eq 0 ]]; then
echo "refuse: go_named $pkgs in $dir without required names" >&2
return 1
fi
local names=("$@") regex
regex="^($(IFS='|'; echo "${names[*]}"))\$"
# shellcheck disable=SC2086
REQUIRE_TESTS="${names[*]}" go_json "$dir" $flags -count=1 -run "$regex" $pkgs
}
# go_pkg_named DIR FLAGS ENTRY... groups <package>:<TestName> entries by
# package and runs go_named once per package.
go_pkg_named() {
local dir="$1" flags="$2"
shift 2
if [[ "$#" -eq 0 ]]; then
echo "refuse: go_pkg_named in $dir without required names" >&2
return 1
fi
local entry pkg name pkgs=()
declare -A by=()
for entry in "$@"; do
pkg="${entry%%:*}"
name="${entry#*:}"
if [[ -z "$pkg" || -z "$name" || "$pkg" == "$entry" ]]; then
echo "refuse: malformed required test entry $entry" >&2
return 1
fi
if [[ -z "${by[$pkg]+x}" ]]; then
pkgs+=("$pkg")
by[$pkg]="$name"
else
by[$pkg]="${by[$pkg]} $name"
fi
done
for pkg in "${pkgs[@]}"; do
# shellcheck disable=SC2086
go_named "$dir" "$pkg" "$flags" ${by[$pkg]}
done
}
# go_short DIR PACKAGES runs go test -short PACKAGES in DIR; on failure it
# prints the tail of the output and fails. GO_ENV and GO_FLAGS are
# word-split into the command.
go_short() {
local dir="$1" pkgs="${2:-./...}" out
# shellcheck disable=SC2086
if ! out="$(cd "$dir" && env ${GO_ENV:-} go test ${GO_FLAGS:-} -count=1 -short $pkgs 2>&1)"; then
printf '%s\n' "$out" | grep -v '^ok ' | tail -n 60 >&2
echo "refuse: go test -short $pkgs in $dir" >&2
return 1
fi
echo "short: $dir ($pkgs ${GO_FLAGS:-})"
}
go_vet() {
local dir="$1"
shift
# shellcheck disable=SC2086
(cd "$dir" && env ${GO_ENV:-} go vet "$@")
echo "vet: $dir ($*)"
}
require_dir() {
[[ -d "$1" ]] || {
echo "refuse: $2 $1 not found" >&2
return 1
}
}
expect_detector() {
local detector="$1" name="$2" want="$3" payload="$4" log dc=0
log="$(mktemp)"
printf '%s\n' "$payload" >"$log"
"$detector" "$log" >/dev/null 2>&1 || dc=$?
rm -f "$log"
if [[ "$dc" -ne "$want" ]]; then
echo "refuse: self-test $detector $name: exit $dc, want $want" >&2
return 1
fi
}
# expect_mode NAME WANT_EXIT WANT_TEXT MODE ENV... runs this script in MODE
# with ENV and checks its exit status and that its output carries WANT_TEXT.
expect_mode() {
local name="$1" want="$2" text="$3" mode="$4"
shift 4
local out rc=0
out="$(env "$@" bash "${BASH_SOURCE[0]}" "$mode" 2>&1)" || rc=$?
if [[ "$rc" -ne "$want" ]] || ! grep -qF -- "$text" <<<"$out"; then
printf '%s\n' "$out" | tail -n 10 >&2
echo "refuse: self-test $name: exit $rc (want $want), output must carry \"$text\"" >&2
return 1
fi
}
# marker_callers DIR prints every non-test .go file under DIR/modules,
# outside modules/pier and modules/bouncer, that calls the dispatch setter.
marker_callers() {
local dir="$1"
(cd "$dir" && grep -rlE '(bouncer\.)?WithDispatch\(' --include='*.go' modules 2>/dev/null) |
grep -vE '_test\.go$' | grep -vE '^modules/(pier|bouncer)/' || true
}
# removed_exports reads a unified diff and prints every removed line that
# declares an exported func, method or type.
removed_exports() {
grep -E '^-(func ([A-Z]|\([^)]*\) [A-Z])|type [A-Z])' | grep -vE '^--- ' || true
}
run_self_test() {
bash -n "${BASH_SOURCE[0]}"
expect_detector detect_go pass 0 '{"Action":"pass","Package":"p","Test":"TestToolsList"}'
expect_detector detect_go fail 1 '{"Action":"pass","Package":"p","Test":"TestA"}
{"Action":"fail","Package":"p","Test":"TestDispatchMarker"}'
expect_detector detect_go package-fail 1 '{"Action":"pass","Package":"p","Test":"TestA"}
{"Action":"fail","Package":"p"}'
expect_detector detect_go build 1 '{"Action":"build-fail","ImportPath":"p"}'
expect_detector detect_go skip 2 '{"Action":"pass","Package":"p","Test":"TestA"}
{"Action":"skip","Package":"p","Test":"TestToolsList"}'
expect_detector detect_go subtest-skip 2 '{"Action":"skip","Package":"p","Test":"TestToolsList/union_of_both_guards"}
{"Action":"pass","Package":"p","Test":"TestToolsList"}'
expect_detector detect_go zero 3 '{"Action":"pass","Package":"p"}'
expect_detector detect_go no-tests 3 '{"Action":"output","Package":"p","Output":"testing: warning: no tests to run\n"}
{"Action":"pass","Package":"p"}'
expect_detector detect_go nonjson 4 '{"Action":"pass",'
REQUIRE_TESTS="TestToolsList TestDispatchMarker" expect_detector detect_go missing-named 5 \
'{"Action":"pass","Package":"p","Test":"TestToolsList"}'
REQUIRE_TESTS="TestDispatch" expect_detector detect_go prefix-is-not-the-test 5 \
'{"Action":"pass","Package":"p","Test":"TestDispatchMarker"}'
REQUIRE_TESTS="TestDispatchMarker" expect_detector detect_go subtest-is-not-parent 5 \
'{"Action":"pass","Package":"p","Test":"TestOuter/TestDispatchMarker"}
{"Action":"pass","Package":"p","Test":"TestOther"}'
local ok=' ✓ tests/account/AccountContributions.test.ts (9 tests) 15ms
✓ tests/form/RecordActionsPrompt.test.ts (8 tests) 147ms
Test Files 2 passed (2)
Tests 17 passed (17)'
REQUIRE_FILES="tests/account/AccountContributions.test.ts tests/form/RecordActionsPrompt.test.ts" expect_detector detect_vitest pass 0 "$ok"
REQUIRE_FILES="tests/form/FormView.test.ts" expect_detector detect_vitest missing-file 5 "$ok"
expect_detector detect_vitest failed 1 ' ✓ tests/account/AccountContributions.test.ts (9 tests) 15ms
Test Files 1 failed | 1 passed (2)
Tests 1 failed | 16 passed (17)'
expect_detector detect_vitest skipped 1 ' ✓ tests/account/AccountContributions.test.ts (9 tests | 1 skipped) 15ms
Test Files 1 passed (1)
Tests 8 passed | 1 skipped (9)'
expect_detector detect_vitest no-summary 3 ' ✓ tests/account/AccountContributions.test.ts (9 tests) 15ms'
expect_detector detect_vitest zero 3 ' Tests 0 passed (0)'
# The marker rule on planted files: a caller outside pier and bouncer is
# found, a test file and pier's own call are not.
local tmp
tmp="$(mktemp -d)"
mkdir -p "$tmp/modules/pier" "$tmp/modules/bouncer" "$tmp/modules/cabana"
printf 'package pier\nfunc f() { _ = bouncer.WithDispatch(nil, nil) }\n' >"$tmp/modules/pier/dispatch.go"
printf 'package bouncer\nfunc WithDispatch() {}\n' >"$tmp/modules/bouncer/dispatch.go"
printf 'package cabana\nfunc f() { _ = bouncer.WithDispatch(nil, nil) }\n' >"$tmp/modules/cabana/x_test.go"
if [[ -n "$(marker_callers "$tmp")" ]]; then
rm -rf "$tmp"
echo "refuse: self-test marker_callers flagged pier, bouncer or a test file" >&2
return 1
fi
printf 'package cabana\nfunc g() { _ = bouncer.WithDispatch(nil, nil) }\n' >"$tmp/modules/cabana/forge.go"
if [[ "$(marker_callers "$tmp")" != "modules/cabana/forge.go" ]]; then
rm -rf "$tmp"
echo "refuse: self-test marker_callers missed a caller outside pier" >&2
return 1
fi
# The docker NOT RUN rule: a docker that does not answer.
mkdir -p "$tmp/bin"
printf '#!/bin/sh\nexit 1\n' >"$tmp/bin/docker"
chmod +x "$tmp/bin/docker"
expect_mode docker-not-run 0 "NOT RUN docker" --docker-gate "PATH=$tmp/bin:$PATH" PHASE197_REQUIRE_DOCKER=0
expect_mode docker-required 1 "FAIL docker" --docker-gate "PATH=$tmp/bin:$PATH" PHASE197_REQUIRE_DOCKER=1
expect_mode docker-stage-needs-daemon 1 "needs a reachable Docker daemon" --docker "PATH=$tmp/bin:$PATH"
rm -rf "$tmp"
# The additive check refuses a removed export and accepts additions.
local diff_bad diff_ok line
diff_bad=$'--- a/classes/x.go\n+++ b/classes/x.go\n-func LinkedBackendUser(ctx context.Context) error {\n+func linkedBackendUser(ctx context.Context) error {'
diff_ok=$'--- a/classes/x.go\n+++ b/classes/x.go\n+func FireSubjectChanged() {\n-func helper() {}\n-\t// func Old() is gone'
if [[ -z "$(removed_exports <<<"$diff_bad")" ]]; then
echo "refuse: self-test removed_exports missed a removed exported func" >&2
return 1
fi
if [[ -n "$(removed_exports <<<"$diff_ok")" ]]; then
echo "refuse: self-test removed_exports refused an additive diff" >&2
return 1
fi
for line in '-type MCPTokenVerifier struct {' '-func (s *BackendLinks) SetLink(ctx context.Context) error {' '-func (Plugin) MCP() pact.MCP {'; do
if [[ -z "$(removed_exports <<<"$line")" ]]; then
echo "refuse: self-test removed_exports missed: $line" >&2
return 1
fi
done
local mode
for mode in self-test go plugins consumers docker admin docs marker all; do
grep -q -- "^ --$mode" "${BASH_SOURCE[0]}" || {
echo "refuse: missing mode --$mode" >&2
return 1
}
done
echo "phase19.7 self-test passed"
}
run_go() {
go_vet "$ROOT" ./...
go_vet "$ROOT" -tags "$MCP_TAG" ./...
go_short "$ROOT" ./...
(cd "$ROOT" && go -C examples/hello test -count=1 ./...) >/dev/null
echo "hello: examples/hello passed"
(cd "$ROOT" && go build -tags "$MCP_TAG" ./...)
echo "build: go build -tags $MCP_TAG ./..."
local sdk
sdk="$(cd "$ROOT" && go -C examples/hello list -deps ./... | grep -E 'modelcontextprotocol|google/jsonschema-go|modules/pier$' || true)"
if [[ -n "$sdk" ]]; then
printf '%s\n' "$sdk" >&2
echo "refuse: the untagged hello binary links the MCP module or its SDK (D-30)" >&2
return 1
fi
echo "untagged: examples/hello links no MCP SDK"
go_pkg_named "$ROOT" "-short" "${GO_TESTS[@]}"
go_pkg_named "$ROOT" "-short -tags $MCP_TAG" "${GO_TAGGED_TESTS[@]}"
echo "phase19.7 go passed"
}
run_marker() {
local callers deps
callers="$(marker_callers "$ROOT")"
if [[ -n "$callers" ]]; then
printf '%s\n' "$callers" >&2
echo "refuse: a framework file outside modules/pier and modules/bouncer calls bouncer.WithDispatch (D-17)" >&2
return 1
fi
if ! (cd "$ROOT" && grep -q 'bouncer.WithDispatch(' modules/pier/dispatch.go); then
echo "refuse: modules/pier/dispatch.go no longer sets the marker; update the marker rule" >&2
return 1
fi
echo "marker: only modules/pier sets the dispatch marker"
deps="$(cd "$ROOT" && go list -deps ./modules/pact | grep -E 'modelcontextprotocol|google/jsonschema-go|summercms/modules/bouncer$' || true)"
if [[ -n "$deps" ]]; then
printf '%s\n' "$deps" >&2
echo "refuse: modules/pact depends on the MCP SDK, jsonschema-go or bouncer" >&2
return 1
fi
echo "pact: no MCP SDK, jsonschema-go or bouncer dependency"
echo "phase19.7 marker passed"
}
run_plugins() {
echo "TODO stage plugins" >&2
return 1
}
run_consumers() {
echo "TODO stage consumers" >&2
return 1
}
docker_up() {
command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1
}
run_docker() {
if ! docker_up; then
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
}
# docker_gate is the docker step of --all: run the stage when a daemon
# answers, fail with PHASE197_REQUIRE_DOCKER=1, print NOT RUN otherwise.
docker_gate() {
if docker_up; then
if bash "${BASH_SOURCE[0]}" --docker; then
echo "PASS docker"
return 0
fi
echo "FAIL docker"
return 1
fi
if [[ "${PHASE197_REQUIRE_DOCKER:-0}" == "1" ]]; then
echo "FAIL docker: no reachable Docker daemon and PHASE197_REQUIRE_DOCKER=1"
return 1
fi
cat >&2 <<'EOF'
************************************************************************
NOT RUN docker: no reachable Docker daemon on this host.
The Postgres suites of the framework, the user, OAuth server and blog
plugins and the demo's end-to-end MCP test did NOT run. Run on a Docker
host (through sudo -u golem when the socket is denied) before
/msd-verify-work:
bash scripts/check-phase19.7.sh docker
************************************************************************
EOF
echo "NOT RUN docker (Docker-host only; see the notice above)"
}
run_admin() {
echo "TODO stage admin" >&2
return 1
}
run_docs() {
echo "TODO stage docs" >&2
return 1
}
run_all() {
local stage
for stage in self-test go plugins consumers admin docs marker; do
if bash "${BASH_SOURCE[0]}" "--$stage"; then
echo "PASS $stage"
else
echo "FAIL $stage"
exit 1
fi
done
docker_gate || exit 1
echo "Phase 19.7 gate passed"
}
case "${1:---all}" in
--self-test | self-test) run_self_test ;;
--go | go) run_go ;;
--plugins | plugins) run_plugins ;;
--consumers | consumers) run_consumers ;;
--docker | docker) run_docker ;;
--docker-gate) docker_gate ;;
--admin | admin) run_admin ;;
--docs | docs) run_docs ;;
--marker | marker) run_marker ;;
--all | all) run_all ;;
*) usage ;;
esac