#!/usr/bin/env bash set -euo pipefail cd "$(dirname "$0")/.." python3 scripts/test_capy_doc_generators.py python3 scripts/check_capy_doc_links.py --self-test python3 scripts/build_site_highlighter.py --check node scripts/test_site_highlighter.js python3 scripts/test_doc_example_results.py python3 scripts/test_run_doc_examples.py python3 scripts/test_doc_example_contexts.py python3 scripts/run_doc_examples.py --check host="${BEARER_TEST_HTTP_HOST:-bearer.openfu.com}" base="${BEARER_TEST_HTTP_BASE:-http://127.0.0.1}/examples/capy-reference" reference_dir=site/examples/capy-reference [[ -f "$reference_dir/index.capy" && -f "$reference_dir/style.css" ]] pages=(index syntax types ownership interop status) targets=(install-and-first-program source-structure-and-syntax values-and-types functions-and-closures units-components-and-exports errors-testing-and-style) for index in "${!pages[@]}"; do path="$base/${pages[$index]}.capy" headers=$(curl -fsS --max-time 30 -o /dev/null -D - -H "Host: $host" "$path" | tr -d '\r') [[ "$headers" == *"HTTP/1.1 302"* && "$headers" == *"Location: /doc/guide/${targets[$index]}/"* ]] || { echo "Capy reference redirect mismatch for $path" >&2; exit 1; } done doc_base="${BEARER_TEST_HTTP_BASE:-http://127.0.0.1}/doc/" headers=$(curl -fsS --max-time 30 -o /dev/null -D - -H "Host: $host" "${doc_base}type/dvalue/" | tr -d "\r") [[ "$headers" == *"HTTP/1.1 301"* && "$headers" == *"Location: /doc/type/any/"* ]] || { echo "Dynamic value documentation redirect failed" >&2; exit 1; } python3 - <<'PY' from pathlib import Path root = Path.cwd() if (root / "site/doc/components/doc_page.capy").exists(): raise SystemExit("Documentation retains the dynamic detail component") if 'component_render("/doc/components/doc_' in (root / "scripts/generate_capy_docs.py").read_text(): raise SystemExit("Documentation generator retains dynamic detail components") for path in (root / "site/doc/content").glob("*/*.capy"): lines = [line.strip() for line in path.read_text().splitlines() if line.strip()] if len(lines) < 3 or lines[0] != "function COMPONENT(props : any)" or lines[1] != "{" or lines[-1] != "}": raise SystemExit(f"Documentation route has an unexpected shape: {path}") if any(not line.startswith("print(") for line in lines[2:-1]): raise SystemExit(f"Documentation route does not print static detail HTML directly: {path}") for component in (root / "site/doc/components/index.capy", root / "site/doc/components/all.capy"): text = component.read_text() for heading in ("API Functions", "Types", "Handlers", "How-to", "Learn Capy"): if f">{heading}<" not in text: raise SystemExit(f"Documentation listing lacks {heading}: {component}") if "Clear a dynamic value" in text: raise SystemExit(f"Documentation listing uses an API prose title: {component}") search = (root / "site/doc/components/search.capy").read_text() if 'class=\\\"search-kind\\\"' not in search: raise SystemExit("Documentation search does not label result kinds") PY status() { curl -sS --max-time 60 -o /dev/null -w '%{http_code}' -H "Host: $host" "$1"; } for path in api/zzzznotreal/ how-to/zzzznotreal/ api/component-exists/ api/component-2/ api/string-2/ '?''p=0''_String'; do [[ "$(status "$doc_base$path")" == 404 ]] || { echo "Old or unknown documentation URL did not return 404: $path" >&2; exit 1; } done for path in api/component/ api/string/ type/string/ type/any/ type/request/ handler/render/ handler/component/ handler/cli/; do [[ "$(status "$doc_base$path")" == 200 ]] || { echo "Canonical documentation URL did not return 200: $path" >&2; exit 1; } done python3 - "$doc_base" "$host" <<'PY' import html from pathlib import Path import re import sys import urllib.request base, host = sys.argv[1:] for source in sorted(Path("site/doc/source/how-to").glob("*.txt")): title = re.search(r"(?m)^:title\n([^\n]+)", source.read_text()) title = title[1] if title else source.stem title = html.escape(title, quote=False).replace('"', """) url = base + "how-to/" + source.stem + "/" request = urllib.request.Request(url, headers={"Host": host}) with urllib.request.urlopen(request, timeout=60) as response: body = response.read().decode() if ">" + title + "" not in body: raise SystemExit(f"How-to page did not render its title: {url}") print("All how-to documentation pages rendered") PY component_page=$(curl -fsS --max-time 60 -H "Host: $host" "${doc_base}api/component/") [[ "$component_page" == *">component"* ]] || { echo "Component documentation page did not render" >&2; exit 1; } route_gate="site/doc/content/how-to/route-file-gate.capy" trap 'rm -f "$route_gate"' EXIT cat >"$route_gate" <<'EOF' function COMPONENT(props : any) { print("
route file gate
") } EOF route_added=$(curl -fsS --max-time 60 -H "Host: $host" "${doc_base}how-to/route-file-gate/") [[ "$route_added" == *"route file gate"* ]] || { echo "A documentation page file did not reach its route" >&2; exit 1; } rm -f "$route_gate" [[ "$(status "${doc_base}how-to/route-file-gate/")" == 404 ]] || { echo "A deleted documentation page file did not return 404" >&2; exit 1; } guide=$(curl -fsS --max-time 60 -H "Host: $host" "${doc_base}guide/dynamic-values/") [[ "$guide" == *"8. Dynamic values"* && "$guide" == *"One object, many names"* && "$guide" == *'class="guide-navigation"'* ]] || { echo "Capy guide page omitted content or navigation" >&2; exit 1; } constructor=$(curl -fsS --max-time 60 -H "Host: $host" "${doc_base}api/string/") [[ "$constructor" == *"function string(value : as string, opt : any = {}) string"* && "$constructor" != *"DOC EXAMPLE ERROR"* ]] || { echo "Capy constructor API page omitted its signature" >&2; exit 1; } contract=$(curl -fsS --max-time 60 -H "Host: $host" "${doc_base}api/http-request/") [[ "$contract" == *'

Description

'* && "$contract" == *'class="related-link" href="/doc/api/socket-connect/">socket_connect'* ]] || { echo "Structured API contract sections or related links are incomplete" >&2; exit 1; } index=$(curl -fsS --max-time 60 -H "Host: $host" "$doc_base") [[ "$index" != *'>String()<'* && "$index" != *'>COMPONENT()<'* && "$index" != *'>CLI()<'* ]] || { echo "Type or handler index entry rendered as a function" >&2; exit 1; } [[ "$index" != *"route-file-gate"* ]] || { echo "Documentation index retained a deleted page" >&2; exit 1; } singlepage=$(curl -fsS --max-time 180 -H "Host: $host" "${doc_base}all/") [[ "$singlepage" == *'/doc/guide/install-and-first-program/'* && "$singlepage" == *'/doc/how-to/read-request-context/'* && "$singlepage" == *'/doc/type/any/'* && "$singlepage" == *'/doc/api/http-request/'* && "$singlepage" == *'>How-to'* ]] || { echo "Combined documentation route list omitted canonical links or the how-to section" >&2; exit 1; } search=$(curl -fsS --max-time 60 -H "Host: $host" "${doc_base}search/?q=http_request") [[ "$search" == *'/doc/api/http-request/'* && "$search" == *'http_request'* ]] || { echo "Documentation search omitted a structured API page" >&2; exit 1; } how_to_search=$(curl -fsS --max-time 60 -H "Host: $host" "${doc_base}search/?q=request") [[ "$how_to_search" == *'/doc/how-to/read-request-context/'* && "$how_to_search" == *'search-how-to'* && "$how_to_search" == *'>How-to'* ]] || { echo "Documentation search omitted the how-to section" >&2; exit 1; } landing=$(curl -fsS --max-time 60 -H "Host: $host" "${BEARER_TEST_HTTP_BASE:-http://127.0.0.1}/info/") all_wrapper=$(curl -fsS --max-time 60 -H "Host: $host" "${doc_base}all/index.capy") search_wrapper=$(curl -fsS --max-time 60 -H "Host: $host" "${doc_base}search/index.capy") for page in landing index component_page singlepage search all_wrapper search_wrapper; do [[ "${!page}" == *'rel="icon" type="image/png" href="/favicon.png"'* ]] || { echo "$page omitted the favicon" >&2; exit 1; } done curl -fsS --max-time 30 -H "Host: $host" "${BEARER_TEST_HTTP_BASE:-http://127.0.0.1}/favicon.png" | cmp - site/favicon.png python3 scripts/test_doc_example_pages.py echo "Capy reference pages and canonical documentation routes passed"