diff --git a/scripts/check-phase33.sh b/scripts/check-phase33.sh index 4eaeb7fe..f6c497c5 100755 --- a/scripts/check-phase33.sh +++ b/scripts/check-phase33.sh @@ -4,7 +4,7 @@ # plugins, the four Nuxt layers and the applications that consume them. # # Stages: -# self-test the go test -json and vitest detectors on planted output, a +# self-test the go test -json, vitest and TAP detectors on planted output, a # failing go test in a planted module, the marker rule on a # planted repository, the missing-repository failure, the docker # NOT RUN rule on a faked host, and the additive comparison on @@ -18,12 +18,18 @@ # required name (*_UNIT_TESTS); npm run test (vitest) in # sm-seo-plugin and sm-journal-plugin. # layers vue-seo-layer, vue-journal-layer, vue-menu-layer and -# vue-faq-layer: npm run test, npm run build:playground and the -# layer's SSR smoke; node testing/guard-check.mjs in -# vue-seo-layer (the build-guard fixtures). +# vue-faq-layer: npm run test (node --test, an all-passing TAP +# summary), npm run build:playground and the layer's SSR +# smokes; in vue-seo-layer guard:check (the build-guard +# fixtures) and purge:check; in vue-journal-layer every +# expectation file (expect-acme-title-h1 and -title-p with +# NUXT_PUBLIC_JOURNAL_POST_TITLE_TAG), the purge checks and +# check-git-install.sh --self-test. # consumers sm-demo-app, sm-grzybyfunkcjonalne-app, sm-summercmsio-app and # sm-bm-app: the workspace check and go build ./...; npm run -# build in each Nuxt app (vue-*/ with a build script). +# build in each Nuxt app (vue-*/ with a build script); the +# 33-12 and 33-13 app patches pass git apply --check, and the +# 33-14 demo locale patches apply on top of the 33-12 ones. # docker Docker host only: the Postgres suites (go test ./... without # -short) of the three Go plugins with the phase's suites by # required name (*_DOCKER_TESTS; TestChromeRenderer too when a @@ -404,6 +410,46 @@ npm_vitest() { rm -f "$log" } +# detect_tap reads a node --test --test-reporter=tap run. Exit 1 a failed +# test ("not ok", "# fail" or "# cancelled" above 0), 2 skipped or todo +# tests, 3 no all-passing summary. +detect_tap() { + python3 - "$1" <<'PY' +import re, sys +text = open(sys.argv[1], encoding="utf-8", errors="replace").read() +text = re.sub(r"\x1b\[[0-9;]*m", "", text) +counts = {k: int(v) for k, v in re.findall(r"^# (tests|pass|fail|skipped|todo|cancelled) (\d+)\s*$", text, re.M)} +if re.search(r"^\s*not ok\b", text, re.M) or counts.get("fail", 0) > 0 or counts.get("cancelled", 0) > 0: + print("refuse: TAP reports failed tests", file=sys.stderr) + sys.exit(1) +if counts.get("skipped", 0) > 0 or counts.get("todo", 0) > 0 or re.search(r"^\s*ok .*# (SKIP|TODO)\b", text, re.M | re.I): + print("refuse: TAP reports skipped or todo tests", file=sys.stderr) + sys.exit(2) +if "fail" not in counts or counts.get("pass", 0) == 0 or counts.get("tests", -1) != counts.get("pass"): + print("refuse: no all-passing TAP summary", file=sys.stderr) + sys.exit(3) +PY +} + +# node_tap DIR runs npm run test (node --test with the TAP reporter) in DIR +# and requires an all-passing TAP summary. +node_tap() { + local dir="$1" log rc=0 dc=0 + require_script "$dir" test + log="$(mktemp)" + npm --prefix "$dir" run test >"$log" 2>&1 || rc=$? + detect_tap "$log" || dc=$? + if [[ "$rc" -ne 0 || "$dc" -ne 0 ]]; then + grep -E '^\s*not ok|# (fail|skipped|todo)|Error' "$log" | head -n 40 >&2 || true + tail -n 20 "$log" >&2 + rm -f "$log" + echo "refuse: npm run test in $dir (test=$rc detect=$dc)" >&2 + return 1 + fi + echo "tap: $dir $(grep -E '^# (tests|pass|fail) ' "$log" | tr '\n' ' ')" + rm -f "$log" +} + # require_script DIR NAME refuses DIR without node_modules or without the # npm script NAME in its package.json. require_script() { @@ -515,6 +561,25 @@ run_self_test() { expect_detector detect_vitest failed 1 ' Test Files 1 failed | 1 passed (2) Tests 1 failed | 16 passed (17)' expect_detector detect_vitest no-summary 3 ' ✓ tests/a.test.ts (12 tests) 15ms' + local tap='TAP version 13 +ok 1 - a +ok 2 - b +1..2 +# tests 2 +# suites 0 +# pass 2 +# fail 0 +# cancelled 0 +# skipped 0 +# todo 0' + expect_detector detect_tap pass 0 "$tap" + expect_detector detect_tap not-ok 1 "${tap/ok 2 - b/not ok 2 - b}" + expect_detector detect_tap fail-count 1 "${tap/\# fail 0/\# fail 1}" + expect_detector detect_tap skipped 2 "${tap/\# skipped 0/\# skipped 1}" + expect_detector detect_tap skip-directive 2 "${tap/ok 2 - b/ok 2 - b # SKIP not here}" + expect_detector detect_tap no-summary 3 'TAP version 13 +ok 1 - a' + expect_detector detect_tap zero 3 "$(printf '# tests 0\n# pass 0\n# fail 0')" local tmp tmp="$(mktemp -d)" @@ -695,14 +760,29 @@ run_layers() { local layer require_all "Nuxt layer" "$SEO_LAYER" "$JOURNAL_LAYER" "$MENU_LAYER" "$FAQ_LAYER" for layer in "$SEO_LAYER" "$JOURNAL_LAYER" "$MENU_LAYER" "$FAQ_LAYER"; do - npm_vitest "$layer" + node_tap "$layer" npm_script "$layer" build:playground done + # vue-seo-layer: every smoke expectation (the layer on, off, the CMS + # down, without the 404 token, with drift), the build-guard fixtures + # and the purge checks. npm_script "$SEO_LAYER" smoke:ssr - node_run "$JOURNAL_LAYER" testing/smoke-ssr.mjs --output .playground/.output --fixtures testing/fixtures/acme --expect testing/expect-acme.txt + npm_script "$SEO_LAYER" guard:check + npm_script "$SEO_LAYER" purge:check + # vue-journal-layer: the acme expectations, the post title forced to h1 + # and to p (postTitleTag), the link hint and the breadcrumbs off, the + # purge checks and the git-install lockfile reading. + local journal_smoke=(testing/smoke-ssr.mjs --output .playground/.output --fixtures testing/fixtures/acme) + node_run "$JOURNAL_LAYER" "${journal_smoke[@]}" --expect testing/expect-acme.txt + node_run "$JOURNAL_LAYER" "${journal_smoke[@]}" --expect testing/expect-acme-title-h1.txt --env NUXT_PUBLIC_JOURNAL_POST_TITLE_TAG=h1 + node_run "$JOURNAL_LAYER" "${journal_smoke[@]}" --expect testing/expect-acme-title-p.txt --env NUXT_PUBLIC_JOURNAL_POST_TITLE_TAG=p + node_run "$JOURNAL_LAYER" "${journal_smoke[@]}" --expect testing/expect-acme-hint-off.txt --env NUXT_PUBLIC_SEO_LINKS_NEW_TAB_HINT=false + node_run "$JOURNAL_LAYER" "${journal_smoke[@]}" --expect testing/expect-acme-breadcrumbs-off.txt --env NUXT_PUBLIC_JOURNAL_BREADCRUMBS=false + node_run "$JOURNAL_LAYER" testing/purge-check.mjs --output .playground/.output --fixtures testing/fixtures/acme --kind post --slug hello-acme --path /blog/hello-acme + node_run "$JOURNAL_LAYER" testing/purge-token-check.mjs --output .playground/.output --fixtures testing/fixtures/acme --path /blog/hello-acme + (cd "$JOURNAL_LAYER" && bash scripts/check-git-install.sh --self-test) npm_script "$MENU_LAYER" smoke:ssr npm_script "$FAQ_LAYER" smoke:ssr - node_run "$SEO_LAYER" testing/guard-check.mjs echo "phase33 layers passed" } @@ -718,9 +798,54 @@ run_consumers() { npm_script "$(dirname "$pkg")" build done done + # The app patches of plans 33-12, 33-13 and 33-14 apply to the apps as + # committed; 33-14's demo locale patches apply on top of the 33-12 demo + # patches (checked in scratch worktrees). + patch_check "$DEMO_APP" 33-12-sm-demo-app.patch + patch_check "$DEMO_APP/vue-demo-app" 33-12-vue-demo-app.patch + patch_check "$SUMMERCMSIO_APP" 33-12-sm-summercmsio-app.patch + patch_check "$SUMMERCMSIO_APP/vue-summercmsio-app" 33-12-vue-summercmsio-app.patch + patch_check "$GRZYBY_APP" 33-13-sm-grzybyfunkcjonalne-app.patch + patch_check "$GRZYBY_APP/vue-grzybyfunkcjonalne-app" 33-13-vue-grzybyfunkcjonalne-app.patch + scratch_init + stacked_patch_check "$DEMO_APP/vue-demo-app" 33-12-vue-demo-app.patch 33-14-vue-demo-app-locale.patch + if [[ -f "$PHASE_DIR/33-14-sm-demo-app-locale.patch" ]]; then + stacked_patch_check "$DEMO_APP" 33-12-sm-demo-app.patch 33-14-sm-demo-app-locale.patch + fi echo "phase33 consumers passed" } +# PHASE_DIR holds the app patches. +PHASE_DIR="$ROOT/.planning/phases/33-seo-output-audit-and-content" + +# patch_check REPO PATCH runs git apply --check of a phase patch in REPO +# (--allow-empty: an empty patch, such as summercms.io's Go side, passes). +patch_check() { + local repo="$1" patch="$PHASE_DIR/$2" + require_dir "$repo" "patch target" + require_file "$patch" "app patch" + if ! git -C "$repo" apply --check --allow-empty "$patch"; then + echo "refuse: $2 does not apply to $repo" >&2 + return 1 + fi + echo "patch: $2 applies to $repo" +} + +# stacked_patch_check REPO BASE PATCH applies BASE in a scratch worktree of +# REPO's HEAD and runs git apply --check of PATCH on top of it. +stacked_patch_check() { + local repo="$1" base="$PHASE_DIR/$2" patch="$PHASE_DIR/$3" wt + require_file "$base" "app patch" + require_file "$patch" "app patch" + wt="$SCRATCH/stack-$(basename "$repo")-$RANDOM" + add_worktree "$repo" "$wt" HEAD + if ! git -C "$wt" apply --allow-empty "$base" || ! git -C "$wt" apply --check --allow-empty "$patch"; then + echo "refuse: $3 does not apply to $repo with $2" >&2 + return 1 + fi + echo "patch: $3 applies to $repo on top of $2" +} + docker_up() { command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1 }