\n \"xk\"" ), 'Expected Lexbor to keep post-heading text in the MathML mi element for issue 373.' );
+
+$worker_result_372 = \HtmlApiFuzz\Worker::run(
+ array(
+ 'input-base64' => base64_encode( $issue_372 ),
+ 'mode' => \HtmlApiFuzz\Generator::MODE_FRAGMENT_BODY,
+ 'profile' => 'replay',
+ 'seed' => '372',
+ 'output-dir' => $work_dir . '/issue-372',
+ 'max-tokens' => '200',
+ 'max-nodes' => '200',
+ 'dom-oracle' => \HtmlApiFuzz\OracleRenderer::KIND_LEXBOR_SOURCE,
+ 'lexbor-oracle-bin' => $binary,
+ )
+);
+html_api_fuzz_lexbor_smoke_assert( true === ( $worker_result_372['ok'] ?? null ), 'Expected issue 372 to pass Worker against the Lexbor source oracle.' );
+html_api_fuzz_lexbor_smoke_assert( \HtmlApiFuzz\OracleRenderer::KIND_LEXBOR_SOURCE === ( $worker_result_372['oracle']['kind'] ?? null ), 'Expected Worker result to record Lexbor source oracle kind.' );
+
+$worker_replay_372 = \HtmlApiFuzz\read_json_file( $work_dir . '/issue-372/replay.json' );
+html_api_fuzz_lexbor_smoke_assert( \HtmlApiFuzz\OracleRenderer::KIND_LEXBOR_SOURCE === ( $worker_replay_372['options']['domOracle'] ?? null ), 'Expected replay options to preserve the Lexbor source oracle kind.' );
+html_api_fuzz_lexbor_smoke_assert( $binary === ( $worker_replay_372['options']['lexborOracleBin'] ?? null ), 'Expected replay options to preserve the Lexbor source oracle binary.' );
+html_api_fuzz_lexbor_smoke_assert( ( $metadata['lexborCommit'] ?? null ) === ( $worker_replay_372['oracle']['lexborCommit'] ?? null ), 'Expected replay metadata to preserve the Lexbor source commit.' );
+
+$replay_dir = $work_dir . '/issue-372-replay';
+$proc = \HtmlApiFuzz\run_php_process(
+ array(
+ dirname( __DIR__ ) . '/replay.php',
+ '--replay',
+ $work_dir . '/issue-372/replay.json',
+ '--output-dir',
+ $replay_dir,
+ ),
+ \HtmlApiFuzz\repo_root(),
+ 10000,
+ $work_dir . '/replay.log'
+);
+html_api_fuzz_lexbor_smoke_assert( 0 === $proc['code'], 'Expected replay to pass while preserving the Lexbor source oracle.' );
+$replayed = \HtmlApiFuzz\read_json_file( $replay_dir . '/result.json' );
+html_api_fuzz_lexbor_smoke_assert( \HtmlApiFuzz\OracleRenderer::KIND_LEXBOR_SOURCE === ( $replayed['oracle']['kind'] ?? null ), 'Expected replayed result to use the Lexbor source oracle.' );
+
+$worker_result_373 = \HtmlApiFuzz\Worker::run(
+ array(
+ 'input-base64' => base64_encode( $issue_373 ),
+ 'mode' => \HtmlApiFuzz\Generator::MODE_FRAGMENT_BODY,
+ 'profile' => 'replay',
+ 'seed' => '373',
+ 'output-dir' => $work_dir . '/issue-373',
+ 'max-tokens' => '200',
+ 'max-nodes' => '200',
+ 'dom-oracle' => \HtmlApiFuzz\OracleRenderer::KIND_LEXBOR_SOURCE,
+ 'lexbor-oracle-bin' => $binary,
+ )
+);
+html_api_fuzz_lexbor_smoke_assert( true === ( $worker_result_373['ok'] ?? null ), 'Expected issue 373 to pass Worker against the Lexbor source oracle.' );
+
+\HtmlApiFuzz\remove_dir_recursive( $work_dir );
+html_api_fuzz_lexbor_smoke_assert( ! is_dir( $work_dir ), 'Expected smoke work directory cleanup.' );
+
+echo "OK lexbor-oracle-smoke\n";
diff --git a/tools/html-api-fuzz/tests/lexbor-source-integrity-smoke.sh b/tools/html-api-fuzz/tests/lexbor-source-integrity-smoke.sh
new file mode 100755
index 0000000000000..f8db27c7ce529
--- /dev/null
+++ b/tools/html-api-fuzz/tests/lexbor-source-integrity-smoke.sh
@@ -0,0 +1,79 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
+FUZZ_ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
+BUILD_SCRIPT="$FUZZ_ROOT/oracles/lexbor/build.sh"
+TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/html-api-fuzz-lexbor-source.XXXXXX")"
+trap 'rm -rf "$TMP_ROOT"' EXIT HUP INT TERM
+
+fail() {
+ printf 'FAIL: %s\n' "$*" >&2
+ exit 1
+}
+
+SOURCE_DIR="$TMP_ROOT/source"
+FAKE_BIN="$TMP_ROOT/fake-bin"
+BUILD_LOG="$TMP_ROOT/build-tools.log"
+mkdir -p "$SOURCE_DIR" "$FAKE_BIN"
+
+git -C "$SOURCE_DIR" init -q
+git -C "$SOURCE_DIR" config user.name 'HTML API fuzz smoke'
+git -C "$SOURCE_DIR" config user.email 'html-api-fuzz@example.invalid'
+printf 'tracked\n' > "$SOURCE_DIR/tracked.txt"
+printf 'ignored.cache\n' > "$SOURCE_DIR/.gitignore"
+git -C "$SOURCE_DIR" add tracked.txt .gitignore
+git -C "$SOURCE_DIR" commit -qm 'seed clean source'
+REF="$(git -C "$SOURCE_DIR" rev-parse HEAD)"
+
+for tool in cmake cc; do
+ cat > "$FAKE_BIN/$tool" <<'EOF'
+#!/usr/bin/env sh
+printf '%s\n' "$(basename "$0")" >> "$HTML_API_FUZZ_LEXBOR_BUILD_LOG"
+exit 97
+EOF
+ chmod 0700 "$FAKE_BIN/$tool"
+done
+
+reset_source() {
+ git -C "$SOURCE_DIR" reset --hard -q "$REF"
+ git -C "$SOURCE_DIR" clean -fdxq
+ : > "$BUILD_LOG"
+}
+
+expect_dirty_rejection() {
+ local label="$1"
+ local expected_status="$2"
+ local output status
+ set +e
+ output="$(
+ env \
+ PATH="$FAKE_BIN:$PATH" \
+ HTML_API_FUZZ_LEXBOR_BUILD_LOG="$BUILD_LOG" \
+ LEXBOR_COMMIT="$REF" \
+ LEXBOR_SOURCE_DIR="$SOURCE_DIR" \
+ LEXBOR_BUILD_DIR="$TMP_ROOT/build-$label" \
+ LEXBOR_INSTALL_DIR="$TMP_ROOT/install-$label" \
+ "$BUILD_SCRIPT" 2>&1
+ )"
+ status=$?
+ set -e
+ [[ "$status" -ne 0 ]] || fail "$label source dirt was accepted"
+ [[ "$output" == *'Lexbor source checkout is dirty; refusing to build unpinned bytes:'* ]] || fail "$label source dirt did not report the integrity rejection: $output"
+ [[ "$output" == *"$expected_status"* ]] || fail "$label source dirt did not report $expected_status: $output"
+ [[ ! -s "$BUILD_LOG" ]] || fail "$label source dirt reached a build tool: $(cat "$BUILD_LOG")"
+}
+
+reset_source
+printf 'modified\n' >> "$SOURCE_DIR/tracked.txt"
+expect_dirty_rejection tracked ' M tracked.txt'
+
+reset_source
+printf 'untracked\n' > "$SOURCE_DIR/untracked.txt"
+expect_dirty_rejection untracked '?? untracked.txt'
+
+reset_source
+printf 'ignored\n' > "$SOURCE_DIR/ignored.cache"
+expect_dirty_rejection ignored '!! ignored.cache'
+
+printf 'PASS: Lexbor rejects tracked, untracked, and ignored source dirt before building\n'
diff --git a/tools/html-api-fuzz/tests/oracle-install-integrity-smoke.sh b/tools/html-api-fuzz/tests/oracle-install-integrity-smoke.sh
new file mode 100755
index 0000000000000..aaf0e85a3dafe
--- /dev/null
+++ b/tools/html-api-fuzz/tests/oracle-install-integrity-smoke.sh
@@ -0,0 +1,372 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
+FUZZ_ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
+CHROME_DIR="$FUZZ_ROOT/oracles/chrome"
+HTML5EVER_DIR="$FUZZ_ROOT/oracles/html5ever"
+CHROME_INSTALLER="$CHROME_DIR/install.sh"
+RUST_INSTALLER="$HTML5EVER_DIR/install-rust.sh"
+CHROME_VERSION="$(tr -d '[:space:]' < "$CHROME_DIR/VERSION")"
+RUSTUP_VERSION='1.28.2'
+
+fail() {
+ printf 'FAIL: %s\n' "$*" >&2
+ exit 1
+}
+
+sha256_file() {
+ if command -v sha256sum >/dev/null 2>&1; then
+ sha256sum "$1" | awk '{ print $1 }'
+ elif command -v shasum >/dev/null 2>&1; then
+ shasum -a 256 "$1" | awk '{ print $1 }'
+ else
+ fail 'sha256sum or shasum is required'
+ fi
+}
+
+file_mode() {
+ case "$(uname -s)" in
+ Darwin) /usr/bin/stat -f '%Lp' "$1" ;;
+ Linux) stat -c '%a' "$1" ;;
+ *) fail 'file-mode check requires macOS or Linux' ;;
+ esac
+}
+
+validate_manifest() {
+ local manifest="$1"
+ shift
+ local expected_count="$#"
+ local actual_count
+ actual_count="$(awk 'NF && $1 !~ /^#/ { count++ } END { print count + 0 }' "$manifest")"
+ [[ "$actual_count" -eq "$expected_count" ]] || fail "$manifest contains $actual_count entries; expected $expected_count"
+
+ local name matches fields digest
+ for name in "$@"; do
+ matches="$(awk -v name="$name" '$2 == name { count++ } END { print count + 0 }' "$manifest")"
+ [[ "$matches" -eq 1 ]] || fail "$manifest must contain exactly one $name entry"
+ fields="$(awk -v name="$name" '$2 == name { print NF }' "$manifest")"
+ digest="$(awk -v name="$name" '$2 == name { print $1 }' "$manifest")"
+ [[ "$fields" -eq 2 ]] || fail "$name checksum entry must contain exactly two fields"
+ [[ "$digest" =~ ^[0-9a-f]{64}$ ]] || fail "$name checksum is not lowercase SHA-256"
+ done
+}
+
+validate_manifest "$CHROME_DIR/SHA256SUMS" \
+ "chrome-$CHROME_VERSION-mac-arm64.zip" \
+ "chrome-$CHROME_VERSION-mac-x64.zip" \
+ "chrome-$CHROME_VERSION-linux64.zip"
+validate_manifest "$HTML5EVER_DIR/RUSTUP_SHA256SUMS" \
+ "rustup-init-$RUSTUP_VERSION-aarch64-apple-darwin" \
+ "rustup-init-$RUSTUP_VERSION-x86_64-apple-darwin" \
+ "rustup-init-$RUSTUP_VERSION-aarch64-unknown-linux-gnu" \
+ "rustup-init-$RUSTUP_VERSION-x86_64-unknown-linux-gnu"
+
+case "$(uname -s):$(uname -m)" in
+ Darwin:arm64)
+ CHROME_PLATFORM='mac-arm64'
+ CHROME_ARCHIVE_DIR='chrome-mac-arm64'
+ CHROME_EXECUTABLE_RELATIVE='Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'
+ RUST_TARGET='aarch64-apple-darwin'
+ ;;
+ Darwin:x86_64)
+ CHROME_PLATFORM='mac-x64'
+ CHROME_ARCHIVE_DIR='chrome-mac-x64'
+ CHROME_EXECUTABLE_RELATIVE='Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'
+ RUST_TARGET='x86_64-apple-darwin'
+ ;;
+ Linux:x86_64|Linux:amd64)
+ CHROME_PLATFORM='linux64'
+ CHROME_ARCHIVE_DIR='chrome-linux64'
+ CHROME_EXECUTABLE_RELATIVE='chrome'
+ RUST_TARGET='x86_64-unknown-linux-gnu'
+ ;;
+ Linux:aarch64)
+ fail 'Chrome installer does not support Linux aarch64'
+ ;;
+ *)
+ fail "unsupported smoke-test host: $(uname -s) $(uname -m)"
+ ;;
+esac
+
+CHROME_ARCHIVE_NAME="chrome-$CHROME_VERSION-$CHROME_PLATFORM.zip"
+CHROME_SHA256="$(awk -v name="$CHROME_ARCHIVE_NAME" '$2 == name { print $1 }' "$CHROME_DIR/SHA256SUMS")"
+MARKER_CONTENT="$(printf 'version=%s\nplatform=%s\narchive_sha256=%s' "$CHROME_VERSION" "$CHROME_PLATFORM" "$CHROME_SHA256")"
+
+TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/html-api-fuzz-install-integrity.XXXXXX")"
+trap 'rm -rf "$TMP_ROOT"' EXIT HUP INT TERM
+NO_EXTERNAL_BIN="$TMP_ROOT/no-external-bin"
+EXTERNAL_LOG="$TMP_ROOT/external-invocations"
+mkdir -p "$NO_EXTERNAL_BIN"
+for external_command in curl unzip; do
+ {
+ printf '%s\n' '#!/bin/sh'
+ printf 'printf "%%s\\n" %s >> "$HTML_API_FUZZ_EXTERNAL_LOG"\n' "$external_command"
+ printf '%s\n' 'exit 97'
+ } > "$NO_EXTERNAL_BIN/$external_command"
+ chmod 0755 "$NO_EXTERNAL_BIN/$external_command"
+done
+
+assert_no_external() {
+ [[ ! -e "$EXTERNAL_LOG" ]] || fail "unexpected external command invocation: $(tr '\n' ' ' < "$EXTERNAL_LOG")"
+}
+
+# --print-path is deliberately only a path calculation. It must not depend on
+# either the checksum manifest or an installer-side external command.
+print_fixture="$TMP_ROOT/chrome-print-path"
+print_root="$TMP_ROOT/chrome-print-root"
+mkdir -p "$print_fixture"
+cp "$CHROME_INSTALLER" "$print_fixture/install.sh"
+cp "$CHROME_DIR/VERSION" "$print_fixture/VERSION"
+PATH="$NO_EXTERNAL_BIN:$PATH" \
+ HTML_API_FUZZ_EXTERNAL_LOG="$EXTERNAL_LOG" \
+ HTML_API_FUZZ_CHROME_INSTALL_ROOT="$print_root" \
+ "$print_fixture/install.sh" --print-path > "$TMP_ROOT/print-path.out"
+expected_print_path="$print_root/$CHROME_VERSION/$CHROME_PLATFORM/$CHROME_ARCHIVE_DIR/$CHROME_EXECUTABLE_RELATIVE"
+[[ "$(cat "$TMP_ROOT/print-path.out")" = "$expected_print_path" ]] || fail '--print-path returned an unexpected path'
+assert_no_external
+
+write_fake_chrome() {
+ local executable="$1"
+ mkdir -p "$(dirname "$executable")"
+ {
+ printf '%s\n' '#!/bin/sh'
+ printf '%s\n' 'printf "%s\n" invoked >> "$HTML_API_FUZZ_INVOCATION_LOG"'
+ printf '%s\n' 'printf "Google Chrome for Testing %s\n" "$HTML_API_FUZZ_FAKE_VERSION"'
+ printf '%s\n' 'exit "${HTML_API_FUZZ_FAKE_EXIT_STATUS:-0}"'
+ } > "$executable"
+ chmod 0755 "$executable"
+}
+
+# An unmarked or stale legacy install must not execute before its archive is
+# authenticated. The corrupt cached archive also makes this path network-free.
+legacy_root="$TMP_ROOT/chrome-legacy"
+legacy_destination="$legacy_root/$CHROME_VERSION/$CHROME_PLATFORM"
+legacy_executable="$legacy_destination/$CHROME_ARCHIVE_DIR/$CHROME_EXECUTABLE_RELATIVE"
+legacy_archive="$legacy_root/.downloads/$CHROME_ARCHIVE_NAME"
+legacy_log="$TMP_ROOT/legacy-invocations"
+write_fake_chrome "$legacy_executable"
+mkdir -p "$(dirname "$legacy_archive")"
+printf '%s\n' 'corrupt chrome archive' > "$legacy_archive"
+
+if PATH="$NO_EXTERNAL_BIN:$PATH" \
+ HTML_API_FUZZ_EXTERNAL_LOG="$EXTERNAL_LOG" \
+ HTML_API_FUZZ_CHROME_INSTALL_ROOT="$legacy_root" \
+ HTML_API_FUZZ_INVOCATION_LOG="$legacy_log" \
+ HTML_API_FUZZ_FAKE_VERSION="$CHROME_VERSION" \
+ "$CHROME_INSTALLER" > "$TMP_ROOT/legacy.out" 2> "$TMP_ROOT/legacy.err"; then
+ fail 'corrupt Chrome archive was accepted for an unmarked legacy install'
+fi
+[[ ! -e "$legacy_log" ]] || fail 'unmarked Chrome executable was invoked before archive verification'
+grep -q 'SHA-256 mismatch' "$TMP_ROOT/legacy.err" || fail 'legacy rejection did not report a checksum mismatch'
+assert_no_external
+
+printf '%s\n' 'stale marker' > "$legacy_destination/.html-api-fuzz-verified"
+if PATH="$NO_EXTERNAL_BIN:$PATH" \
+ HTML_API_FUZZ_EXTERNAL_LOG="$EXTERNAL_LOG" \
+ HTML_API_FUZZ_CHROME_INSTALL_ROOT="$legacy_root" \
+ HTML_API_FUZZ_INVOCATION_LOG="$legacy_log" \
+ HTML_API_FUZZ_FAKE_VERSION="$CHROME_VERSION" \
+ "$CHROME_INSTALLER" > "$TMP_ROOT/stale.out" 2> "$TMP_ROOT/stale.err"; then
+ fail 'corrupt Chrome archive was accepted for a stale marker'
+fi
+[[ ! -e "$legacy_log" ]] || fail 'stale-marker Chrome executable was invoked before archive verification'
+assert_no_external
+
+# A correctly marked install can take the archive-free fast path.
+fast_root="$TMP_ROOT/chrome-fast"
+fast_destination="$fast_root/$CHROME_VERSION/$CHROME_PLATFORM"
+fast_executable="$fast_destination/$CHROME_ARCHIVE_DIR/$CHROME_EXECUTABLE_RELATIVE"
+fast_log="$TMP_ROOT/fast-invocations"
+write_fake_chrome "$fast_executable"
+printf '%s\n' "$MARKER_CONTENT" > "$fast_destination/.html-api-fuzz-verified"
+PATH="$NO_EXTERNAL_BIN:$PATH" \
+ HTML_API_FUZZ_EXTERNAL_LOG="$EXTERNAL_LOG" \
+ HTML_API_FUZZ_CHROME_INSTALL_ROOT="$fast_root" \
+ HTML_API_FUZZ_INVOCATION_LOG="$fast_log" \
+ HTML_API_FUZZ_FAKE_VERSION="$CHROME_VERSION" \
+ "$CHROME_INSTALLER" > "$TMP_ROOT/fast.out"
+[[ "$(cat "$TMP_ROOT/fast.out")" = "$fast_executable" ]] || fail 'valid-marker fast path returned the wrong executable'
+[[ "$(wc -l < "$fast_log" | tr -d '[:space:]')" -eq 1 ]] || fail 'valid-marker fast path did not invoke Chrome exactly once'
+assert_no_external
+
+# Once the marker authenticates the install provenance, a reported version
+# mismatch is an explicit hard failure.
+wrong_root="$TMP_ROOT/chrome-wrong-version"
+wrong_destination="$wrong_root/$CHROME_VERSION/$CHROME_PLATFORM"
+wrong_executable="$wrong_destination/$CHROME_ARCHIVE_DIR/$CHROME_EXECUTABLE_RELATIVE"
+wrong_log="$TMP_ROOT/wrong-version-invocations"
+write_fake_chrome "$wrong_executable"
+printf '%s\n' "$MARKER_CONTENT" > "$wrong_destination/.html-api-fuzz-verified"
+if PATH="$NO_EXTERNAL_BIN:$PATH" \
+ HTML_API_FUZZ_EXTERNAL_LOG="$EXTERNAL_LOG" \
+ HTML_API_FUZZ_CHROME_INSTALL_ROOT="$wrong_root" \
+ HTML_API_FUZZ_INVOCATION_LOG="$wrong_log" \
+ HTML_API_FUZZ_FAKE_VERSION='0.0.0.0' \
+ "$CHROME_INSTALLER" > "$TMP_ROOT/wrong.out" 2> "$TMP_ROOT/wrong.err"; then
+ fail 'marker-backed wrong Chrome version was accepted'
+fi
+[[ -s "$wrong_log" ]] || fail 'marker-backed Chrome was not invoked for its version check'
+grep -q 'expected' "$TMP_ROOT/wrong.err" || fail 'wrong Chrome version failure was not explicit'
+assert_no_external
+
+# A successful-looking version string cannot mask a nonzero browser exit.
+nonzero_root="$TMP_ROOT/chrome-nonzero-version"
+nonzero_destination="$nonzero_root/$CHROME_VERSION/$CHROME_PLATFORM"
+nonzero_executable="$nonzero_destination/$CHROME_ARCHIVE_DIR/$CHROME_EXECUTABLE_RELATIVE"
+nonzero_log="$TMP_ROOT/nonzero-version-invocations"
+write_fake_chrome "$nonzero_executable"
+printf '%s\n' "$MARKER_CONTENT" > "$nonzero_destination/.html-api-fuzz-verified"
+if PATH="$NO_EXTERNAL_BIN:$PATH" \
+ HTML_API_FUZZ_EXTERNAL_LOG="$EXTERNAL_LOG" \
+ HTML_API_FUZZ_CHROME_INSTALL_ROOT="$nonzero_root" \
+ HTML_API_FUZZ_INVOCATION_LOG="$nonzero_log" \
+ HTML_API_FUZZ_FAKE_VERSION="$CHROME_VERSION" \
+ HTML_API_FUZZ_FAKE_EXIT_STATUS='23' \
+ "$CHROME_INSTALLER" > "$TMP_ROOT/nonzero.out" 2> "$TMP_ROOT/nonzero.err"; then
+ fail 'nonzero Chrome version command was accepted'
+fi
+[[ -s "$nonzero_log" ]] || fail 'nonzero Chrome version command was not invoked'
+grep -q 'failed its version check' "$TMP_ROOT/nonzero.err" || fail 'nonzero Chrome version failure was not explicit'
+assert_no_external
+
+# Even a checksum-valid archive is harmless until its temporary extraction has
+# the expected executable and version. Use a copied fixture and fake unzip to
+# model a malformed layout without changing the production manifest.
+fixture_dir="$TMP_ROOT/chrome-fixture"
+fixture_root="$TMP_ROOT/chrome-layout"
+fixture_bin="$TMP_ROOT/fixture-bin"
+mkdir -p "$fixture_dir" "$fixture_bin" "$fixture_root/.downloads"
+cp "$CHROME_INSTALLER" "$fixture_dir/install.sh"
+cp "$CHROME_DIR/VERSION" "$fixture_dir/VERSION"
+fixture_archive="$fixture_root/.downloads/$CHROME_ARCHIVE_NAME"
+printf '%s\n' 'checksum-valid synthetic archive' > "$fixture_archive"
+fixture_sha256="$(sha256_file "$fixture_archive")"
+printf '%s %s\n' "$fixture_sha256" "$CHROME_ARCHIVE_NAME" > "$fixture_dir/SHA256SUMS"
+{
+ printf '%s\n' '#!/bin/sh'
+ printf '%s\n' 'destination=' 'while [ "$#" -gt 0 ]; do' \
+ ' if [ "$1" = "-d" ]; then shift; destination="$1"; fi' \
+ ' shift' 'done' 'mkdir -p "$destination/unexpected-layout"'
+} > "$fixture_bin/unzip"
+chmod 0755 "$fixture_bin/unzip"
+layout_destination="$fixture_root/$CHROME_VERSION/$CHROME_PLATFORM"
+layout_executable="$layout_destination/$CHROME_ARCHIVE_DIR/$CHROME_EXECUTABLE_RELATIVE"
+layout_log="$TMP_ROOT/layout-invocations"
+write_fake_chrome "$layout_executable"
+printf '%s\n' 'preserve me' > "$layout_destination/preserved"
+if PATH="$fixture_bin:$PATH" \
+ HTML_API_FUZZ_CHROME_INSTALL_ROOT="$fixture_root" \
+ HTML_API_FUZZ_INVOCATION_LOG="$layout_log" \
+ HTML_API_FUZZ_FAKE_VERSION="$CHROME_VERSION" \
+ "$fixture_dir/install.sh" > "$TMP_ROOT/layout.out" 2> "$TMP_ROOT/layout.err"; then
+ fail 'malformed Chrome archive layout was accepted'
+fi
+[[ -f "$layout_destination/preserved" ]] || fail 'malformed Chrome archive replaced the existing install'
+[[ ! -e "$layout_log" ]] || fail 'unmarked Chrome executable ran during malformed-layout handling'
+grep -q 'did not contain the expected executable' "$TMP_ROOT/layout.err" || fail 'malformed layout failure was not explicit'
+
+# A checksum-valid archive with the right layout but wrong executable version
+# is also rejected while the old destination is still intact.
+version_fixture_dir="$TMP_ROOT/chrome-version-fixture"
+version_fixture_root="$TMP_ROOT/chrome-extracted-version"
+version_fixture_bin="$TMP_ROOT/version-fixture-bin"
+mkdir -p "$version_fixture_dir" "$version_fixture_bin" "$version_fixture_root/.downloads"
+cp "$CHROME_INSTALLER" "$version_fixture_dir/install.sh"
+cp "$CHROME_DIR/VERSION" "$version_fixture_dir/VERSION"
+version_fixture_archive="$version_fixture_root/.downloads/$CHROME_ARCHIVE_NAME"
+printf '%s\n' 'checksum-valid wrong-version archive' > "$version_fixture_archive"
+version_fixture_sha256="$(sha256_file "$version_fixture_archive")"
+printf '%s %s\n' "$version_fixture_sha256" "$CHROME_ARCHIVE_NAME" > "$version_fixture_dir/SHA256SUMS"
+extracted_source="$TMP_ROOT/wrong-extracted-chrome"
+{
+ printf '%s\n' '#!/bin/sh'
+ printf '%s\n' 'printf "%s\n" invoked >> "$HTML_API_FUZZ_TEMP_INVOCATION_LOG"'
+ printf '%s\n' 'printf "%s\n" "Google Chrome for Testing 0.0.0.0"'
+} > "$extracted_source"
+chmod 0755 "$extracted_source"
+{
+ printf '%s\n' '#!/bin/sh'
+ printf '%s\n' 'destination=' 'while [ "$#" -gt 0 ]; do' \
+ ' if [ "$1" = "-d" ]; then shift; destination="$1"; fi' \
+ ' shift' 'done'
+ printf '%s\n' 'executable="$destination/$HTML_API_FUZZ_TEST_ARCHIVE_DIR/$HTML_API_FUZZ_TEST_EXECUTABLE_RELATIVE"'
+ printf '%s\n' 'mkdir -p "$(dirname "$executable")"'
+ printf '%s\n' 'cp "$HTML_API_FUZZ_TEST_EXTRACTED_EXECUTABLE" "$executable"'
+ printf '%s\n' 'chmod 0755 "$executable"'
+} > "$version_fixture_bin/unzip"
+chmod 0755 "$version_fixture_bin/unzip"
+version_destination="$version_fixture_root/$CHROME_VERSION/$CHROME_PLATFORM"
+version_old_executable="$version_destination/$CHROME_ARCHIVE_DIR/$CHROME_EXECUTABLE_RELATIVE"
+version_old_log="$TMP_ROOT/version-old-invocations"
+version_temp_log="$TMP_ROOT/version-temp-invocations"
+write_fake_chrome "$version_old_executable"
+printf '%s\n' 'preserve me too' > "$version_destination/preserved"
+if PATH="$version_fixture_bin:$PATH" \
+ HTML_API_FUZZ_CHROME_INSTALL_ROOT="$version_fixture_root" \
+ HTML_API_FUZZ_INVOCATION_LOG="$version_old_log" \
+ HTML_API_FUZZ_TEMP_INVOCATION_LOG="$version_temp_log" \
+ HTML_API_FUZZ_FAKE_VERSION="$CHROME_VERSION" \
+ HTML_API_FUZZ_TEST_ARCHIVE_DIR="$CHROME_ARCHIVE_DIR" \
+ HTML_API_FUZZ_TEST_EXECUTABLE_RELATIVE="$CHROME_EXECUTABLE_RELATIVE" \
+ HTML_API_FUZZ_TEST_EXTRACTED_EXECUTABLE="$extracted_source" \
+ "$version_fixture_dir/install.sh" > "$TMP_ROOT/extracted-version.out" 2> "$TMP_ROOT/extracted-version.err"; then
+ fail 'wrong extracted Chrome version was accepted'
+fi
+[[ -f "$version_destination/preserved" ]] || fail 'wrong-version archive replaced the existing install'
+[[ ! -e "$version_old_log" ]] || fail 'old unmarked Chrome executable ran during extracted-version handling'
+[[ "$(wc -l < "$version_temp_log" | tr -d '[:space:]')" -eq 1 ]] || fail 'temporary wrong-version Chrome was not validated exactly once'
+grep -q 'does not match pin' "$TMP_ROOT/extracted-version.err" || fail 'wrong extracted version failure was not explicit'
+
+# A corrupt cached rustup bootstrap must fail before chmod or execution, with
+# no checksum fetched from the same origin.
+rust_root="$TMP_ROOT/rust"
+rustup_init="$rust_root/downloads/rustup-init-$RUSTUP_VERSION-$RUST_TARGET"
+rust_log="$TMP_ROOT/rustup-invocations"
+mkdir -p "$(dirname "$rustup_init")"
+{
+ printf '%s\n' '#!/bin/sh'
+ printf '%s\n' 'printf "%s\n" invoked >> "$HTML_API_FUZZ_INVOCATION_LOG"'
+} > "$rustup_init"
+chmod 0644 "$rustup_init"
+rustup_mode_before="$(file_mode "$rustup_init")"
+if PATH="$NO_EXTERNAL_BIN:$PATH" \
+ HTML_API_FUZZ_EXTERNAL_LOG="$EXTERNAL_LOG" \
+ HTML5EVER_RUST_ROOT="$rust_root" \
+ CARGO_HOME="$rust_root/cargo" \
+ RUSTUP_HOME="$rust_root/rustup" \
+ HTML_API_FUZZ_INVOCATION_LOG="$rust_log" \
+ "$RUST_INSTALLER" > "$TMP_ROOT/rust.out" 2> "$TMP_ROOT/rust.err"; then
+ fail 'corrupt cached rustup-init was accepted'
+fi
+[[ ! -e "$rust_log" ]] || fail 'corrupt rustup-init was executed'
+[[ ! -x "$rustup_init" ]] || fail 'corrupt rustup-init was made executable before verification'
+[[ "$(file_mode "$rustup_init")" = "$rustup_mode_before" ]] || fail 'corrupt rustup-init mode changed before verification'
+grep -q 'SHA-256 mismatch' "$TMP_ROOT/rust.err" || fail 'rustup rejection did not report a checksum mismatch'
+assert_no_external
+
+# Keep a separately executable corrupt fixture so no-execution coverage does
+# not rely on the bootstrap's initial mode.
+rust_exec_root="$TMP_ROOT/rust-executable"
+rust_exec_init="$rust_exec_root/downloads/rustup-init-$RUSTUP_VERSION-$RUST_TARGET"
+rust_exec_log="$TMP_ROOT/rustup-executable-invocations"
+mkdir -p "$(dirname "$rust_exec_init")"
+{
+ printf '%s\n' '#!/bin/sh'
+ printf '%s\n' 'printf "%s\n" invoked >> "$HTML_API_FUZZ_INVOCATION_LOG"'
+} > "$rust_exec_init"
+chmod 0755 "$rust_exec_init"
+if PATH="$NO_EXTERNAL_BIN:$PATH" \
+ HTML_API_FUZZ_EXTERNAL_LOG="$EXTERNAL_LOG" \
+ HTML5EVER_RUST_ROOT="$rust_exec_root" \
+ CARGO_HOME="$rust_exec_root/cargo" \
+ RUSTUP_HOME="$rust_exec_root/rustup" \
+ HTML_API_FUZZ_INVOCATION_LOG="$rust_exec_log" \
+ "$RUST_INSTALLER" > "$TMP_ROOT/rust-exec.out" 2> "$TMP_ROOT/rust-exec.err"; then
+ fail 'executable corrupt cached rustup-init was accepted'
+fi
+[[ ! -e "$rust_exec_log" ]] || fail 'executable corrupt rustup-init was executed'
+grep -q 'SHA-256 mismatch' "$TMP_ROOT/rust-exec.err" || fail 'executable rustup rejection did not report a checksum mismatch'
+assert_no_external
+
+printf '%s\n' 'oracle install integrity smoke passed'
diff --git a/tools/html-api-fuzz/tests/preflight-verifier-smoke.php b/tools/html-api-fuzz/tests/preflight-verifier-smoke.php
new file mode 100755
index 0000000000000..54dacf99c81b8
--- /dev/null
+++ b/tools/html-api-fuzz/tests/preflight-verifier-smoke.php
@@ -0,0 +1,377 @@
+#!/usr/bin/env php
+ array( 'pipe', 'r' ),
+ 1 => array( 'pipe', 'w' ),
+ 2 => array( 'pipe', 'w' ),
+ );
+ $process = proc_open( $command, $spec, $pipes, $cwd );
+ if ( ! is_resource( $process ) ) {
+ html_api_fuzz_preflight_smoke_fail( 'Could not start subprocess.' );
+ }
+ fclose( $pipes[0] );
+ stream_set_blocking( $pipes[1], false );
+ stream_set_blocking( $pipes[2], false );
+ $stdout = '';
+ $stderr = '';
+ $start = microtime( true );
+ $timed_out = false;
+ while ( true ) {
+ $stdout .= stream_get_contents( $pipes[1] );
+ $stderr .= stream_get_contents( $pipes[2] );
+ $status = proc_get_status( $process );
+ if ( ! $status['running'] ) {
+ break;
+ }
+ if ( ( microtime( true ) - $start ) * 1000 > $timeout_ms ) {
+ $timed_out = true;
+ proc_terminate( $process );
+ usleep( 200000 );
+ $status = proc_get_status( $process );
+ if ( $status['running'] ) {
+ proc_terminate( $process, 9 );
+ }
+ break;
+ }
+ usleep( 10000 );
+ }
+ $stdout .= stream_get_contents( $pipes[1] );
+ $stderr .= stream_get_contents( $pipes[2] );
+ fclose( $pipes[1] );
+ fclose( $pipes[2] );
+ $code = proc_close( $process );
+ return array(
+ 'code' => $timed_out ? null : $code,
+ 'timedOut' => $timed_out,
+ 'output' => $stdout . $stderr,
+ );
+}
+
+function html_api_fuzz_preflight_smoke_definitions(): array {
+ $root = \HtmlApiFuzz\repo_root() . '/tools/html-api-fuzz/oracles';
+ $chrome_version = trim( file_get_contents( $root . '/chrome/VERSION' ) );
+ return array(
+ 'lexbor' => array(
+ 'kind' => \HtmlApiFuzz\OracleRenderer::KIND_LEXBOR_SOURCE,
+ 'available' => true,
+ 'lexborCommit' => trim( file_get_contents( $root . '/lexbor/COMMIT' ) ),
+ 'lexborVersion' => 'test-version',
+ ),
+ 'html5ever' => array(
+ 'kind' => \HtmlApiFuzz\OracleRenderer::KIND_HTML5EVER_SOURCE,
+ 'available' => true,
+ 'html5everVersion' => '0.39.0',
+ 'html5everChecksum' => '46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8',
+ 'markup5everRcdomVersion' => '0.39.0+unofficial',
+ 'markup5everRcdomChecksum' => '3ac010f19d6c4af81eeb4018a39d7a115de9d285af45c126a4ac02e6fc5716b7',
+ 'rustToolchain' => '1.88.0',
+ ),
+ 'chrome' => array(
+ 'kind' => \HtmlApiFuzz\OracleRenderer::KIND_CHROME_CDP,
+ 'available' => true,
+ 'pinnedChromeVersion' => $chrome_version,
+ 'browserVersion' => $chrome_version,
+ 'browserPid' => 4242,
+ ),
+ );
+}
+
+function html_api_fuzz_preflight_smoke_fixture( string $directory, string $mutation = 'valid' ): void {
+ $definitions = html_api_fuzz_preflight_smoke_definitions();
+ $start_seed = 10;
+ $seed_stride = 3;
+ $seeds = array( 10, 13, 16 );
+ foreach ( $definitions as $label => $oracle ) {
+ $state = array(
+ 'stopReason' => 'max-seeds',
+ 'startSeed' => $start_seed,
+ 'seedStride' => $seed_stride,
+ 'nextSeed' => 19,
+ 'profile' => 'text-fragment',
+ 'mode' => \HtmlApiFuzz\Generator::MODE_FRAGMENT_BODY,
+ 'payloadPolicy' => 'valid-utf8',
+ 'fragmentContext' => 'body',
+ 'corpusMutatePercent' => 0,
+ 'forcePrimaryOracle' => true,
+ 'oracle' => $oracle,
+ );
+ $rows = array();
+ foreach ( $seeds as $index => $seed ) {
+ $rows[] = array(
+ 'seed' => $seed,
+ 'ok' => 2 !== $index,
+ 'status' => 2 === $index ? 'failed' : 'ok',
+ 'failureClass' => 2 === $index ? 'tree-mismatch' : null,
+ 'workerCode' => 2 === $index ? 2 : 0,
+ 'workerTimedOut' => false,
+ 'inputSha1' => sha1( 'shared-seed-' . $seed ),
+ 'oracle' => $oracle,
+ 'oracleExecuted' => true,
+ );
+ }
+
+ if ( 'duplicate' === $mutation && 'lexbor' === $label ) {
+ $rows[] = $rows[0];
+ } elseif ( 'missing' === $mutation && 'html5ever' === $label ) {
+ array_pop( $rows );
+ } elseif ( 'malformed-hash' === $mutation && 'lexbor' === $label ) {
+ $rows[0]['inputSha1'] = 'not-a-sha1';
+ } elseif ( 'hash-mismatch' === $mutation && 'html5ever' === $label ) {
+ $rows[0]['inputSha1'] = sha1( 'different-input' );
+ } elseif ( 'chrome-pid' === $mutation && 'chrome' === $label ) {
+ $rows[2]['oracle']['browserPid'] = 4343;
+ } elseif ( 'worker-failed' === $mutation && 'lexbor' === $label ) {
+ $rows[0]['ok'] = false;
+ $rows[0]['status'] = 'worker-failed';
+ $rows[0]['failureClass'] = 'worker-failed';
+ } elseif ( 'worker-code-one' === $mutation && 'lexbor' === $label ) {
+ $rows[2]['workerCode'] = 1;
+ } elseif ( 'ok-code-two' === $mutation && 'lexbor' === $label ) {
+ $rows[0]['workerCode'] = 2;
+ } elseif ( 'state-bounds' === $mutation && 'lexbor' === $label ) {
+ $state['nextSeed'] = 16;
+ } elseif ( 'generator-state' === $mutation && 'lexbor' === $label ) {
+ $state['fragmentContext'] = 'svg';
+ } elseif ( 'force-state' === $mutation && 'lexbor' === $label ) {
+ $state['forcePrimaryOracle'] = false;
+ } elseif ( 'oracle-not-executed' === $mutation && 'lexbor' === $label ) {
+ $rows[0]['oracleExecuted'] = false;
+ }
+
+ $lane = $directory . '/' . $label;
+ \HtmlApiFuzz\ensure_dir( $lane );
+ \HtmlApiFuzz\write_json_file( $lane . '/state.json', $state );
+ $store = new \HtmlApiFuzz\ResultStore( $lane . '/' . \HtmlApiFuzz\ResultStore::FILENAME );
+ foreach ( $rows as $row ) {
+ $store->record_attempt( $row );
+ }
+ $store->close();
+ if ( 'lexbor' === $label && in_array( $mutation, array( 'malformed-seed', 'malformed-timeout', 'malformed-oracle-executed' ), true ) ) {
+ $db = new SQLite3( $lane . '/' . \HtmlApiFuzz\ResultStore::FILENAME );
+ if ( 'malformed-seed' === $mutation ) {
+ $db->exec( "UPDATE attempts SET seed = '10junk' WHERE id = 1" );
+ } elseif ( 'malformed-timeout' === $mutation ) {
+ $db->exec( "UPDATE attempts SET worker_timed_out = 'not-a-number' WHERE id = 1" );
+ } else {
+ $db->exec( "UPDATE attempts SET oracle_executed = 'not-a-number' WHERE id = 1" );
+ }
+ $db->close();
+ }
+ }
+}
+
+function html_api_fuzz_preflight_smoke_verify( string $directory, bool $expected_ok, string $message ): void {
+ $process = \HtmlApiFuzz\run_php_process(
+ array(
+ dirname( __DIR__ ) . '/verify-preflight.php',
+ '--run-dir',
+ $directory,
+ '--max-seeds',
+ '3',
+ '--start-seed',
+ '10',
+ '--seed-stride',
+ '3',
+ ),
+ \HtmlApiFuzz\repo_root(),
+ 10000
+ );
+ if ( $expected_ok ) {
+ html_api_fuzz_preflight_smoke_assert( 0 === $process['code'], $message . ': ' . $process['output'] );
+ } else {
+ html_api_fuzz_preflight_smoke_assert( 0 !== $process['code'] && false !== strpos( $process['output'], $message ), "Expected verifier rejection containing '{$message}': {$process['output']}" );
+ }
+}
+
+function html_api_fuzz_preflight_smoke_v2_schema( SQLite3 $db ): void {
+ $db->exec(
+ 'CREATE TABLE attempts (
+ id INTEGER PRIMARY KEY,
+ created_at TEXT NOT NULL,
+ seed INTEGER NOT NULL,
+ ok INTEGER NOT NULL,
+ status TEXT NOT NULL,
+ failure_class TEXT,
+ signature_hash TEXT,
+ family_key TEXT,
+ oracle_finding_class TEXT,
+ oracle_finding_type TEXT,
+ oracle_suspected_owner TEXT,
+ oracle_signature_hash TEXT,
+ oracle_family_key TEXT,
+ oracle_kind TEXT,
+ oracle_version TEXT,
+ oracle_commit TEXT,
+ oracle_binary TEXT,
+ profile TEXT,
+ mode TEXT,
+ payload_policy TEXT,
+ input_source TEXT,
+ input_sha1 TEXT,
+ input_length INTEGER,
+ duration_ms INTEGER,
+ worker_code INTEGER,
+ worker_timed_out INTEGER NOT NULL DEFAULT 0,
+ artifacts_retained INTEGER NOT NULL DEFAULT 0,
+ failure_artifacts_retained INTEGER,
+ oracle_artifacts_retained INTEGER,
+ summary_json TEXT,
+ result_json TEXT,
+ replay_json TEXT
+ )'
+ );
+ $db->exec( 'PRAGMA user_version = 2' );
+}
+
+function html_api_fuzz_preflight_smoke_v3_schema( SQLite3 $db ): void {
+ html_api_fuzz_preflight_smoke_v2_schema( $db );
+ $db->exec( 'ALTER TABLE attempts ADD COLUMN oracle_browser_pid INTEGER' );
+ $db->exec( 'PRAGMA user_version = 3' );
+}
+
+function html_api_fuzz_preflight_smoke_migration( string $path, int $source_version ): void {
+ $db = new SQLite3( $path, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE );
+ if ( 2 === $source_version ) {
+ html_api_fuzz_preflight_smoke_v2_schema( $db );
+ } else {
+ html_api_fuzz_preflight_smoke_v3_schema( $db );
+ }
+ $db->close();
+
+ $store = new \HtmlApiFuzz\ResultStore( $path );
+ $chrome_oracle = html_api_fuzz_preflight_smoke_definitions()['chrome'];
+ $chrome_oracle['browserPid'] = 5151;
+ $store->record_attempt(
+ array(
+ 'seed' => 1,
+ 'ok' => true,
+ 'status' => 'ok',
+ 'inputSha1' => sha1( 'migration-' . $source_version ),
+ 'workerCode' => 0,
+ 'workerTimedOut' => false,
+ 'oracle' => $chrome_oracle,
+ 'oracleExecuted' => true,
+ )
+ );
+ $store->close();
+
+ $db = new SQLite3( $path, SQLITE3_OPEN_READONLY );
+ html_api_fuzz_preflight_smoke_assert( 4 === (int) $db->querySingle( 'PRAGMA user_version' ), "Expected ResultStore v{$source_version} to migrate to v4." );
+ $columns = array();
+ $column_rows = $db->query( 'PRAGMA table_info(attempts)' );
+ while ( false !== ( $column = $column_rows->fetchArray( SQLITE3_ASSOC ) ) ) {
+ $columns[] = $column['name'];
+ }
+ html_api_fuzz_preflight_smoke_assert( in_array( 'oracle_browser_pid', $columns, true ), 'Expected migration to include oracle_browser_pid.' );
+ html_api_fuzz_preflight_smoke_assert( in_array( 'oracle_executed', $columns, true ), 'Expected migration to add oracle_executed.' );
+ html_api_fuzz_preflight_smoke_assert( 5151 === (int) $db->querySingle( 'SELECT oracle_browser_pid FROM attempts' ), 'Expected migrated store to persist browser PID.' );
+ html_api_fuzz_preflight_smoke_assert( 1 === (int) $db->querySingle( 'SELECT oracle_executed FROM attempts' ), 'Expected migrated store to persist oracle execution.' );
+ $db->close();
+}
+
+$repo_root = \HtmlApiFuzz\repo_root();
+$work_dir = sys_get_temp_dir() . '/html-api-fuzz-preflight-smoke-' . \HtmlApiFuzz\timestamp();
+\HtmlApiFuzz\ensure_dir( $work_dir );
+$failure = null;
+
+try {
+ $valid = $work_dir . '/valid';
+ html_api_fuzz_preflight_smoke_fixture( $valid );
+ html_api_fuzz_preflight_smoke_verify( $valid, true, 'Expected exact fixture to pass' );
+ $chrome_db = new SQLite3( $valid . '/chrome/' . \HtmlApiFuzz\ResultStore::FILENAME, SQLITE3_OPEN_READONLY );
+ html_api_fuzz_preflight_smoke_assert( 0 === (int) $chrome_db->querySingle( 'SELECT COUNT(*) FROM attempts WHERE oracle_browser_pid != 4242 OR oracle_browser_pid IS NULL' ), 'Expected every Chrome PID to persist.' );
+ $chrome_db->close();
+
+ $invalid_cases = array(
+ 'duplicate' => 'duplicate seed',
+ 'missing' => 'recorded 2 rows instead of 3',
+ 'malformed-hash' => 'valid input SHA-1',
+ 'hash-mismatch' => 'inputs differ from the shared-seed baseline',
+ 'chrome-pid' => 'did not use the pinned live browser PID',
+ 'worker-failed' => 'had infrastructure failure',
+ 'worker-code-one' => 'inconsistent worker status',
+ 'ok-code-two' => 'inconsistent worker status',
+ 'state-bounds' => 'recorded inconsistent seed bounds',
+ 'generator-state' => 'deterministic preflight generator configuration',
+ 'force-state' => 'deterministic preflight generator configuration',
+ 'oracle-not-executed' => 'did not prove primary oracle execution',
+ 'malformed-seed' => 'invalid seed scalar',
+ 'malformed-timeout' => 'invalid worker timeout flag',
+ 'malformed-oracle-executed' => 'did not prove primary oracle execution',
+ );
+ foreach ( $invalid_cases as $mutation => $diagnostic ) {
+ $case_dir = $work_dir . '/' . $mutation;
+ html_api_fuzz_preflight_smoke_fixture( $case_dir, $mutation );
+ html_api_fuzz_preflight_smoke_verify( $case_dir, false, $diagnostic );
+ }
+
+ $migration_dir = $work_dir . '/migrations';
+ \HtmlApiFuzz\ensure_dir( $migration_dir );
+ html_api_fuzz_preflight_smoke_migration( $migration_dir . '/v2.sqlite', 2 );
+ html_api_fuzz_preflight_smoke_migration( $migration_dir . '/v3.sqlite', 3 );
+
+ $preflight = $repo_root . '/tools/html-api-fuzz/preflight.sh';
+ $valid_environment = array(
+ 'MAX_SEEDS' => '3',
+ 'START_SEED' => '1',
+ 'SEED_STRIDE' => '1',
+ 'BATCH_SIZE' => '2',
+ 'MAX_INPUT_BYTES'=> '128',
+ );
+ $shell_cases = array(
+ 'max-zero' => array( 'MAX_SEEDS', '0', 'positive decimal integer' ),
+ 'max-text' => array( 'MAX_SEEDS', 'three', 'positive decimal integer' ),
+ 'stride-zero' => array( 'SEED_STRIDE', '0', 'positive decimal integer' ),
+ 'batch-zero' => array( 'BATCH_SIZE', '0', 'positive decimal integer' ),
+ 'input-zero' => array( 'MAX_INPUT_BYTES', '0', 'positive decimal integer' ),
+ 'start-text' => array( 'START_SEED', 'one', 'Expected START_SEED to be an integer' ),
+ 'max-range' => array( 'MAX_SEEDS', (string) PHP_INT_MAX . '0', 'fit in a PHP integer' ),
+ 'product-overflow' => array( 'MAX_SEEDS', (string) PHP_INT_MAX, 'MAX_SEEDS * SEED_STRIDE exceeds' ),
+ 'next-overflow' => array( 'START_SEED', (string) PHP_INT_MAX, 'final nextSeed exceeds' ),
+ );
+ foreach ( $shell_cases as $case => $definition ) {
+ $environment = $valid_environment;
+ $environment[ $definition[0] ] = $definition[1];
+ if ( 'product-overflow' === $case ) {
+ $environment['SEED_STRIDE'] = '2';
+ }
+ $output_dir = $work_dir . '/shell-' . $case;
+ $command = array( '/usr/bin/env' );
+ foreach ( $environment as $name => $value ) {
+ $command[] = $name . '=' . $value;
+ }
+ $command[] = 'OUTPUT_DIR=' . $output_dir;
+ $command[] = $preflight;
+ $process = html_api_fuzz_preflight_smoke_run_command( $command, $repo_root );
+ html_api_fuzz_preflight_smoke_assert( ! $process['timedOut'], "Expected {$case} validation to finish before install." );
+ html_api_fuzz_preflight_smoke_assert( 0 !== $process['code'] && false !== strpos( $process['output'], $definition[2] ), "Expected {$case} diagnostic: {$process['output']}" );
+ html_api_fuzz_preflight_smoke_assert( ! file_exists( $output_dir ), "Expected {$case} to fail before output creation." );
+ }
+} catch ( Throwable $error ) {
+ $failure = $error;
+} finally {
+ \HtmlApiFuzz\remove_dir_recursive( $work_dir );
+}
+
+if ( null !== $failure ) {
+ fwrite( STDERR, 'FAIL: ' . $failure->getMessage() . "\n" );
+ exit( 1 );
+}
+if ( is_dir( $work_dir ) ) {
+ fwrite( STDERR, "FAIL: Expected preflight smoke work directory cleanup.\n" );
+ exit( 1 );
+}
+echo "OK preflight-verifier-smoke\n";
diff --git a/tools/html-api-fuzz/tests/replay-oracle-compatibility-smoke.php b/tools/html-api-fuzz/tests/replay-oracle-compatibility-smoke.php
new file mode 100644
index 0000000000000..3ac784c296480
--- /dev/null
+++ b/tools/html-api-fuzz/tests/replay-oracle-compatibility-smoke.php
@@ -0,0 +1,184 @@
+#!/usr/bin/env php
+ $version[1] ?? null,
+ 'checksum' => $checksum[1] ?? null,
+ );
+ }
+ html_api_fuzz_replay_compatibility_fail( "Could not find {$name} in Cargo.lock." );
+}
+
+function html_api_fuzz_replay_compatibility_fake_oracle( string $path, array $oracle ): void {
+ $version = base64_encode(
+ json_encode(
+ array(
+ 'status' => 'ok',
+ 'oracle' => $oracle,
+ ),
+ JSON_THROW_ON_ERROR
+ ) . "\n"
+ );
+ $render = base64_encode(
+ json_encode(
+ array(
+ 'status' => 'ok',
+ 'oracle' => $oracle,
+ 'nodeCount' => 0,
+ 'tree' => "\n",
+ 'treeBase64' => base64_encode( "\n" ),
+ ),
+ JSON_THROW_ON_ERROR
+ ) . "\n"
+ );
+ $script = "#!/usr/bin/env php\n \HtmlApiFuzz\OracleRenderer::KIND_HTML5EVER_SOURCE,
+ 'available' => true,
+ 'html5everVersion' => $html5ever['version'],
+ 'html5everChecksum' => $html5ever['checksum'],
+ 'markup5everRcdomVersion' => $rcdom['version'],
+ 'markup5everRcdomChecksum' => $rcdom['checksum'],
+ 'rustToolchain' => $toolchain_match[1] ?? null,
+ );
+ $oracle_path = $work_dir . '/fake-html5ever-oracle.php';
+ html_api_fuzz_replay_compatibility_fake_oracle( $oracle_path, $oracle );
+ $renderer = \HtmlApiFuzz\OracleRenderer::from_options(
+ array(
+ 'dom-oracle' => \HtmlApiFuzz\OracleRenderer::KIND_HTML5EVER_SOURCE,
+ 'html5ever-oracle-bin' => $oracle_path,
+ )
+ );
+ $current = $renderer->metadata();
+ html_api_fuzz_replay_compatibility_assert( true === ( $current['available'] ?? false ), 'Expected the complete fake html5ever identity to be valid.' );
+
+ $keys = array( 'markup5everRcdomVersion', 'markup5everRcdomChecksum', 'rustToolchain' );
+ $legacy = $oracle;
+ foreach ( $keys as $key ) {
+ unset( $legacy[ $key ] );
+ }
+ $renderer->assert_replay_compatible( $legacy );
+ $legacy_empty = $oracle;
+ foreach ( $keys as $key ) {
+ $legacy_empty[ $key ] = '';
+ }
+ $renderer->assert_replay_compatible( $legacy_empty );
+
+ foreach ( $keys as $key ) {
+ $recorded_oracle = $oracle;
+ $recorded_oracle[ $key ] = 'wrong-' . $key;
+ $replay = array(
+ 'inputBase64' => base64_encode( 'x' ),
+ 'mode' => \HtmlApiFuzz\Generator::MODE_FRAGMENT_BODY,
+ 'profile' => 'replay-compatibility-smoke',
+ 'seed' => 1,
+ 'fragmentContext' => 'body',
+ 'limits' => array(
+ 'maxTokens' => 20,
+ 'maxNodes' => 20,
+ ),
+ 'options' => array(
+ 'domOracle' => \HtmlApiFuzz\OracleRenderer::KIND_HTML5EVER_SOURCE,
+ 'html5everOracleBin' => $oracle_path,
+ ),
+ 'oracle' => $recorded_oracle,
+ 'signature' => array( 'hash' => str_repeat( 'a', 64 ) ),
+ );
+ $replay_path = $work_dir . '/replay-' . $key . '.json';
+ \HtmlApiFuzz\write_json_file( $replay_path, $replay );
+ foreach ( array( 'replay.php', 'minimize.php' ) as $script_name ) {
+ html_api_fuzz_replay_compatibility_command(
+ $root . '/tools/html-api-fuzz/' . $script_name,
+ $replay_path,
+ $work_dir . '/output-' . $script_name . '-' . $key,
+ $oracle_path,
+ $key
+ );
+ }
+ }
+} catch ( Throwable $error ) {
+ $failure = $error;
+} finally {
+ \HtmlApiFuzz\remove_dir_recursive( $work_dir );
+}
+
+if ( null !== $failure ) {
+ fwrite( STDERR, 'FAIL: ' . $failure->getMessage() . "\n" );
+ exit( 1 );
+}
+if ( is_dir( $work_dir ) ) {
+ fwrite( STDERR, "FAIL: Expected replay compatibility work directory cleanup.\n" );
+ exit( 1 );
+}
+echo "OK replay-oracle-compatibility-smoke\n";
diff --git a/tools/html-api-fuzz/tests/result-store-smoke.php b/tools/html-api-fuzz/tests/result-store-smoke.php
new file mode 100644
index 0000000000000..7d73a14ea5dd0
--- /dev/null
+++ b/tools/html-api-fuzz/tests/result-store-smoke.php
@@ -0,0 +1,239 @@
+#!/usr/bin/env php
+ 'chrome-cdp',
+ 'browserVersion' => '150.0.7871.114',
+ 'chromeExecutable' => '/tmp/chrome-for-testing',
+);
+$lexbor_oracle = array(
+ 'kind' => 'lexbor-source',
+ 'lexborVersion' => '2.10.0',
+ 'lexborCommit' => '481c444261a132190a3fb746d6d2f60824af3717',
+ 'binary' => '/tmp/lexbor-tree-oracle',
+);
+
+$pass_summary = array(
+ 'kind' => 'attempt',
+ 'ok' => true,
+ 'status' => 'passed',
+ 'failureClass' => null,
+ 'seed' => 11,
+ 'profile' => 'document',
+ 'mode' => 'document',
+ 'payloadPolicy' => 'utf8',
+ 'inputSource' => 'generated',
+ 'inputSha1' => sha1( 'pass' ),
+ 'inputLength' => 4,
+ 'signature' => null,
+ 'oracle' => $chrome_oracle,
+ 'oracleExecuted' => true,
+ 'artifactsRetained' => false,
+ 'resultPath' => null,
+ 'replayPath' => null,
+ 'logPath' => null,
+ 'durationMs' => 12,
+ 'workerCode' => 0,
+ 'workerTimedOut' => false,
+);
+$pass_id = $store->record_attempt( $pass_summary );
+
+$failure_summary = array(
+ 'kind' => 'failure',
+ 'ok' => false,
+ 'status' => 'failed',
+ 'failureClass' => 'tree-mismatch',
+ 'seed' => 12,
+ 'profile' => 'document',
+ 'mode' => 'document',
+ 'payloadPolicy' => 'utf8',
+ 'inputSource' => 'generated',
+ 'inputSha1' => sha1( 'fail' ),
+ 'inputLength' => 8,
+ 'signature' => array(
+ 'hash' => 'abc123def456',
+ 'familyKey' => 'fam456789abc',
+ ),
+ 'oracle' => $lexbor_oracle,
+ 'oracleExecuted' => true,
+ 'artifactsRetained' => true,
+ 'resultPath' => $work_dir . '/seed-12/primary/result.json',
+ 'replayPath' => $work_dir . '/seed-12/primary/replay.json',
+ 'logPath' => null,
+ 'durationMs' => 30,
+ 'workerCode' => 2,
+ 'workerTimedOut' => false,
+);
+$failure_result = array(
+ 'ok' => false,
+ 'status' => 'failed',
+ 'failureClass' => 'tree-mismatch',
+ 'signature' => array( 'hash' => 'abc123def456' ),
+);
+$failure_replay = array(
+ 'kind' => 'html-api-fuzz-replay',
+ 'seed' => 12,
+ 'inputBase64' => base64_encode( 'fail ' ),
+);
+$failure_summary['failureArtifactsRetained'] = true;
+$failure_summary['oracleArtifactsRetained'] = false;
+$failure_id = $store->record_attempt( $failure_summary, $failure_result, $failure_replay );
+
+$pruned_summary = $failure_summary;
+$pruned_summary['seed'] = 13;
+$pruned_summary['artifactsRetained'] = false;
+$pruned_summary['failureArtifactsRetained'] = false;
+$pruned_summary['oracleArtifactsRetained'] = false;
+$pruned_summary['resultPath'] = null;
+$pruned_summary['replayPath'] = null;
+$pruned_id = $store->record_attempt( $pruned_summary, $failure_result, $failure_replay );
+
+$same_seed_summary = $pruned_summary;
+$same_seed_summary['signature'] = array(
+ 'hash' => 'newseedabc123',
+ 'familyKey' => 'newseedfam456',
+);
+$same_seed_result = array(
+ 'ok' => false,
+ 'status' => 'failed',
+ 'failureClass' => 'tree-mismatch',
+ 'signature' => array( 'hash' => 'newseedabc123' ),
+);
+$same_seed_replay = array(
+ 'kind' => 'html-api-fuzz-replay',
+ 'seed' => 13,
+ 'inputBase64' => base64_encode( 'new replay ' ),
+);
+$same_seed_id = $store->record_attempt( $same_seed_summary, $same_seed_result, $same_seed_replay );
+
+$oracle_summary = array(
+ 'kind' => 'oracle-finding',
+ 'ok' => true,
+ 'status' => 'oracle-tolerated',
+ 'failureClass' => 'oracle-tolerated',
+ 'seed' => 14,
+ 'profile' => 'document',
+ 'mode' => 'document',
+ 'payloadPolicy' => 'utf8',
+ 'inputSource' => 'generated',
+ 'inputSha1' => sha1( 'oracle' ),
+ 'inputLength' => 12,
+ 'signature' => null,
+ 'oracle' => $chrome_oracle,
+ 'oracleExecuted' => true,
+ 'oracleFinding' => array(
+ 'classification' => 'oracle-bug',
+ 'type' => 'dom-xlink-dropped-local-name-after-xlink',
+ 'suspectedOwner' => 'Legacy Lexbor oracle',
+ 'signature' => array(
+ 'hash' => 'oracle-abc123',
+ 'familyKey' => 'oracle-fam123',
+ ),
+ ),
+ 'artifactsRetained' => true,
+ 'failureArtifactsRetained' => false,
+ 'oracleArtifactsRetained' => true,
+ 'resultPath' => $work_dir . '/seed-14/primary/result.json',
+ 'replayPath' => $work_dir . '/seed-14/primary/replay.json',
+ 'logPath' => null,
+ 'durationMs' => 18,
+ 'workerCode' => 0,
+ 'workerTimedOut' => false,
+);
+$oracle_result = array(
+ 'ok' => true,
+ 'status' => 'oracle-tolerated',
+ 'oracleFinding' => $oracle_summary['oracleFinding'],
+);
+$oracle_replay = array(
+ 'kind' => 'html-api-fuzz-replay',
+ 'seed' => 14,
+ 'inputBase64' => base64_encode( ' ' ),
+ 'oracleFinding' => $oracle_summary['oracleFinding'],
+);
+$oracle_id = $store->record_attempt( $oracle_summary, $oracle_result, $oracle_replay );
+
+html_api_fuzz_smoke_assert( 5 === $store->count_attempts(), 'Expected five recorded attempts.' );
+html_api_fuzz_smoke_assert( array( 12 ) === $store->retained_seeds( 'abc123def456' ), 'Expected seed 12 as the retained exemplar for the signature.' );
+html_api_fuzz_smoke_assert( array() === $store->retained_seeds( 'unseen' ), 'Expected no retained exemplars for an unseen signature.' );
+html_api_fuzz_smoke_assert( array( 14 ) === $store->oracle_retained_seeds( 'oracle-abc123' ), 'Expected seed 14 as the retained exemplar for the oracle signature.' );
+html_api_fuzz_smoke_assert( $store->seed_artifacts_retained( 12 ), 'Expected seed 12 to be marked as retained.' );
+html_api_fuzz_smoke_assert( ! $store->seed_artifacts_retained( 13 ), 'Expected seed 13 not to be marked as retained.' );
+html_api_fuzz_smoke_assert( 5 === $store->max_id(), 'Expected max id of five.' );
+
+$stored_replay = $store->replay_for_seed( 13 );
+html_api_fuzz_smoke_assert( is_array( $stored_replay ) && base64_encode( 'new replay ' ) === ( $stored_replay['inputBase64'] ?? null ), 'Expected seed replay lookup to return the most recent replay for compatibility.' );
+html_api_fuzz_smoke_assert( is_array( $store->replay_for_attempt_id( $pruned_id ) ) && base64_encode( 'fail ' ) === ( $store->replay_for_attempt_id( $pruned_id )['inputBase64'] ?? null ), 'Expected exact attempt replay lookup to survive same-seed reruns.' );
+html_api_fuzz_smoke_assert( is_array( $store->replay_for_attempt_id( $same_seed_id ) ) && base64_encode( 'new replay ' ) === ( $store->replay_for_attempt_id( $same_seed_id )['inputBase64'] ?? null ), 'Expected exact attempt replay lookup to retrieve the newer same-seed replay.' );
+html_api_fuzz_smoke_assert( null === $store->replay_for_seed( 11 ), 'Expected no stored replay for a passing seed.' );
+$stored_oracle_replay = $store->replay_for_seed( 14 );
+html_api_fuzz_smoke_assert( is_array( $stored_oracle_replay ) && base64_encode( ' ' ) === ( $stored_oracle_replay['inputBase64'] ?? null ), 'Expected the oracle finding replay to be retrievable from the store.' );
+html_api_fuzz_smoke_assert( is_array( $store->replay_for_attempt_id( $oracle_id ) ) && base64_encode( ' ' ) === ( $store->replay_for_attempt_id( $oracle_id )['inputBase64'] ?? null ), 'Expected the oracle finding replay to be retrievable by attempt id.' );
+
+$failures = $store->failures_after( 0, $store->max_id() );
+html_api_fuzz_smoke_assert( 3 === count( $failures ), 'Expected three failure rows.' );
+html_api_fuzz_smoke_assert( 12 === ( $failures[0]['record']['seed'] ?? null ), 'Expected the first failure record to be seed 12.' );
+html_api_fuzz_smoke_assert( 'abc123def456' === ( $failures[0]['record']['signature']['hash'] ?? null ), 'Expected the failure record to carry its signature.' );
+
+$tail = $store->failures_after( $failures[0]['id'], $store->max_id() );
+html_api_fuzz_smoke_assert( 2 === count( $tail ) && 13 === ( $tail[0]['record']['seed'] ?? null ) && 13 === ( $tail[1]['record']['seed'] ?? null ), 'Expected incremental reads to resume after an offset.' );
+
+$oracle_findings = $store->oracle_findings_after( 0, $store->max_id() );
+html_api_fuzz_smoke_assert( 1 === count( $oracle_findings ), 'Expected one oracle finding row.' );
+html_api_fuzz_smoke_assert( 14 === ( $oracle_findings[0]['record']['seed'] ?? null ), 'Expected the oracle finding record to be seed 14.' );
+html_api_fuzz_smoke_assert( 'oracle-abc123' === ( $oracle_findings[0]['record']['oracleFinding']['signature']['hash'] ?? null ), 'Expected the oracle finding record to carry its oracle signature.' );
+
+$store->close();
+
+// Reopen read-only as the watcher does and confirm persistence.
+$reader = new \HtmlApiFuzz\ResultStore( $db_path, true );
+html_api_fuzz_smoke_assert( 5 === $reader->count_attempts(), 'Expected attempts to persist across reopen.' );
+html_api_fuzz_smoke_assert( 3 === count( $reader->failures_after( 0, $reader->max_id() ) ), 'Expected failures to persist across reopen.' );
+html_api_fuzz_smoke_assert( 1 === count( $reader->oracle_findings_after( 0, $reader->max_id() ) ), 'Expected oracle findings to persist across reopen.' );
+$reader->close();
+
+// The grouping columns must be queryable without json_extract.
+$raw = new SQLite3( $db_path, SQLITE3_OPEN_READONLY );
+html_api_fuzz_smoke_assert( 2 === (int) $raw->querySingle( "SELECT COUNT(*) FROM attempts WHERE family_key = 'fam456789abc'" ), 'Expected family_key to be stored per failure row.' );
+html_api_fuzz_smoke_assert( 1 === (int) $raw->querySingle( "SELECT COUNT(*) FROM attempts WHERE oracle_family_key = 'oracle-fam123'" ), 'Expected oracle_family_key to be stored per oracle finding row.' );
+html_api_fuzz_smoke_assert( 1 === (int) $raw->querySingle( "SELECT COUNT(*) FROM attempts WHERE oracle_signature_hash = 'oracle-abc123'" ), 'Expected oracle_signature_hash to be queryable.' );
+html_api_fuzz_smoke_assert( 1 === (int) $raw->querySingle( "SELECT COUNT(*) FROM attempts WHERE signature_hash = 'abc123def456' AND failure_artifacts_retained = 1" ), 'Expected failure retention to use its own budget flag.' );
+html_api_fuzz_smoke_assert( 1 === (int) $raw->querySingle( "SELECT COUNT(*) FROM attempts WHERE oracle_signature_hash = 'oracle-abc123' AND oracle_artifacts_retained = 1" ), 'Expected oracle retention to use its own budget flag.' );
+html_api_fuzz_smoke_assert( 1 === (int) $raw->querySingle( "SELECT COUNT(*) FROM attempts WHERE seed = 11 AND oracle_kind = 'chrome-cdp' AND oracle_version = '150.0.7871.114' AND oracle_binary = '/tmp/chrome-for-testing'" ), 'Expected passing rows to keep Chrome oracle metadata in scalar columns.' );
+html_api_fuzz_smoke_assert( 3 === (int) $raw->querySingle( "SELECT COUNT(*) FROM attempts WHERE oracle_kind = 'lexbor-source' AND oracle_version = '2.10.0' AND oracle_commit = '481c444261a132190a3fb746d6d2f60824af3717'" ), 'Expected Lexbor oracle metadata to be queryable for failure rows.' );
+html_api_fuzz_smoke_assert( 3 === (int) $raw->querySingle( "SELECT COUNT(*) FROM attempts WHERE oracle_binary = '/tmp/lexbor-tree-oracle'" ), 'Expected Lexbor oracle binary to be stored in a scalar column.' );
+html_api_fuzz_smoke_assert( 5 === (int) $raw->querySingle( 'SELECT COUNT(*) FROM attempts WHERE oracle_executed = 1' ), 'Expected primary oracle execution to be stored for every fixture row.' );
+$raw->close();
+
+$future_db_path = $work_dir . '/future.sqlite';
+$future = new SQLite3( $future_db_path, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE );
+$future->exec( 'PRAGMA user_version = 99' );
+$future->close();
+$future_store = new \HtmlApiFuzz\ResultStore( $future_db_path );
+$future_store->close();
+$future = new SQLite3( $future_db_path, SQLITE3_OPEN_READONLY );
+html_api_fuzz_smoke_assert( 99 === (int) $future->querySingle( 'PRAGMA user_version' ), 'Opening a future schema should not downgrade user_version.' );
+$future->close();
+
+\HtmlApiFuzz\remove_dir_recursive( $work_dir );
+html_api_fuzz_smoke_assert( ! is_dir( $work_dir ), 'Expected remove_dir_recursive to delete the work directory.' );
+
+echo "OK result-store-smoke\n";
diff --git a/tools/html-api-fuzz/tests/runner-retention-smoke.php b/tools/html-api-fuzz/tests/runner-retention-smoke.php
new file mode 100644
index 0000000000000..2189cf84b4c6f
--- /dev/null
+++ b/tools/html-api-fuzz/tests/runner-retention-smoke.php
@@ -0,0 +1,232 @@
+#!/usr/bin/env php
+querySingle( 'SELECT COUNT(*) FROM attempts' ), 'Expected six recorded attempts.' );
+html_api_fuzz_smoke_assert( 0 === (int) $db->querySingle( 'SELECT COUNT(*) FROM attempts WHERE ok = 1 AND artifacts_retained = 1' ), 'Expected no retained artifacts for passing attempts.' );
+$rows = $db->query( 'SELECT seed, artifacts_retained FROM attempts' );
+while ( false !== ( $row = $rows->fetchArray( SQLITE3_ASSOC ) ) ) {
+ html_api_fuzz_smoke_assert(
+ is_dir( $run_dir . '/seed-' . $row['seed'] ) === (bool) $row['artifacts_retained'],
+ "Expected seed {$row['seed']} directory presence to match artifacts_retained={$row['artifacts_retained']}."
+ );
+}
+$db->close();
+
+/*
+ * 2. The failure path, deterministically: --fail-unsupported turns the many
+ * unsupported fragment contexts into failures with repeating signatures, so
+ * a cap of 1 must prune repeats while archiving their replay documents.
+ */
+$cap = 1;
+$fail_run_dir = $work_dir . '/fail-run';
+$proc = \HtmlApiFuzz\run_php_process(
+ array(
+ $runner,
+ '--output-dir',
+ $fail_run_dir,
+ '--max-seeds',
+ '40',
+ '--duration-seconds',
+ '0',
+ '--batch-size',
+ '10',
+ '--max-input-bytes',
+ '512',
+ '--fail-unsupported',
+ '--max-keep-per-signature',
+ (string) $cap,
+ ),
+ $repo_root,
+ 300000
+);
+html_api_fuzz_smoke_assert( 0 === $proc['code'], 'Expected failure-path runner to exit cleanly: ' . substr( $proc['output'], -1000 ) );
+
+$db = new SQLite3( $fail_run_dir . '/' . \HtmlApiFuzz\ResultStore::FILENAME, SQLITE3_OPEN_READONLY );
+// Precondition guard: this run must actually exercise pruning. If generator
+// or signature changes stop producing repeated signatures here, fail loudly
+// so the test can be re-tuned instead of silently going vacuous.
+$pruned_failures = (int) $db->querySingle( 'SELECT COUNT(*) FROM attempts WHERE ok = 0 AND artifacts_retained = 0' );
+html_api_fuzz_smoke_assert( $pruned_failures > 0, 'Expected the failure-path run to prune at least one over-cap failure; re-tune the seed range.' );
+html_api_fuzz_smoke_assert( 0 === (int) $db->querySingle( 'SELECT COUNT(*) FROM attempts WHERE ok = 0 AND summary_json IS NULL' ), 'Expected failures to store their summary JSON.' );
+html_api_fuzz_smoke_assert( 0 === (int) $db->querySingle( 'SELECT COUNT(*) FROM attempts WHERE ok = 0 AND artifacts_retained = 0 AND replay_json IS NULL' ), 'Expected pruned failures to archive their replay JSON.' );
+
+$max_retained_per_signature = (int) $db->querySingle(
+ 'SELECT COALESCE( MAX( n ), 0 ) FROM ( SELECT COUNT(*) AS n FROM attempts WHERE artifacts_retained = 1 AND signature_hash IS NOT NULL GROUP BY signature_hash )'
+);
+html_api_fuzz_smoke_assert( $max_retained_per_signature <= $cap, 'Expected retained exemplars per signature to respect the cap.' );
+
+// Every signature with failures keeps its first exemplar on disk.
+$sig_rows = $db->query( 'SELECT signature_hash, MAX(artifacts_retained) AS retained FROM attempts WHERE ok = 0 AND signature_hash IS NOT NULL GROUP BY signature_hash' );
+while ( false !== ( $row = $sig_rows->fetchArray( SQLITE3_ASSOC ) ) ) {
+ html_api_fuzz_smoke_assert( 1 === (int) $row['retained'], "Expected signature {$row['signature_hash']} to retain its first exemplar." );
+}
+
+$rows = $db->query( 'SELECT seed, artifacts_retained FROM attempts' );
+while ( false !== ( $row = $rows->fetchArray( SQLITE3_ASSOC ) ) ) {
+ html_api_fuzz_smoke_assert(
+ is_dir( $fail_run_dir . '/seed-' . $row['seed'] ) === (bool) $row['artifacts_retained'],
+ "Expected seed {$row['seed']} directory presence to match artifacts_retained={$row['artifacts_retained']}."
+ );
+}
+
+$pruned = $db->querySingle( 'SELECT seed, signature_hash FROM attempts WHERE ok = 0 AND artifacts_retained = 0 LIMIT 1', true );
+$db->close();
+
+// A pruned failure must be reproducible from the store alone.
+$proc = \HtmlApiFuzz\run_php_process(
+ array(
+ $replay,
+ '--store',
+ $fail_run_dir . '/' . \HtmlApiFuzz\ResultStore::FILENAME,
+ '--seed',
+ (string) $pruned['seed'],
+ '--output-dir',
+ $work_dir . '/store-replay',
+ ),
+ $repo_root,
+ 60000
+);
+$replay_report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( is_array( $replay_report ), 'Expected replay --store to produce a JSON report: ' . substr( $proc['output'], -1000 ) );
+html_api_fuzz_smoke_assert( false === ( $replay_report['ok'] ?? true ), 'Expected the store replay to reproduce a failure.' );
+html_api_fuzz_smoke_assert(
+ ( $replay_report['signature']['hash'] ?? null ) === $pruned['signature_hash'],
+ 'Expected the store replay to reproduce the original signature.'
+);
+
+/*
+ * 3. A pre-existing stop file must refuse to start rather than silently
+ * succeed with zero seeds.
+ */
+$stop_run_dir = $work_dir . '/stop-run';
+\HtmlApiFuzz\ensure_dir( $stop_run_dir );
+file_put_contents( $stop_run_dir . '/STOP', "{}\n" );
+$proc = \HtmlApiFuzz\run_php_process(
+ array( $runner, '--output-dir', $stop_run_dir, '--max-seeds', '0', '--duration-seconds', '0' ),
+ $repo_root,
+ 60000
+);
+html_api_fuzz_smoke_assert( 0 !== $proc['code'], 'Expected runner to refuse to start over a pre-existing stop file.' );
+html_api_fuzz_smoke_assert( false !== strpos( $proc['output'], 'Stop file already exists' ), 'Expected a clear stale stop file message.' );
+html_api_fuzz_smoke_assert( ! is_file( $stop_run_dir . '/state.json' ), 'Expected no state to be written when refusing to start.' );
+
+/*
+ * 4. Mid-run graceful stop: an indefinite runner must finish its in-flight
+ * batch, record it, and exit with stopReason stop-requested.
+ */
+$mid_run_dir = $work_dir . '/mid-run';
+$spec = array( 0 => array( 'pipe', 'r' ), 1 => array( 'pipe', 'w' ), 2 => array( 'pipe', 'w' ) );
+$process = proc_open(
+ array( PHP_BINARY, $runner, '--output-dir', $mid_run_dir, '--max-seeds', '0', '--duration-seconds', '0', '--batch-size', '5', '--max-input-bytes', '512' ),
+ $spec,
+ $pipes,
+ $repo_root
+);
+html_api_fuzz_smoke_assert( is_resource( $process ), 'Expected the indefinite runner to start.' );
+fclose( $pipes[0] );
+stream_set_blocking( $pipes[1], false );
+stream_set_blocking( $pipes[2], false );
+
+$deadline = microtime( true ) + 120.0;
+$progress = false;
+while ( microtime( true ) < $deadline ) {
+ $mid_state = is_file( $mid_run_dir . '/state.json' ) ? @json_decode( (string) @file_get_contents( $mid_run_dir . '/state.json' ), true ) : null;
+ $attempted = is_array( $mid_state )
+ ? (int) ( $mid_state['successes'] ?? 0 ) + (int) ( $mid_state['failures'] ?? 0 ) + (int) ( $mid_state['unsupported'] ?? 0 )
+ + (int) ( $mid_state['oracleParseErrors'] ?? 0 ) + (int) ( $mid_state['oracleUnsupported'] ?? 0 ) + (int) ( $mid_state['oracleTolerated'] ?? 0 )
+ : 0;
+ if ( $attempted > 0 ) {
+ $progress = true;
+ break;
+ }
+ usleep( 100000 );
+}
+html_api_fuzz_smoke_assert( $progress, 'Expected the indefinite runner to record progress before the stop request.' );
+
+// Request the stop through the stop tool to cover its run-dir path.
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--run-dir', $mid_run_dir ), $repo_root, 30000 );
+html_api_fuzz_smoke_assert( 0 === $proc['code'], 'Expected stop.php to succeed: ' . substr( $proc['output'], -500 ) );
+html_api_fuzz_smoke_assert( is_file( $mid_run_dir . '/STOP' ), 'Expected stop.php to create the stop file.' );
+
+$deadline = microtime( true ) + 120.0;
+$exited = false;
+$code = null;
+while ( microtime( true ) < $deadline ) {
+ stream_get_contents( $pipes[1] );
+ stream_get_contents( $pipes[2] );
+ $status = proc_get_status( $process );
+ if ( ! $status['running'] ) {
+ $exited = true;
+ $code = $status['exitcode'];
+ break;
+ }
+ usleep( 100000 );
+}
+if ( ! $exited ) {
+ proc_terminate( $process, 9 );
+}
+fclose( $pipes[1] );
+fclose( $pipes[2] );
+proc_close( $process );
+html_api_fuzz_smoke_assert( $exited, 'Expected the runner to exit after the stop request.' );
+html_api_fuzz_smoke_assert( 0 === $code, 'Expected the stopped runner to exit cleanly.' );
+
+$mid_state = \HtmlApiFuzz\read_json_file( $mid_run_dir . '/state.json' );
+html_api_fuzz_smoke_assert( 'stop-requested' === ( $mid_state['stopReason'] ?? null ), 'Expected stopReason stop-requested after a mid-run stop.' );
+
+$db = new SQLite3( $mid_run_dir . '/' . \HtmlApiFuzz\ResultStore::FILENAME, SQLITE3_OPEN_READONLY );
+html_api_fuzz_smoke_assert( 0 < (int) $db->querySingle( 'SELECT COUNT(*) FROM attempts' ), 'Expected the in-flight batch to be recorded before stopping.' );
+$db->close();
+
+\HtmlApiFuzz\remove_dir_recursive( $work_dir );
+
+echo "OK runner-retention-smoke\n";
diff --git a/tools/html-api-fuzz/tests/source-oracle-protocol-smoke.php b/tools/html-api-fuzz/tests/source-oracle-protocol-smoke.php
new file mode 100755
index 0000000000000..dee385807a946
--- /dev/null
+++ b/tools/html-api-fuzz/tests/source-oracle-protocol-smoke.php
@@ -0,0 +1,354 @@
+#!/usr/bin/env php
+ $version[1] ?? null,
+ 'checksum' => $checksum[1] ?? null,
+ );
+ }
+ html_api_fuzz_source_protocol_fail( "Could not find {$name} in Cargo.lock." );
+}
+
+function html_api_fuzz_source_protocol_fake(
+ string $directory,
+ array $version_result,
+ array $render_result,
+ int $version_exit = 0,
+ int $render_exit = 0
+): string {
+ static $counter = 0;
+ ++$counter;
+ $path = $directory . '/fake-source-oracle-' . $counter . '.php';
+ $version_json = base64_encode( json_encode( $version_result, JSON_THROW_ON_ERROR ) . "\n" );
+ $render_json = base64_encode( json_encode( $render_result, JSON_THROW_ON_ERROR ) . "\n" );
+ $script = "#!/usr/bin/env php\n $kind );
+ if ( \HtmlApiFuzz\OracleRenderer::KIND_LEXBOR_SOURCE === $kind ) {
+ $options['lexbor-oracle-bin'] = $binary;
+ } else {
+ $options['html5ever-oracle-bin'] = $binary;
+ }
+ return \HtmlApiFuzz\OracleRenderer::from_options( $options );
+}
+
+function html_api_fuzz_source_protocol_version( array $oracle, string $status = 'ok' ): array {
+ return array(
+ 'status' => $status,
+ 'oracle' => $oracle,
+ );
+}
+
+function html_api_fuzz_source_protocol_render( array $oracle, array $fields = array() ): array {
+ return array_merge(
+ array(
+ 'status' => 'ok',
+ 'oracle' => $oracle,
+ 'nodeCount' => 1,
+ 'tree' => "\"x\"\n\n",
+ 'treeBase64' => base64_encode( "\"x\"\n\n" ),
+ ),
+ $fields
+ );
+}
+
+function html_api_fuzz_source_protocol_metadata_case(
+ string $directory,
+ string $kind,
+ array $version,
+ bool $available,
+ string $message,
+ int $version_exit = 0
+): array {
+ $binary = html_api_fuzz_source_protocol_fake( $directory, $version, array(), $version_exit );
+ $metadata = html_api_fuzz_source_protocol_renderer( $kind, $binary )->metadata();
+ html_api_fuzz_source_protocol_assert( $available === ( $metadata['available'] ?? false ), $message );
+ return array( 'binary' => $binary, 'metadata' => $metadata );
+}
+
+function html_api_fuzz_source_protocol_render_case(
+ string $directory,
+ array $oracle,
+ array $render,
+ string $expected_status,
+ string $expected_failure_class,
+ string $message,
+ int $render_exit = 0
+): array {
+ $version = html_api_fuzz_source_protocol_version( $oracle );
+ $binary = html_api_fuzz_source_protocol_fake( $directory, $version, $render, 0, $render_exit );
+ $result = html_api_fuzz_source_protocol_renderer( \HtmlApiFuzz\OracleRenderer::KIND_LEXBOR_SOURCE, $binary )->render(
+ 'x',
+ \HtmlApiFuzz\Generator::MODE_FRAGMENT_BODY,
+ array( 'maxNodes' => 20 ),
+ 'body'
+ );
+ html_api_fuzz_source_protocol_assert( $expected_status === ( $result['status'] ?? null ), $message . ' status' );
+ html_api_fuzz_source_protocol_assert( $expected_failure_class === ( $result['failureClass'] ?? '' ), $message . ' failure class' );
+ return $result;
+}
+
+$root = \HtmlApiFuzz\repo_root();
+$work_dir = sys_get_temp_dir() . '/html-api-fuzz-source-protocol-' . \HtmlApiFuzz\timestamp();
+\HtmlApiFuzz\ensure_dir( $work_dir );
+$failure = null;
+
+try {
+ $lexbor_oracle = array(
+ 'kind' => \HtmlApiFuzz\OracleRenderer::KIND_LEXBOR_SOURCE,
+ 'available' => true,
+ 'lexborCommit' => trim( file_get_contents( $root . '/tools/html-api-fuzz/oracles/lexbor/COMMIT' ) ),
+ 'lexborVersion' => 'test-version',
+ );
+ $valid_lexbor_version = html_api_fuzz_source_protocol_version( $lexbor_oracle );
+ html_api_fuzz_source_protocol_metadata_case( $work_dir, \HtmlApiFuzz\OracleRenderer::KIND_LEXBOR_SOURCE, $valid_lexbor_version, true, 'Expected valid Lexbor identity to be available.' );
+ $untrusted_lexbor_oracle = array_merge( $lexbor_oracle, array( 'binary' => '/attacker/lexbor', 'arbitrary' => 'discard' ) );
+ $lexbor_metadata_case = html_api_fuzz_source_protocol_metadata_case(
+ $work_dir,
+ \HtmlApiFuzz\OracleRenderer::KIND_LEXBOR_SOURCE,
+ html_api_fuzz_source_protocol_version( $untrusted_lexbor_oracle ),
+ true,
+ 'Expected valid Lexbor identity with unknown metadata to be available.'
+ );
+ html_api_fuzz_source_protocol_assert( $lexbor_metadata_case['binary'] === ( $lexbor_metadata_case['metadata']['binary'] ?? null ), 'Lexbor version metadata overrode the configured binary.' );
+ html_api_fuzz_source_protocol_assert( ! array_key_exists( 'arbitrary', $lexbor_metadata_case['metadata'] ), 'Lexbor version metadata retained an unknown field.' );
+
+ $lexbor_mutations = array(
+ 'wrong kind' => array( 'kind' => 'html5ever-source' ),
+ 'missing kind' => array( 'kind' => null ),
+ 'available false' => array( 'available' => false ),
+ 'missing available' => array( 'available' => null ),
+ 'wrong commit' => array( 'lexborCommit' => str_repeat( '0', 40 ) ),
+ 'missing version' => array( 'lexborVersion' => null ),
+ 'empty version' => array( 'lexborVersion' => '' ),
+ 'non-string version' => array( 'lexborVersion' => 310 ),
+ );
+ foreach ( $lexbor_mutations as $label => $mutation ) {
+ $oracle = $lexbor_oracle;
+ $key = array_key_first( $mutation );
+ if ( null === $mutation[ $key ] ) {
+ unset( $oracle[ $key ] );
+ } else {
+ $oracle[ $key ] = $mutation[ $key ];
+ }
+ html_api_fuzz_source_protocol_metadata_case(
+ $work_dir,
+ \HtmlApiFuzz\OracleRenderer::KIND_LEXBOR_SOURCE,
+ html_api_fuzz_source_protocol_version( $oracle ),
+ false,
+ "Expected Lexbor metadata to reject {$label}."
+ );
+ }
+ html_api_fuzz_source_protocol_metadata_case(
+ $work_dir,
+ \HtmlApiFuzz\OracleRenderer::KIND_LEXBOR_SOURCE,
+ html_api_fuzz_source_protocol_version( $lexbor_oracle, 'error' ),
+ false,
+ 'Expected Lexbor metadata to reject a non-ok version status.'
+ );
+ html_api_fuzz_source_protocol_metadata_case(
+ $work_dir,
+ \HtmlApiFuzz\OracleRenderer::KIND_LEXBOR_SOURCE,
+ $valid_lexbor_version,
+ false,
+ 'Expected Lexbor metadata to reject a nonzero version exit.',
+ 3
+ );
+
+ $lock = file_get_contents( $root . '/tools/html-api-fuzz/oracles/html5ever/Cargo.lock' );
+ $html5ever = html_api_fuzz_source_protocol_locked_package( $lock, 'html5ever' );
+ $rcdom = html_api_fuzz_source_protocol_locked_package( $lock, 'markup5ever_rcdom' );
+ $toolchain = file_get_contents( $root . '/tools/html-api-fuzz/oracles/html5ever/rust-toolchain.toml' );
+ preg_match( '/^channel\s*=\s*"([^\"]+)"/m', $toolchain, $toolchain_match );
+ $html5ever_oracle = array(
+ 'kind' => \HtmlApiFuzz\OracleRenderer::KIND_HTML5EVER_SOURCE,
+ 'available' => true,
+ 'html5everVersion' => $html5ever['version'],
+ 'html5everChecksum' => $html5ever['checksum'],
+ 'markup5everRcdomVersion' => $rcdom['version'],
+ 'markup5everRcdomChecksum' => $rcdom['checksum'],
+ 'rustToolchain' => $toolchain_match[1] ?? null,
+ );
+ html_api_fuzz_source_protocol_metadata_case(
+ $work_dir,
+ \HtmlApiFuzz\OracleRenderer::KIND_HTML5EVER_SOURCE,
+ html_api_fuzz_source_protocol_version( $html5ever_oracle ),
+ true,
+ 'Expected valid html5ever identity to be available.'
+ );
+ $untrusted_html5ever_oracle = array_merge( $html5ever_oracle, array( 'binary' => '/attacker/html5ever', 'arbitrary' => 'discard' ) );
+ $html5ever_metadata_case = html_api_fuzz_source_protocol_metadata_case(
+ $work_dir,
+ \HtmlApiFuzz\OracleRenderer::KIND_HTML5EVER_SOURCE,
+ html_api_fuzz_source_protocol_version( $untrusted_html5ever_oracle ),
+ true,
+ 'Expected valid html5ever identity with unknown metadata to be available.'
+ );
+ html_api_fuzz_source_protocol_assert( $html5ever_metadata_case['binary'] === ( $html5ever_metadata_case['metadata']['binary'] ?? null ), 'html5ever version metadata overrode the configured binary.' );
+ html_api_fuzz_source_protocol_assert( ! array_key_exists( 'arbitrary', $html5ever_metadata_case['metadata'] ), 'html5ever version metadata retained an unknown field.' );
+ foreach ( array( 'html5everVersion', 'html5everChecksum', 'markup5everRcdomVersion', 'markup5everRcdomChecksum', 'rustToolchain' ) as $field ) {
+ $wrong = $html5ever_oracle;
+ $wrong[ $field ] = 'wrong';
+ html_api_fuzz_source_protocol_metadata_case(
+ $work_dir,
+ \HtmlApiFuzz\OracleRenderer::KIND_HTML5EVER_SOURCE,
+ html_api_fuzz_source_protocol_version( $wrong ),
+ false,
+ "Expected html5ever metadata to reject wrong {$field}."
+ );
+ $missing = $html5ever_oracle;
+ unset( $missing[ $field ] );
+ html_api_fuzz_source_protocol_metadata_case(
+ $work_dir,
+ \HtmlApiFuzz\OracleRenderer::KIND_HTML5EVER_SOURCE,
+ html_api_fuzz_source_protocol_version( $missing ),
+ false,
+ "Expected html5ever metadata to reject missing {$field}."
+ );
+ }
+
+ $valid_render = html_api_fuzz_source_protocol_render( $lexbor_oracle );
+ $valid = html_api_fuzz_source_protocol_render_case( $work_dir, $lexbor_oracle, $valid_render, 'ok', '', 'Expected a valid ok result.' );
+ html_api_fuzz_source_protocol_assert( "\"x\"\n\n" === ( $valid['tree'] ?? null ), 'Expected valid treeBase64 to decode.' );
+ $untrusted_render_oracle = array_merge( $lexbor_oracle, array( 'binary' => '/attacker/lexbor', 'arbitrary' => 'discard' ) );
+ $untrusted_render = html_api_fuzz_source_protocol_render_case(
+ $work_dir,
+ $lexbor_oracle,
+ html_api_fuzz_source_protocol_render( $untrusted_render_oracle ),
+ 'ok',
+ '',
+ 'Expected valid render identity with unknown metadata to succeed.'
+ );
+ $render_binary = $untrusted_render['oracle']['binary'] ?? null;
+ html_api_fuzz_source_protocol_assert( is_string( $render_binary ) && str_starts_with( $untrusted_render['process']['command'] ?? '', escapeshellarg( $render_binary ) . ' ' ), 'Render metadata did not retain the configured executable path.' );
+ html_api_fuzz_source_protocol_assert( ! array_key_exists( 'arbitrary', $untrusted_render['oracle'] ), 'Render metadata retained an unknown field.' );
+ $drifted_render_oracle = $lexbor_oracle;
+ $drifted_render_oracle['lexborVersion'] = 'different-version';
+ html_api_fuzz_source_protocol_render_case(
+ $work_dir,
+ $lexbor_oracle,
+ html_api_fuzz_source_protocol_render( $drifted_render_oracle ),
+ 'error',
+ 'oracle-renderer-error',
+ 'Expected render identity drift after version validation to fail.'
+ );
+
+ $unsupported = html_api_fuzz_source_protocol_render(
+ $lexbor_oracle,
+ array(
+ 'status' => 'unsupported',
+ 'failureClass' => 'oracle-unsupported',
+ 'unsupported' => array( 'message' => 'not supported', 'extra' => 'discard' ),
+ )
+ );
+ $unsupported_result = html_api_fuzz_source_protocol_render_case( $work_dir, $lexbor_oracle, $unsupported, 'unsupported', 'oracle-unsupported', 'Expected a well-formed unsupported result.' );
+ html_api_fuzz_source_protocol_assert( array( 'message' => 'not supported' ) === ( $unsupported_result['unsupported'] ?? null ), 'Unsupported response retained untrusted details.' );
+
+ foreach ( array( 'oracle-parse-error', 'node-limit-exceeded', 'oracle-renderer-error' ) as $failure_class ) {
+ $semantic_error = html_api_fuzz_source_protocol_render(
+ $lexbor_oracle,
+ array(
+ 'status' => 'error',
+ 'failureClass' => $failure_class,
+ 'error' => 'semantic ' . $failure_class,
+ )
+ );
+ $semantic_result = html_api_fuzz_source_protocol_render_case( $work_dir, $lexbor_oracle, $semantic_error, 'error', $failure_class, "Expected allowed semantic error {$failure_class}." );
+ html_api_fuzz_source_protocol_assert( 'semantic ' . $failure_class === ( $semantic_result['error'] ?? null ), "Expected {$failure_class} details to survive." );
+ html_api_fuzz_source_protocol_assert( 0 === ( $semantic_result['process']['code'] ?? null ), "Expected {$failure_class} to exit zero." );
+ }
+ $semantic_error = html_api_fuzz_source_protocol_render(
+ $lexbor_oracle,
+ array(
+ 'status' => 'error',
+ 'failureClass' => 'node-limit-exceeded',
+ 'error' => 'limit',
+ )
+ );
+
+ html_api_fuzz_source_protocol_render_case( $work_dir, $lexbor_oracle, $semantic_error, 'error', 'oracle-renderer-error', 'Expected a nonzero render exit to be a process failure.', 4 );
+ html_api_fuzz_source_protocol_render_case( $work_dir, $lexbor_oracle, html_api_fuzz_source_protocol_render( $lexbor_oracle, array( 'status' => 'unknown' ) ), 'error', 'oracle-renderer-error', 'Expected an unknown status to fail.' );
+ foreach ( array( 'unknown-error-class', 'oracle-cli-error' ) as $failure_class ) {
+ $invalid_error_class = html_api_fuzz_source_protocol_render(
+ $lexbor_oracle,
+ array(
+ 'status' => 'error',
+ 'failureClass' => $failure_class,
+ 'error' => 'invalid class',
+ )
+ );
+ html_api_fuzz_source_protocol_render_case( $work_dir, $lexbor_oracle, $invalid_error_class, 'error', 'oracle-renderer-error', "Expected {$failure_class} to fail." );
+ }
+ $wrong_render_identity = $lexbor_oracle;
+ $wrong_render_identity['lexborCommit'] = str_repeat( 'f', 40 );
+ html_api_fuzz_source_protocol_render_case( $work_dir, $lexbor_oracle, html_api_fuzz_source_protocol_render( $wrong_render_identity ), 'error', 'oracle-renderer-error', 'Expected render identity mismatch to fail.' );
+ $tree_without_base64 = html_api_fuzz_source_protocol_render( $lexbor_oracle );
+ unset( $tree_without_base64['treeBase64'] );
+ html_api_fuzz_source_protocol_render_case( $work_dir, $lexbor_oracle, $tree_without_base64, 'error', 'oracle-renderer-error', 'Expected tree without treeBase64 to fail.' );
+ html_api_fuzz_source_protocol_render_case( $work_dir, $lexbor_oracle, html_api_fuzz_source_protocol_render( $lexbor_oracle, array( 'treeBase64' => '***' ) ), 'error', 'oracle-renderer-error', 'Expected invalid treeBase64 to fail.' );
+ html_api_fuzz_source_protocol_render_case( $work_dir, $lexbor_oracle, html_api_fuzz_source_protocol_render( $lexbor_oracle, array( 'tree' => 'different' ) ), 'error', 'oracle-renderer-error', 'Expected disagreeing trees to fail.' );
+ foreach ( array( 'missing' => null, 'non-integer' => '1', 'negative' => -1 ) as $label => $node_count ) {
+ $render = html_api_fuzz_source_protocol_render( $lexbor_oracle );
+ if ( null === $node_count ) {
+ unset( $render['nodeCount'] );
+ } else {
+ $render['nodeCount'] = $node_count;
+ }
+ html_api_fuzz_source_protocol_render_case( $work_dir, $lexbor_oracle, $render, 'error', 'oracle-renderer-error', "Expected {$label} nodeCount to fail." );
+ }
+ $malformed_unsupported = $unsupported;
+ $malformed_unsupported['failureClass'] = 'wrong';
+ html_api_fuzz_source_protocol_render_case( $work_dir, $lexbor_oracle, $malformed_unsupported, 'error', 'oracle-renderer-error', 'Expected malformed unsupported failureClass to fail.' );
+ $malformed_unsupported = $unsupported;
+ unset( $malformed_unsupported['unsupported']['message'] );
+ html_api_fuzz_source_protocol_render_case( $work_dir, $lexbor_oracle, $malformed_unsupported, 'error', 'oracle-renderer-error', 'Expected malformed unsupported details to fail.' );
+ $malformed_error = $semantic_error;
+ unset( $malformed_error['failureClass'] );
+ html_api_fuzz_source_protocol_render_case( $work_dir, $lexbor_oracle, $malformed_error, 'error', 'oracle-renderer-error', 'Expected a missing error failureClass to fail.' );
+ $malformed_error = $semantic_error;
+ unset( $malformed_error['error'] );
+ html_api_fuzz_source_protocol_render_case( $work_dir, $lexbor_oracle, $malformed_error, 'error', 'oracle-renderer-error', 'Expected missing error details to fail.' );
+} catch ( Throwable $error ) {
+ $failure = $error;
+} finally {
+ \HtmlApiFuzz\remove_dir_recursive( $work_dir );
+}
+
+if ( null !== $failure ) {
+ fwrite( STDERR, 'FAIL: ' . $failure->getMessage() . "\n" );
+ exit( 1 );
+}
+if ( is_dir( $work_dir ) ) {
+ fwrite( STDERR, "FAIL: Expected protocol smoke work directory cleanup.\n" );
+ exit( 1 );
+}
+echo "OK source-oracle-protocol-smoke\n";
diff --git a/tools/html-api-fuzz/tests/stop-smoke.php b/tools/html-api-fuzz/tests/stop-smoke.php
new file mode 100644
index 0000000000000..54d904381edba
--- /dev/null
+++ b/tools/html-api-fuzz/tests/stop-smoke.php
@@ -0,0 +1,531 @@
+#!/usr/bin/env php
+ 'html-api-fuzz-runner-state',
+ 'updatedAt' => gmdate( 'c' ),
+ 'stopFile' => dirname( $path ) . '/STOP',
+ 'stopReason' => null,
+ ),
+ $overrides
+ )
+ );
+}
+
+function html_api_fuzz_smoke_touch( string $path, int $mtime ): void {
+ html_api_fuzz_smoke_assert( touch( $path, $mtime ), "Expected touch to succeed for {$path}." );
+ clearstatcache( true, $path );
+ html_api_fuzz_smoke_assert( $mtime === (int) filemtime( $path ), "Expected mtime {$mtime} for {$path}." );
+}
+
+$stop_tool = dirname( __DIR__ ) . '/stop.php';
+$runner_tool = dirname( __DIR__ ) . '/runner.php';
+$repo_root = \HtmlApiFuzz\repo_root();
+$work_dir = sys_get_temp_dir() . '/html-api-fuzz-stop-' . \HtmlApiFuzz\timestamp();
+$repo_artifacts_dir = $repo_root . '/artifacts';
+$repo_fuzz_artifacts_dir = $repo_artifacts_dir . '/html-api-fuzz';
+$had_repo_artifacts_dir = is_dir( $repo_artifacts_dir );
+$had_repo_fuzz_artifacts_dir = is_dir( $repo_fuzz_artifacts_dir );
+$repo_relative_run_dir = $repo_fuzz_artifacts_dir . '/run-stop-smoke-' . basename( $work_dir );
+
+register_shutdown_function(
+ static function () use ( $work_dir, $repo_relative_run_dir, $repo_fuzz_artifacts_dir, $repo_artifacts_dir, $had_repo_fuzz_artifacts_dir, $had_repo_artifacts_dir ): void {
+ \HtmlApiFuzz\remove_dir_recursive( $work_dir );
+ \HtmlApiFuzz\remove_dir_recursive( $repo_relative_run_dir );
+ if ( ! $had_repo_fuzz_artifacts_dir ) {
+ @rmdir( $repo_fuzz_artifacts_dir );
+ }
+ if ( ! $had_repo_artifacts_dir ) {
+ @rmdir( $repo_artifacts_dir );
+ }
+ }
+);
+
+/*
+ * Discovery must prefer an unfinished launcher run over a more recently
+ * touched but already finished one.
+ */
+$launcher_artifacts = $work_dir . '/launcher-discovery';
+$finished_run = $launcher_artifacts . '/run-finished';
+\HtmlApiFuzz\ensure_dir( $finished_run );
+html_api_fuzz_smoke_write_runner_state(
+ $finished_run . '/state.json',
+ array(
+ 'stopReason' => 'max-seeds',
+ )
+);
+
+$active_run = $launcher_artifacts . '/run-active';
+\HtmlApiFuzz\ensure_dir( $active_run );
+\HtmlApiFuzz\write_json_file(
+ $active_run . '/launcher-state.json',
+ array(
+ 'kind' => 'html-api-fuzz-launcher-state',
+ 'finished' => false,
+ 'updatedAt' => gmdate( 'c' ),
+ )
+);
+// Make the finished run the more recently touched one.
+html_api_fuzz_smoke_touch( $finished_run . '/state.json', time() + 5 );
+
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--artifacts-dir', $launcher_artifacts ), $repo_root, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 0 === $proc['code'] && is_array( $report ), 'Expected stop.php launcher discovery to succeed: ' . substr( $proc['output'], -500 ) );
+html_api_fuzz_smoke_assert( $active_run === ( $report['runDir'] ?? null ), 'Expected discovery to prefer the unfinished launcher run.' );
+html_api_fuzz_smoke_assert( is_file( $active_run . '/STOP' ), 'Expected the stop file in the active launcher run.' );
+html_api_fuzz_smoke_assert( ! is_file( $finished_run . '/STOP' ), 'Expected no stop file in the finished run.' );
+html_api_fuzz_smoke_assert( false === ( $report['looksFinished'] ?? null ), 'Expected the chosen launcher run not to look finished.' );
+
+// Lane runner state alone is also enough to mark a launch run unfinished.
+$lane_artifacts = $work_dir . '/lane-discovery';
+$lane_run = $lane_artifacts . '/run-lane-active';
+\HtmlApiFuzz\ensure_dir( $lane_run . '/lane-00' );
+html_api_fuzz_smoke_write_runner_state( $lane_run . '/lane-00/state.json' );
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--artifacts-dir', $lane_artifacts ), $repo_root, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 0 === $proc['code'] && $lane_run === ( $report['runDir'] ?? null ), 'Expected lane runner state to mark a launch run unfinished.' );
+html_api_fuzz_smoke_assert( is_file( $lane_run . '/STOP' ), 'Expected the stop file in the lane-active run.' );
+
+/*
+ * A standalone runner writes root state.json and may advertise a custom
+ * stopFile. A newer malformed runner state without stopReason must not be
+ * treated as unfinished.
+ */
+$standalone_artifacts = $work_dir . '/standalone-discovery';
+$missing_run = $standalone_artifacts . '/run-missing-stop-reason';
+\HtmlApiFuzz\ensure_dir( $missing_run );
+\HtmlApiFuzz\write_json_file(
+ $missing_run . '/state.json',
+ array(
+ 'kind' => 'html-api-fuzz-runner-state',
+ 'updatedAt' => gmdate( 'c' ),
+ )
+);
+html_api_fuzz_smoke_touch( $missing_run . '/state.json', time() + 10 );
+
+$standalone_run = $standalone_artifacts . '/run-standalone-active';
+$custom_stop = $standalone_artifacts . '/custom-stop/STOP';
+\HtmlApiFuzz\ensure_dir( $standalone_run );
+html_api_fuzz_smoke_write_runner_state(
+ $standalone_run . '/state.json',
+ array(
+ 'stopFile' => $custom_stop,
+ )
+);
+
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--artifacts-dir', $standalone_artifacts ), $repo_root, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 0 === $proc['code'] && is_array( $report ), 'Expected stop.php standalone discovery to succeed: ' . substr( $proc['output'], -500 ) );
+html_api_fuzz_smoke_assert( $standalone_run === ( $report['runDir'] ?? null ), 'Expected discovery to prefer the unfinished standalone run.' );
+html_api_fuzz_smoke_assert( $standalone_run . '/STOP' === ( $report['stopFile'] ?? null ), 'Expected discovery to report the run-dir stop file as the primary stop file.' );
+html_api_fuzz_smoke_assert( in_array( $custom_stop, $report['stopFiles'] ?? array(), true ), 'Expected discovery to include the standalone runner custom stop file.' );
+html_api_fuzz_smoke_assert( is_file( $custom_stop ), 'Expected the custom stop file to be created.' );
+html_api_fuzz_smoke_assert( is_file( $standalone_run . '/STOP' ), 'Expected the run-dir stop file to be created for watcher and orchestrator paths.' );
+html_api_fuzz_smoke_assert( ! is_file( $missing_run . '/STOP' ), 'Expected no stop file in the malformed runner-state run.' );
+html_api_fuzz_smoke_assert( false === ( $report['looksFinished'] ?? null ), 'Expected the standalone run not to look finished.' );
+
+// A second invocation reports the existing request instead of failing.
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--artifacts-dir', $standalone_artifacts ), $repo_root, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 0 === $proc['code'] && true === ( $report['alreadyRequested'] ?? null ), 'Expected a repeat stop request to be reported as already requested.' );
+
+// Explicit --run-dir inspection also honors the custom stop file.
+unlink( $custom_stop );
+unlink( $standalone_run . '/STOP' );
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--run-dir', $standalone_run ), $repo_root, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 0 === $proc['code'] && $standalone_run . '/STOP' === ( $report['stopFile'] ?? null ), 'Expected --run-dir to report the run-dir stop file as primary.' );
+html_api_fuzz_smoke_assert( in_array( $custom_stop, $report['stopFiles'] ?? array(), true ), 'Expected --run-dir to include the standalone runner custom stop file.' );
+html_api_fuzz_smoke_assert( is_file( $custom_stop ), 'Expected --run-dir to create the custom stop file.' );
+html_api_fuzz_smoke_assert( is_file( $standalone_run . '/STOP' ), 'Expected --run-dir to create the run-dir stop file too.' );
+
+// The README-documented relative command works from the repo root.
+$repo_relative_run_arg = 'artifacts/html-api-fuzz/' . basename( $repo_relative_run_dir );
+\HtmlApiFuzz\ensure_dir( $repo_relative_run_dir );
+\HtmlApiFuzz\write_json_file(
+ $repo_relative_run_dir . '/launcher-state.json',
+ array(
+ 'kind' => 'html-api-fuzz-launcher-state',
+ 'finished' => false,
+ 'updatedAt' => gmdate( 'c' ),
+ )
+);
+$proc = \HtmlApiFuzz\run_php_process( array( 'tools/html-api-fuzz/stop.php', '--run-dir', $repo_relative_run_arg ), $repo_root, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 0 === $proc['code'] && $repo_relative_run_arg === ( $report['runDir'] ?? null ), 'Expected README-style relative --run-dir to succeed: ' . substr( $proc['output'], -500 ) );
+html_api_fuzz_smoke_assert( $repo_relative_run_arg . '/STOP' === ( $report['stopFile'] ?? null ), 'Expected README-style relative --run-dir to report a relative run-dir stop file.' );
+html_api_fuzz_smoke_assert( is_file( $repo_relative_run_dir . '/STOP' ), 'Expected README-style relative --run-dir to create RUN_DIR/STOP.' );
+
+// Explicit --stop-file is added to the discovered stop files.
+unlink( $custom_stop );
+unlink( $standalone_run . '/STOP' );
+$override_stop = $standalone_artifacts . '/override-stop/STOP';
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--run-dir', $standalone_run, '--stop-file', $override_stop ), $repo_root, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 0 === $proc['code'] && $standalone_run . '/STOP' === ( $report['stopFile'] ?? null ), 'Expected --run-dir --stop-file to report the run-dir stop file as primary.' );
+html_api_fuzz_smoke_assert( in_array( $override_stop, $report['stopFiles'] ?? array(), true ), 'Expected --stop-file to be included in stopFiles.' );
+html_api_fuzz_smoke_assert( is_file( $override_stop ), 'Expected --stop-file to create the override stop file.' );
+html_api_fuzz_smoke_assert( is_file( $custom_stop ), 'Expected --run-dir --stop-file to create the advertised custom stop file too.' );
+html_api_fuzz_smoke_assert( is_file( $standalone_run . '/STOP' ), 'Expected --run-dir --stop-file to create the run-dir stop file too.' );
+
+// Explicit --stop-file also works as a direct write without run discovery.
+$direct_stop = $work_dir . '/direct-stop/STOP';
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--stop-file', $direct_stop ), $repo_root, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 0 === $proc['code'] && is_array( $report ), 'Expected direct --stop-file to succeed without run discovery: ' . substr( $proc['output'], -500 ) );
+html_api_fuzz_smoke_assert( null === ( $report['runDir'] ?? null ), 'Expected direct --stop-file to report no run directory.' );
+html_api_fuzz_smoke_assert( is_file( $direct_stop ), 'Expected direct --stop-file to create the requested file.' );
+
+// A run directory without state can only be stopped unambiguously with --stop-file.
+$no_state_run = $work_dir . '/no-state-run';
+\HtmlApiFuzz\ensure_dir( $no_state_run );
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--run-dir', $no_state_run ), $repo_root, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 2 === $proc['code'] && false === ( $report['ok'] ?? null ), 'Expected --run-dir without state to report warning status.' );
+$no_state_custom_stop = $work_dir . '/no-state-custom-stop/STOP';
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--run-dir', $no_state_run, '--stop-file', $no_state_custom_stop ), $repo_root, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 0 === $proc['code'] && is_file( $no_state_custom_stop ) && is_file( $no_state_run . '/STOP' ), 'Expected --run-dir --stop-file without state to write both stop files.' );
+
+// Bare and ambiguous CLI invocations fail.
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--stop-file' ), $repo_root, 30000 );
+html_api_fuzz_smoke_assert( 0 !== $proc['code'] && false !== strpos( $proc['stderr'], 'non-empty path' ), 'Expected bare --stop-file to fail with a path error.' );
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--run-dir', '--stop-file', $work_dir . '/bare-run-dir-stop/STOP' ), $repo_root, 30000 );
+html_api_fuzz_smoke_assert( 0 !== $proc['code'] && false !== strpos( $proc['stderr'], 'non-empty path' ), 'Expected bare --run-dir to fail with a path error.' );
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--stop-stale-seconds' ), $repo_root, 30000 );
+html_api_fuzz_smoke_assert( 0 !== $proc['code'] && false !== strpos( $proc['stderr'], 'numeric' ), 'Expected bare --stop-stale-seconds to fail with a numeric error.' );
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--stop-stale-seconds', 'nope' ), $repo_root, 30000 );
+html_api_fuzz_smoke_assert( 0 !== $proc['code'] && false !== strpos( $proc['stderr'], 'numeric' ), 'Expected non-numeric --stop-stale-seconds to fail with a numeric error.' );
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--artifacts-dir' ), $repo_root, 30000 );
+html_api_fuzz_smoke_assert( 0 !== $proc['code'] && false !== strpos( $proc['stderr'], 'non-empty path' ), 'Expected bare --artifacts-dir to fail with a path error.' );
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--artifacts-dir', $launcher_artifacts, '--stop-file', $work_dir . '/ambiguous-stop/STOP' ), $repo_root, 30000 );
+html_api_fuzz_smoke_assert( 0 !== $proc['code'] && false !== strpos( $proc['stderr'], 'Pass --run-dir' ), 'Expected --artifacts-dir --stop-file without --run-dir to fail as ambiguous.' );
+$proc = \HtmlApiFuzz\run_php_process( array( $runner_tool, '--max-seeds', '1', '--stop-file=' ), $repo_root, 30000 );
+html_api_fuzz_smoke_assert( 0 !== $proc['code'] && false !== strpos( $proc['stderr'], 'non-empty path' ), 'Expected runner --stop-file= to fail with a path error.' );
+
+// Relative advertised stop files are resolved from real runner cwd, not stop.php's cwd.
+$relative_artifacts = $work_dir . '/relative-discovery';
+$relative_run = $relative_artifacts . '/run-relative-stop';
+$relative_cwd = $work_dir . '/relative-cwd';
+$relative_invoke_cwd = $work_dir . '/relative-invoke-cwd';
+$relative_stop = 'custom-relative-stop/STOP';
+\HtmlApiFuzz\ensure_dir( $relative_cwd );
+\HtmlApiFuzz\ensure_dir( $relative_invoke_cwd );
+$relative_cwd_real = realpath( $relative_cwd );
+html_api_fuzz_smoke_assert( is_string( $relative_cwd_real ), 'Expected relative runner cwd realpath.' );
+$proc = \HtmlApiFuzz\run_php_process(
+ array(
+ $runner_tool,
+ '--output-dir',
+ $relative_run,
+ '--max-seeds',
+ '1',
+ '--stop-file',
+ $relative_stop,
+ ),
+ $relative_cwd,
+ 30000
+);
+html_api_fuzz_smoke_assert( 0 === $proc['code'], 'Expected real runner with relative stop file to finish: ' . substr( $proc['output'], -500 ) );
+$runner_state = \HtmlApiFuzz\read_json_file( $relative_run . '/state.json' );
+html_api_fuzz_smoke_assert( is_array( $runner_state ) && $relative_cwd_real === ( $runner_state['cwd'] ?? null ), 'Expected runner state to record its cwd.' );
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--run-dir', $relative_run ), $relative_invoke_cwd, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+$expected_relative_stop = rtrim( $relative_cwd_real, DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR . $relative_stop;
+html_api_fuzz_smoke_assert( 0 === $proc['code'] && in_array( $expected_relative_stop, $report['stopFiles'] ?? array(), true ), 'Expected relative advertised stopFile to resolve from runner cwd.' );
+html_api_fuzz_smoke_assert( is_file( $expected_relative_stop ), 'Expected the runner-cwd-relative stop file to be created.' );
+html_api_fuzz_smoke_assert( ! is_file( $relative_invoke_cwd . '/' . $relative_stop ), 'Expected no stop file relative to stop.php invocation cwd.' );
+
+// On POSIX, a leading backslash is still relative to runner cwd.
+if ( '\\' !== DIRECTORY_SEPARATOR ) {
+ $backslash_run = $work_dir . '/backslash-relative-stop';
+ $backslash_cwd = $work_dir . '/backslash-cwd';
+ $backslash_stop = '\\custom-backslash-stop/STOP';
+ \HtmlApiFuzz\ensure_dir( $backslash_run );
+ \HtmlApiFuzz\ensure_dir( $backslash_cwd );
+ html_api_fuzz_smoke_write_runner_state(
+ $backslash_run . '/state.json',
+ array(
+ 'stopFile' => $backslash_stop,
+ 'cwd' => $backslash_cwd,
+ )
+ );
+ $proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--run-dir', $backslash_run ), $relative_invoke_cwd, 30000 );
+ $report = json_decode( trim( $proc['stdout'] ), true );
+ $expected_backslash_stop = rtrim( $backslash_cwd, DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR . $backslash_stop;
+ html_api_fuzz_smoke_assert( 0 === $proc['code'] && in_array( $expected_backslash_stop, $report['stopFiles'] ?? array(), true ), 'Expected POSIX leading-backslash stopFile to resolve from runner cwd.' );
+ html_api_fuzz_smoke_assert( is_file( $expected_backslash_stop ), 'Expected POSIX leading-backslash stop file to be created under runner cwd.' );
+}
+
+// Legacy active relative stopFile state without cwd warns because the watched file is ambiguous.
+$legacy_relative_run = $work_dir . '/legacy-relative-stop';
+$legacy_relative_cwd = $work_dir . '/legacy-relative-cwd';
+$legacy_relative_stop = 'legacy-relative-caller-cwd/STOP';
+\HtmlApiFuzz\ensure_dir( $legacy_relative_run );
+\HtmlApiFuzz\ensure_dir( $legacy_relative_cwd );
+html_api_fuzz_smoke_write_runner_state(
+ $legacy_relative_run . '/state.json',
+ array(
+ 'stopFile' => $legacy_relative_stop,
+ )
+);
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--run-dir', $legacy_relative_run ), $legacy_relative_cwd, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 2 === $proc['code'] && false === ( $report['ok'] ?? null ), 'Expected legacy relative stopFile without cwd to report warning status.' );
+html_api_fuzz_smoke_assert( false !== strpos( $proc['stderr'], 'exact watched file may be unknown' ), 'Expected legacy relative stopFile without absolute cwd to warn.' );
+html_api_fuzz_smoke_assert( is_file( $legacy_relative_run . '/STOP' ), 'Expected legacy relative stopFile warning path to write RUN_DIR/STOP.' );
+html_api_fuzz_smoke_assert( is_file( $legacy_relative_cwd . '/' . $legacy_relative_stop ), 'Expected legacy relative stopFile warning path to write a caller-cwd candidate.' );
+
+$finished_legacy_relative_run = $work_dir . '/finished-legacy-relative-stop';
+$finished_legacy_relative_cwd = $work_dir . '/finished-legacy-relative-cwd';
+$finished_legacy_relative_stop = 'finished-legacy-relative-caller-cwd/STOP';
+\HtmlApiFuzz\ensure_dir( $finished_legacy_relative_run );
+\HtmlApiFuzz\ensure_dir( $finished_legacy_relative_cwd );
+html_api_fuzz_smoke_write_runner_state(
+ $finished_legacy_relative_run . '/state.json',
+ array(
+ 'stopFile' => $finished_legacy_relative_stop,
+ 'stopReason' => 'max-seeds',
+ )
+);
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--run-dir', $finished_legacy_relative_run ), $finished_legacy_relative_cwd, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 2 === $proc['code'] && false === ( $report['ok'] ?? null ), 'Expected finished legacy relative stopFile without cwd to report warning status.' );
+html_api_fuzz_smoke_assert( false !== strpos( $proc['stderr'], 'exact watched file may be unknown' ), 'Expected finished legacy relative stopFile without cwd to warn.' );
+html_api_fuzz_smoke_assert( is_file( $finished_legacy_relative_run . '/STOP' ), 'Expected finished legacy relative stopFile warning path to write RUN_DIR/STOP.' );
+html_api_fuzz_smoke_assert( is_file( $finished_legacy_relative_cwd . '/' . $finished_legacy_relative_stop ), 'Expected finished legacy relative stopFile warning path to write a caller-cwd candidate.' );
+
+// Active malformed advertised stop files must not report unqualified success.
+$bad_advertised_run = $work_dir . '/bad-advertised-stop';
+\HtmlApiFuzz\ensure_dir( $bad_advertised_run );
+html_api_fuzz_smoke_write_runner_state(
+ $bad_advertised_run . '/state.json',
+ array(
+ 'stopFile' => '',
+ )
+);
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--run-dir', $bad_advertised_run ), $repo_root, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 2 === $proc['code'] && is_file( $bad_advertised_run . '/STOP' ), 'Expected RUN_DIR/STOP to be written with warning status when advertised stopFile is empty.' );
+html_api_fuzz_smoke_assert( false === ( $report['ok'] ?? null ) && false !== strpos( $proc['stderr'], 'exact watched file may be unknown' ), 'Expected empty advertised stopFile to warn.' );
+
+$unknown_kind_run = $work_dir . '/unknown-kind-runner-state';
+$unknown_kind_stop = $work_dir . '/unknown-kind-stop/STOP';
+\HtmlApiFuzz\ensure_dir( $unknown_kind_run );
+\HtmlApiFuzz\write_json_file(
+ $unknown_kind_run . '/state.json',
+ array(
+ 'updatedAt' => gmdate( 'c' ),
+ 'stopFile' => $unknown_kind_stop,
+ 'stopReason' => null,
+ )
+);
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--run-dir', $unknown_kind_run ), $repo_root, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 2 === $proc['code'] && false === ( $report['ok'] ?? null ), 'Expected runner-like state with missing kind to report warning status.' );
+html_api_fuzz_smoke_assert( is_file( $unknown_kind_stop ), 'Expected runner-like state with missing kind to write the advertised custom stop file.' );
+html_api_fuzz_smoke_assert( is_file( $unknown_kind_run . '/STOP' ), 'Expected runner-like state with missing kind to write RUN_DIR/STOP.' );
+html_api_fuzz_smoke_assert( false !== strpos( $proc['stderr'], 'missing or unknown kind' ), 'Expected runner-like state with missing kind to warn.' );
+
+$missing_stop_file_run = $work_dir . '/missing-stop-file';
+\HtmlApiFuzz\ensure_dir( $missing_stop_file_run );
+\HtmlApiFuzz\write_json_file(
+ $missing_stop_file_run . '/state.json',
+ array(
+ 'kind' => 'html-api-fuzz-runner-state',
+ 'updatedAt' => gmdate( 'c' ),
+ 'stopReason' => null,
+ )
+);
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--run-dir', $missing_stop_file_run ), $repo_root, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 2 === $proc['code'] && false === ( $report['ok'] ?? null ), 'Expected active runner state missing stopFile to report warning status.' );
+html_api_fuzz_smoke_assert( false !== strpos( $proc['stderr'], 'missing or malformed' ), 'Expected active runner state missing stopFile to warn.' );
+
+$relative_cwd_run = $work_dir . '/relative-cwd-stop';
+$relative_bad_cwd = $work_dir . '/relative-bad-cwd';
+\HtmlApiFuzz\ensure_dir( $relative_cwd_run );
+\HtmlApiFuzz\ensure_dir( $relative_bad_cwd );
+html_api_fuzz_smoke_write_runner_state(
+ $relative_cwd_run . '/state.json',
+ array(
+ 'stopFile' => 'relative-cwd-caller-cwd/STOP',
+ 'cwd' => 'not-absolute',
+ )
+);
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--run-dir', $relative_cwd_run ), $relative_bad_cwd, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 2 === $proc['code'] && false === ( $report['ok'] ?? null ), 'Expected relative recorded cwd to report warning status.' );
+html_api_fuzz_smoke_assert( false !== strpos( $proc['stderr'], 'no recorded absolute cwd' ), 'Expected relative recorded cwd to warn.' );
+html_api_fuzz_smoke_assert( is_file( $relative_bad_cwd . '/relative-cwd-caller-cwd/STOP' ), 'Expected relative recorded cwd warning path to write a caller-cwd candidate.' );
+
+$unknown_runner_run = $work_dir . '/unknown-runner-stop-file';
+$unknown_runner_cwd = $work_dir . '/unknown-runner-cwd';
+\HtmlApiFuzz\ensure_dir( $unknown_runner_run );
+\HtmlApiFuzz\ensure_dir( $unknown_runner_cwd );
+\HtmlApiFuzz\write_json_file(
+ $unknown_runner_run . '/state.json',
+ array(
+ 'kind' => 'html-api-fuzz-runner-state',
+ 'updatedAt' => gmdate( 'c' ),
+ 'stopFile' => 'unknown-runner-caller-cwd/STOP',
+ )
+);
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--run-dir', $unknown_runner_run ), $unknown_runner_cwd, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 2 === $proc['code'] && false === ( $report['ok'] ?? null ), 'Expected runner state without stopReason and relative stopFile to warn.' );
+
+// Unreadable state warns and still writes RUN_DIR/STOP.
+if ( '\\' !== DIRECTORY_SEPARATOR ) {
+ $unreadable_run = $work_dir . '/unreadable-state';
+ \HtmlApiFuzz\ensure_dir( $unreadable_run );
+ $unreadable_state = $unreadable_run . '/state.json';
+ html_api_fuzz_smoke_write_runner_state( $unreadable_state );
+ html_api_fuzz_smoke_assert( chmod( $unreadable_state, 0000 ), 'Expected chmod to make state unreadable.' );
+ if ( ! is_readable( $unreadable_state ) ) {
+ $proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--run-dir', $unreadable_run ), $repo_root, 30000 );
+ chmod( $unreadable_state, 0600 );
+ $report = json_decode( trim( $proc['stdout'] ), true );
+ html_api_fuzz_smoke_assert( 2 === $proc['code'] && false === ( $report['ok'] ?? null ), 'Expected unreadable state to report warning status.' );
+ html_api_fuzz_smoke_assert( is_file( $unreadable_run . '/STOP' ), 'Expected unreadable state fallback to write RUN_DIR/STOP.' );
+ } else {
+ chmod( $unreadable_state, 0600 );
+ }
+}
+
+// Unreadable in-progress state warns and still writes RUN_DIR/STOP.
+$corrupt_run = $work_dir . '/corrupt-state';
+\HtmlApiFuzz\ensure_dir( $corrupt_run );
+file_put_contents( $corrupt_run . '/state.json', "{not-json\n" );
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--run-dir', $corrupt_run ), $repo_root, 30000 );
+html_api_fuzz_smoke_assert( 2 === $proc['code'] && is_file( $corrupt_run . '/STOP' ), 'Expected corrupt state fallback to write RUN_DIR/STOP with warning status.' );
+html_api_fuzz_smoke_assert( false !== strpos( $proc['stderr'], 'could not read' ), 'Expected corrupt state fallback to warn.' );
+
+// Stale corrupt state is not preferred during discovery.
+$stale_corrupt_artifacts = $work_dir . '/stale-corrupt-discovery';
+$stale_corrupt_run = $stale_corrupt_artifacts . '/run-stale-corrupt';
+$recent_finished_run = $stale_corrupt_artifacts . '/run-recent-finished';
+\HtmlApiFuzz\ensure_dir( $stale_corrupt_run );
+file_put_contents( $stale_corrupt_run . '/state.json', "{not-json\n" );
+html_api_fuzz_smoke_touch( $stale_corrupt_run . '/state.json', time() - 3600 );
+\HtmlApiFuzz\ensure_dir( $recent_finished_run );
+html_api_fuzz_smoke_write_runner_state(
+ $recent_finished_run . '/state.json',
+ array(
+ 'stopReason' => 'max-seeds',
+ )
+);
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--artifacts-dir', $stale_corrupt_artifacts, '--stop-stale-seconds', '10' ), $repo_root, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 0 === $proc['code'] && $recent_finished_run === ( $report['runDir'] ?? null ), 'Expected stale corrupt state not to be preferred during discovery.' );
+html_api_fuzz_smoke_assert( true === ( $report['looksFinished'] ?? null ), 'Expected recent finished fallback to report looksFinished.' );
+html_api_fuzz_smoke_assert( is_file( $recent_finished_run . '/STOP' ), 'Expected recent finished fallback to create STOP.' );
+html_api_fuzz_smoke_assert( false !== strpos( $proc['stderr'], 'already looks stopped' ), 'Expected recent finished fallback to warn.' );
+
+// A stale runner state does not count as unfinished and therefore warns.
+$stale_artifacts = $work_dir . '/stale-discovery';
+$stale_run = $stale_artifacts . '/run-stale';
+$stale_custom = $stale_artifacts . '/custom-stale-stop/STOP';
+\HtmlApiFuzz\ensure_dir( $stale_run );
+html_api_fuzz_smoke_write_runner_state(
+ $stale_run . '/state.json',
+ array(
+ 'updatedAt' => gmdate( 'c', time() - 3600 ),
+ 'batchBudgetMs' => 0,
+ 'stopFile' => $stale_custom,
+ )
+);
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--artifacts-dir', $stale_artifacts, '--stop-stale-seconds', '10' ), $repo_root, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 0 === $proc['code'] && true === ( $report['looksFinished'] ?? null ), 'Expected a stale-only artifacts dir to be reported as looksFinished.' );
+html_api_fuzz_smoke_assert( is_file( $stale_custom ), 'Expected stale custom stop file to be created for the targeted run.' );
+html_api_fuzz_smoke_assert( is_file( $stale_run . '/STOP' ), 'Expected stale run-dir stop file to be created for watcher and orchestrator paths.' );
+html_api_fuzz_smoke_assert( false !== strpos( $proc['stderr'], 'already looks stopped' ), 'Expected a warning when only stale runs exist.' );
+
+// Missing updatedAt falls back to state file mtime for stale detection.
+$mtime_stale_artifacts = $work_dir . '/mtime-stale-discovery';
+$mtime_stale_run = $mtime_stale_artifacts . '/run-mtime-stale';
+\HtmlApiFuzz\ensure_dir( $mtime_stale_run );
+\HtmlApiFuzz\write_json_file(
+ $mtime_stale_run . '/state.json',
+ array(
+ 'kind' => 'html-api-fuzz-runner-state',
+ 'batchBudgetMs' => 0,
+ 'stopReason' => null,
+ )
+);
+html_api_fuzz_smoke_touch( $mtime_stale_run . '/state.json', time() - 3600 );
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--artifacts-dir', $mtime_stale_artifacts, '--stop-stale-seconds', '10' ), $repo_root, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 0 === $proc['code'] && true === ( $report['looksFinished'] ?? null ), 'Expected missing updatedAt to use stale file mtime.' );
+
+// A large batch budget floors the stale threshold so long-running batches still look active.
+$budget_artifacts = $work_dir . '/batch-budget-discovery';
+$budget_run = $budget_artifacts . '/run-budget-active';
+\HtmlApiFuzz\ensure_dir( $budget_run );
+html_api_fuzz_smoke_write_runner_state(
+ $budget_run . '/state.json',
+ array(
+ 'updatedAt' => gmdate( 'c', time() - 30 ),
+ 'batchBudgetMs' => 600000,
+ )
+);
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--artifacts-dir', $budget_artifacts, '--stop-stale-seconds', '10' ), $repo_root, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 0 === $proc['code'] && false === ( $report['looksFinished'] ?? null ), 'Expected batch budget floor to keep the old runner state active.' );
+
+// A stale launcher state does not count as unfinished and therefore warns.
+$stale_launcher_artifacts = $work_dir . '/stale-launcher-discovery';
+$stale_launcher_run = $stale_launcher_artifacts . '/run-stale-launcher';
+\HtmlApiFuzz\ensure_dir( $stale_launcher_run );
+\HtmlApiFuzz\write_json_file(
+ $stale_launcher_run . '/launcher-state.json',
+ array(
+ 'kind' => 'html-api-fuzz-launcher-state',
+ 'finished' => false,
+ 'updatedAt' => gmdate( 'c', time() - 3600 ),
+ )
+);
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--artifacts-dir', $stale_launcher_artifacts, '--stop-stale-seconds', '10' ), $repo_root, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 0 === $proc['code'] && true === ( $report['looksFinished'] ?? null ), 'Expected a stale launcher-only artifacts dir to be reported as looksFinished.' );
+html_api_fuzz_smoke_assert( false !== strpos( $proc['stderr'], 'already looks stopped' ), 'Expected a warning when only stale launcher state exists.' );
+
+// Same-second active runs are ordered deterministically by the run path.
+$tie_artifacts = $work_dir . '/tie-discovery';
+$first_tie_run = $tie_artifacts . '/run-20260101T000000000001Z';
+$next_tie_run = $tie_artifacts . '/run-20260101T000000000002Z';
+\HtmlApiFuzz\ensure_dir( $first_tie_run );
+\HtmlApiFuzz\ensure_dir( $next_tie_run );
+html_api_fuzz_smoke_write_runner_state( $first_tie_run . '/state.json' );
+html_api_fuzz_smoke_write_runner_state( $next_tie_run . '/state.json' );
+$same_mtime = time() + 20;
+html_api_fuzz_smoke_touch( $first_tie_run . '/state.json', $same_mtime );
+html_api_fuzz_smoke_touch( $next_tie_run . '/state.json', $same_mtime );
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--artifacts-dir', $tie_artifacts ), $repo_root, 30000 );
+$report = json_decode( trim( $proc['stdout'] ), true );
+html_api_fuzz_smoke_assert( 0 === $proc['code'] && $next_tie_run === ( $report['runDir'] ?? null ), 'Expected same-second active runs to prefer the later path.' );
+
+// An empty artifacts dir is an error, not a silent success.
+$empty_dir = $work_dir . '/empty';
+\HtmlApiFuzz\ensure_dir( $empty_dir );
+$proc = \HtmlApiFuzz\run_php_process( array( $stop_tool, '--artifacts-dir', $empty_dir ), $repo_root, 30000 );
+html_api_fuzz_smoke_assert( 0 !== $proc['code'], 'Expected stop.php to fail when no run directory exists.' );
+
+echo "OK stop-smoke\n";
diff --git a/tools/html-api-fuzz/tests/tree-renderer-normalization-smoke.php b/tools/html-api-fuzz/tests/tree-renderer-normalization-smoke.php
new file mode 100644
index 0000000000000..cf9fedbcd9af9
--- /dev/null
+++ b/tools/html-api-fuzz/tests/tree-renderer-normalization-smoke.php
@@ -0,0 +1,396 @@
+#!/usr/bin/env php
+ base64_encode( $input ),
+ 'profile' => 'replay',
+ 'mode' => $mode,
+ 'output-dir' => $tmp . '/' . $name,
+ 'max-tokens' => '2000',
+ 'max-nodes' => '3000',
+ )
+ );
+}
+
+/*
+ * Synthetic compare_trees() cases exercise the comparison logic directly and
+ * need no external oracle process.
+ *
+ * The comparison must keep failing on structural differences: scalar
+ * tolerance only applies when the spec substitution explains the entire
+ * differing line.
+ */
+$synthetic_mismatch = \HtmlApiFuzz\TreeRenderer::compare_trees( "
\n \"a\"\n\n", "
\n \"b\"\n\n" );
+html_api_fuzz_tree_normalization_assert( false === ( $synthetic_mismatch['ok'] ?? null ), 'Structural tree mismatches should still fail.' );
+html_api_fuzz_tree_normalization_assert( is_array( $synthetic_mismatch['firstDifference'] ?? null ) && 2 === ( $synthetic_mismatch['firstDifference']['line'] ?? null ), 'Structural mismatch should report the first differing line.' );
+
+$synthetic_structure_with_nul = \HtmlApiFuzz\TreeRenderer::compare_trees( "
\n x=\"\\0\"\n \"a\"\n\n", "
\n x=\"\xEF\xBF\xBD\"\n \"b\"\n\n" );
+html_api_fuzz_tree_normalization_assert( false === ( $synthetic_structure_with_nul['ok'] ?? null ), 'Scalar tolerance must not mask structural differences on other lines.' );
+
+$synthetic_tolerated = \HtmlApiFuzz\TreeRenderer::compare_trees( "
\n x=\"\\0\"\n\n", "
\n x=\"\xEF\xBF\xBD\"\n\n" );
+html_api_fuzz_tree_normalization_assert( false === ( $synthetic_tolerated['ok'] ?? null ), 'Raw NUL differences must be reported now that deferred byte processing is applied at read interfaces.' );
+html_api_fuzz_tree_normalization_assert( ! isset( $synthetic_tolerated['scalarToleratedLines'] ), 'Raw NUL differences should not report tolerated lines.' );
+
+$synthetic_nul_with_agreed_cr = \HtmlApiFuzz\TreeRenderer::compare_trees( "
\n x=\"\\0)\\r\"\n\n", "
\n x=\"\xEF\xBF\xBD)\\r\"\n\n" );
+html_api_fuzz_tree_normalization_assert( false === ( $synthetic_nul_with_agreed_cr['ok'] ?? null ), 'A raw NUL must fail even beside an agreed escaped CR.' );
+
+$synthetic_cr_only_wordpress = \HtmlApiFuzz\TreeRenderer::compare_trees( "
\n x=\"a\\nb\"\n\n", "
\n x=\"a\\rb\"\n\n" );
+html_api_fuzz_tree_normalization_assert( false === ( $synthetic_cr_only_wordpress['ok'] ?? null ), 'A DOM-side CR where WordPress holds LF is not the spec substitution and must fail.' );
+
+$synthetic_cr_before_decoded_lf = \HtmlApiFuzz\TreeRenderer::compare_trees( "
\n x=\"\\r\\n\"\n\n", "
\n x=\"\\n\\n\"\n\n" );
+html_api_fuzz_tree_normalization_assert( false === ( $synthetic_cr_before_decoded_lf['ok'] ?? null ), 'WordPress CR+LF opposite DOM LF+LF must be reported.' );
+
+$synthetic_raw_crlf = \HtmlApiFuzz\TreeRenderer::compare_trees( "
\n x=\"\\r\\nX\"\n\n", "
\n x=\"\\nX\"\n\n" );
+html_api_fuzz_tree_normalization_assert( false === ( $synthetic_raw_crlf['ok'] ?? null ), 'Raw CRLF opposite a DOM LF must be reported.' );
+
+$synthetic_backslash_collision = \HtmlApiFuzz\TreeRenderer::compare_trees( "
\n x=\"\\\\r\"\n\n", "
\n x=\"\\\\n\"\n\n" );
+html_api_fuzz_tree_normalization_assert( false === ( $synthetic_backslash_collision['ok'] ?? null ), 'A literal backslash followed by r must not be rewritten as a CR escape.' );
+
+$synthetic_repeated_cr_lf = \HtmlApiFuzz\TreeRenderer::compare_trees(
+ "
\n x=\"" . str_repeat( '\\r\\n', 500 ) . "\"\n\n",
+ "
\n x=\"" . str_repeat( '\\n\\n', 500 ) . "\"\n\n"
+);
+html_api_fuzz_tree_normalization_assert( false === ( $synthetic_repeated_cr_lf['ok'] ?? null ), 'A long run of raw CR plus decoded LF pairs must be reported.' );
+
+/* Current trunk applies the HTML input preprocessing at the read boundary,
+ * so every synthetic NUL/CR tree difference is a real mismatch.
+ */
+$synthetic_text_nul = \HtmlApiFuzz\TreeRenderer::compare_trees( "
\n \"a\\0b\"\n\n", "
\n \"a\xEF\xBF\xBDb\"\n\n" );
+html_api_fuzz_tree_normalization_assert( false === ( $synthetic_text_nul['ok'] ?? null ), 'NUL differences on text lines must be reported.' );
+
+$synthetic_text_cr = \HtmlApiFuzz\TreeRenderer::compare_trees( "
\n \"x\\ry\"\n\n", "
\n \"x\\ny\"\n\n" );
+html_api_fuzz_tree_normalization_assert( false === ( $synthetic_text_cr['ok'] ?? null ), 'CR differences on text lines must be reported.' );
+
+$synthetic_comment_nul = \HtmlApiFuzz\TreeRenderer::compare_trees( "
\n \n\n", "
\n \n\n" );
+html_api_fuzz_tree_normalization_assert( false === ( $synthetic_comment_nul['ok'] ?? null ), 'NUL differences on comment lines must be reported.' );
+
+$synthetic_tag_name_nul = \HtmlApiFuzz\TreeRenderer::compare_trees( "
\n
\n\n", "\n
\n\n" );
+html_api_fuzz_tree_normalization_assert( false === ( $synthetic_tag_name_nul['ok'] ?? null ), 'NUL differences on tag-name lines must be reported.' );
+
+$synthetic_quoted_attribute_name_nul = \HtmlApiFuzz\TreeRenderer::compare_trees( "\n \"a\\0\"=\"\"\n\n", "
\n \"a\xEF\xBF\xBD\"=\"\"\n\n" );
+html_api_fuzz_tree_normalization_assert( false === ( $synthetic_quoted_attribute_name_nul['ok'] ?? null ), 'NUL differences in quoted attribute names must be reported.' );
+
+/*
+ * The tokenizer permits `<` and `!` in attribute names, so `
`
+ * carries an attribute named `