fix(scripts): green check-phase10 hygiene after Phase 17 and run it from check-phase17 (T-10-24, T-10-25)
- line-exact, path-bound T-17-10 raw-HTML exceptions for the RichEditorField read-only display and the inert template parse in lossy.ts - dist-origin allow-list adds the never-requested literals https://prosemirror.net and https://admin.invalid - self-test plants: the display line in another file, a template parse elsewhere, a foreign origin in the dist JS - --spa also runs the Vitest browser suite, fail-closed - check-phase17: browser-suite required names in the admin stage, Phase 10 hygiene in the docs stage, asserted by its self-test
This commit is contained in:
@@ -4,6 +4,13 @@
|
||||
# named test that did not run, OpenAPI or dist drift, a hygiene violation or
|
||||
# an evidence gap. --self-test proves each detector fails closed.
|
||||
#
|
||||
# Hygiene exceptions are line-exact and bound to their file: the MarkdownField
|
||||
# preview (server-rendered), and since Phase 17 (T-17-10) the RichEditorField
|
||||
# read-only view (DOMPurify displayHtml, T-10-16) and the inert template parse
|
||||
# in richeditor/lossy.ts. The dist-origin rule allows only the literals that
|
||||
# are never requested (namespace URIs, docs links in error text, the
|
||||
# reserved-TLD parse base of isAllowedLink).
|
||||
#
|
||||
# Allow-list: none. The fonoteka.go parity failures TestMigrateSeedsCanonicalGenres
|
||||
# and TestSchemaMatchesPHPSnapshot, accepted until then, pass since fonoteka.go
|
||||
# 21c0f12 (fix(09): update parity expectations for the backend admin schema),
|
||||
@@ -224,9 +231,10 @@ run_self_test() {
|
||||
done
|
||||
|
||||
# The hygiene stage passes on a scratch copy of the tree and fails once a
|
||||
# raw-HTML directive, a direct fetch, an XMLHttpRequest outside the files.ts
|
||||
# upload, an app name, a browser-storage call or an extra admin/src/api file
|
||||
# is planted in it.
|
||||
# raw-HTML directive (also the RichEditorField display line or the lossy.ts
|
||||
# template parse moved to another file), a direct fetch, an XMLHttpRequest
|
||||
# outside the files.ts upload, an app name, a browser-storage call, an extra
|
||||
# admin/src/api file or another origin in the dist JS is planted in it.
|
||||
local scratch
|
||||
scratch="$(mktemp -d)"
|
||||
trap 'rm -rf "$scratch"' RETURN
|
||||
@@ -242,8 +250,9 @@ run_self_test() {
|
||||
# Each plant is imported by a scratch test, so only its own rule can fire;
|
||||
# the refusal must name that rule.
|
||||
local plant want out
|
||||
for plant in vhtml fetch appname storage xhr apifile; do
|
||||
rm -rf "$scratch/admin/src/__plant" "$scratch/admin/tests/__plant.test.ts" "$scratch/admin/src/api/__plant.ts"
|
||||
for plant in vhtml vhtml-elsewhere template-parse fetch appname storage xhr apifile origin; do
|
||||
rm -rf "$scratch/admin/src/__plant" "$scratch/admin/tests/__plant.test.ts" "$scratch/admin/src/api/__plant.ts" \
|
||||
"$scratch/modules/boardwalk/dist/assets/__plant.js"
|
||||
mkdir -p "$scratch/admin/src/__plant"
|
||||
case "$plant" in
|
||||
vhtml)
|
||||
@@ -251,6 +260,20 @@ run_self_test() {
|
||||
printf "import Plant from '../src/__plant/Plant.vue'\n" >"$scratch/admin/tests/__plant.test.ts"
|
||||
want="raw-HTML directive"
|
||||
;;
|
||||
vhtml-elsewhere)
|
||||
# The RichEditorField exception is bound to its path.
|
||||
printf '<template>\n%s\n</template>\n' " <div v-if=\"display !== ''\" class=\"summer-markdown\" v-html=\"display\" />" >"$scratch/admin/src/__plant/Plant.vue"
|
||||
printf "import Plant from '../src/__plant/Plant.vue'\n" >"$scratch/admin/tests/__plant.test.ts"
|
||||
want="raw-HTML directive"
|
||||
;;
|
||||
template-parse)
|
||||
printf "export function parse(html: string) {\n const template = document.createElement('template')\n template.innerHTML = html\n return template.content\n}\n" >"$scratch/admin/src/__plant/plant.ts"
|
||||
want="raw-HTML directive"
|
||||
;;
|
||||
origin)
|
||||
printf 'const u="https://evil.example/x";export{u};\n' >"$scratch/modules/boardwalk/dist/assets/__plant.js"
|
||||
want="JS names another origin"
|
||||
;;
|
||||
fetch)
|
||||
printf 'export const load = () => fetch("/x")\n' >"$scratch/admin/src/__plant/plant.ts"
|
||||
want="direct fetch"
|
||||
@@ -330,6 +353,19 @@ run_spa() {
|
||||
exit 1
|
||||
fi
|
||||
grep -E 'Test Files|Tests ' "$log" || true
|
||||
# The richeditor display needs a real engine (T-10-16): Vitest browser mode
|
||||
# in Playwright Chromium, refused the same fail-closed way.
|
||||
set +e
|
||||
npm --prefix "$ROOT/admin" run test:browser >"$log" 2>&1
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ "$rc" -ne 0 ]] || grep -qE 'No test files found|Unhandled|FAIL | skipped| todo' "$log"; then
|
||||
tail -n 60 "$log" >&2
|
||||
rm -f "$log"
|
||||
echo "refuse: admin Vitest browser run failed (exit $rc)" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -E 'Test Files|Tests ' "$log" || true
|
||||
rm -f "$log"
|
||||
echo "phase10 spa passed"
|
||||
}
|
||||
@@ -364,9 +400,16 @@ hygiene_checks() {
|
||||
admin/src admin/tests admin/openapi modules/boardwalk modules/cabana modules/phrasebook 2>/dev/null || true)"
|
||||
[[ -z "$hits" ]] || fail "application names in the framework: $hits"
|
||||
# Plugin and server strings are rendered as text only.
|
||||
# The markdown preview binds only the HTML POST /markdown/preview answers (cabana.RenderMarkdown); every other raw-HTML sink is still refused.
|
||||
# The markdown preview binds only the HTML POST /markdown/preview answers (cabana.RenderMarkdown).
|
||||
# T-17-10: the RichEditorField read-only view binds only displayHtml output (DOMPurify, T-10-16), never the stored value.
|
||||
# T-17-10: lossy.ts parses into a template, whose contents are inert and are read for tag names only, never bound.
|
||||
# Each exception is the exact line in its own file; every other raw-HTML sink is still refused.
|
||||
local richeditor_display
|
||||
richeditor_display="^admin/src/components/form/fields/RichEditorField\\.vue:[0-9]+:[[:space:]]*<div v-if=\"display !== ''\" class=\"summer-markdown\" v-html=\"display\" />[[:space:]]*\$"
|
||||
hits="$(cd "$tree" && grep -rnE 'v-html|innerHTML|outerHTML|insertAdjacentHTML' admin/src 2>/dev/null |
|
||||
grep -vE '^admin/src/components/form/fields/MarkdownField\.vue:[0-9]+:[[:space:]]*v-html="sanitizedHtml"[[:space:]]*$' || true)"
|
||||
grep -vE -e '^admin/src/components/form/fields/MarkdownField\.vue:[0-9]+:[[:space:]]*v-html="sanitizedHtml"[[:space:]]*$' \
|
||||
-e "$richeditor_display" \
|
||||
-e '^admin/src/components/form/richeditor/lossy\.ts:[0-9]+:[[:space:]]*template\.innerHTML = html[[:space:]]*$' || true)"
|
||||
[[ -z "$hits" ]] || fail "raw-HTML directive in admin/src: $hits"
|
||||
# All HTTP goes through the typed openapi-fetch client.
|
||||
# Uploads in admin/src/api/files.ts use XMLHttpRequest because only XHR reports upload progress (12.2-04); that XHR sends X-Requested-With (T-12.2-32) and refreshes through client.ts; every other XHR, fetch or axios call is still refused.
|
||||
@@ -387,11 +430,13 @@ hygiene_checks() {
|
||||
hits="$(cd "$tree" && grep -rnE 'localStorage|sessionStorage|indexedDB|document\.cookie' admin/src 2>/dev/null |
|
||||
grep -v -e '^admin/src/state/useSidebar.ts:' -e '^admin/src/state/listState.ts:' || true)"
|
||||
[[ -z "$hits" ]] || fail "browser storage outside admin/src/state/useSidebar.ts and admin/src/state/listState.ts: $hits"
|
||||
# The embedded build loads nothing from another origin.
|
||||
# The embedded build loads nothing from another origin. Allowed literals are
|
||||
# never requested: https://prosemirror.net is ProseMirror error text and
|
||||
# https://admin.invalid the reserved-TLD parse base in isAllowedLink.
|
||||
hits="$(cd "$tree" && grep -noE '(src|href)="(https?:)?//[^"]*"|url\((["'"'"']?)(https?:)?//' modules/boardwalk/dist/index.html modules/boardwalk/dist/assets/*.css 2>/dev/null || true)"
|
||||
[[ -z "$hits" ]] || fail "modules/boardwalk/dist references another origin: $hits"
|
||||
hits="$(cd "$tree" && grep -ohE 'https?://[A-Za-z0-9.-]+' modules/boardwalk/dist/assets/*.js 2>/dev/null | sort -u |
|
||||
grep -vxE 'http://www\.w3\.org|https://vuejs\.org|http://local' || true)"
|
||||
grep -vxE 'http://www\.w3\.org|https://vuejs\.org|http://local|https://prosemirror\.net|https://admin\.invalid' || true)"
|
||||
[[ -z "$hits" ]] || fail "modules/boardwalk/dist JS names another origin: $hits"
|
||||
# Icons: named imports only, and never the deprecated lucide package.
|
||||
hits="$(cd "$tree" && grep -rnE "import \* as [A-Za-z_]+ from '@lucide/vue'|lucide-vue-next" admin/src admin/package.json 2>/dev/null || true)"
|
||||
|
||||
@@ -11,12 +11,15 @@
|
||||
# name (-short).
|
||||
# plugins go vet ./... in Journal, sitemap, menu, FAQ and the host; their
|
||||
# Docker-free suites by required name (-short).
|
||||
# admin the admin SPA typecheck and the vitest JSON report with required
|
||||
# spec files and test names.
|
||||
# admin the admin SPA typecheck and the vitest JSON reports with
|
||||
# required spec files and test names: the happy-dom suite and the
|
||||
# browser suite (vitest.browser.config.ts, Playwright Chromium;
|
||||
# the richeditor read-only display, T-17-10).
|
||||
# layers pnpm test (node --test, TAP) in both layers with required test
|
||||
# files and names.
|
||||
# docs TestDocsTree, summer docs:build --check, check-admin-openapi.sh
|
||||
# --check, check-admin-dist.sh.
|
||||
# --check, check-admin-dist.sh, and check-phase10.sh --hygiene, so
|
||||
# admin changes made here cannot silently break Phase 10's gate.
|
||||
# postgres Docker host only: the testcontainers suites by required name in
|
||||
# every Go repository. Without a reachable Docker daemon this
|
||||
# stage fails.
|
||||
@@ -116,7 +119,6 @@ ADMIN_REQUIRE=(
|
||||
"RichEditorField renders read-only values through the display policy, never as stored"
|
||||
"richeditor policy helpers accepts policy links only"
|
||||
"richeditor policy helpers compares tag names case-insensitively for the lossy note"
|
||||
"richeditor policy helpers unwraps unknown elements and drops scripts for display"
|
||||
"RichEditorField value contract an empty paragraph is the empty string, any content its HTML"
|
||||
"RichEditorField value contract a value from outside replaces the document and leaves the field untouched"
|
||||
"RichEditorField value contract shows the lossy note only for markup outside the editor schema"
|
||||
@@ -142,6 +144,18 @@ ADMIN_REQUIRE=(
|
||||
"reorder tree rules refuses a drop into the node itself or its own subtree and leaves the tree as it was"
|
||||
"reorder tree rules moves only where a sibling or parent exists, and flattens pre-order from 1"
|
||||
)
|
||||
# admin stage, browser suite (vitest.browser.config.ts): spec files and full
|
||||
# test names, copied from a --reporter=json run.
|
||||
ADMIN_BROWSER_FILES=(tests/browser/richeditor-display.test.ts)
|
||||
ADMIN_BROWSER_REQUIRE=(
|
||||
"richeditor display in a real browser engine runs where form controls clobber form properties"
|
||||
"richeditor display in a real browser engine drops clobbered forms with everything inside them"
|
||||
"richeditor display in a real browser engine keeps a form control named localName from breaking the view"
|
||||
"richeditor display in a real browser engine drops an off-origin form, iframes, meta refresh and disallowed links"
|
||||
"richeditor display in a real browser engine parses into an inert document, so nothing loads before the value is bound"
|
||||
"richeditor policy helpers unwraps unknown elements and drops scripts for display"
|
||||
"RichEditorField renders read-only values exactly through the display policy"
|
||||
)
|
||||
|
||||
# layers stage: test files (must exist and match the test script's glob) and
|
||||
# TAP test names.
|
||||
@@ -389,11 +403,13 @@ go_named() {
|
||||
REQUIRE_TESTS="${names[*]}" go_json "$dir" $flags -count=1 -run "$regex" $pkgs
|
||||
}
|
||||
|
||||
# vitest_json DIR [VITEST ARGS...]: extra arguments (such as --config) go to vitest.
|
||||
vitest_json() {
|
||||
local dir="$1" report out rc=0 dc=0
|
||||
shift
|
||||
report="$(mktemp)"
|
||||
out="$(mktemp)"
|
||||
(cd "$dir" && ./node_modules/.bin/vitest run --reporter=json --outputFile="$report") >"$out" 2>&1 || rc=$?
|
||||
(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
|
||||
@@ -529,6 +545,11 @@ ok 1 - a
|
||||
REQUIRE_FILES='tests/a.ts' expect_files outside-glob 5 "$tmp" || { rm -rf "$tmp"; return 1; }
|
||||
rm -rf "$tmp"
|
||||
|
||||
grep -qF 'bash "$ROOT/scripts/check-phase10.sh" --hygiene' "${BASH_SOURCE[0]}" || {
|
||||
echo "refuse: the docs stage does not run check-phase10.sh --hygiene" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
local mode
|
||||
for mode in self-test go plugins admin layers docs postgres all; do
|
||||
grep -q -- "^ --$mode" "${BASH_SOURCE[0]}" || {
|
||||
@@ -583,6 +604,8 @@ run_plugins() {
|
||||
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"
|
||||
REQUIRE_FILES="$(printf '%s\n' "${ADMIN_BROWSER_FILES[@]}")" REQUIRE_NAMES="$(printf '%s\n' "${ADMIN_BROWSER_REQUIRE[@]}")" \
|
||||
vitest_json "$ROOT/admin" --config vitest.browser.config.ts
|
||||
echo "phase17 admin passed"
|
||||
}
|
||||
|
||||
@@ -599,6 +622,9 @@ run_docs() {
|
||||
(cd "$ROOT" && go run ./cmd/summer docs:build --check)
|
||||
bash "$ROOT/scripts/check-admin-openapi.sh" --check
|
||||
bash "$ROOT/scripts/check-admin-dist.sh"
|
||||
# Cross-phase: Phase 10's SPA hygiene (raw-HTML sinks, dist origins, every
|
||||
# module imported by a test) must stay green after Phase 17 admin changes.
|
||||
bash "$ROOT/scripts/check-phase10.sh" --hygiene
|
||||
echo "phase17 docs passed"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user