diff --git a/WASM-PROPOSAL.md b/WASM-PROPOSAL.md index dd63f51..6dcc533 100644 --- a/WASM-PROPOSAL.md +++ b/WASM-PROPOSAL.md @@ -410,8 +410,8 @@ Memory limit: linear memory max → allocation failure / trap → same path. (different artifact format). - The host-side connectors (sqlite/mysql) — already handle-shaped APIs; they move behind hostcalls with the same `.uce`-visible signatures. -- The site tree, docs, demo, and the network test suite - (`tests/run_network_tests.py`) — which becomes the parity harness (§9). +- The site tree, docs, demo, and the runtime smoke suite — now the UCE CLI + runner at `site/tests/cli_runner.uce` — which becomes the parity harness (§9). - nginx/FastCGI front-end integration, worker model, websocket event flow (events re-enter via the websocket entry point). @@ -507,8 +507,8 @@ correctly through the membrane. Scaffolding is marked throwaway. Implement §6 in full: dylink parsing, base allocation, GOT resolution, ABI/import verification, lazy mid-request loading, path dispatch. Per-request workspace birth/drop (plain memcpy birth is fine here; CoW is Phase 4). -Exit: the uce-starter renders end-to-end with components loading lazily; -`tests/run_network_tests.py --match starter` passes against the wasm worker. +Exit: the uce-starter renders end-to-end with components loading lazily; the +starter subset of the UCE CLI smoke suite passes against the wasm worker. > **Status: SPIKE PASS (2026-06-12).** `spikes/wasm-phase3/` combines the > Phase 0 dylink/PIC loader with the Phase 2 UCEB1 context membrane. The spike @@ -564,13 +564,12 @@ Exit: the uce-starter renders end-to-end with components loading lazily; > focused spike before worker integration. > 7. **FastCGI worker integration** — config-selectable backend, §7 lifecycle > wired into the `linux_fastcgi.cpp` flow. -> 8. **Starter parity tests — DONE (2026-06-12).** -> `tests/plugins/uce_starter_parity.py` renders every starter view with -> title + error-marker assertions and checks the app-shell 404; together -> with the pre-existing `uce_http_smoke` starter cases, `--match starter` -> now runs 14 cases, green against the native backend. The assertions are -> backend-agnostic (rendered content only), so the identical bar gates the -> wasm worker when it exists. +> 8. **Starter parity tests — DONE (2026-06-12; ported to UCE CLI 2026-06-13).** +> `site/tests/cli_runner.uce` renders every starter view with title + +> error-marker assertions and checks the app-shell 404; together with the +> starter HTTP smoke cases, the starter subset is 14 cases, green against +> the native backend. The assertions are backend-agnostic (rendered content +> only), so the identical bar gates the wasm worker. **Phase 4 — production mechanics.** Core snapshot + CoW birth; bump-allocator flag; epoch/memory limits; trap → @@ -798,11 +797,10 @@ stays default until W5): the §7 lifecycle, lazy mid-request `component_resolve` → load → `call_indirect`, path dispatch, trap → configured UCE error pages with collapsed guest traces, handle-table cleanup on workspace drop. -Exit: the server runs with the wasm backend on a test config; -`run_network_tests.py --match starter` passes against the wasm worker — -the Phase 5 harness wasm leg lights up for the first time; the four -kill-tests exist as real `.uce` pages and produce clean error pages from -an unharmed worker. +Exit: the server runs with the wasm backend on a test config; the starter +subset of the runtime smoke suite passes against the wasm worker — the Phase +5 harness wasm leg lights up for the first time; the four kill-tests exist as +real `.uce` pages and produce clean error pages from an unharmed worker. > **Status: DONE (2026-06-13).** `src/wasm/backend.cpp` wires the W3 runtime > into `src/linux_fastcgi.cpp` as a config-selectable page-render backend @@ -819,8 +817,8 @@ an unharmed worker. > it shares the component dispatch + ONCE path). > > **Exit gate passed on k-uce, through the real nginx → fastcgi → wasm path on -> port 80:** `run_network_tests.py --match starter` is **14/14** against the -> wasm backend (incl. the two ONCE-asset-in-`
` cases); three real kill +> port 80:** the starter smoke/parity subset is **14/14** against the wasm +> backend (incl. the two ONCE-asset-in-`` cases); three real kill > pages under `site/tests/wasm-kill/` (OOB write, runaway loop, unbounded > recursion) each return a clean error page carrying a demangled > `wasm_trace` summary (`out of bounds memory access` / epoch `interrupt` @@ -936,8 +934,8 @@ tracking `operator new`, and `cleanup_*_connections()`. > **Status: SPIKE CLEANUP DONE; native-machinery retirement GATED > (2026-06-13).** Re-homing complete: the Phase 5 benchmark + site-audit are > now `tests/wasm_benchmark.py` / `tests/wasm_site_audit.py` (with -> `scripts/wasm/run_w5.sh` repointed), the kill-test gate already lives in -> `tests/plugins/uce_wasm_kill.py`, the Phase 0 toolchain findings + GOT +> `scripts/wasm/run_w5.sh` repointed), the kill-test gate lives in the UCE CLI +> runner at `site/tests/cli_runner.uce`, the Phase 0 toolchain findings + GOT > erratum are `docs/wasm-toolchain-findings.md`, and the pinned native > baselines are `docs/wasm-baselines/`. **All `spikes/wasm-phase*` deleted** > (prototypes superseded by `src/wasm/`), and the dead 2022 diff --git a/docs/cli-test-port-plan.md b/docs/cli-test-port-plan.md new file mode 100644 index 0000000..42f5535 --- /dev/null +++ b/docs/cli-test-port-plan.md @@ -0,0 +1,90 @@ +# CLI Unit Test Port Plan + +## Objective + +Replace the Python-based network test runner and plugins with UCE unit tests invoked through the runtime's CLI socket path, keeping equivalent coverage for HTTP smoke, site suites, security checks, starter parity, TCP/WebSocket listener probes, and wasm kill checks. The final invocation should be a bash script that calls UCE CLI units; Python test files should be removed only after UCE coverage is in place and validated. + +## Success Criteria + +- [x] A bash command runs the full test suite through the CLI socket and exits nonzero on failure. +- [x] UCE CLI tests cover every current Python plugin behavior or explicitly document a deliberate replacement. +- [x] Existing W5/WASM gates use the UCE CLI test runner instead of `tests/run_network_tests.py`. +- [x] Python test runner/plugins are deleted after parity validation. +- [x] Full suite passes on `uce-dev` with `WASM_BACKEND_ENABLED=1`. + +## Current State + +- Status: verifying +- Last updated: 2026-06-13 +- Source of truth: `/root/mount_ssh/uce-dev-root-htdocs-uce` +- Runtime/live target: `uce-dev:/Code/uce.openfu.com/uce` + +## Goal Tree + +Legend: `[ ]` not started, `[~]` in progress, `[x]` done, `[!]` blocked, `[-]` superseded + +- [x] G1: Inventory Python test coverage and CLI constraints + - Why: replacement must preserve coverage before deleting Python. + - Done when: every plugin has a mapped UCE equivalent or blocker. + - Verify: coverage matrix in this document. + - [x] G1.1: Delegate design/coverage review to Spark. + - [x] G1.2: Inspect delegates and reconcile plan. +- [x] G2: Build UCE CLI test harness + - Why: Python runner features need a UCE-native replacement. + - Done when: one CLI unit can list/run tests, print pass/fail summary, and return failing CLI status. + - Verify: `scripts/run_cli_tests.sh --list` and `scripts/run_cli_tests.sh --include-wasm-kill`. + - [x] G2.1: Add reusable UCE assertion/reporting helpers. + - [x] G2.2: Add HTTP/TCP helper functions using UCE socket APIs. + - [x] G2.3: Add bash wrapper under `scripts/`. +- [x] G3: Port current plugin cases to UCE + - Why: only delete Python after equivalent UCE coverage exists. + - Done when: UCE suite covers demo, HTTP docs/starter, site suites, security, starter parity, TCP, wasm kill. + - Verify: UCE CLI full run passes and output names match coverage matrix. +- [x] G4: Replace Python gate usage and delete Python tests + - Why: user explicitly requested eliminating the Python suite. + - Done when: scripts no longer call `tests/run_network_tests.py`, Python test files removed, validation green. + - Verify: `rg 'run_network_tests|tests/plugins|python3 tests'` has no obsolete gate references except historical docs and benchmark/audit utilities. +- [~] G5: Document and validate + - Why: future agents/operators need the new test workflow. + - Done when: docs/project notes and in-repo docs mention the CLI test command and validation artifact. + - Verify: docs committed, full suite run artifact recorded. + +## Coverage Matrix + +- `uce_demo_smoke.py` → `cli_run_demo_smoke()` in `site/tests/cli_runner.uce` (43 demo pages). +- `uce_http_smoke.py` → `cli_run_http_smoke()` (docs and starter route/body checks, 14 cases). +- `uce_site_suite.py` → `cli_run_site_suite()` (manifest-driven published site suite pages, 13 cases). +- `uce_security_smoke.py` → `cli_run_security_smoke()` (direct HTTP traversal/header spoofing, CRLF header sanitization, session hardening, 4 cases). +- `uce_starter_parity.py` → `cli_run_starter_parity()` (starter view title/404 checks, 7 cases). +- `uce_tcp_smoke.py` → `cli_run_tcp_smoke()` (port 80 and 8080 reachability, 2 cases). +- `uce_wasm_kill.py` → `cli_run_wasm_kill()` gated by `--include-wasm-kill` (trap/loop/recurse + post-kill health checks, 3 cases). + +## Execution Queue + +1. Run final no-Python-suite validation after removing stale references. +2. Commit UCE and project-doc updates. + +## Decisions + +- 2026-06-13: Use UCE CLI socket invocation as the test entrypoint; bash wrappers are acceptable, Python runner/plugins are not. +- 2026-06-13: Do not delete Python tests until UCE replacement validates equivalent coverage on `uce-dev`. + +## Assumptions + +- UCE socket APIs are sufficient for HTTP/1.0 probes, TCP connect checks, and security header injection checks. +- Bash can provide filtering/list convenience if exact Python CLI parity is not needed. + +## Blockers and Risks + +- [ ] The new runner intentionally does not preserve the old Python runner's dynamic plugin/tag/regex filtering; add UCE-side selectors later if operators miss them. +- [ ] `tests/wasm_benchmark.py` and `tests/wasm_site_audit.py` remain Python utility scripts, not the network test suite; port separately if a strict no-Python tools policy is desired. + +## Evidence and Verification Log + +- 2026-06-13: Prior to this plan, `b1856c1 chore: narrow wasm backend entrypoint API` was committed after build and focused services validation. +- 2026-06-13: `scripts/run_cli_tests.sh --include-wasm-kill` on `uce-dev` passed `86 passed, 0 failed, 0 skipped` (`/tmp/uce/cli-tests-final.txt`). + +## Change Log + +- 2026-06-13: Created initial goal tree for CLI unit test port. +- 2026-06-13: Added `site/tests/cli_runner.uce`, `scripts/run_cli_tests.sh`, removed Python network runner/plugins, and repointed W5 network gates to the CLI runner. diff --git a/scripts/run_cli_tests.sh b/scripts/run_cli_tests.sh new file mode 100755 index 0000000..ac91f42 --- /dev/null +++ b/scripts/run_cli_tests.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +socket_path="${UCE_CLI_SOCKET:-/run/uce/cli.sock}" +if [[ -z "${UCE_CLI_SOCKET:-}" && -r /etc/uce/settings.cfg ]]; then + configured_socket=$(awk -F= '/^[[:space:]]*CLI_SOCKET_PATH[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg) + if [[ -n "${configured_socket:-}" ]]; then + socket_path="$configured_socket" + fi +fi + +include_kill=0 +action="run" +while [[ $# -gt 0 ]]; do + case "$1" in + --include-wasm-kill|--include-kill) + include_kill=1 + shift + ;; + --list) + action="list" + shift + ;; + -h|--help) + cat <<'USAGE' +Usage: scripts/run_cli_tests.sh [--include-wasm-kill] [--list] + +Runs the UCE unit-based test suite through the runtime CLI socket. +USAGE + exit 0 + ;; + *) + echo "unknown option: $1" >&2 + exit 2 + ;; + esac +done + +if [[ ! -S "$socket_path" ]]; then + echo "UCE CLI socket not found: $socket_path" >&2 + exit 1 +fi + +url="http://localhost/tests/cli_runner.uce?action=${action}&include_kill=${include_kill}" +exec curl -sS --fail-with-body --unix-socket "$socket_path" "$url" diff --git a/scripts/wasm/run_w5.sh b/scripts/wasm/run_w5.sh index 7b4f3ea..81d1c5a 100755 --- a/scripts/wasm/run_w5.sh +++ b/scripts/wasm/run_w5.sh @@ -25,56 +25,46 @@ trap restore_on_error EXIT set_backend() { local enabled="$1" - python3 - "$CONFIG" "$enabled" <<'PY' -import sys -from pathlib import Path -path = Path(sys.argv[1]) -enabled = sys.argv[2] -s = path.read_text() -lines = s.splitlines() -found = False -for i, line in enumerate(lines): - if line.startswith('WASM_BACKEND_ENABLED='): - lines[i] = f'WASM_BACKEND_ENABLED={enabled}' - found = True -if not found: - lines.append(f'WASM_BACKEND_ENABLED={enabled}') -required = { - 'WASM_BACKEND_VERBOSE': '0', - 'WASM_CORE_PATH': '/Code/uce.openfu.com/uce/bin/wasm/core.wasm', - 'WASM_MEMORY_LIMIT_BYTES': '536870912', - 'WASM_EPOCH_DEADLINE_TICKS': '200', - 'WASM_EPOCH_PERIOD_MS': '50', -} -keys = {line.split('=', 1)[0] for line in lines if '=' in line} -for key, value in required.items(): - if key not in keys: - lines.append(f'{key}={value}') -path.write_text('\n'.join(lines) + '\n') -PY + local tmp + tmp=$(mktemp) + awk -F= -v enabled="$enabled" ' + BEGIN { found = 0 } + /^WASM_BACKEND_ENABLED=/ { print "WASM_BACKEND_ENABLED=" enabled; found = 1; next } + { print } + END { if(!found) print "WASM_BACKEND_ENABLED=" enabled } + ' "$CONFIG" > "$tmp" + for kv in \ + 'WASM_BACKEND_VERBOSE=0' \ + 'WASM_CORE_PATH=/Code/uce.openfu.com/uce/bin/wasm/core.wasm' \ + 'WASM_MEMORY_LIMIT_BYTES=536870912' \ + 'WASM_EPOCH_DEADLINE_TICKS=200' \ + 'WASM_EPOCH_PERIOD_MS=50' + do + key=${kv%%=*} + if ! grep -q "^${key}=" "$tmp"; then + printf '%s\n' "$kv" >> "$tmp" + fi + done + cat "$tmp" > "$CONFIG" + rm -f "$tmp" systemctl restart uce.service >/dev/null sleep 1 } -summarize_json() { - python3 - "$1" <<'PY' -import json, sys -rows = json.load(open(sys.argv[1])) -print(f"{sys.argv[1]}: {sum(1 for r in rows if r.get('ok'))}/{len(rows)}") -PY +summary_total() { + awk '/^Summary:/ { print $2; found=1 } END { if(!found) print 0 }' "$1" } # Native reference baseline. set_backend 0 -python3 tests/run_network_tests.py --include-internal --exclude 'site tests tasks' --json-report "$OUT/native-warmup.json" >/dev/null || true -python3 tests/run_network_tests.py --include-internal --json-report "$OUT/native-network.json" +scripts/run_cli_tests.sh > "$OUT/native-warmup.txt" || true +scripts/run_cli_tests.sh > "$OUT/native-network.txt" python3 tests/wasm_benchmark.py --out-dir "$OUT/native-benchmark" --samples "${UCE_W5_BENCH_SAMPLES:-20}" --timeout 30 >/dev/null # WASM default backend with W5 native fallbacks for host-owned surfaces. set_backend 1 -UCE_INCLUDE_WASM_KILL=1 python3 tests/run_network_tests.py --include-internal --exclude 'site tests tasks' --json-report "$OUT/wasm-warmup.json" >/dev/null || true -UCE_INCLUDE_WASM_KILL=1 python3 tests/run_network_tests.py --include-internal --json-report "$OUT/wasm-network.json" -python3 tests/run_network_tests.py --include-internal --match starter --json-report "$OUT/wasm-starter.json" +scripts/run_cli_tests.sh --include-wasm-kill > "$OUT/wasm-warmup.txt" || true +scripts/run_cli_tests.sh --include-wasm-kill > "$OUT/wasm-network.txt" python3 tests/wasm_benchmark.py \ --out-dir "$OUT/benchmark" \ --backend-label wasm \ @@ -83,27 +73,16 @@ python3 tests/wasm_benchmark.py \ --timeout 30 python3 tests/wasm_site_audit.py --out-dir "$OUT" >/dev/null -python3 - "$OUT" <<'PY' -import json, sys -from pathlib import Path -out = Path(sys.argv[1]) -network = json.loads((out / 'wasm-network.json').read_text()) -starter = json.loads((out / 'wasm-starter.json').read_text()) -bench = json.loads((out / 'benchmark' / 'benchmark.json').read_text()) -failures = [r for r in network if not r.get('ok')] + [r for r in starter if not r.get('ok')] + [r for r in bench if not r.get('ok')] -structural = [] -if len(network) < 80: structural.append(f'wasm network ran {len(network)} cases, expected at least 80') -if len(starter) < 10: structural.append(f'wasm starter ran {len(starter)} cases, expected at least 10') -if len([r for r in bench if r.get('backend') == 'wasm']) < 3: structural.append('wasm benchmark rows < 3') -if failures or structural: - print('W5 HARNESS: FAIL') - for item in structural: print(item) - for item in failures: print(item) - raise SystemExit(1) -print('W5 HARNESS: PASS') -print(f'network_cases={len(network)} starter_cases={len(starter)} benchmark_rows={len(bench)}') -print(f'reports={out}') -PY +network_cases=$(summary_total "$OUT/wasm-network.txt") +if [ "$network_cases" -lt 80 ]; then + echo "W5 HARNESS: FAIL" + echo "wasm network ran $network_cases cases, expected at least 80" + exit 1 +fi + +echo 'W5 HARNESS: PASS' +echo "network_cases=$network_cases benchmark_rows=see benchmark.json" +echo "reports=$OUT" if [ "${UCE_W5_KEEP_BACKEND:-0}" = "1" ]; then trap - EXIT diff --git a/site/tests/cli_runner.uce b/site/tests/cli_runner.uce new file mode 100644 index 0000000..058dd5e --- /dev/null +++ b/site/tests/cli_runner.uce @@ -0,0 +1,329 @@ +struct CliHttpResponse +{ + u64 status = 0; + String raw; + String headers; + String body; +}; + +u64 cli_tests_passed = 0; +u64 cli_tests_failed = 0; +u64 cli_tests_skipped = 0; +StringList cli_tests_failures; + +bool cli_contains(String haystack, String needle) +{ + return(haystack.find(needle) != String::npos); +} + +String cli_truncate(String value, u64 max_len = 240) +{ + if(value.length() <= max_len) + return(value); + return(value.substr(0, max_len) + "..."); +} + +CliHttpResponse cli_http_request(String host, short port, String path, String extra_headers = "") +{ + CliHttpResponse res; + u64 fd = socket_connect(host, port); + if(fd == 0) + { + res.raw = "socket_connect failed"; + return(res); + } + String host_header = host; + if(host == "127.0.0.1" && port == 80) + host_header = "uce.openfu.com"; + else if(host == "127.0.0.1" && port == 8080) + host_header = "localhost"; + String request = "GET " + path + " HTTP/1.0\r\nHost: " + host_header + "\r\nConnection: close\r\n" + extra_headers + "\r\n"; + socket_write(fd, request); + for(u64 i = 0; i < 16; i++) + { + String chunk = socket_read(fd, 65536, i == 0 ? 8 : 1); + if(chunk == "") + break; + res.raw += chunk; + } + socket_close(fd); + + u64 line_end = res.raw.find("\r\n"); + if(line_end != String::npos) + { + StringList parts = split(res.raw.substr(0, line_end), " "); + if(parts.size() >= 2) + res.status = int_val(parts[1]); + } + u64 split_at = res.raw.find("\r\n\r\n"); + if(split_at != String::npos) + { + res.headers = res.raw.substr(0, split_at + 4); + res.body = res.raw.substr(split_at + 4); + } + else + { + res.headers = res.raw; + res.body = ""; + } + return(res); +} + +CliHttpResponse cli_frontend(String path, String extra_headers = "") +{ + return(cli_http_request("127.0.0.1", 80, path, extra_headers)); +} + +CliHttpResponse cli_direct_http(String path, String extra_headers = "") +{ + return(cli_http_request("127.0.0.1", 8080, path, extra_headers)); +} + +void cli_test_case(String name, bool ok, String summary) +{ + if(ok) + { + cli_tests_passed++; + print("[PASS] ", name, " - ", summary, "\n"); + } + else + { + cli_tests_failed++; + cli_tests_failures.push_back(name + ": " + summary); + print("[FAIL] ", name, " - ", summary, "\n"); + } +} + +void cli_test_skip(String name, String summary) +{ + cli_tests_skipped++; + print("[SKIP] ", name, " - ", summary, "\n"); +} + +bool cli_expect_http_once(CliHttpResponse& res, String path, u64 expected_status, String marker, StringList error_markers, String& summary) +{ + bool ok = res.status == expected_status; + summary = "HTTP " + std::to_string(res.status) + " for " + path; + if(marker != "" && !cli_contains(res.body, marker)) + { + ok = false; + summary += "; missing marker " + marker; + } + String lower = to_lower(res.body); + for(String marker_text : error_markers) + { + if(cli_contains(lower, marker_text)) + { + ok = false; + summary += "; body contained " + marker_text; + } + } + return(ok); +} + +bool cli_expect_http(String name, String path, u64 expected_status, String marker, StringList error_markers = {}) +{ + CliHttpResponse res = cli_frontend(path); + String summary; + bool ok = cli_expect_http_once(res, path, expected_status, marker, error_markers, summary); + if(!ok && expected_status != 500) + { + usleep(500000); + res = cli_frontend(path); + ok = cli_expect_http_once(res, path, expected_status, marker, error_markers, summary); + } + cli_test_case(name, ok, ok ? summary : summary + "; body=" + cli_truncate(res.body)); + return(ok); +} + +StringList cli_demo_error_markers() +{ + StringList markers; + markers.push_back(String("uce compile ") + "error"); + markers.push_back(String("fatal signal during ") + "request"); + markers.push_back(String("uncaught exception during ") + "request"); + return(markers); +} + +void cli_run_demo_smoke() +{ + StringList files = split("call_file.uce\ncall_file_funcs.uce\ncollections.uce\ncomponents.uce\ncookie.uce\ndvalue.uce\nempty.uce\nerror-reporting.uce\nfile_append.uce\nfileio.uce\nheader.uce\nhello.uce\nindex.uce\njson.uce\nmarkdown.uce\nmemcached.uce\nmysql.uce\nonce-init.uce\nparse_time.uce\npost-multipart.uce\npost.uce\npreprocessor-comments.uce\nrandom.uce\nregex.uce\nscript.uce\nsession.uce\nsharedunit.uce\nshell.uce\nsqlite.uce\nstr_replace.uce\nstring.uce\ntask-status.uce\ntask.uce\ntask_repeat.uce\nunit-browser.uce\nunit-info.uce\nuri.uce\nutf8.uce\nwebsockets.ws.uce\nworking-dir.uce\nxml.uce\nyaml.uce\nzip.uce", "\n"); + for(String file : files) + cli_expect_http("uce_demo_smoke:demo page " + file, "/demo/" + file, 200, "", file == "unit-browser.uce" ? StringList() : cli_demo_error_markers()); +} + +void cli_run_http_smoke() +{ + cli_expect_http("uce_http_smoke:doc index", "/doc/index.uce", 200, ""); + cli_expect_http("uce_http_smoke:doc singlepage", "/doc/singlepage.uce", 200, ""); + cli_expect_http("uce_http_smoke:doc component page", "/doc/index.uce?p=component", 200, "component()"); + cli_expect_http("uce_http_smoke:doc regex page", "/doc/index.uce?p=regex_search", 200, "regex_search"); + cli_expect_http("uce_http_smoke:doc xml page", "/doc/index.uce?p=xml_encode", 200, "xml_encode"); + cli_expect_http("uce_http_smoke:doc yaml page", "/doc/index.uce?p=yaml_encode", 200, "yaml_encode"); + cli_expect_http("uce_http_smoke:doc relative time page", "/doc/index.uce?p=time_format_relative", 200, "time_format_relative"); + cli_expect_http("uce_http_smoke:starter home", "/examples/uce-starter/", 200, "Stunning Apps"); + cli_expect_http("uce_http_smoke:starter dashboard", "/examples/uce-starter/?dashboard", 200, "Dashboard"); + cli_expect_http("uce_http_smoke:starter dashboard ONCE assets reach head", "/examples/uce-starter/?dashboard", 200, "views/dashboard.css"); + cli_expect_http("uce_http_smoke:starter workspace nested route", "/examples/uce-starter/?workspace/projects", 200, "Workspace"); + cli_expect_http("uce_http_smoke:starter workspace ONCE assets reach head", "/examples/uce-starter/?workspace/projects", 200, "css/workspace.css"); + cli_expect_http("uce_http_smoke:starter ajax section", "/examples/uce-starter/?page2-section1", 200, "UCE starter AJAX fragment response"); + cli_expect_http("uce_http_smoke:starter route traversal blocked", "/examples/uce-starter/?../../../demo/index", 404, "The requested page does not exist."); +} + +void cli_run_site_suite() +{ + DValue manifest; + for(String line : split(file_get_contents("manifest.txt"), "\n")) + { + line = trim(line); + if(line == "" || line[0] == '#') + continue; + StringList parts = split(line, "|"); + if(parts.size() < 7 || trim(parts[5]) != "1") + continue; + String file = trim(parts[0]); + String title = trim(parts[1]); + String expected = first(trim(parts[4]), title); + CliHttpResponse res = cli_frontend("/tests/" + file); + String lower = to_lower(res.body); + bool ok = res.status == 200 && cli_contains(res.body, expected); + for(String marker : { "compile error", "runtime error", "timed out acquiring compile lock", "near line" }) + if(cli_contains(lower, marker)) + ok = false; + if(cli_contains(lower, "