Fail-closed stages go, admin, editor, layer, docs and postgres with go, vitest-JSON and TAP detectors, the editor corpus copy comparison and a self-test that proves each fails on planted output. --all runs postgres only when a Docker daemon answers and otherwise prints a loud NOT RUN notice; PHASE162_REQUIRE_POSTGRES=1 makes that a failure.
558 lines
23 KiB
Bash
Executable File
558 lines
23 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Phase 16.2 fail-closed gate: plugin-provided admin field types (summercms.go
|
||
# pact and cabana, the admin SPA) and the Journal Blog Editor
|
||
# (sm-journal-plugin Go and editor element, vue-journal-layer embeds).
|
||
#
|
||
# Stages:
|
||
# go go vet in both Go repositories; the framework's field type tests
|
||
# and the Journal classes and controllers tests (including the
|
||
# editor-corpus policy cross-check); the 16.1 policy test files
|
||
# must be unchanged since c60abc6.
|
||
# admin the admin SPA typecheck and full vitest suite.
|
||
# editor the Blog Editor typecheck and full vitest suite (smoke, corpus,
|
||
# preview, element, schema, serializer, embed).
|
||
# layer the vue-journal-layer node --test suite and the editor corpus
|
||
# copy, which must equal the plugin's byte for byte (cmp).
|
||
# docs TestDocsTree, the admin OpenAPI and dist drift gates and the
|
||
# plugin's assets/dist drift gate.
|
||
# postgres Docker host only: the testcontainers suites (cabana, including
|
||
# TestFieldTypeSavePostgres; the Journal admin harness, including
|
||
# TestPostsAdminResaveKeepsHTML, TestPostsAdminSaveSanitizesHTML,
|
||
# TestCategoriesAdminContent and TestCategoriesAdminBlogEditor).
|
||
# Without a reachable Docker daemon this stage fails.
|
||
#
|
||
# Every stage exits non-zero on a failing command; a go test -json run that
|
||
# fails, skips, matches zero tests, prints "no tests to run" or misses a named
|
||
# required test; a vitest JSON report with a failed, skipped or todo test, a
|
||
# failed file, no passing test or a missing required file or test; or a TAP
|
||
# run with "not ok", a non-zero "# fail", a skip, no pass or a missing named
|
||
# test. --self-test proves the detectors and the corpus comparison fail on
|
||
# planted output.
|
||
#
|
||
# --all runs self-test, go, admin, editor, layer and docs, then postgres when
|
||
# a Docker daemon answers. When none does, --all says so loudly (NOT RUN
|
||
# postgres) and the Docker suites stay a manual step for a Docker host:
|
||
# bash scripts/check-phase16.2.sh postgres
|
||
# Set PHASE162_REQUIRE_POSTGRES=1 to make --all fail instead.
|
||
#
|
||
# Environment overrides: PHASE162_ROOT (this repository), JOURNAL (default
|
||
# ../sm-journal-plugin), LAYER (default ../vue-journal-layer).
|
||
set -euo pipefail
|
||
|
||
unset FORCE_COLOR
|
||
export NO_COLOR=1
|
||
|
||
ROOT="${PHASE162_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||
JOURNAL="${JOURNAL:-$ROOT/../sm-journal-plugin}"
|
||
LAYER="${LAYER:-$ROOT/../vue-journal-layer}"
|
||
POLICY_BASE="c60abc6"
|
||
|
||
# Framework tests (modules/pact, modules/cabana) that must pass by name.
|
||
FRAMEWORK_RUN='^(TestFormFieldErrorMessage|TestFormFieldTypeContract|TestFieldTypeRegistry|TestFieldTypeOptions|TestFieldTypeCompile|TestFieldTypeDecode|TestFieldTypeNotices|TestFieldTypeUntouched|TestFieldTypeLoad|TestFieldTypeListValue|TestFieldTypeWrites|TestFieldTypesRoute|TestFieldUpload|TestFieldTypeTracerSmoke|TestFieldTypeExpansionSmoke|TestPhase10CSRF|TestPhase09ContractInventory|TestPhase09PermissionMatrix|ExampleCompileFormWithTypes)$'
|
||
FRAMEWORK_REQUIRE=(
|
||
TestFormFieldErrorMessage TestFormFieldTypeContract
|
||
TestFieldTypeRegistry TestFieldTypeOptions TestFieldTypeCompile TestFieldTypeDecode
|
||
TestFieldTypeNotices TestFieldTypeUntouched TestFieldTypeLoad TestFieldTypeListValue
|
||
TestFieldTypeWrites TestFieldTypesRoute TestFieldUpload
|
||
TestFieldTypeTracerSmoke TestFieldTypeExpansionSmoke
|
||
TestPhase10CSRF TestPhase09ContractInventory TestPhase09PermissionMatrix
|
||
ExampleCompileFormWithTypes
|
||
)
|
||
# Journal classes and controllers tests that must pass by name (the packages
|
||
# run whole; these are the phase's and the 16.1 policy's).
|
||
JOURNAL_REQUIRE=(
|
||
TestBlogEditorDecode TestBlogEditorNotices TestBlogEditorLoad TestLooksLikeHTML
|
||
TestBlogEditorOptions TestBlogEditorListValue TestBlogEditorUpload TestStoreMedia
|
||
TestEditorOutputPassesPolicy TestSanitizeHTMLReport
|
||
TestFormatHTMLSanitizesUnsafeHTML TestFormatHTMLDuplicateAttributes TestCheckHTML TestSanitizeHTML
|
||
TestMediaObjectPath TestMediaFolderPatternRejectsDotDot
|
||
TestPostsEditorHooks TestBlogEditorTracerSmoke TestBlogEditorUploadSmoke
|
||
TestWriteMediaStoreError
|
||
)
|
||
# Docker-host suites.
|
||
PG_FRAMEWORK_REQUIRE=(TestFieldTypeSavePostgres TestListRelationSearchPostgres)
|
||
PG_JOURNAL_REQUIRE=(
|
||
TestPostsFormCompiles TestPostsAdminCreateSmoke TestPostsAdminResaveKeepsHTML
|
||
TestPostsAdminSaveSanitizesHTML TestCategoriesAdminContent TestCategoriesAdminBlogEditor
|
||
TestJournalSVGEmbed
|
||
)
|
||
|
||
ADMIN_FILES=(
|
||
tests/form/PluginField.test.ts tests/form/FormField.test.ts tests/form/registry.test.ts
|
||
tests/form/formState.test.ts tests/app/fieldTypes.test.ts tests/app/pluginAssets.test.ts
|
||
tests/list/ListView.test.ts tests/list/CellValue.test.ts tests/shell/AppShell.test.ts
|
||
)
|
||
ADMIN_REQUIRE=(
|
||
"untouched value (D-03) keeps the served HTML identical when the element echoes it on connect"
|
||
"PluginField does not write back the value the element just emitted"
|
||
"loadFieldTypes loads each script URL once, even across two runs and two types"
|
||
"loadFieldTypes keeps field type styles enabled on every screen"
|
||
"View site link (Phase 16.2 quick follow-up) opens the site root in a new tab without an opener or referrer"
|
||
"plugin field type columns (Phase 16.2) renders the row value of a column type it does not know as plain text"
|
||
)
|
||
EDITOR_FILES=(
|
||
tests/blogeditor/smoke.test.ts tests/blogeditor/corpus.test.ts tests/blogeditor/preview.test.ts
|
||
tests/blogeditor/element.test.ts tests/blogeditor/schema.test.ts tests/blogeditor/serializer.test.ts
|
||
tests/blogeditor/embed.test.ts
|
||
)
|
||
EDITOR_REQUIRE=(
|
||
"untouched value (D-03) returns the imported HTML post unchanged after a Source tab round trip"
|
||
"block reuse keeps every other block, separator and comment byte for byte after one edit"
|
||
"editor corpus (D-05) matches the committed editor-corpus.json"
|
||
"isPolicyUrl refuses \"java\\tscript:alert(1)\""
|
||
)
|
||
LAYER_REQUIRE=(
|
||
"the sink turns a stored placeholder into the iframe"
|
||
"a raw iframe in stored HTML is stripped; only the placeholder becomes an iframe"
|
||
"the sinks expand after sanitizing and the payload transforms never expand"
|
||
"the Go policy negatives are stripped the same way here"
|
||
"the editor corpus has the entries the mirror rules are for"
|
||
)
|
||
|
||
usage() {
|
||
cat >&2 <<'EOF'
|
||
usage:
|
||
check-phase16.2.sh --self-test
|
||
check-phase16.2.sh go | admin | editor | layer | docs | postgres
|
||
check-phase16.2.sh --go | --admin | --editor | --layer | --docs | --postgres
|
||
check-phase16.2.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_tap reads node --test --test-reporter=tap output. Exit 1 "not ok" or
|
||
# a fail/cancelled count, 2 skip/todo, 3 no pass, 5 missing required name.
|
||
# REQUIRE_NAMES holds one test name per line.
|
||
detect_tap() {
|
||
python3 - "$1" <<'PY'
|
||
import os, re, sys
|
||
path = sys.argv[1]
|
||
require = [n for n in os.environ.get("REQUIRE_NAMES", "").split("\n") if n.strip()]
|
||
passed = set()
|
||
counts = {}
|
||
with open(path, encoding="utf-8", errors="replace") as fh:
|
||
for raw in fh:
|
||
line = raw.rstrip("\n")
|
||
body = line.lstrip()
|
||
if body.startswith("not ok"):
|
||
print(f"refuse: {body}", file=sys.stderr)
|
||
sys.exit(1)
|
||
m = re.match(r"ok \d+ - (.*?)(\s+#\s*(SKIP|TODO)\b.*)?$", body, re.I)
|
||
if m:
|
||
if m.group(2):
|
||
print(f"refuse: {body}", file=sys.stderr)
|
||
sys.exit(2)
|
||
passed.add(m.group(1))
|
||
continue
|
||
m = re.match(r"# (tests|suites|pass|fail|cancelled|skipped|todo) (\d+)$", line)
|
||
if m:
|
||
counts[m.group(1)] = int(m.group(2))
|
||
for key in ("fail", "cancelled"):
|
||
if counts.get(key, 0) > 0:
|
||
print(f"refuse: {counts[key]} {key}", file=sys.stderr)
|
||
sys.exit(1)
|
||
for key in ("skipped", "todo"):
|
||
if counts.get(key, 0) > 0:
|
||
print(f"refuse: {counts[key]} {key}", file=sys.stderr)
|
||
sys.exit(2)
|
||
if counts.get("pass", 0) < 1 or not passed:
|
||
print("refuse: no passing 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 --reporter=json report. Exit 1 a failed test
|
||
# or file or an unsuccessful run, 2 a skipped or todo test, 3 no passing
|
||
# test, 4 an unreadable report, 5 a missing required file or test. Required
|
||
# files (REQUIRE_FILES, one path suffix per line) must each have a passing
|
||
# test; required names (REQUIRE_NAMES, one full test name per line) must
|
||
# pass.
|
||
detect_vitest() {
|
||
python3 - "$1" <<'PY'
|
||
import json, os, sys
|
||
path = sys.argv[1]
|
||
files = [n for n in os.environ.get("REQUIRE_FILES", "").split("\n") if n.strip()]
|
||
names = [n for n in os.environ.get("REQUIRE_NAMES", "").split("\n") if n.strip()]
|
||
try:
|
||
with open(path, encoding="utf-8") as fh:
|
||
report = json.load(fh)
|
||
except (OSError, ValueError) as err:
|
||
print(f"refuse: unreadable vitest report: {err}", file=sys.stderr)
|
||
sys.exit(4)
|
||
results = report.get("testResults") or []
|
||
bad = [r.get("name", "?") for r in results if r.get("status") != "passed"]
|
||
if bad or report.get("numFailedTests", 0) or report.get("numFailedTestSuites", 0) or report.get("success") is not True:
|
||
print("refuse: failed " + ", ".join(bad or ["run"]), file=sys.stderr)
|
||
sys.exit(1)
|
||
if report.get("numPendingTests", 0) or report.get("numTodoTests", 0):
|
||
print(f"refuse: {report.get('numPendingTests', 0)} skipped, {report.get('numTodoTests', 0)} todo", file=sys.stderr)
|
||
sys.exit(2)
|
||
passed = set()
|
||
by_file = {}
|
||
for r in results:
|
||
for a in r.get("assertionResults") or []:
|
||
status = a.get("status")
|
||
if status != "passed":
|
||
print(f"refuse: {status} {a.get('fullName')}", file=sys.stderr)
|
||
sys.exit(2 if status in ("skipped", "pending", "todo") else 1)
|
||
passed.add(a.get("fullName", ""))
|
||
by_file[r.get("name", "")] = by_file.get(r.get("name", ""), 0) + 1
|
||
if report.get("numPassedTests", 0) < 1 or not passed:
|
||
print("refuse: no passing tests", file=sys.stderr)
|
||
sys.exit(3)
|
||
missing = [f for f in files if not any(name.endswith("/" + f) and count > 0 for name, count in by_file.items())]
|
||
missing += [n for n in names if n not in passed]
|
||
if missing:
|
||
print("refuse: required files or tests did not pass: " + "; ".join(missing), file=sys.stderr)
|
||
sys.exit(5)
|
||
print(f"vitest: {report.get('numPassedTests')} passed in {len(results)} files")
|
||
PY
|
||
}
|
||
|
||
go_json() {
|
||
local dir="$1"
|
||
shift
|
||
local log err rc=0 dc=0
|
||
log="$(mktemp)"
|
||
err="$(mktemp)"
|
||
(cd "$dir" && 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 "_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"
|
||
}
|
||
|
||
# vitest_json runs a repository's vitest with the JSON reporter and checks it.
|
||
vitest_json() {
|
||
local dir="$1" report out rc=0 dc=0
|
||
report="$(mktemp)"
|
||
out="$(mktemp)"
|
||
(cd "$dir" && ./node_modules/.bin/vitest run --reporter=json --outputFile="$report") >"$out" 2>&1 || rc=$?
|
||
detect_vitest "$report" || dc=$?
|
||
if [[ "$rc" -ne 0 || "$dc" -ne 0 ]]; then
|
||
grep -E 'FAIL|Error|×' "$out" | tail -n 40 >&2 || true
|
||
rm -f "$report" "$out"
|
||
echo "refuse: vitest in $dir (test=$rc detect=$dc)" >&2
|
||
return 1
|
||
fi
|
||
rm -f "$report" "$out"
|
||
}
|
||
|
||
# node_tap runs `npm --prefix <dir> test` (node --test, TAP) and checks it.
|
||
node_tap() {
|
||
local dir="$1" log rc=0 dc=0
|
||
log="$(mktemp)"
|
||
npm --prefix "$dir" test >"$log" 2>&1 || rc=$?
|
||
detect_tap "$log" || dc=$?
|
||
if [[ "$rc" -ne 0 || "$dc" -ne 0 ]]; then
|
||
grep -E '^\s*(not ok|# )|Error' "$log" | tail -n 40 >&2 || true
|
||
rm -f "$log"
|
||
echo "refuse: npm test in $dir (test=$rc detect=$dc)" >&2
|
||
return 1
|
||
fi
|
||
grep -E '^# (tests|pass|fail) ' "$log"
|
||
rm -f "$log"
|
||
}
|
||
|
||
# same_corpus fails unless the two editor corpus copies are byte-identical.
|
||
same_corpus() {
|
||
local plugin_copy="$1" layer_copy="$2"
|
||
if [[ ! -s "$plugin_copy" || ! -s "$layer_copy" ]]; then
|
||
echo "refuse: editor corpus missing or empty: $plugin_copy $layer_copy" >&2
|
||
return 1
|
||
fi
|
||
if ! cmp -s "$plugin_copy" "$layer_copy"; then
|
||
echo "refuse: the layer's editor corpus differs from the plugin's; copy $plugin_copy to $layer_copy" >&2
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
run_self_test() {
|
||
bash -n "${BASH_SOURCE[0]}"
|
||
|
||
expect_detector detect_go pass 0 '{"Action":"pass","Package":"p","Test":"TestFieldTypeUntouched"}'
|
||
expect_detector detect_go fail 1 '{"Action":"pass","Package":"p","Test":"TestA"}
|
||
{"Action":"fail","Package":"p","Test":"TestFieldUpload"}'
|
||
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":"TestPostsAdminResaveKeepsHTML"}'
|
||
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="TestFieldTypeUntouched TestBlogEditorLoad" expect_detector detect_go missing-named 5 \
|
||
'{"Action":"pass","Package":"p","Test":"TestFieldTypeUntouched"}'
|
||
REQUIRE_TESTS="TestFieldType" expect_detector detect_go prefix-is-not-the-test 5 \
|
||
'{"Action":"pass","Package":"p","Test":"TestFieldTypeUntouched"}'
|
||
|
||
local ok='TAP version 13
|
||
ok 1 - the sink turns a stored placeholder into the iframe
|
||
1..1
|
||
# tests 1
|
||
# pass 1
|
||
# fail 0
|
||
# cancelled 0
|
||
# skipped 0
|
||
# todo 0'
|
||
expect_detector detect_tap pass 0 "$ok"
|
||
expect_detector detect_tap not-ok 1 'ok 1 - a
|
||
not ok 2 - b
|
||
# pass 1
|
||
# fail 1'
|
||
expect_detector detect_tap nested-not-ok 1 ' not ok 1 - sub
|
||
ok 1 - a
|
||
# pass 1
|
||
# fail 0'
|
||
expect_detector detect_tap fail-count 1 'ok 1 - a
|
||
# pass 1
|
||
# fail 1'
|
||
expect_detector detect_tap skip 2 'ok 1 - a # SKIP later
|
||
# pass 1'
|
||
expect_detector detect_tap todo-count 2 'ok 1 - a
|
||
# pass 1
|
||
# todo 1'
|
||
expect_detector detect_tap zero 3 '# tests 0
|
||
# pass 0
|
||
# fail 0'
|
||
REQUIRE_NAMES=$'the sink turns a stored placeholder into the iframe\nmissing' expect_detector detect_tap missing-named 5 "$ok"
|
||
|
||
local vok='{"success":true,"numPassedTests":2,"numFailedTests":0,"numFailedTestSuites":0,"numPendingTests":0,"numTodoTests":0,"testResults":[{"name":"/r/tests/form/PluginField.test.ts","status":"passed","assertionResults":[{"fullName":"PluginField a","status":"passed"},{"fullName":"PluginField b","status":"passed"}]}]}'
|
||
expect_detector detect_vitest pass 0 "$vok"
|
||
REQUIRE_FILES='tests/form/PluginField.test.ts' REQUIRE_NAMES='PluginField a' expect_detector detect_vitest named 0 "$vok"
|
||
expect_detector detect_vitest failed-test 1 '{"success":false,"numPassedTests":1,"numFailedTests":1,"testResults":[{"name":"/r/a.test.ts","status":"failed","assertionResults":[{"fullName":"x","status":"failed"}]}]}'
|
||
expect_detector detect_vitest failed-file 1 '{"success":false,"numPassedTests":0,"numFailedTests":0,"numFailedTestSuites":1,"testResults":[{"name":"/r/a.test.ts","status":"failed","assertionResults":[]}]}'
|
||
expect_detector detect_vitest unsuccessful 1 '{"success":false,"numPassedTests":1,"numFailedTests":0,"testResults":[{"name":"/r/a.test.ts","status":"passed","assertionResults":[{"fullName":"x","status":"passed"}]}]}'
|
||
expect_detector detect_vitest skipped 2 '{"success":true,"numPassedTests":1,"numPendingTests":1,"testResults":[{"name":"/r/a.test.ts","status":"passed","assertionResults":[{"fullName":"x","status":"passed"},{"fullName":"y","status":"skipped"}]}]}'
|
||
expect_detector detect_vitest todo 2 '{"success":true,"numPassedTests":1,"numTodoTests":1,"testResults":[{"name":"/r/a.test.ts","status":"passed","assertionResults":[{"fullName":"x","status":"passed"}]}]}'
|
||
expect_detector detect_vitest zero 3 '{"success":true,"numPassedTests":0,"testResults":[]}'
|
||
expect_detector detect_vitest unreadable 4 'not json'
|
||
REQUIRE_FILES='tests/blogeditor/element.test.ts' expect_detector detect_vitest missing-file 5 "$vok"
|
||
REQUIRE_NAMES='PluginField c' expect_detector detect_vitest missing-name 5 "$vok"
|
||
|
||
# The corpus comparison fails on a planted difference and on a missing copy.
|
||
local a b
|
||
a="$(mktemp)"
|
||
b="$(mktemp)"
|
||
printf '[{"name":"x","html":"<p>a</p>"}]\n' >"$a"
|
||
cp "$a" "$b"
|
||
same_corpus "$a" "$b" || {
|
||
echo "refuse: self-test corpus: identical copies refused" >&2
|
||
rm -f "$a" "$b"
|
||
return 1
|
||
}
|
||
printf '[{"name":"x","html":"<p>b</p>"}]\n' >"$b"
|
||
if same_corpus "$a" "$b" 2>/dev/null; then
|
||
echo "refuse: self-test corpus: a planted difference passed" >&2
|
||
rm -f "$a" "$b"
|
||
return 1
|
||
fi
|
||
: >"$b"
|
||
if same_corpus "$a" "$b" 2>/dev/null; then
|
||
echo "refuse: self-test corpus: an empty copy passed" >&2
|
||
rm -f "$a" "$b"
|
||
return 1
|
||
fi
|
||
rm -f "$a" "$b"
|
||
|
||
local mode
|
||
for mode in self-test go admin editor layer docs postgres all; do
|
||
grep -q -- "^ --$mode" "${BASH_SOURCE[0]}" || {
|
||
echo "refuse: missing mode --$mode" >&2
|
||
return 1
|
||
}
|
||
done
|
||
echo "phase16.2 self-test passed"
|
||
}
|
||
|
||
require_dir() {
|
||
[[ -d "$1" ]] || {
|
||
echo "refuse: $2 $1 not found" >&2
|
||
return 1
|
||
}
|
||
}
|
||
|
||
run_go() {
|
||
require_dir "$JOURNAL" "Journal plugin"
|
||
(cd "$ROOT" && go vet ./...)
|
||
REQUIRE_TESTS="${FRAMEWORK_REQUIRE[*]}" go_json "$ROOT" -count=1 -run "$FRAMEWORK_RUN" ./modules/pact ./modules/cabana
|
||
go -C "$JOURNAL" vet ./...
|
||
REQUIRE_TESTS="${JOURNAL_REQUIRE[*]}" go_json "$JOURNAL" -count=1 ./classes/ ./controllers/...
|
||
if ! git -C "$JOURNAL" diff --quiet "$POLICY_BASE" -- classes/format_html_test.go classes/check_html_test.go classes/format_translation_test.go; then
|
||
echo "refuse: the 16.1 policy tests changed since $POLICY_BASE" >&2
|
||
return 1
|
||
fi
|
||
echo "phase16.2 go passed"
|
||
}
|
||
|
||
run_admin() {
|
||
(cd "$ROOT/admin" && ./node_modules/.bin/vue-tsc --noEmit)
|
||
REQUIRE_FILES="$(printf '%s\n' "${ADMIN_FILES[@]}")" REQUIRE_NAMES="$(printf '%s\n' "${ADMIN_REQUIRE[@]}")" vitest_json "$ROOT/admin"
|
||
echo "phase16.2 admin passed"
|
||
}
|
||
|
||
run_editor() {
|
||
require_dir "$JOURNAL" "Journal plugin"
|
||
(cd "$JOURNAL" && ./node_modules/.bin/tsc --noEmit)
|
||
REQUIRE_FILES="$(printf '%s\n' "${EDITOR_FILES[@]}")" REQUIRE_NAMES="$(printf '%s\n' "${EDITOR_REQUIRE[@]}")" vitest_json "$JOURNAL"
|
||
echo "phase16.2 editor passed"
|
||
}
|
||
|
||
run_layer() {
|
||
require_dir "$LAYER" "layer repository"
|
||
REQUIRE_NAMES="$(printf '%s\n' "${LAYER_REQUIRE[@]}")" node_tap "$LAYER"
|
||
same_corpus "$JOURNAL/tests/blogeditor/fixtures/editor-corpus.json" "$LAYER/tests/fixtures/editor-corpus.json"
|
||
echo "corpus: the layer copy equals the plugin's"
|
||
echo "phase16.2 layer passed"
|
||
}
|
||
|
||
run_docs() {
|
||
require_dir "$JOURNAL" "Journal plugin"
|
||
REQUIRE_TESTS="TestDocsTree" go_json "$ROOT" -count=1 -run '^TestDocsTree$' ./cmd/summer
|
||
bash "$ROOT/scripts/check-admin-openapi.sh" --check
|
||
bash "$ROOT/scripts/check-admin-dist.sh"
|
||
bash "$JOURNAL/scripts/check-dist.sh"
|
||
echo "phase16.2 docs passed"
|
||
}
|
||
|
||
docker_up() {
|
||
command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1
|
||
}
|
||
|
||
run_postgres() {
|
||
require_dir "$JOURNAL" "Journal plugin"
|
||
if ! docker_up; then
|
||
echo "refuse: the postgres stage needs a reachable Docker daemon (testcontainers); run it on a Docker host" >&2
|
||
return 1
|
||
fi
|
||
REQUIRE_TESTS="${PG_FRAMEWORK_REQUIRE[*]}" go_json "$ROOT" -count=1 -timeout 15m ./modules/cabana
|
||
REQUIRE_TESTS="${PG_JOURNAL_REQUIRE[*]}" go_json "$JOURNAL" -count=1 -timeout 20m ./...
|
||
echo "phase16.2 postgres passed"
|
||
}
|
||
|
||
run_all() {
|
||
local stage
|
||
for stage in self-test go admin editor layer docs; do
|
||
if bash "${BASH_SOURCE[0]}" "--$stage"; then
|
||
echo "PASS $stage"
|
||
else
|
||
echo "FAIL $stage"
|
||
exit 1
|
||
fi
|
||
done
|
||
if docker_up; then
|
||
if bash "${BASH_SOURCE[0]}" --postgres; then
|
||
echo "PASS postgres"
|
||
else
|
||
echo "FAIL postgres"
|
||
exit 1
|
||
fi
|
||
elif [[ "${PHASE162_REQUIRE_POSTGRES:-0}" == "1" ]]; then
|
||
echo "FAIL postgres: no reachable Docker daemon and PHASE162_REQUIRE_POSTGRES=1"
|
||
exit 1
|
||
else
|
||
cat >&2 <<'EOF'
|
||
************************************************************************
|
||
NOT RUN postgres: no reachable Docker daemon on this host.
|
||
The testcontainers suites (TestFieldTypeSavePostgres, the Journal admin
|
||
harness incl. TestPostsAdminResaveKeepsHTML, TestPostsAdminSaveSanitizesHTML,
|
||
TestCategoriesAdminContent, TestCategoriesAdminBlogEditor) did NOT run.
|
||
Run on a Docker host before /msd-verify-work:
|
||
bash scripts/check-phase16.2.sh postgres
|
||
************************************************************************
|
||
EOF
|
||
echo "NOT RUN postgres (Docker-host only; see the notice above)"
|
||
fi
|
||
echo "Phase 16.2 gate passed"
|
||
}
|
||
|
||
case "${1:---all}" in
|
||
--self-test | self-test) run_self_test ;;
|
||
--go | go) run_go ;;
|
||
--admin | admin) run_admin ;;
|
||
--editor | editor) run_editor ;;
|
||
--layer | layer) run_layer ;;
|
||
--docs | docs) run_docs ;;
|
||
--postgres | postgres) run_postgres ;;
|
||
--all | all) run_all ;;
|
||
*) usage ;;
|
||
esac
|