#!/usr/bin/env bash
set -Eeuo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
TMP_DIR="$(mktemp -d)"

export CODEX_LINUX_FEATURES_CONFIG="$REPO_DIR/linux-features/features.example.json"

cleanup() {
    rm -rf "$TMP_DIR"
}
trap cleanup EXIT

info() {
    echo "[smoke] $*" >&2
}

fail() {
    echo "[smoke][FAIL] $*" >&2
    exit 1
}

assert_file_exists() {
    local path="$1"
    [ -f "$path" ] || fail "Expected file to exist: $path"
}

assert_file_not_exists() {
    local path="$1"
    [ ! -e "$path" ] || fail "Expected file not to exist: $path"
}

assert_mode() {
    local path="$1"
    local expected="$2"
    local actual
    actual="$(python3 - "$path" <<'PY'
import os
import sys

print(format(os.lstat(sys.argv[1]).st_mode & 0o777, "o"))
PY
)"
    [ "$actual" = "$expected" ] || fail "Expected mode $expected for $path, got $actual"
}

assert_contains() {
    local path="$1"
    local pattern="$2"
    grep -q -- "$pattern" "$path" || fail "Expected '$pattern' in $path"
}

assert_not_contains() {
    local path="$1"
    local pattern="$2"
    if grep -q -- "$pattern" "$path"; then
        fail "Did not expect '$pattern' in $path"
    fi
}

assert_occurrence_count() {
    local path="$1"
    local pattern="$2"
    local expected="$3"
    local actual
    actual="$(grep -o -- "$pattern" "$path" | wc -l | tr -d ' ')"
    [ "$actual" = "$expected" ] || fail "Expected '$pattern' to appear $expected times in $path, found $actual"
}

assert_json_enabled_equals() {
    local path="$1"
    local expected_json="$2"
    node - "$path" "$expected_json" <<'NODE' || fail "Expected $path enabled list to equal $expected_json"
const fs = require("node:fs");
const path = process.argv[2];
const expected = JSON.parse(process.argv[3]);
const actual = JSON.parse(fs.readFileSync(path, "utf8")).enabled;
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
  console.error(`expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
  process.exit(1);
}
NODE
}

make_wizard_feature_root() {
    local features_root="$1"
    mkdir -p \
        "$features_root/conversation-mode" \
        "$features_root/example-feature" \
        "$features_root/read-aloud" \
        "$features_root/read-aloud-mcp" \
        "$features_root/remote-mobile-control"
    printf '%s\n' '{"enabled":[]}' > "$features_root/features.example.json"
    cat > "$features_root/conversation-mode/feature.json" <<'JSON'
{"id":"conversation-mode","name":"Conversation mode","description":"Voice conversation loop."}
JSON
    printf '%s\n' '# Conversation Mode' > "$features_root/conversation-mode/README.md"
    cat > "$features_root/example-feature/feature.json" <<'JSON'
{"id":"example-feature","title":"Example Linux Feature","description":"Developer sample."}
JSON
    printf '%s\n' '# Example Linux Feature' > "$features_root/example-feature/README.md"
    cat > "$features_root/read-aloud/feature.json" <<'JSON'
{"id":"read-aloud","name":"Read aloud","description":"Read assistant responses aloud."}
JSON
    printf '%s\n' '# Read Aloud' > "$features_root/read-aloud/README.md"
    cat > "$features_root/read-aloud-mcp/feature.json" <<'JSON'
{"id":"read-aloud-mcp","title":"Read Aloud MCP","description":"Read Aloud MCP plugin staging."}
JSON
    printf '%s\n' '# Read Aloud MCP' > "$features_root/read-aloud-mcp/README.md"
    cat > "$features_root/remote-mobile-control/feature.json" <<'JSON'
{"id":"remote-mobile-control","title":"Experimental Remote Mobile Control","description":"Mobile host enrollment patches."}
JSON
    printf '%s\n' '# Remote Mobile Control' > "$features_root/remote-mobile-control/README.md"
}

make_fake_browser_upstream_app() {
    local app_dir="$1"
    local resources_dir="$app_dir/Contents/Resources"
    mkdir -p \
        "$resources_dir/plugins/openai-bundled/.agents/plugins" \
        "$resources_dir/plugins/openai-bundled/plugins/browser/.codex-plugin" \
        "$resources_dir/plugins/openai-bundled/plugins/browser/scripts"
    cat > "$resources_dir/plugins/openai-bundled/.agents/plugins/marketplace.json" <<'JSON'
{"plugins":[{"name":"browser","source":{"source":"local","path":"./plugins/browser"},"policy":{"installation":"AVAILABLE","authentication":"ON_INSTALL"},"category":"Engineering"}]}
JSON
    cat > "$resources_dir/plugins/openai-bundled/plugins/browser/.codex-plugin/plugin.json" <<'JSON'
{"name":"browser","version":"0.1.0-alpha2","interface":{"category":"Engineering"}}
JSON
    cat > "$resources_dir/plugins/openai-bundled/plugins/browser/scripts/browser-client.mjs" <<'JS'
function lu(e){let t=globalThis.nodeRepl?.env[e];return typeof t=="string"?t:void 0}function th(){let e=import.meta.__codexNativePipe;return e==null||typeof e.createConnection!="function"?null:e}var I2=new Set(["about:blank"]);function Gb(e){if(I2.has(e))return!0;let t;try{t=new URL(e)}catch{return!1}return t.protocol==="http:"||t.protocol==="https:"}class Uf{async fetchBlocked(e){let r=await bS(e.endpoint,{method:"GET"});if(!r.ok)throw new Error(ae(`Browser Use cannot determine if ${e.displayUrl} is allowed. Please try again later or use another source.`));let n=await r.json();return TF(n)}}export function setupAtlasRuntime() {}
JS
}

make_fake_app() {
    local app_dir="$1"
    bash "$REPO_DIR/tests/fixtures/create-packaged-app-fixture.sh" "$app_dir"
}

make_stub_bin_dir() {
    local bin_dir="$1"
    mkdir -p "$bin_dir"
}

test_common_helper_sourcing() {
    info "Checking shared packaging helpers"
    local probe_file="$TMP_DIR/probe.txt"
    touch "$probe_file"

    # shellcheck disable=SC1091
    source "$REPO_DIR/scripts/lib/package-common.sh"
    ensure_file_exists "$probe_file" "probe file"
}

test_package_payload_permission_normalization() {
    info "Checking package payload permission normalization"
    local root="$TMP_DIR/package-permissions"
    local app_root="$root/opt/codex-desktop"
    local private_file="$app_root/.codex-linux/features/private/secret.txt"

    mkdir -p "$app_root/content/webview" "$root/usr/bin" "$(dirname "$private_file")"
    printf '%s\n' '#!/bin/bash' 'echo start' > "$app_root/start.sh"
    printf '%s\n' '<!doctype html>' > "$app_root/content/webview/index.html"
    printf '%s\n' '#!/bin/bash' 'exec /opt/codex-desktop/start.sh "$@"' > "$root/usr/bin/codex-desktop"
    printf '%s\n' 'secret' > "$private_file"
    cat > "$app_root/.codex-linux/linux-features-staged.json" <<'JSON'
{
  "version": 1,
  "resources": [
    {
      "id": "private",
      "type": "resource",
      "target": ".codex-linux/features/private/secret.txt",
      "mode": "0600"
    }
  ],
  "runtimeHooks": []
}
JSON
    chmod 0700 "$root/opt" "$app_root" "$app_root/content" "$app_root/content/webview"
    chmod 0700 "$app_root/start.sh" "$root/usr/bin/codex-desktop"
    chmod 0600 "$app_root/content/webview/index.html" "$private_file"

    # shellcheck disable=SC1091
    source "$REPO_DIR/scripts/lib/package-common.sh"
    normalize_package_payload_permissions "$root"
    PACKAGE_NAME="codex-desktop" restore_linux_feature_payload_permissions "$root"

    assert_mode "$app_root" "755"
    assert_mode "$app_root/content/webview" "755"
    assert_mode "$app_root/start.sh" "755"
    assert_mode "$root/usr/bin/codex-desktop" "755"
    assert_mode "$app_root/content/webview/index.html" "644"
    assert_mode "$private_file" "600"
}

test_deb_builder_smoke() {
    info "Running Debian packaging smoke test"
    local workspace="$TMP_DIR/deb"
    local bin_dir="$workspace/bin"
    local app_dir="$workspace/app"
    local dist_dir="$workspace/dist"
    local pkg_root="$workspace/deb-root"
    local updater_bin="$workspace/codex-update-manager"
    local capture_dir="$workspace/capture"

    mkdir -p "$workspace" "$dist_dir" "$capture_dir"
    make_stub_bin_dir "$bin_dir"
    make_fake_app "$app_dir"
    printf '#!/usr/bin/env bash\nexit 0\n' > "$updater_bin"
    chmod +x "$updater_bin"

    cat > "$bin_dir/dpkg" <<'SCRIPT'
#!/usr/bin/env bash
if [ "$1" = "--print-architecture" ]; then
    echo amd64
    exit 0
fi
exit 0
SCRIPT
    cat > "$bin_dir/dpkg-deb" <<'SCRIPT'
#!/usr/bin/env bash
output="${@: -1}"
printf '%s\n' "$*" > "$CAPTURE_DIR/dpkg-deb-args"
printf '%s\n' "${DPKG_DEB_THREADS_MAX:-}" > "$CAPTURE_DIR/dpkg-deb-threads"
mkdir -p "$(dirname "$output")"
touch "$output"
SCRIPT
    cat > "$bin_dir/cargo" <<'SCRIPT'
#!/usr/bin/env bash
echo "cargo should not be called when UPDATER_BINARY_SOURCE exists" >&2
exit 99
SCRIPT
    chmod +x "$bin_dir/dpkg" "$bin_dir/dpkg-deb" "$bin_dir/cargo"

    PATH="$bin_dir:$PATH" \
    APP_DIR_OVERRIDE="$app_dir" \
    PKG_ROOT_OVERRIDE="$pkg_root" \
    DIST_DIR_OVERRIDE="$dist_dir" \
    CAPTURE_DIR="$capture_dir" \
    UPDATER_BINARY_SOURCE="$updater_bin" \
    MAX_BUILD_THREADS=6 \
    PACKAGE_VERSION="2026.03.24.120000+deadbeef" \
    bash "$REPO_DIR/scripts/build-deb.sh"

    assert_file_exists "$dist_dir/codex-desktop_2026.03.24.120000+deadbeef_amd64.deb"
    [ "$(cat "$capture_dir/dpkg-deb-threads")" = "6" ] \
        || fail "Expected MAX_BUILD_THREADS to reach dpkg-deb"
    assert_file_exists "$pkg_root/DEBIAN/prerm"
    assert_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "Name=New Window"
    assert_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "Name=Check for Updates"
    assert_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "Name=Install Ready Update"
    assert_file_exists "$pkg_root/DEBIAN/postrm"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/lib/package-common.sh"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/lib/patch-chrome-plugin.js"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/lib/node-runtime.sh"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/lib/linux-update-bridge-patch.js"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/lib/patch-report.js"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/lib/rebuild-report.sh"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/lib/build-info.js"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/lib/build-info.sh"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/lib/linux-features.js"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/lib/linux-features.sh"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/lib/linux-target-context.js"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/patches/engine.js"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/patches/registry.js"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/patches/shared.js"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/patches/core/all-linux/main-process/lifecycle/patch.js"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/patches/core/all-linux/webview/theme-and-sunset/patch.js"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/patches/core/distro/nixos/README.md"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/patches/core/desktop/i3/README.md"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/patches/core/package/deb/README.md"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/linux-features/README.md"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/linux-features/example-feature/feature.json"
    assert_file_not_exists "$pkg_root/opt/codex-desktop/update-builder/linux-features/features.json"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/node-runtime/bin/node"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/Cargo.toml"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/CHANGELOG.md"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/computer-use-linux/Cargo.toml"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/read-aloud-linux/Cargo.toml"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/updater/Cargo.toml"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/plugins/openai-bundled/plugins/computer-use/.mcp.json"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/plugins/openai-bundled/plugins/read-aloud/.mcp.json"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/.codex-linux/source-info.json"
    assert_file_exists "$pkg_root/opt/codex-desktop/.codex-linux/codex-packaged-runtime.sh"
    assert_file_exists "$pkg_root/opt/codex-desktop/.codex-linux/codex-desktop-entry-doctor.sh"
    assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/packaging/linux/codex-desktop-entry-doctor.sh"
    assert_file_exists "$pkg_root/opt/codex-desktop/resources/node-runtime/bin/node"
}

test_deb_builder_rebuilds_deleted_updater_source() {
    info "Checking package builder recovers from deleted updater binary source"
    local workspace="$TMP_DIR/deb-deleted-updater-source"
    local bin_dir="$workspace/bin"
    local app_dir="$workspace/app"
    local dist_dir="$workspace/dist"
    local pkg_root="$workspace/deb-root"
    local cargo_target_dir="$workspace/cargo-target"

    mkdir -p "$workspace" "$dist_dir"
    make_stub_bin_dir "$bin_dir"
    make_fake_app "$app_dir"

    cat > "$bin_dir/dpkg" <<'SCRIPT'
#!/usr/bin/env bash
if [ "$1" = "--print-architecture" ]; then
    echo amd64
    exit 0
fi
exit 0
SCRIPT
    cat > "$bin_dir/dpkg-deb" <<'SCRIPT'
#!/usr/bin/env bash
output="${@: -1}"
mkdir -p "$(dirname "$output")"
touch "$output"
SCRIPT
    cat > "$bin_dir/cargo" <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
target_dir="${CARGO_TARGET_DIR:-target}"
mkdir -p "$target_dir/release"
cat > "$target_dir/release/codex-update-manager" <<'BIN'
#!/usr/bin/env bash
echo rebuilt updater
BIN
chmod +x "$target_dir/release/codex-update-manager"
SCRIPT
    chmod +x "$bin_dir/dpkg" "$bin_dir/dpkg-deb" "$bin_dir/cargo"

    PATH="$bin_dir:$PATH" \
    APP_DIR_OVERRIDE="$app_dir" \
    PKG_ROOT_OVERRIDE="$pkg_root" \
    DIST_DIR_OVERRIDE="$dist_dir" \
    CARGO_TARGET_DIR="$cargo_target_dir" \
    UPDATER_BINARY_SOURCE="$workspace/codex-update-manager (deleted)" \
    PACKAGE_VERSION="2026.03.24.120000+rebuilt" \
    bash "$REPO_DIR/scripts/build-deb.sh"

    assert_file_exists "$dist_dir/codex-desktop_2026.03.24.120000+rebuilt_amd64.deb"
    assert_file_exists "$pkg_root/usr/bin/codex-update-manager"
    assert_contains "$pkg_root/usr/bin/codex-update-manager" "rebuilt updater"
}

test_update_builder_preserves_enabled_linux_features_config() {
    info "Checking update-builder preserves sanitized enabled Linux feature config"
    local workspace="$TMP_DIR/update-builder-linux-features"
    local root="$workspace/root"
    local app_dir="$workspace/app"
    local features_root="$workspace/linux-features"
    local feature_config="$workspace/features.json"
    local staged_config="$root/opt/codex-desktop/update-builder/linux-features/features.json"
    local staged_local_manifest="$root/opt/codex-desktop/update-builder/linux-features/local/local-tool/feature.json"
    local source_info="$root/opt/codex-desktop/update-builder/.codex-linux/source-info.json"

    mkdir -p "$workspace"
    make_fake_app "$app_dir"
    mkdir -p "$features_root/example-feature" "$features_root/local/local-tool"
    printf '%s\n' '# Linux Features' > "$features_root/README.md"
    printf '%s\n' '{"enabled":[]}' > "$features_root/features.example.json"
    printf '%s\n' '{"id":"example-feature","title":"Example Linux Feature"}' \
        > "$features_root/example-feature/feature.json"
    printf '%s\n' '# Example Linux Feature' > "$features_root/example-feature/README.md"
    printf '%s\n' '{"id":"local-tool","title":"Local Tool"}' \
        > "$features_root/local/local-tool/feature.json"
    printf '%s\n' '# Local Tool' > "$features_root/local/local-tool/README.md"
    cat > "$feature_config" <<'JSON'
{
  "enabled": [
    "example-feature",
    "local-tool"
  ],
  "localComment": "should not be packaged"
}
JSON

    (
        export APP_DIR="$app_dir"
        export PACKAGE_NAME="codex-desktop"
        export UPDATER_SERVICE_SOURCE="$REPO_DIR/packaging/linux/codex-update-manager.service"
        export CODEX_LINUX_FEATURES_ROOT="$features_root"
        export CODEX_LINUX_FEATURES_CONFIG="$feature_config"
        export CODEX_LINUX_SOURCE_REMOTE="https://builder:secret-token@example.com/org/repo.git"
        export SOURCE_DATE_EPOCH="1710000000"

        # shellcheck disable=SC1091
        source "$REPO_DIR/scripts/lib/package-common.sh"
        stage_update_builder_bundle "$root"
    )

    assert_file_exists "$staged_config"
    assert_file_exists "$staged_local_manifest"
    assert_contains "$staged_config" "example-feature"
    assert_contains "$staged_config" "local-tool"
    assert_not_contains "$staged_config" "localComment"

    node - "$staged_config" <<'NODE' || fail "Expected staged Linux features config to be sanitized"
const fs = require("node:fs");
const configPath = process.argv[2];
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
if (JSON.stringify(config) !== JSON.stringify({ enabled: ["example-feature", "local-tool"] })) {
  process.exit(1);
}
NODE

    node - "$source_info" <<'NODE' || fail "Expected staged source info to be sanitized and reproducible"
const fs = require("node:fs");
const sourceInfoPath = process.argv[2];
const info = JSON.parse(fs.readFileSync(sourceInfoPath, "utf8"));
if (info.remote !== "https://example.com/org/repo.git") {
  throw new Error(`unexpected remote: ${info.remote}`);
}
if (info.capturedAt !== new Date(1710000000 * 1000).toISOString()) {
  throw new Error(`unexpected capturedAt: ${info.capturedAt}`);
}
NODE
}

test_update_builder_source_info_survives_without_git_checkout() {
    info "Checking update-builder source info survives packaged no-git rebuild layout"
    local workspace="$TMP_DIR/update-builder-source-info"
    local update_builder="$workspace/update-builder"
    local source_info="$update_builder/.codex-linux/source-info.json"

    mkdir -p "$update_builder/.codex-linux" "$update_builder/updater"
    cat > "$update_builder/updater/Cargo.toml" <<'TOML'
[package]
name = "codex-update-manager"
version = "0.8.1"
TOML
    cat > "$source_info" <<'JSON'
{
  "commit": "0123456789012345678901234567890123456789",
  "branch": "main",
  "remote": "https://builder:secret-token@example.com/org/repo.git",
  "provenance": "packaged-update-builder",
  "capturedAt": "2026-05-29T00:00:00.000Z"
}
JSON

    (
        export REPO_DIR="$update_builder"
        export SOURCE_DATE_EPOCH="1710000000"

        # shellcheck disable=SC1091
        source "$SCRIPT_DIR/../scripts/lib/package-common.sh"
        stage_update_builder_source_info "$update_builder"
    )

    node - "$source_info" <<'NODE' || fail "Expected staged source info to preserve installed metadata"
const fs = require("node:fs");
const info = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
if (info.commit !== "0123456789012345678901234567890123456789") {
  throw new Error(`unexpected commit: ${info.commit}`);
}
if (info.version !== "0.8.1") {
  throw new Error(`unexpected version: ${info.version}`);
}
if (info.remote !== "https://example.com/org/repo.git") {
  throw new Error(`unexpected remote: ${info.remote}`);
}
if (info.recapturedAt !== new Date(1710000000 * 1000).toISOString()) {
  throw new Error(`unexpected recapturedAt: ${info.recapturedAt}`);
}
NODE
}

test_linux_feature_package_hook_discovery_failure_blocks_build() {
    info "Checking Linux feature package hook discovery failure blocks package staging"
    local workspace="$TMP_DIR/package-hook-discovery-failure"
    local root="$workspace/root"
    local app_dir="$workspace/app"
    local features_root="$workspace/linux-features"
    local feature_config="$features_root/features.json"
    local output_log="$workspace/output.log"

    mkdir -p "$root" "$features_root/bad-package-hook"
    make_fake_app "$app_dir"
    printf '%s\n' '{"enabled":[]}' > "$features_root/features.example.json"
    cat > "$features_root/bad-package-hook/feature.json" <<'JSON'
{
  "id": "bad-package-hook",
  "title": "Bad Package Hook",
  "packageHooks": [
    {
      "path": "missing.sh",
      "formats": ["deb"]
    }
  ]
}
JSON
    printf '%s\n' '# Bad Package Hook' > "$features_root/bad-package-hook/README.md"
    printf '%s\n' '{"enabled":["bad-package-hook"]}' > "$feature_config"

    if (
        export APP_DIR="$app_dir"
        export PACKAGE_NAME="codex-desktop"
        export PACKAGE_VERSION="2026.03.24.120000+hookfailure"
        export CODEX_LINUX_FEATURES_ROOT="$features_root"
        export CODEX_LINUX_FEATURES_CONFIG="$feature_config"

        # shellcheck disable=SC1091
        source "$REPO_DIR/scripts/lib/package-common.sh"
        run_linux_feature_package_hooks "$root" "deb"
    ) >"$output_log" 2>&1; then
        fail "Expected package hook discovery failure to stop package staging"
    fi

    assert_contains "$output_log" "Failed to discover Linux feature package hooks for deb"
    assert_contains "$output_log" "packageHook 1 not found"
}

test_deb_builder_respects_package_identity() {
    info "Running side-by-side Debian packaging smoke test"
    local workspace="$TMP_DIR/deb-identity"
    local bin_dir="$workspace/bin"
    local app_dir="$workspace/app"
    local dist_dir="$workspace/dist"
    local pkg_root="$workspace/deb-root"
    local updater_bin="$workspace/codex-update-manager"

    mkdir -p "$workspace" "$dist_dir"
    make_stub_bin_dir "$bin_dir"
    make_fake_app "$app_dir"
    printf '#!/usr/bin/env bash\nexit 0\n' > "$updater_bin"
    chmod +x "$updater_bin"

    cat > "$bin_dir/dpkg" <<'SCRIPT'
#!/usr/bin/env bash
if [ "$1" = "--print-architecture" ]; then
    echo amd64
    exit 0
fi
exit 0
SCRIPT
    cat > "$bin_dir/dpkg-deb" <<'SCRIPT'
#!/usr/bin/env bash
output="${@: -1}"
mkdir -p "$(dirname "$output")"
touch "$output"
SCRIPT
    cat > "$bin_dir/cargo" <<'SCRIPT'
#!/usr/bin/env bash
echo "cargo should not be called when UPDATER_BINARY_SOURCE exists" >&2
exit 99
SCRIPT
    chmod +x "$bin_dir/dpkg" "$bin_dir/dpkg-deb" "$bin_dir/cargo"

    PATH="$bin_dir:$PATH" \
    APP_DIR_OVERRIDE="$app_dir" \
    PKG_ROOT_OVERRIDE="$pkg_root" \
    DIST_DIR_OVERRIDE="$dist_dir" \
    UPDATER_BINARY_SOURCE="$updater_bin" \
    PACKAGE_NAME="codex-cua-lab" \
    PACKAGE_DISPLAY_NAME="Codex CUA Lab" \
    PACKAGE_VERSION="2026.03.24.120000+deadbeef" \
    bash "$REPO_DIR/scripts/build-deb.sh"

    assert_file_exists "$dist_dir/codex-cua-lab_2026.03.24.120000+deadbeef_amd64.deb"
    assert_file_exists "$pkg_root/usr/bin/codex-cua-lab"
    assert_file_exists "$pkg_root/opt/codex-cua-lab/start.sh"
    assert_contains "$pkg_root/DEBIAN/control" "Package: codex-cua-lab"
    assert_contains "$pkg_root/usr/share/applications/codex-cua-lab.desktop" "Name=Codex CUA Lab"
    assert_contains "$pkg_root/usr/share/applications/codex-cua-lab.desktop" "CHROME_DESKTOP=codex-cua-lab.desktop"
    assert_contains "$pkg_root/usr/share/applications/codex-cua-lab.desktop" "/usr/bin/codex-cua-lab %u"
    assert_contains "$pkg_root/usr/share/applications/codex-cua-lab.desktop" "MimeType=x-scheme-handler/codex;x-scheme-handler/codex-browser-sidebar;"
    assert_contains "$pkg_root/usr/share/applications/codex-cua-lab.desktop" "StartupWMClass=codex-cua-lab"
    assert_contains "$pkg_root/usr/share/applications/codex-cua-lab.desktop" "X-GNOME-WMClass=codex-cua-lab"
    assert_contains "$pkg_root/opt/codex-cua-lab/.codex-linux/codex-packaged-runtime.sh" 'CHROME_DESKTOP="codex-cua-lab.desktop"'
}

test_deb_builder_without_updater() {
    info "Running no-updater Debian packaging smoke test"
    local workspace="$TMP_DIR/deb-no-updater"
    local bin_dir="$workspace/bin"
    local app_dir="$workspace/app"
    local dist_dir="$workspace/dist"
    local pkg_root="$workspace/deb-root"

    mkdir -p "$workspace" "$dist_dir"
    make_stub_bin_dir "$bin_dir"
    make_fake_app "$app_dir"

    cat > "$bin_dir/dpkg" <<'SCRIPT'
#!/usr/bin/env bash
if [ "$1" = "--print-architecture" ]; then
    echo amd64
    exit 0
fi
exit 0
SCRIPT
    cat > "$bin_dir/dpkg-deb" <<'SCRIPT'
#!/usr/bin/env bash
output="${@: -1}"
mkdir -p "$(dirname "$output")"
touch "$output"
SCRIPT
    cat > "$bin_dir/cargo" <<'SCRIPT'
#!/usr/bin/env bash
echo "cargo should not be called when PACKAGE_WITH_UPDATER=0" >&2
exit 99
SCRIPT
    chmod +x "$bin_dir/dpkg" "$bin_dir/dpkg-deb" "$bin_dir/cargo"

    PATH="$bin_dir:$PATH" \
    APP_DIR_OVERRIDE="$app_dir" \
    PKG_ROOT_OVERRIDE="$pkg_root" \
    DIST_DIR_OVERRIDE="$dist_dir" \
    PACKAGE_WITH_UPDATER=0 \
    PACKAGE_VERSION="2026.03.24.120000+manual" \
    bash "$REPO_DIR/scripts/build-deb.sh"

    assert_file_exists "$dist_dir/codex-desktop_2026.03.24.120000+manual_amd64.deb"
    assert_file_exists "$pkg_root/usr/bin/codex-desktop"
    assert_file_exists "$pkg_root/DEBIAN/postinst"
    assert_file_exists "$pkg_root/DEBIAN/prerm"
    assert_file_exists "$pkg_root/opt/codex-desktop/.codex-linux/codex-packaged-runtime.sh"
    assert_file_exists "$pkg_root/opt/codex-desktop/.codex-linux/codex-no-updater-transition-cleanup.sh"
    assert_file_not_exists "$pkg_root/usr/bin/codex-update-manager"
    assert_file_not_exists "$pkg_root/usr/lib/systemd/user/codex-update-manager.service"
    assert_file_not_exists "$pkg_root/usr/share/polkit-1/actions/com.github.ilysenko.codex-desktop-linux.update.policy"
    assert_file_not_exists "$pkg_root/opt/codex-desktop/update-builder"
    assert_file_not_exists "$pkg_root/DEBIAN/postrm"
    assert_not_contains "$pkg_root/DEBIAN/control" "pkexec"
    assert_not_contains "$pkg_root/DEBIAN/control" "polkit"
    assert_not_contains "$pkg_root/DEBIAN/control" "Local auto-updates"
    assert_contains "$pkg_root/DEBIAN/control" "without codex-update-manager"
    assert_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "Actions=new-window;"
    assert_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "Desktop Action new-window"
    assert_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "CODEX_MULTI_LAUNCH=1 /usr/bin/codex-desktop --new-instance"
    assert_not_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "Desktop Action CheckForUpdates"
    assert_not_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "InstallReadyUpdate"
    assert_not_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "codex-update-manager"
    assert_not_contains "$pkg_root/opt/codex-desktop/.codex-linux/codex-packaged-runtime.sh" "systemctl"
    assert_not_contains "$pkg_root/opt/codex-desktop/.codex-linux/codex-packaged-runtime.sh" "codex-update-manager"
    assert_contains "$pkg_root/opt/codex-desktop/.codex-linux/codex-packaged-runtime.sh" 'CHROME_DESKTOP="codex-desktop.desktop"'
    assert_contains "$pkg_root/opt/codex-desktop/.codex-linux/codex-desktop-entry-doctor.sh" "codex_desktop_repair_system_package_shadow_entries"
    assert_contains "$pkg_root/opt/codex-desktop/.codex-linux/codex-no-updater-transition-cleanup.sh" "codex_no_updater_cleanup_update_manager_service"
    assert_contains "$pkg_root/opt/codex-desktop/.codex-linux/codex-no-updater-transition-cleanup.sh" "stop \"\$SERVICE_NAME\""
    assert_contains "$pkg_root/opt/codex-desktop/.codex-linux/codex-no-updater-transition-cleanup.sh" "disable \"\$SERVICE_NAME\""
    assert_contains "$pkg_root/opt/codex-desktop/.codex-linux/codex-no-updater-transition-cleanup.sh" "daemon-reload"
    assert_contains "$pkg_root/opt/codex-desktop/.codex-linux/codex-no-updater-transition-cleanup.sh" "codex_no_updater_cleanup_user_enablement_links"
    assert_contains "$pkg_root/opt/codex-desktop/.codex-linux/codex-no-updater-transition-cleanup.sh" "default.target.wants"
    assert_contains "$pkg_root/DEBIAN/postinst" "codex_no_updater_cleanup_update_manager_service"
    assert_contains "$pkg_root/DEBIAN/postinst" "codex_desktop_repair_system_package_shadow_entries"
    assert_contains "$pkg_root/DEBIAN/prerm" "codex_no_updater_cleanup_update_manager_service"
    assert_not_contains "$pkg_root/DEBIAN/postinst" "update-builder"
    assert_not_contains "$pkg_root/DEBIAN/prerm" "update-builder"
}

test_no_updater_cleanup_helper_removes_inactive_user_enablement() {
    info "Checking no-updater inactive user service cleanup"
    local workspace="$TMP_DIR/no-updater-cleanup"
    local bin_dir="$workspace/bin"
    local helper="$workspace/codex-no-updater-transition-cleanup.sh"
    local fake_home="$workspace/home/codexuser"
    local service_link="$fake_home/.config/systemd/user/default.target.wants/codex-update-manager.service"

    mkdir -p "$bin_dir" "$(dirname "$service_link")"
    ln -s /usr/lib/systemd/user/codex-update-manager.service "$service_link"

    render_no_updater_transition_cleanup_helper "$helper"

    cat > "$bin_dir/getent" <<'SCRIPT'
#!/usr/bin/env bash
if [ "${1:-}" = "passwd" ]; then
    printf 'codexuser:x:1000:1000::%s:/bin/sh\n' "$FAKE_HOME"
fi
SCRIPT
    cat > "$bin_dir/runuser" <<'SCRIPT'
#!/usr/bin/env bash
if [ "${1:-}" = "-u" ]; then
    shift 2
fi
if [ "${1:-}" = "--" ]; then
    shift
fi
exec "$@"
SCRIPT
    cat > "$bin_dir/systemctl" <<'SCRIPT'
#!/usr/bin/env bash
exit 0
SCRIPT
    chmod +x "$bin_dir/getent" "$bin_dir/runuser" "$bin_dir/systemctl"

    PATH="$bin_dir:$PATH" FAKE_HOME="$fake_home" sh -c \
        '. "$1"; codex_no_updater_cleanup_update_manager_service' \
        _ "$helper"

    assert_file_not_exists "$service_link"
}

test_rpm_builder_smoke() {
    info "Running RPM packaging smoke test"
    local workspace="$TMP_DIR/rpm"
    local bin_dir="$workspace/bin"
    local app_dir="$workspace/app"
    local dist_dir="$workspace/dist"
    local updater_bin="$workspace/codex-update-manager"
    local capture_dir="$workspace/capture"

    mkdir -p "$workspace" "$dist_dir" "$capture_dir"
    make_stub_bin_dir "$bin_dir"
    make_fake_app "$app_dir"
    chmod 0700 "$app_dir" "$app_dir/content" "$app_dir/content/webview"
    chmod 0700 "$app_dir/start.sh"
    chmod 0600 "$app_dir/content/webview/index.html"
    printf '#!/usr/bin/env bash\nexit 0\n' > "$updater_bin"
    chmod +x "$updater_bin"

    cat > "$bin_dir/rpmbuild" <<'SCRIPT'
#!/usr/bin/env bash
rpmdir=""
binary_payload=""
spec_file="${@: -1}"
while [ $# -gt 0 ]; do
    if [ "$1" = "--define" ]; then
        case "$2" in
            _rpmdir\ *) rpmdir="${2#_rpmdir }" ;;
            _binary_payload\ *) binary_payload="${2#_binary_payload }" ;;
        esac
        shift 2
        continue
    fi
    shift
done
[ -n "$rpmdir" ] || exit 1
if [ -n "${CAPTURE_DIR:-}" ]; then
    cp "$spec_file" "$CAPTURE_DIR/codex-desktop.spec"
    printf '%s\n' "$binary_payload" > "$CAPTURE_DIR/rpm-binary-payload"
    staging_dir="$(sed -n 's|cp -a "\(.*\)/\." "%{buildroot}/"|\1|p' "$spec_file" | head -n 1)"
    if [ -n "$staging_dir" ] && [ -d "$staging_dir" ]; then
        cp -a "$staging_dir" "$CAPTURE_DIR/staging"
    fi
fi
mkdir -p "$rpmdir/x86_64"
touch "$rpmdir/x86_64/codex-desktop-2026.03.24.120000-deadbeef.x86_64.rpm"
SCRIPT
    cat > "$bin_dir/cargo" <<'SCRIPT'
#!/usr/bin/env bash
echo "cargo should not be called when UPDATER_BINARY_SOURCE exists" >&2
exit 99
SCRIPT
    chmod +x "$bin_dir/rpmbuild" "$bin_dir/cargo"

    PATH="$bin_dir:$PATH" \
    CAPTURE_DIR="$capture_dir" \
    APP_DIR_OVERRIDE="$app_dir" \
    DIST_DIR_OVERRIDE="$dist_dir" \
    UPDATER_BINARY_SOURCE="$updater_bin" \
    PACKAGE_VERSION="2026.03.24.120000+deadbeef" \
    bash "$REPO_DIR/scripts/build-rpm.sh"

    assert_file_exists "$dist_dir/codex-desktop-2026.03.24.120000-deadbeef.x86_64.rpm"
    [ "$(cat "$capture_dir/rpm-binary-payload")" = "" ] \
        || fail "Expected default RPM binary payload to use tool default"

    rm -rf "$dist_dir" "$capture_dir"
    mkdir -p "$dist_dir" "$capture_dir"

    PATH="$bin_dir:$PATH" \
    CAPTURE_DIR="$capture_dir" \
    APP_DIR_OVERRIDE="$app_dir" \
    DIST_DIR_OVERRIDE="$dist_dir" \
    PACKAGE_WITH_UPDATER=0 \
    PACKAGE_VERSION="2026.03.24.120000+manual" \
    MAX_BUILD_THREADS=8 \
    bash "$REPO_DIR/scripts/build-rpm.sh"

    assert_file_exists "$dist_dir/codex-desktop-2026.03.24.120000-manual.x86_64.rpm"
    assert_file_exists "$capture_dir/codex-desktop.spec"
    [ "$(cat "$capture_dir/rpm-binary-payload")" = "w19T8.zstdio" ] \
        || fail "Expected MAX_BUILD_THREADS to reach rpmbuild payload compression"
    assert_file_exists "$capture_dir/staging/opt/codex-desktop/.codex-linux/codex-no-updater-transition-cleanup.sh"
    assert_file_not_exists "$capture_dir/staging/usr/bin/codex-update-manager"
    assert_file_not_exists "$capture_dir/staging/usr/lib/systemd/user/codex-update-manager.service"
    assert_file_not_exists "$capture_dir/staging/usr/share/polkit-1/actions/com.github.ilysenko.codex-desktop-linux.update.policy"
    assert_file_not_exists "$capture_dir/staging/opt/codex-desktop/update-builder"
    assert_mode "$capture_dir/staging/opt/codex-desktop" "755"
    assert_mode "$capture_dir/staging/opt/codex-desktop/content/webview" "755"
    assert_mode "$capture_dir/staging/opt/codex-desktop/start.sh" "755"
    assert_mode "$capture_dir/staging/opt/codex-desktop/content/webview/index.html" "644"
    assert_contains "$capture_dir/codex-desktop.spec" "%if 0"
    assert_contains "$capture_dir/codex-desktop.spec" "codex_elf_suffix ()(64bit)"
    assert_contains "$capture_dir/codex-desktop.spec" "libatk-bridge-2.0.so.0"
    assert_contains "$capture_dir/codex-desktop.spec" "libgbm.so.1"
    assert_not_contains "$capture_dir/codex-desktop.spec" "at-spi2-atk"
    assert_not_contains "$capture_dir/codex-desktop.spec" "mesa-libgbm"
    assert_contains "$capture_dir/codex-desktop.spec" "codex_no_updater_cleanup_update_manager_service"
    assert_contains "$capture_dir/staging/opt/codex-desktop/.codex-linux/codex-no-updater-transition-cleanup.sh" "codex_no_updater_cleanup_user_enablement_links"

    rm -rf "$dist_dir" "$capture_dir"
    mkdir -p "$dist_dir" "$capture_dir"

    PATH="$bin_dir:$PATH" \
    CAPTURE_DIR="$capture_dir" \
    APP_DIR_OVERRIDE="$app_dir" \
    DIST_DIR_OVERRIDE="$dist_dir" \
    UPDATER_BINARY_SOURCE="$updater_bin" \
    PACKAGE_VERSION="2026.03.24.120000+payload" \
    RPM_BINARY_PAYLOAD="w19.zstdio" \
    bash "$REPO_DIR/scripts/build-rpm.sh"

    [ "$(cat "$capture_dir/rpm-binary-payload")" = "w19.zstdio" ] \
        || fail "Expected RPM_BINARY_PAYLOAD to override tool default"
}

test_pacman_builder_without_updater_transition_hook() {
    info "Running no-updater pacman packaging hook smoke test"
    if [ "$(id -u)" -eq 0 ]; then
        info "Skipping pacman no-updater hook smoke test as root"
        return
    fi

    local workspace="$TMP_DIR/pacman-no-updater"
    local bin_dir="$workspace/bin"
    local app_dir="$workspace/app"
    local dist_dir="$workspace/dist"
    local capture_dir="$workspace/capture"
    local ampersand_tmpdir="$workspace/ampersand&tmp"
    local base_makepkg_conf="$workspace/base-makepkg.conf"

    mkdir -p "$workspace" "$dist_dir" "$capture_dir" "$ampersand_tmpdir"
    make_stub_bin_dir "$bin_dir"
    make_fake_app "$app_dir"
    printf 'MAKEFLAGS="-j12"\n' > "$base_makepkg_conf"

    cat > "$bin_dir/makepkg" <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
cp PKGBUILD "$CAPTURE_DIR/PKGBUILD"
cp codex-desktop.install "$CAPTURE_DIR/codex-desktop.install"
printf '%s\n' "${MAKEPKG_CONF:-}" > "$CAPTURE_DIR/makepkg-conf-path"
if [ -n "${MAKEPKG_CONF:-}" ]; then
    cp "$MAKEPKG_CONF" "$CAPTURE_DIR/makepkg.conf"
    bash -c 'set -euo pipefail; . "$1"; printf "%s\n" "$MAKEFLAGS"' _ "$MAKEPKG_CONF" > "$CAPTURE_DIR/makepkg-evaluated-makeflags"
fi
pkgname="$(sed -n 's/^pkgname=//p' PKGBUILD)"
pkgver="$(sed -n 's/^pkgver=//p' PKGBUILD)"
pkgrel="$(sed -n 's/^pkgrel=//p' PKGBUILD)"
arch="$(sed -n "s/^arch=('\([^']*\)').*/\1/p" PKGBUILD)"
mkdir -p "$PKGDEST"
touch "$PKGDEST/${pkgname}-${pkgver}-${pkgrel}-${arch}.pkg.tar.zst"
SCRIPT
    cat > "$bin_dir/cargo" <<'SCRIPT'
#!/usr/bin/env bash
echo "cargo should not be called when PACKAGE_WITH_UPDATER=0" >&2
exit 99
SCRIPT
    chmod +x "$bin_dir/makepkg" "$bin_dir/cargo"

    local package_path
    package_path="$(
        TMPDIR="$ampersand_tmpdir" \
        PATH="$bin_dir:$PATH" \
        CAPTURE_DIR="$capture_dir" \
        APP_DIR_OVERRIDE="$app_dir" \
        DIST_DIR_OVERRIDE="$dist_dir" \
        MAKEPKG_CONF="$base_makepkg_conf" \
        PACKAGE_WITH_UPDATER=0 \
        MAX_BUILD_THREADS=5 \
        PACKAGE_VERSION="2026.03.24.120000+manual" \
        bash "$REPO_DIR/scripts/build-pacman.sh"
    )"

    assert_file_exists "$dist_dir/codex-desktop-2026.03.24.120000+manual-1-x86_64.pkg.tar.zst"
    [ "$package_path" = "$dist_dir/codex-desktop-2026.03.24.120000+manual-1-x86_64.pkg.tar.zst" ] || fail "Expected build-pacman.sh to print built package path, got: $package_path"
    assert_file_exists "$dist_dir/codex-desktop-latest.pkg.tar.zst"
    [ "$(readlink "$dist_dir/codex-desktop-latest.pkg.tar.zst")" = "codex-desktop-2026.03.24.120000+manual-1-x86_64.pkg.tar.zst" ] || fail "Expected latest pacman symlink to point at built package"
    assert_file_exists "$capture_dir/PKGBUILD"
    assert_file_exists "$capture_dir/codex-desktop.install"
    assert_file_exists "$capture_dir/makepkg.conf"
    assert_contains "$capture_dir/makepkg.conf" "MAKEFLAGS=\"\${MAKEFLAGS:+\$MAKEFLAGS }-j5\""
    [ "$(cat "$capture_dir/makepkg-evaluated-makeflags")" = "-j12 -j5" ] \
        || fail "Expected generated makepkg config to make MAX_BUILD_THREADS win over existing MAKEFLAGS"
    assert_contains "$capture_dir/makepkg.conf" "COMPRESSZST=(zstd -c -z -T5 -)"
    assert_contains "$capture_dir/PKGBUILD" "pkgver=2026.03.24.120000+manual"
    assert_contains "$capture_dir/PKGBUILD" "pkgrel=1"
    assert_contains "$capture_dir/PKGBUILD" "ampersand&tmp"
    assert_not_contains "$capture_dir/PKGBUILD" "__STAGING_DIR__"
    assert_contains "$capture_dir/PKGBUILD" "install=codex-desktop.install"
    assert_not_contains "$capture_dir/PKGBUILD" "'polkit'"
    assert_contains "$capture_dir/codex-desktop.install" "codex_no_updater_cleanup_update_manager_service"
    assert_contains "$capture_dir/codex-desktop.install" "post_upgrade"
    assert_contains "$capture_dir/codex-desktop.install" "pre_remove"
    assert_contains "$capture_dir/codex-desktop.install" "codex-no-updater-transition-cleanup.sh"
    assert_not_contains "$capture_dir/codex-desktop.install" "update-builder"
}

test_appimage_builder_smoke() {
    info "Running AppImage packaging smoke test"
    local workspace="$TMP_DIR/appimage"
    local bin_dir="$workspace/bin"
    local app_dir="$workspace/app"
    local dist_dir="$workspace/dist"
    local appdir="$workspace/codex-desktop.AppDir"
    local capture_dir="$workspace/capture"
    local arch

    case "$(uname -m)" in
        x86_64) arch="x86_64" ;;
        aarch64|arm64) arch="aarch64" ;;
        armv7l|armhf) arch="armhf" ;;
        *) fail "Unsupported AppImage smoke-test architecture: $(uname -m)" ;;
    esac

    mkdir -p "$workspace" "$dist_dir" "$capture_dir"
    make_stub_bin_dir "$bin_dir"
    make_fake_app "$app_dir"

    cat > "$bin_dir/appimagetool" <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail

saw_no_appstream=0
previous=""
last=""
for arg in "$@"; do
    [ "$arg" = "--no-appstream" ] && saw_no_appstream=1
    previous="$last"
    last="$arg"
done

[ "$saw_no_appstream" -eq 1 ] || exit 2
[ -n "$previous" ] || exit 3
[ -d "$previous" ] || exit 4
[ -n "${ARCH:-}" ] || exit 5
[ -n "${VERSION:-}" ] || exit 6

mkdir -p "$(dirname "$last")" "$CAPTURE_DIR"
cp -a "$previous" "$CAPTURE_DIR/AppDir"
printf '%s\n' "$ARCH" > "$CAPTURE_DIR/arch"
printf '%s\n' "$VERSION" > "$CAPTURE_DIR/version"
touch "$last"
SCRIPT
    chmod +x "$bin_dir/appimagetool"

    PATH="$bin_dir:$PATH" \
    CAPTURE_DIR="$capture_dir" \
    APP_DIR_OVERRIDE="$app_dir" \
    DIST_DIR_OVERRIDE="$dist_dir" \
    APPIMAGE_APPDIR_OVERRIDE="$appdir" \
    PACKAGE_VERSION="2026.03.24.120000+appimage" \
    bash "$REPO_DIR/scripts/build-appimage.sh"

    assert_file_exists "$dist_dir/codex-desktop-2026.03.24.120000+appimage-$arch.AppImage"
    assert_file_exists "$capture_dir/AppDir/AppRun"
    [ -x "$capture_dir/AppDir/AppRun" ] || fail "Expected AppRun to be executable"
    assert_file_exists "$capture_dir/AppDir/codex-desktop.desktop"
    assert_file_exists "$capture_dir/AppDir/codex-desktop.png"
    assert_file_exists "$capture_dir/AppDir/.DirIcon"
    assert_file_exists "$capture_dir/AppDir/usr/share/applications/codex-desktop.desktop"
    assert_file_exists "$capture_dir/AppDir/usr/share/icons/hicolor/256x256/apps/codex-desktop.png"
    assert_file_exists "$capture_dir/AppDir/opt/codex-desktop/start.sh"
    assert_file_exists "$capture_dir/AppDir/opt/codex-desktop/.codex-linux/codex-desktop.png"
    assert_file_exists "$capture_dir/AppDir/opt/codex-desktop/.codex-linux/codex-packaged-runtime.sh"
    assert_file_exists "$capture_dir/AppDir/opt/codex-desktop/resources/node-runtime/bin/node"
    assert_file_not_exists "$capture_dir/AppDir/usr/bin/codex-update-manager"
    assert_file_not_exists "$capture_dir/AppDir/usr/lib/systemd/user/codex-update-manager.service"
    assert_file_not_exists "$capture_dir/AppDir/usr/share/polkit-1/actions/com.github.ilysenko.codex-desktop-linux.update.policy"
    assert_file_not_exists "$capture_dir/AppDir/opt/codex-desktop/update-builder"
    assert_contains "$capture_dir/AppDir/codex-desktop.desktop" "Exec=AppRun %u"
    assert_contains "$capture_dir/AppDir/codex-desktop.desktop" "Icon=codex-desktop"
    assert_contains "$capture_dir/AppDir/codex-desktop.desktop" "X-AppImage-Version=2026.03.24.120000+appimage"
    assert_contains "$capture_dir/AppDir/codex-desktop.desktop" "Actions=new-window;"
    assert_contains "$capture_dir/AppDir/codex-desktop.desktop" "[Desktop Action new-window]"
    assert_not_contains "$capture_dir/AppDir/codex-desktop.desktop" "codex-update-manager"
    assert_contains "$capture_dir/AppDir/opt/codex-desktop/.codex-linux/codex-packaged-runtime.sh" 'CHROME_DESKTOP="codex-desktop.desktop"'
    assert_not_contains "$capture_dir/AppDir/opt/codex-desktop/.codex-linux/codex-packaged-runtime.sh" "/usr/share/applications"
    [ "$(cat "$capture_dir/arch")" = "$arch" ] || fail "Expected appimagetool ARCH=$arch"
    [ "$(cat "$capture_dir/version")" = "2026.03.24.120000+appimage" ] || fail "Expected appimagetool VERSION override"
}

test_missing_input_failure() {
    info "Checking missing-input failure path"
    local workspace="$TMP_DIR/missing"
    local bin_dir="$workspace/bin"
    local rpm_app_dir="$workspace/rpm-app"
    local rpm_log="$workspace/rpm-missing-runtime.log"

    mkdir -p "$workspace"
    make_stub_bin_dir "$bin_dir"
    make_fake_app "$rpm_app_dir"
    cat > "$bin_dir/dpkg" <<'SCRIPT'
#!/usr/bin/env bash
echo amd64
SCRIPT
    cat > "$bin_dir/dpkg-deb" <<'SCRIPT'
#!/usr/bin/env bash
exit 0
SCRIPT
    chmod +x "$bin_dir/dpkg" "$bin_dir/dpkg-deb"

    if PATH="$bin_dir:$PATH" APP_DIR_OVERRIDE="$workspace/does-not-exist" PKG_ROOT_OVERRIDE="$workspace/deb-root" bash "$REPO_DIR/scripts/build-deb.sh" >/dev/null 2>&1; then
        fail "build-deb.sh should fail when APP_DIR is missing"
    fi

    if APP_DIR_OVERRIDE="$rpm_app_dir" PACKAGED_RUNTIME_SOURCE="$workspace/does-not-exist.sh" bash "$REPO_DIR/scripts/build-rpm.sh" >"$rpm_log" 2>&1; then
        fail "build-rpm.sh should fail when PACKAGED_RUNTIME_SOURCE is missing"
    fi
    assert_contains "$rpm_log" "Missing packaged launcher runtime helper"
}

test_make_install_reports_missing_native_packages() {
    info "Checking make install missing-package diagnostics"
    local workspace="$TMP_DIR/make-install-missing"
    local output_log
    local format
    local expected

    mkdir -p "$workspace/dist"

    for format in pacman rpm deb; do
        output_log="$workspace/$format.log"
        case "$format" in
            pacman) expected="No pacman package found. Run 'make pacman' first." ;;
            rpm) expected="No RPM package found. Run 'make rpm' first." ;;
            deb) expected="No Debian package found. Run 'make deb' first." ;;
        esac

        if make -f "$REPO_DIR/Makefile" -C "$workspace" install \
            NATIVE_PKG_FORMAT_CMD="printf $format" >"$output_log" 2>&1
        then
            fail "make install should fail when no $format package exists"
        fi

        assert_contains "$output_log" "$expected"
    done
}

test_make_build_app_uses_installer_download_flow_by_default() {
    info "Checking make build-app default DMG behavior"
    local workspace="$TMP_DIR/make-build-app"
    local install_log="$workspace/install-args.log"
    local first_line
    local second_line

    mkdir -p "$workspace"

    cat > "$workspace/install.sh" <<'SCRIPT'
#!/usr/bin/env bash
set -eu
printf '%s\n' "$#" > "$TEST_INSTALL_LOG"
if [ "$#" -gt 0 ]; then
    printf '%s\n' "$1" >> "$TEST_INSTALL_LOG"
fi
SCRIPT
    chmod +x "$workspace/install.sh"

    TEST_INSTALL_LOG="$install_log" make -f "$REPO_DIR/Makefile" -C "$workspace" build-app >/dev/null

    assert_file_exists "$install_log"
    first_line="$(sed -n '1p' "$install_log")"
    second_line="$(sed -n '2p' "$install_log")"
    [ "$first_line" = "1" ] || fail "Expected make build-app to call install.sh with a single default argument slot, got: $(cat "$install_log")"
    [ -z "$second_line" ] || fail "Expected make build-app default DMG argument to be empty so install.sh falls back to reuse/download, got: $(cat "$install_log")"
}

test_make_build_app_fresh_uses_installer_fresh_flow() {
    info "Checking make build-app-fresh DMG behavior"
    local workspace="$TMP_DIR/make-build-app-fresh"
    local install_log="$workspace/install-args.log"
    local first_line
    local second_line
    local third_line

    mkdir -p "$workspace"

    cat > "$workspace/install.sh" <<'SCRIPT'
#!/usr/bin/env bash
set -eu
printf '%s\n' "$#" > "$TEST_INSTALL_LOG"
for arg in "$@"; do
    printf '%s\n' "$arg" >> "$TEST_INSTALL_LOG"
done
SCRIPT
    chmod +x "$workspace/install.sh"

    TEST_INSTALL_LOG="$install_log" make -f "$REPO_DIR/Makefile" -C "$workspace" build-app-fresh >/dev/null

    assert_file_exists "$install_log"
    first_line="$(sed -n '1p' "$install_log")"
    second_line="$(sed -n '2p' "$install_log")"
    third_line="$(sed -n '3p' "$install_log")"
    [ "$first_line" = "2" ] || fail "Expected make build-app-fresh to pass --fresh plus the default argument slot, got: $(cat "$install_log")"
    [ "$second_line" = "--fresh" ] || fail "Expected make build-app-fresh to pass --fresh first, got: $(cat "$install_log")"
    [ -z "$third_line" ] || fail "Expected make build-app-fresh default DMG argument to be empty, got: $(cat "$install_log")"
}

test_installer_refreshes_stale_cached_dmg_metadata() {
    info "Checking installer DMG cache freshness metadata branches"
    local workspace="$TMP_DIR/dmg-cache-refresh"
    local bin_dir="$workspace/bin"
    local url="https://persistent.oaistatic.com/codex-app-prod/Codex.dmg"
    local url_sha256

    url_sha256="$(printf '%s' "$url" | sha256sum | awk '{print $1}')"

    mkdir -p "$bin_dir"

    cat >"$bin_dir/curl" <<'SCRIPT'
#!/usr/bin/env bash
set -eu

is_head=0
for arg in "$@"; do
    if [ "$arg" = "-fsSLI" ]; then
        is_head=1
    fi
done

if [ "$is_head" -eq 1 ]; then
    printf '%s\n' "HEAD" >> "$TEST_CURL_LOG"
    if [ "${TEST_HEAD_FAIL:-0}" = "1" ]; then
        exit 22
    fi
    printf 'HTTP/2 200\r\n'
    [ -z "${TEST_ETAG:-}" ] || printf 'ETag: %s\r\n' "$TEST_ETAG"
    [ -z "${TEST_LAST_MODIFIED:-}" ] || printf 'Last-Modified: %s\r\n' "$TEST_LAST_MODIFIED"
    [ -z "${TEST_CONTENT_LENGTH:-}" ] || printf 'Content-Length: %s\r\n' "$TEST_CONTENT_LENGTH"
    printf '\r\n'
    exit 0
fi

printf '%s\n' "GET" >> "$TEST_CURL_LOG"
if [ "${TEST_GET_FAIL:-0}" = "1" ]; then
    exit 23
fi

out=""
while [ "$#" -gt 0 ]; do
    if [ "$1" = "-o" ]; then
        shift
        out="$1"
    fi
    shift || true
done

[ -n "$out" ] || exit 2
printf '%s' "${TEST_DOWNLOAD_CONTENT:-new}" >"$out"
SCRIPT
    chmod +x "$bin_dir/curl"

    run_dmg_cache_case() {
        local source_dir="$1"
        local output_log="$2"
        shift 2

        mkdir -p "$source_dir"
        : >"$source_dir/curl.log"
        env "$@" \
            PATH="$bin_dir:$PATH" \
            TEST_SOURCE_DIR="$source_dir" \
            TEST_CURL_LOG="$source_dir/curl.log" \
            REPO_DIR="$REPO_DIR" \
            bash <<'SCRIPT' >"$output_log" 2>&1
set -Eeuo pipefail

SCRIPT_DIR="$TEST_SOURCE_DIR"
WORK_DIR="$(mktemp -d)"
# shellcheck disable=SC1091
source "$REPO_DIR/scripts/lib/install-helpers.sh"
# shellcheck disable=SC1091
source "$REPO_DIR/scripts/lib/dmg.sh"

dmg_path="$(get_dmg)"
[ "$dmg_path" = "$TEST_SOURCE_DIR/Codex.dmg" ]
SCRIPT
    }

    local no_metadata="$workspace/no-metadata"
    mkdir -p "$no_metadata"
    printf '%s' "old" >"$no_metadata/Codex.dmg"
    run_dmg_cache_case "$no_metadata" "$no_metadata/output.log" \
        TEST_ETAG=fresh-etag \
        TEST_LAST_MODIFIED="Thu, 04 Jun 2026 00:00:00 GMT" \
        TEST_CONTENT_LENGTH=3 \
        TEST_DOWNLOAD_CONTENT=new
    [ "$(cat "$no_metadata/Codex.dmg")" = "new" ] || fail "Expected missing-metadata cache to refresh"
    assert_contains "$no_metadata/Codex.dmg.metadata" "etag=fresh-etag"
    assert_contains "$no_metadata/Codex.dmg.metadata" "url_sha256=$url_sha256"
    assert_contains "$no_metadata/output.log" "Cached DMG has no upstream metadata"
    assert_contains "$no_metadata/output.log" "Refreshing stale cached DMG"

    local matching="$workspace/matching"
    mkdir -p "$matching"
    printf '%s' "old" >"$matching/Codex.dmg"
    cat >"$matching/Codex.dmg.metadata" <<EOF
url_sha256=$url_sha256
etag=same-etag
last_modified=Thu, 04 Jun 2026 00:00:00 GMT
content_length=3
EOF
    run_dmg_cache_case "$matching" "$matching/output.log" \
        TEST_ETAG=same-etag \
        TEST_LAST_MODIFIED="Thu, 04 Jun 2026 00:00:00 GMT" \
        TEST_CONTENT_LENGTH=3 \
        TEST_DOWNLOAD_CONTENT=downloaded
    [ "$(cat "$matching/Codex.dmg")" = "old" ] || fail "Expected matching metadata to reuse cache"
    assert_not_contains "$matching/curl.log" "GET"
    assert_contains "$matching/output.log" "Using cached DMG"

    local differing="$workspace/differing"
    mkdir -p "$differing"
    printf '%s' "old" >"$differing/Codex.dmg"
    cat >"$differing/Codex.dmg.metadata" <<EOF
url_sha256=$url_sha256
etag=old-etag
last_modified=Thu, 04 Jun 2026 00:00:00 GMT
content_length=3
EOF
    run_dmg_cache_case "$differing" "$differing/output.log" \
        TEST_ETAG=fresh-etag \
        TEST_LAST_MODIFIED="Thu, 04 Jun 2026 00:00:00 GMT" \
        TEST_CONTENT_LENGTH=3 \
        TEST_DOWNLOAD_CONTENT=new
    [ "$(cat "$differing/Codex.dmg")" = "new" ] || fail "Expected differing metadata to refresh cache"
    assert_contains "$differing/curl.log" "GET"

    local failed_get="$workspace/failed-get"
    mkdir -p "$failed_get"
    printf '%s' "old" >"$failed_get/Codex.dmg"
    cat >"$failed_get/Codex.dmg.metadata" <<EOF
url_sha256=$url_sha256
etag=old-etag
last_modified=Thu, 04 Jun 2026 00:00:00 GMT
content_length=3
EOF
    if run_dmg_cache_case "$failed_get" "$failed_get/output.log" \
        TEST_ETAG=fresh-etag \
        TEST_LAST_MODIFIED="Thu, 04 Jun 2026 00:00:00 GMT" \
        TEST_CONTENT_LENGTH=3 \
        TEST_GET_FAIL=1
    then
        fail "Expected failed replacement download to fail the refresh"
    fi
    [ "$(cat "$failed_get/Codex.dmg")" = "old" ] || fail "Expected failed refresh to preserve old DMG"
    assert_contains "$failed_get/Codex.dmg.metadata" "etag=old-etag"
    assert_file_not_exists "$failed_get/Codex.dmg.part"

    local head_failure="$workspace/head-failure"
    mkdir -p "$head_failure"
    printf '%s' "old" >"$head_failure/Codex.dmg"
    cat >"$head_failure/Codex.dmg.metadata" <<EOF
url_sha256=$url_sha256
etag=old-etag
last_modified=Thu, 04 Jun 2026 00:00:00 GMT
content_length=3
EOF
    run_dmg_cache_case "$head_failure" "$head_failure/output.log" TEST_HEAD_FAIL=1
    [ "$(cat "$head_failure/Codex.dmg")" = "old" ] || fail "Expected HEAD failure to preserve cache"
    assert_not_contains "$head_failure/curl.log" "GET"
    assert_contains "$head_failure/output.log" "Could not check upstream DMG metadata"

    local head_failure_mismatched_url="$workspace/head-failure-mismatched-url"
    mkdir -p "$head_failure_mismatched_url"
    printf '%s' "old" >"$head_failure_mismatched_url/Codex.dmg"
    cat >"$head_failure_mismatched_url/Codex.dmg.metadata" <<EOF
url_sha256=$url_sha256
etag=old-etag
last_modified=Thu, 04 Jun 2026 00:00:00 GMT
content_length=3
EOF
    if run_dmg_cache_case "$head_failure_mismatched_url" "$head_failure_mismatched_url/output.log" \
        CODEX_UPSTREAM_DMG_URL="https://example.com/Codex.dmg" \
        TEST_HEAD_FAIL=1 \
        TEST_GET_FAIL=1
    then
        fail "Expected HEAD failure with mismatched cached URL metadata to attempt refresh and fail"
    fi
    [ "$(cat "$head_failure_mismatched_url/Codex.dmg")" = "old" ] || fail "Expected failed mismatched-URL refresh to preserve old DMG"
    assert_contains "$head_failure_mismatched_url/Codex.dmg.metadata" "etag=old-etag"
    assert_contains "$head_failure_mismatched_url/curl.log" "GET"
    assert_contains "$head_failure_mismatched_url/output.log" "cached DMG URL metadata does not match current URL"

    local secret_url="$workspace/secret-url"
    mkdir -p "$secret_url"
    run_dmg_cache_case "$secret_url" "$secret_url/output.log" \
        CODEX_UPSTREAM_DMG_URL="https://user:secret@example.com/Codex.dmg?token=topsecret#fragsecret" \
        TEST_ETAG=opaque-etag \
        TEST_CONTENT_LENGTH=3 \
        TEST_DOWNLOAD_CONTENT=new
    [ "$(cat "$secret_url/Codex.dmg")" = "new" ] || fail "Expected HTTPS override URL to download"
    assert_contains "$secret_url/output.log" "URL: https://redacted@example.com/Codex.dmg?REDACTED"
    assert_not_contains "$secret_url/output.log" "topsecret"
    assert_not_contains "$secret_url/output.log" "fragsecret"
    assert_not_contains "$secret_url/Codex.dmg.metadata" "topsecret"
    assert_not_contains "$secret_url/Codex.dmg.metadata" "fragsecret"

    local invalid_url="$workspace/invalid-url"
    mkdir -p "$invalid_url"
    if run_dmg_cache_case "$invalid_url" "$invalid_url/output.log" \
        CODEX_UPSTREAM_DMG_URL="file:///tmp/Codex.dmg"
    then
        fail "Expected non-HTTPS upstream DMG URL to fail"
    fi
    assert_contains "$invalid_url/output.log" "Upstream DMG URL must be an HTTPS URL"
}

test_fresh_install_removes_cached_dmg_metadata() {
    info "Checking --fresh removes cached DMG metadata"
    local workspace="$TMP_DIR/fresh-dmg-metadata"
    local source_dir="$workspace/source"

    mkdir -p "$source_dir"
    printf '%s' "metadata" >"$source_dir/Codex.dmg.metadata"

    TEST_SOURCE_DIR="$source_dir" REPO_DIR="$REPO_DIR" bash <<'SCRIPT'
set -Eeuo pipefail

SCRIPT_DIR="$TEST_SOURCE_DIR"
WORK_DIR="$(mktemp -d)"
INSTALL_DIR="$TEST_SOURCE_DIR/codex-app"
# shellcheck disable=SC1091
source "$REPO_DIR/scripts/lib/install-helpers.sh"

FRESH_INSTALL=1
REUSE_CACHED_DMG=0
prepare_install
SCRIPT

    assert_file_not_exists "$source_dir/Codex.dmg"
    assert_file_not_exists "$source_dir/Codex.dmg.metadata"
}

test_rebuild_candidate_uses_validated_default_dmg() {
    info "Checking rebuild-candidate default DMG validation flow"
    local workspace="$TMP_DIR/rebuild-candidate-dmg"
    local repo="$workspace/repo"
    local explicit_dmg="$workspace/explicit.dmg"
    local explicit_realpath
    local first_line
    local second_line

    mkdir -p "$repo/scripts"
    cp "$REPO_DIR/scripts/rebuild-candidate.sh" "$repo/scripts/rebuild-candidate.sh"
    printf '%s' "cached" >"$repo/Codex.dmg"
    printf '%s' "explicit" >"$explicit_dmg"
    explicit_realpath="$(realpath "$explicit_dmg")"

    cat >"$repo/install.sh" <<'SCRIPT'
#!/usr/bin/env bash
set -eu
{
    printf 'CALL:'
    for arg in "$@"; do
        printf '<%s>' "$arg"
    done
    printf '\n'
} >> "$TEST_REBUILD_LOG"
SCRIPT
    chmod +x "$repo/install.sh"

    TEST_REBUILD_LOG="$workspace/default.log" \
    CODEX_NEXT_APP_DIR="$workspace/next" \
    REBUILD_REPORT_DIR="$workspace/report" \
        bash "$repo/scripts/rebuild-candidate.sh" >"$workspace/default.out" 2>&1
    first_line="$(sed -n '1p' "$workspace/default.log")"
    second_line="$(sed -n '2p' "$workspace/default.log")"
    [[ "$first_line" != *"Codex.dmg"* ]] || fail "Default inspect should let installer validate the cache: $first_line"
    [[ "$second_line" == *"<$repo/Codex.dmg>"* ]] || fail "Default build should pin the validated cache: $second_line"
    assert_contains "$workspace/default.out" "Using validated DMG for build"

    TEST_REBUILD_LOG="$workspace/explicit.log" \
    CODEX_NEXT_APP_DIR="$workspace/next-explicit" \
    REBUILD_REPORT_DIR="$workspace/report-explicit" \
        bash "$repo/scripts/rebuild-candidate.sh" "$explicit_dmg" >"$workspace/explicit.out" 2>&1
    first_line="$(sed -n '1p' "$workspace/explicit.log")"
    second_line="$(sed -n '2p' "$workspace/explicit.log")"
    [[ "$first_line" == *"<$explicit_realpath>"* ]] || fail "Explicit inspect should receive explicit DMG: $first_line"
    [[ "$second_line" == *"<$explicit_realpath>"* ]] || fail "Explicit build should receive explicit DMG: $second_line"
}

test_native_shortcut_targets_compose_existing_flows() {
    info "Checking native install/update shortcut targets"
    local install_log="$TMP_DIR/make-install-native.log"
    local bootstrap_log="$TMP_DIR/make-bootstrap-native.log"
    local update_log="$TMP_DIR/make-update-native.log"
    local setup_log="$TMP_DIR/make-setup-native.log"

    make -n -C "$REPO_DIR" install-native >"$install_log"
    assert_contains "$install_log" './install.sh --fresh'
    assert_contains "$install_log" 'Building native package'
    assert_contains "$install_log" 'Installing latest native package'

    make -n -C "$REPO_DIR" bootstrap-native >"$bootstrap_log"
    assert_contains "$bootstrap_log" 'bash scripts/install-deps.sh'
    assert_contains "$bootstrap_log" 'PATH="$HOME/.cargo/bin:$PATH"'
    assert_contains "$bootstrap_log" 'install-native'
    assert_not_contains "$bootstrap_log" 'bootstrap-wizard.sh'

    make -n -C "$REPO_DIR" update-native >"$update_log"
    assert_contains "$update_log" 'git pull --ff-only'
    assert_contains "$update_log" 'install-native'

    make -n -C "$REPO_DIR" setup-native >"$setup_log"
    assert_contains "$setup_log" 'bash scripts/bootstrap-wizard.sh'
}

test_fedora_dependency_bootstrap_installs_rpmbuild() {
    info "Checking Fedora dependency bootstrap includes rpmbuild"
    local install_deps="$REPO_DIR/scripts/install-deps.sh"
    local helper="$REPO_DIR/scripts/lib/install-helpers.sh"
    local readme="$REPO_DIR/README.md"

    awk '/^install_dnf5\(\) \{/,/^}/' "$install_deps" | grep -q -- "rpm-build" \
        || fail "install_dnf5 must install rpm-build for rpmbuild"
    awk '/^install_dnf\(\) \{/,/^}/' "$install_deps" | grep -q -- "rpm-build" \
        || fail "install_dnf must install rpm-build for rpmbuild"

    assert_contains "$install_deps" "sudo dnf install python3 7zip curl unzip rpm-build @development-tools"
    assert_contains "$install_deps" "sudo dnf install nodejs npm python3 p7zip p7zip-plugins curl unzip rpm-build"
    assert_contains "$helper" "sudo dnf install python3 7zip curl unzip rpm-build @development-tools"
    assert_contains "$helper" "sudo dnf install nodejs npm python3 p7zip p7zip-plugins curl unzip rpm-build"
    assert_contains "$readme" "sudo dnf install python3 7zip curl unzip rpm-build @development-tools"
    assert_contains "$readme" "sudo dnf install python3 p7zip p7zip-plugins curl unzip rpm-build"
}

test_setup_native_wizard_noninteractive_feature_writer() {
    info "Checking setup-native wizard non-interactive feature writer"
    local workspace="$TMP_DIR/setup-native-writer"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"

    make_wizard_feature_root "$features_root"
    cat > "$config" <<'JSON'
{"enabled":["conversation-mode"]}
JSON

    CODEX_BOOTSTRAP_NONINTERACTIVE=1 \
    CODEX_LINUX_FEATURES_ROOT="$features_root" \
    CODEX_LINUX_FEATURES_CONFIG="$config" \
    CODEX_LINUX_FEATURES="remote-mobile-control,read-aloud" \
    CODEX_LINUX_DISABLE_FEATURES="conversation-mode" \
    PACKAGE_WITH_UPDATER=0 \
        bash "$REPO_DIR/scripts/bootstrap-wizard.sh" >"$output_log"

    assert_json_enabled_equals "$config" '["remote-mobile-control","read-aloud"]'
    assert_contains "$output_log" "remote-mobile-control"
    assert_contains "$output_log" "read-aloud"
    assert_contains "$output_log" "Manual-update native package mode selected"
    assert_contains "$output_log" "PACKAGE_WITH_UPDATER=0 make install-native"
    assert_contains "$output_log" "Feature changes apply after rebuilding and reinstalling"
}

test_setup_native_wizard_rejects_invalid_feature_ids() {
    info "Checking setup-native wizard invalid feature validation"
    local workspace="$TMP_DIR/setup-native-invalid-feature"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"

    make_wizard_feature_root "$features_root"
    printf '%s\n' '{"enabled":[]}' > "$config"

    if CODEX_BOOTSTRAP_NONINTERACTIVE=1 \
        CODEX_LINUX_FEATURES_ROOT="$features_root" \
        CODEX_LINUX_FEATURES_CONFIG="$config" \
        CODEX_LINUX_FEATURES="missing-feature" \
            bash "$REPO_DIR/scripts/bootstrap-wizard.sh" >"$output_log" 2>&1; then
        fail "setup wizard should reject unknown feature ids"
    fi

    assert_contains "$output_log" "Unknown Linux feature id: missing-feature"
    assert_json_enabled_equals "$config" '[]'
}

test_setup_native_wizard_rejects_features_without_readme() {
    info "Checking setup-native wizard rejects undocumented Linux features"
    local workspace="$TMP_DIR/setup-native-missing-readme"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"

    make_wizard_feature_root "$features_root"
    rm -f "$features_root/read-aloud/README.md"
    printf '%s\n' '{"enabled":[]}' > "$config"

    if CODEX_BOOTSTRAP_NONINTERACTIVE=1 \
        CODEX_LINUX_FEATURES_ROOT="$features_root" \
        CODEX_LINUX_FEATURES_CONFIG="$config" \
        CODEX_LINUX_FEATURES="read-aloud" \
            bash "$REPO_DIR/scripts/bootstrap-wizard.sh" >"$output_log" 2>&1; then
        fail "setup wizard should reject Linux features without README.md"
    fi

    assert_contains "$output_log" "must include README.md next to feature.json"
    assert_json_enabled_equals "$config" '[]'
}

test_setup_native_wizard_rejects_conflicting_feature_ids() {
    info "Checking setup-native wizard conflicting feature validation"
    local workspace="$TMP_DIR/setup-native-conflicting-feature"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"

    make_wizard_feature_root "$features_root"
    printf '%s\n' '{"enabled":[]}' > "$config"

    if CODEX_BOOTSTRAP_NONINTERACTIVE=1 \
        CODEX_LINUX_FEATURES_ROOT="$features_root" \
        CODEX_LINUX_FEATURES_CONFIG="$config" \
        CODEX_LINUX_FEATURES="read-aloud" \
        CODEX_LINUX_DISABLE_FEATURES="read-aloud" \
            bash "$REPO_DIR/scripts/bootstrap-wizard.sh" >"$output_log" 2>&1; then
        fail "setup wizard should reject conflicting feature ids"
    fi

    assert_contains "$output_log" "Linux feature ids cannot be both enabled and disabled: read-aloud"
    assert_json_enabled_equals "$config" '[]'
}

test_setup_native_wizard_disable_is_non_destructive() {
    info "Checking setup-native wizard opt-out guidance is non-destructive"
    local workspace="$TMP_DIR/setup-native-disable-safe"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"
    local fake_home="$workspace/home"
    local key_file="$fake_home/.config/codex-desktop/remote-control-device-keys-v1.json"
    local model_file="$fake_home/.local/share/codex-desktop/read-aloud/kokoro-venv/bin/python"
    local plugin_cache="$fake_home/.codex/plugins/cache/openai-bundled/read-aloud"

    make_wizard_feature_root "$features_root"
    cat > "$config" <<'JSON'
{"enabled":["remote-mobile-control","read-aloud","read-aloud-mcp"]}
JSON
    mkdir -p "$(dirname "$key_file")" "$(dirname "$model_file")" "$plugin_cache"
    printf '%s\n' '{"deviceKeys":[]}' > "$key_file"
    printf '%s\n' '#!/usr/bin/env python3' > "$model_file"
    printf '%s\n' 'cache marker' > "$plugin_cache/marker"

    HOME="$fake_home" \
    XDG_CONFIG_HOME="$fake_home/.config" \
    XDG_DATA_HOME="$fake_home/.local/share" \
    CODEX_BOOTSTRAP_NONINTERACTIVE=1 \
    CODEX_LINUX_FEATURES_ROOT="$features_root" \
    CODEX_LINUX_FEATURES_CONFIG="$config" \
    CODEX_LINUX_DISABLE_FEATURES="remote-mobile-control,read-aloud,read-aloud-mcp" \
        bash "$REPO_DIR/scripts/bootstrap-wizard.sh" >"$output_log"

    assert_json_enabled_equals "$config" '[]'
    assert_file_exists "$key_file"
    assert_file_exists "$model_file"
    assert_file_exists "$plugin_cache/marker"
    assert_contains "$output_log" "Not deleting $key_file"
    assert_contains "$output_log" "Not removing Read Aloud model files, Python runtimes, or plugin caches"
    assert_contains "$output_log" "$fake_home/.local/share/codex-desktop/read-aloud"
    assert_contains "$output_log" "$plugin_cache"
}

test_setup_native_wizard_accepts_numbered_feature_selection() {
    info "Checking setup-native wizard accepts numbered feature selections"
    local workspace="$TMP_DIR/setup-native-numbered-features"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"
    local fake_home="$workspace/home"

    make_wizard_feature_root "$features_root"
    printf '%s\n' '{"enabled":["remote-mobile-control"]}' > "$config"

    if ! command -v script >/dev/null 2>&1; then
        info "Skipping numbered feature selection smoke test because script(1) is unavailable"
        return
    fi

    (
        export HOME="$fake_home"
        export XDG_CONFIG_HOME="$fake_home/.config"
        export CODEX_LINUX_FEATURES_ROOT="$features_root"
        export CODEX_LINUX_FEATURES_CONFIG="$config"
        {
            printf '1,3-4\n'
            printf '5\n'
            printf '\n'
            printf '\n'
            printf '\n'
            printf '\n'
        } | script -qefc "CODEX_BOOTSTRAP_NO_GUI=1 bash $REPO_DIR/scripts/bootstrap-wizard.sh" /dev/null >"$output_log"
    )

    assert_json_enabled_equals "$config" '["conversation-mode","read-aloud","read-aloud-mcp"]'
    assert_contains "$output_log" "1\\. \\[available\\] conversation-mode - Conversation mode"
    assert_contains "$output_log" "5\\. \\[enabled\\] remote-mobile-control - Experimental Remote Mobile Control"
    assert_contains "$output_log" "Enable feature ids or numbers for the next build"
    assert_contains "$output_log" "Disable feature ids or numbers for the next build"
}

test_setup_native_wizard_rejects_out_of_range_feature_numbers() {
    info "Checking setup-native wizard explains out-of-range feature numbers"
    local workspace="$TMP_DIR/setup-native-feature-number-range"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"

    make_wizard_feature_root "$features_root"
    printf '%s\n' '{"enabled":[]}' > "$config"

    if CODEX_BOOTSTRAP_NONINTERACTIVE=1 \
        CODEX_LINUX_FEATURES_ROOT="$features_root" \
        CODEX_LINUX_FEATURES_CONFIG="$config" \
        CODEX_LINUX_FEATURES="99" \
            bash "$REPO_DIR/scripts/bootstrap-wizard.sh" >"$output_log" 2>&1; then
        fail "setup wizard should reject out-of-range feature numbers"
    fi

    assert_contains "$output_log" "Feature number 99 is out of range for enable"
    assert_contains "$output_log" "Use feature ids, numbers, or ranges like 1,3-4."
    assert_json_enabled_equals "$config" '[]'
}

test_setup_native_wizard_summary_keeps_existing_config() {
    info "Checking setup-native wizard read-only summary keeps existing feature config"
    local workspace="$TMP_DIR/setup-native-summary"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"

    make_wizard_feature_root "$features_root"
    cat > "$config" <<'JSON'
{"enabled":["remote-mobile-control"]}
JSON

    CODEX_BOOTSTRAP_NONINTERACTIVE=1 \
    CODEX_LINUX_FEATURES_ROOT="$features_root" \
    CODEX_LINUX_FEATURES_CONFIG="$config" \
        bash "$REPO_DIR/scripts/bootstrap-wizard.sh" >"$output_log"

    assert_json_enabled_equals "$config" '["remote-mobile-control"]'
    assert_contains "$output_log" "Enabled Linux features: remote-mobile-control"
    assert_contains "$output_log" "Default native package mode includes codex-update-manager"
    assert_contains "$output_log" "make install-native"
}

test_setup_native_wizard_lists_local_features() {
    info "Checking setup-native wizard discovers user-local Linux features"
    local workspace="$TMP_DIR/setup-native-local-feature"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"

    make_wizard_feature_root "$features_root"
    mkdir -p "$features_root/local/local-tool"
    printf '%s\n' '{"id":"local-tool","title":"Local Tool","description":"User-local integration."}' \
        > "$features_root/local/local-tool/feature.json"
    printf '%s\n' '# Local Tool' > "$features_root/local/local-tool/README.md"
    printf '%s\n' '{"enabled":[]}' > "$config"

    CODEX_BOOTSTRAP_NONINTERACTIVE=1 \
    CODEX_LINUX_FEATURES_ROOT="$features_root" \
    CODEX_LINUX_FEATURES_CONFIG="$config" \
    CODEX_LINUX_FEATURES="local-tool" \
        bash "$REPO_DIR/scripts/bootstrap-wizard.sh" >"$output_log"

    assert_json_enabled_equals "$config" '["local-tool"]'
    assert_contains "$output_log" "local-tool \\[local\\] - Local Tool"
    assert_contains "$output_log" "Enabled Linux features: local-tool"
}

test_setup_native_wizard_uses_package_name_for_installed_state() {
    info "Checking setup-native wizard package-name-aware installed state"
    local workspace="$TMP_DIR/setup-native-package-name"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"
    local bin_dir="$workspace/bin"
    local dpkg_args="$workspace/dpkg-query.args"

    make_wizard_feature_root "$features_root"
    printf '%s\n' '{"enabled":[]}' > "$config"
    mkdir -p "$bin_dir"
    cat > "$bin_dir/dpkg-query" <<SCRIPT
#!/usr/bin/env bash
printf '%s\n' "\$*" >> "$dpkg_args"
if [[ "\$*" != *codex-cua-lab* ]]; then
    exit 1
fi
case "\$*" in
    *"deb "*)
        printf 'deb 1.2.3'
        exit 0
        ;;
    *)
        printf '1.2.3'
        exit 0
        ;;
esac
SCRIPT
    chmod +x "$bin_dir/dpkg-query"

    PATH="$bin_dir:$PATH" \
    PACKAGE_NAME="codex-cua-lab" \
    CODEX_BOOTSTRAP_NONINTERACTIVE=1 \
    CODEX_LINUX_FEATURES_ROOT="$features_root" \
    CODEX_LINUX_FEATURES_CONFIG="$config" \
        bash "$REPO_DIR/scripts/bootstrap-wizard.sh" >"$output_log"

    assert_contains "$output_log" "Installed package: deb 1.2.3"
    assert_contains "$output_log" "ydotoold.service(system)="
    assert_contains "$output_log" "ydotoold.service(user)="
    assert_contains "$dpkg_args" "codex-cua-lab"
    assert_not_contains "$dpkg_args" "codex-desktop"
}

test_setup_native_wizard_portal_summary_survives_busctl_sigpipe() {
    info "Checking setup-native wizard portal summary avoids pipefail SIGPIPE false negatives"
    local workspace="$TMP_DIR/setup-native-portal-sigpipe"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"
    local bin_dir="$workspace/bin"

    make_wizard_feature_root "$features_root"
    printf '%s\n' '{"enabled":[]}' > "$config"
    mkdir -p "$bin_dir"
    cat > "$bin_dir/pgrep" <<'SCRIPT'
#!/usr/bin/env bash
exit 1
SCRIPT
    cat > "$bin_dir/busctl" <<'SCRIPT'
#!/usr/bin/env bash
if [ "${1:-}" = "--user" ] && [ "${2:-}" = "--list" ]; then
    printf '%s\n' 'org.freedesktop.portal.Desktop 1234 xdg-desktop-portal'
    exit 141
fi
exit 1
SCRIPT
    chmod +x "$bin_dir/pgrep" "$bin_dir/busctl"

    PATH="$bin_dir:$PATH" \
    CODEX_BOOTSTRAP_NONINTERACTIVE=1 \
    CODEX_LINUX_FEATURES_ROOT="$features_root" \
    CODEX_LINUX_FEATURES_CONFIG="$config" \
        bash "$REPO_DIR/scripts/bootstrap-wizard.sh" >"$output_log" 2>&1

    assert_contains "$output_log" "portal=available on session bus"
}

test_setup_native_wizard_warns_when_conversation_mode_lacks_read_aloud() {
    info "Checking setup-native wizard warns about conversation-mode without Read Aloud"
    local workspace="$TMP_DIR/setup-native-conversation-warning"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"

    make_wizard_feature_root "$features_root"
    printf '%s\n' '{"enabled":["conversation-mode"]}' > "$config"

    CODEX_BOOTSTRAP_NONINTERACTIVE=1 \
    CODEX_LINUX_FEATURES_ROOT="$features_root" \
    CODEX_LINUX_FEATURES_CONFIG="$config" \
        bash "$REPO_DIR/scripts/bootstrap-wizard.sh" >"$output_log" 2>&1

    assert_contains "$output_log" "conversation-mode is enabled without read-aloud"
}

test_setup_native_wizard_dry_runs_deps_and_install_native() {
    info "Checking setup-native wizard dry-run dependency and native install orchestration"
    local workspace="$TMP_DIR/setup-native-dry-run-install"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"

    make_wizard_feature_root "$features_root"
    printf '%s\n' '{"enabled":[]}' > "$config"

    CODEX_BOOTSTRAP_NONINTERACTIVE=1 \
    CODEX_BOOTSTRAP_DRY_RUN=1 \
    CODEX_BOOTSTRAP_INSTALL_DEPS=1 \
    CODEX_BOOTSTRAP_INSTALL_NATIVE=1 \
    CODEX_LINUX_FEATURES_ROOT="$features_root" \
    CODEX_LINUX_FEATURES_CONFIG="$config" \
    PACKAGE_WITH_UPDATER=0 \
        bash "$REPO_DIR/scripts/bootstrap-wizard.sh" >"$output_log"

    assert_contains "$output_log" "Would run: bash scripts/install-deps.sh"
    assert_contains "$output_log" 'Would run: PATH="$HOME/.cargo/bin:$PATH" PACKAGE_WITH_UPDATER=0 make install-native'
    assert_contains "$output_log" "Dry-run mode: no dependency install or native package install command was executed."
}

test_setup_native_wizard_prints_deep_readiness_guidance() {
    info "Checking setup-native wizard detailed Computer Use and Read Aloud readiness"
    local workspace="$TMP_DIR/setup-native-readiness"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"
    local fake_home="$workspace/home"

    make_wizard_feature_root "$features_root"
    printf '%s\n' '{"enabled":["read-aloud","read-aloud-mcp"]}' > "$config"
    mkdir -p "$fake_home/.config/codex-desktop" "$fake_home/.local/share/codex-desktop/read-aloud"

    HOME="$fake_home" \
    XDG_CONFIG_HOME="$fake_home/.config" \
    XDG_DATA_HOME="$fake_home/.local/share" \
    XDG_CURRENT_DESKTOP=KDE \
    DESKTOP_SESSION=plasma \
    XDG_SESSION_DESKTOP=plasma \
    XDG_SESSION_TYPE=wayland \
    CODEX_LINUX_SETTINGS_FILE="$fake_home/.config/codex-desktop/settings.json" \
    CODEX_BOOTSTRAP_NONINTERACTIVE=1 \
    CODEX_LINUX_FEATURES_ROOT="$features_root" \
    CODEX_LINUX_FEATURES_CONFIG="$config" \
        bash "$REPO_DIR/scripts/bootstrap-wizard.sh" >"$output_log"

    assert_contains "$output_log" "Computer Use details:"
    assert_contains "$output_log" "uinput="
    assert_contains "$output_log" "current user in input group="
    assert_contains "$output_log" "Window backend hint: KDE/Plasma -> KWin"
    assert_contains "$output_log" "Suggested ydotool command:"
    assert_contains "$output_log" "Suggested portal package:"
    assert_contains "$output_log" "Read Aloud readiness:"
    assert_contains "$output_log" "Kokoro python:"
    assert_contains "$output_log" "Read Aloud plugin cache:"
}

test_setup_native_wizard_uinput_stat_is_bounded() {
    info "Checking setup-native wizard bounds slow uinput metadata reads"
    local workspace="$TMP_DIR/setup-native-uinput-stat"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"
    local bin_dir="$workspace/bin"
    local fake_uinput="$workspace/uinput"

    make_wizard_feature_root "$features_root"
    printf '%s\n' '{"enabled":[]}' > "$config"
    mkdir -p "$bin_dir"
    printf '%s\n' 'fake uinput' > "$fake_uinput"
    cat > "$bin_dir/stat" <<'SCRIPT'
#!/usr/bin/env bash
sleep 5
printf '%s\n' 'unexpected stat output'
SCRIPT
    chmod +x "$bin_dir/stat"

    PATH="$bin_dir:$PATH" \
    CODEX_BOOTSTRAP_NONINTERACTIVE=1 \
    CODEX_BOOTSTRAP_UINPUT_PATH="$fake_uinput" \
    CODEX_LINUX_FEATURES_ROOT="$features_root" \
    CODEX_LINUX_FEATURES_CONFIG="$config" \
        timeout 3 bash "$REPO_DIR/scripts/bootstrap-wizard.sh" >"$output_log"

    assert_contains "$output_log" "uinput=read/write access"
    assert_not_contains "$output_log" "unexpected stat output"
}

test_setup_native_wizard_read_aloud_paths_match_runtime_defaults() {
    info "Checking setup-native wizard Read Aloud default paths and Linux app id"
    local workspace="$TMP_DIR/setup-native-read-aloud-defaults"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"
    local fake_home="$workspace/home"

    make_wizard_feature_root "$features_root"
    printf '%s\n' '{"enabled":["read-aloud"]}' > "$config"
    mkdir -p "$fake_home/.config/codex-cua-lab" "$fake_home/.local/share/kokoro"
    printf '%s\n' '{"codex-linux-read-aloud-kokoro-python":"/custom/python"}' > "$fake_home/.config/codex-cua-lab/settings.json"
    printf '%s\n' 'model marker' > "$fake_home/.local/share/kokoro/kokoro-v1.0.onnx"
    printf '%s\n' 'voices marker' > "$fake_home/.local/share/kokoro/voices-v1.0.bin"

    HOME="$fake_home" \
    XDG_CONFIG_HOME="$fake_home/.config" \
    XDG_DATA_HOME="$fake_home/.local/share" \
    CODEX_LINUX_APP_ID="codex-cua-lab" \
    CODEX_APP_ID="codex-desktop" \
    CODEX_LINUX_SETTINGS_FILE="$fake_home/.config/codex-cua-lab/settings.json" \
    CODEX_BOOTSTRAP_NONINTERACTIVE=1 \
    CODEX_LINUX_FEATURES_ROOT="$features_root" \
    CODEX_LINUX_FEATURES_CONFIG="$config" \
        bash "$REPO_DIR/scripts/bootstrap-wizard.sh" >"$output_log"

    assert_contains "$output_log" "Settings file: $fake_home/.config/codex-cua-lab/settings.json (file)"
    assert_contains "$output_log" "Kokoro python: /custom/python (missing)"
    assert_contains "$output_log" "Kokoro model: $fake_home/.local/share/kokoro/kokoro-v1.0.onnx (file)"
    assert_contains "$output_log" "Kokoro voices: $fake_home/.local/share/kokoro/voices-v1.0.bin (file)"
    assert_not_contains "$output_log" "$fake_home/.local/share/codex-desktop/read-aloud/kokoro/kokoro-v1.0.onnx"
}

test_setup_native_wizard_sway_hint_is_conservative() {
    info "Checking setup-native wizard Sway backend hint stays conservative"
    local workspace="$TMP_DIR/setup-native-sway-hint"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"

    make_wizard_feature_root "$features_root"
    printf '%s\n' '{"enabled":[]}' > "$config"

    XDG_CURRENT_DESKTOP=sway \
    DESKTOP_SESSION=sway \
    XDG_SESSION_DESKTOP=sway \
    CODEX_BOOTSTRAP_NONINTERACTIVE=1 \
    CODEX_LINUX_FEATURES_ROOT="$features_root" \
    CODEX_LINUX_FEATURES_CONFIG="$config" \
        bash "$REPO_DIR/scripts/bootstrap-wizard.sh" >"$output_log"

    assert_contains "$output_log" "Sway -> not explicitly supported by the current i3 backend"
    assert_not_contains "$output_log" "Sway -> i3 IPC backend through swaymsg"
}

test_setup_native_wizard_cleanup_requires_interactive_confirmation() {
    info "Checking setup-native wizard cleanup refuses non-interactive deletion"
    local workspace="$TMP_DIR/setup-native-cleanup-noninteractive"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"
    local fake_home="$workspace/home"
    local key_file="$fake_home/.config/codex-desktop/remote-control-device-keys-v1.json"

    make_wizard_feature_root "$features_root"
    printf '%s\n' '{"enabled":["remote-mobile-control"]}' > "$config"
    mkdir -p "$(dirname "$key_file")"
    printf '%s\n' '{"deviceKeys":[]}' > "$key_file"

    if HOME="$fake_home" \
        XDG_CONFIG_HOME="$fake_home/.config" \
        CODEX_BOOTSTRAP_NONINTERACTIVE=1 \
        CODEX_BOOTSTRAP_CLEANUP_FEATURES="remote-mobile-control" \
        CODEX_LINUX_FEATURES_ROOT="$features_root" \
        CODEX_LINUX_FEATURES_CONFIG="$config" \
            bash "$REPO_DIR/scripts/bootstrap-wizard.sh" >"$output_log" 2>&1; then
        fail "setup wizard should refuse non-interactive cleanup"
    fi

    assert_file_exists "$key_file"
    assert_contains "$output_log" "Cleanup requires an interactive terminal and exact path confirmation."
}

test_setup_native_wizard_dry_run_cleanup_allows_noninteractive_preview() {
    info "Checking setup-native wizard non-interactive dry-run cleanup preview"
    local workspace="$TMP_DIR/setup-native-cleanup-dry-run-noninteractive"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"
    local fake_home="$workspace/home"
    local key_file="$fake_home/.config/codex-desktop/remote-control-device-keys-v1.json"

    make_wizard_feature_root "$features_root"
    printf '%s\n' '{"enabled":["remote-mobile-control"]}' > "$config"
    mkdir -p "$(dirname "$key_file")"
    printf '%s\n' '{"deviceKeys":[]}' > "$key_file"

    HOME="$fake_home" \
    XDG_CONFIG_HOME="$fake_home/.config" \
    CODEX_BOOTSTRAP_NONINTERACTIVE=1 \
    CODEX_BOOTSTRAP_DRY_RUN=1 \
    CODEX_BOOTSTRAP_CLEANUP_FEATURES="remote-mobile-control" \
    CODEX_LINUX_FEATURES_ROOT="$features_root" \
    CODEX_LINUX_FEATURES_CONFIG="$config" \
        bash "$REPO_DIR/scripts/bootstrap-wizard.sh" >"$output_log"

    assert_file_exists "$key_file"
    assert_contains "$output_log" "Would delete: $key_file"
    assert_not_contains "$output_log" "Cleanup requires an interactive terminal"
}

test_setup_native_wizard_blank_interactive_cleanup_ids_skip_cleanup() {
    info "Checking setup-native wizard skips cleanup when interactive feature ids are blank"
    local workspace="$TMP_DIR/setup-native-cleanup-blank"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"
    local fake_home="$workspace/home"

    make_wizard_feature_root "$features_root"
    printf '%s\n' '{"enabled":["remote-mobile-control"]}' > "$config"

    if ! command -v script >/dev/null 2>&1; then
        info "Skipping blank cleanup smoke test because script(1) is unavailable"
        return
    fi

    (
        export HOME="$fake_home"
        export XDG_CONFIG_HOME="$fake_home/.config"
        export CODEX_LINUX_FEATURES_ROOT="$features_root"
        export CODEX_LINUX_FEATURES_CONFIG="$config"
        {
            printf '\n'
            printf '\n'
            printf '\n'
            printf 'y\n'
            printf '\n'
            printf '\n'
            printf '\n'
        } | script -qefc "CODEX_BOOTSTRAP_NO_GUI=1 bash $REPO_DIR/scripts/bootstrap-wizard.sh" /dev/null >"$output_log"
    )

    assert_json_enabled_equals "$config" '["remote-mobile-control"]'
    assert_contains "$output_log" "No cleanup feature ids provided; skipping feature cleanup."
    assert_contains "$output_log" "Default native package mode includes codex-update-manager"
}

test_setup_native_wizard_dry_run_cleanup_does_not_delete_confirmed_paths() {
    info "Checking setup-native wizard dry-run cleanup is non-destructive"
    local workspace="$TMP_DIR/setup-native-cleanup-dry-run"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"
    local fake_home="$workspace/home"
    local key_file="$fake_home/.config/codex-desktop/remote-control-device-keys-v1.json"

    make_wizard_feature_root "$features_root"
    printf '%s\n' '{"enabled":["remote-mobile-control"]}' > "$config"
    mkdir -p "$(dirname "$key_file")"
    printf '%s\n' '{"deviceKeys":[]}' > "$key_file"

    if ! command -v script >/dev/null 2>&1; then
        info "Skipping dry-run cleanup smoke test because script(1) is unavailable"
        return
    fi

    (
        export HOME="$fake_home"
        export XDG_CONFIG_HOME="$fake_home/.config"
        export CODEX_BOOTSTRAP_DRY_RUN=1
        export CODEX_BOOTSTRAP_CLEANUP_FEATURES="remote-mobile-control"
        export CODEX_LINUX_FEATURES_ROOT="$features_root"
        export CODEX_LINUX_FEATURES_CONFIG="$config"
        {
            printf '\n'
            printf '\n'
            printf '\n'
            printf 'DELETE %s\n' "$key_file"
        } | script -qefc "CODEX_BOOTSTRAP_NO_GUI=1 bash $REPO_DIR/scripts/bootstrap-wizard.sh" /dev/null >"$output_log"
    )

    assert_file_exists "$key_file"
    assert_contains "$output_log" "Would delete: $key_file"
    assert_not_contains "$output_log" "Deleted $key_file"
}

test_setup_native_wizard_cleanup_deletes_only_confirmed_paths() {
    info "Checking setup-native wizard deletes only explicitly confirmed cleanup paths"
    local workspace="$TMP_DIR/setup-native-cleanup-confirmed"
    local features_root="$workspace/linux-features"
    local config="$workspace/features.json"
    local output_log="$workspace/output.log"
    local fake_home="$workspace/home"
    local key_file="$fake_home/.config/codex-desktop/remote-control-device-keys-v1.json"
    local read_aloud_data="$fake_home/.local/share/codex-desktop/read-aloud"
    local plugin_cache="$fake_home/.codex/plugins/cache/openai-bundled/read-aloud"

    make_wizard_feature_root "$features_root"
    printf '%s\n' '{"enabled":["remote-mobile-control","read-aloud"]}' > "$config"
    mkdir -p "$(dirname "$key_file")" "$read_aloud_data" "$plugin_cache"
    printf '%s\n' '{"deviceKeys":[]}' > "$key_file"
    printf '%s\n' 'model marker' > "$read_aloud_data/model"
    printf '%s\n' 'cache marker' > "$plugin_cache/marker"

    if ! command -v script >/dev/null 2>&1; then
        info "Skipping interactive cleanup smoke test because script(1) is unavailable"
        return
    fi

    (
        export HOME="$fake_home"
        export XDG_CONFIG_HOME="$fake_home/.config"
        export XDG_DATA_HOME="$fake_home/.local/share"
        export CODEX_BOOTSTRAP_CLEANUP_FEATURES="remote-mobile-control,read-aloud"
        export CODEX_LINUX_FEATURES_ROOT="$features_root"
        export CODEX_LINUX_FEATURES_CONFIG="$config"
        {
            printf '\n'
            printf '\n'
            printf '\n'
            printf 'DELETE %s\n' "$key_file"
            printf 'DELETE %s\n' "$read_aloud_data"
            printf '\n'
            printf '\n'
            printf '\n'
        } | script -qefc "CODEX_BOOTSTRAP_NO_GUI=1 bash $REPO_DIR/scripts/bootstrap-wizard.sh" /dev/null >"$output_log"
    )

    assert_file_not_exists "$key_file"
    [ ! -e "$read_aloud_data" ] || fail "Expected confirmed Read Aloud data path to be deleted"
    assert_file_exists "$plugin_cache/marker"
    assert_contains "$output_log" "Deleted $key_file"
    assert_contains "$output_log" "Deleted $read_aloud_data"
    assert_contains "$output_log" "Skipped $plugin_cache"
}

test_upstream_build_app_workflow_tracks_dmg_metadata() {
    info "Checking upstream build-app workflow metadata and cache behavior"
    local workflow="$REPO_DIR/.github/workflows/upstream-build-app.yml"

    assert_file_exists "$workflow"
    assert_contains "$workflow" 'name: Upstream Build App'
    assert_contains "$workflow" 'UPSTREAM_DMG_URL: https://persistent.oaistatic.com/codex-app-prod/Codex.dmg'
    assert_contains "$workflow" 'actions/cache@v4'
    assert_contains "$workflow" 'path: /tmp/codex-upstream-ci/Codex.dmg'
    assert_contains "$workflow" 'Last-Modified'
    assert_contains "$workflow" 'sha256sum'
    assert_contains "$workflow" 'CODEX_PATCH_REPORT_JSON="$GITHUB_WORKSPACE/patch-report.json"'
    assert_contains "$workflow" 'node scripts/ci/validate-patch-report.js patch-report.json --profile upstream-build'
    assert_contains "$workflow" 'make build-app DMG=/tmp/codex-upstream-ci/Codex.dmg'
    assert_contains "$workflow" 'DMG Last-Modified'
    assert_contains "$workflow" 'DMG SHA-256'
}

test_installer_detects_electron_version_from_plist() {
    info "Checking Electron version detection from app metadata"
    local workspace="$TMP_DIR/electron-version"
    local app_dir="$workspace/Codex.app"
    local plist_dir="$app_dir/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources"
    local output_log="$workspace/output.log"

    mkdir -p "$plist_dir"
    cat > "$plist_dir/Info.plist" <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleVersion</key>
    <string>42.5.7</string>
</dict>
</plist>
PLIST

    CODEX_INSTALLER_SOURCE_ONLY=1 bash -c \
        'source "$1"; detect_electron_version "$2"; printf "%s\n" "$ELECTRON_VERSION"' \
        _ "$REPO_DIR/install.sh" "$app_dir" >"$output_log" 2>&1

    assert_contains "$output_log" "Detected Electron version from DMG: 42.5.7"
    [ "$(tail -n 1 "$output_log")" = "42.5.7" ] || fail "Expected detected Electron version 42.5.7, got: $(cat "$output_log")"
}

test_installer_keeps_electron_fallback_for_bad_metadata() {
    info "Checking Electron version fallback for malformed metadata"
    local workspace="$TMP_DIR/electron-version-fallback"
    local app_dir="$workspace/Codex.app"
    local plist_dir="$app_dir/Contents/Frameworks/Electron Framework.framework/Versions/A/Resources"
    local output_log="$workspace/output.log"

    mkdir -p "$plist_dir"
    cat > "$plist_dir/Info.plist" <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleVersion</key>
    <string>not-a-version</string>
</dict>
</plist>
PLIST

    CODEX_INSTALLER_SOURCE_ONLY=1 bash -c \
        'source "$1"; detect_electron_version "$2"; printf "%s\n" "$ELECTRON_VERSION"' \
        _ "$REPO_DIR/install.sh" "$app_dir" >"$output_log" 2>&1

    assert_contains "$output_log" "Ignoring invalid Electron version from DMG: not-a-version"
    assert_contains "$output_log" "Could not auto-detect Electron version; using fallback 41.3.0"
    [ "$(tail -n 1 "$output_log")" = "41.3.0" ] || fail "Expected fallback Electron version 41.3.0, got: $(cat "$output_log")"
}

test_port_validation_rejects_oversized_numeric_values() {
    info "Checking oversized numeric webview port validation"
    local workspace="$TMP_DIR/port-validation"
    local install_stdout="$workspace/install.stdout"
    local install_stderr="$workspace/install.stderr"
    local launcher_stdout="$workspace/launcher.stdout"
    local launcher_stderr="$workspace/launcher.stderr"
    local canonical_stdout="$workspace/canonical.stdout"
    local canonical_stderr="$workspace/canonical.stderr"
    local launcher_probe_script="$workspace/launcher-port-probe.sh"
    local start_script="$workspace/start.sh"
    local huge_port="999999999999999999999999"
    local rc

    mkdir -p "$workspace"

    set +e
    CODEX_INSTALLER_SOURCE_ONLY=1 CODEX_WEBVIEW_PORT="$huge_port" bash -c \
        'source "$1"; validate_app_identity' \
        _ "$REPO_DIR/install.sh" >"$install_stdout" 2>"$install_stderr"
    rc=$?
    set -e
    [ "$rc" -ne 0 ] || fail "Expected installer validation to reject oversized CODEX_WEBVIEW_PORT"
    assert_contains "$install_stderr" "CODEX_WEBVIEW_PORT must be between 1 and 65535"
    assert_not_contains "$install_stderr" "integer expected"

    CODEX_INSTALLER_SOURCE_ONLY=1 CODEX_WEBVIEW_PORT=00080 bash -c \
        'source "$1"; validate_app_identity; printf "%s\n" "$CODEX_WEBVIEW_PORT"' \
        _ "$REPO_DIR/install.sh" >"$canonical_stdout" 2>"$canonical_stderr"
    [ "$(cat "$canonical_stdout")" = "80" ] || fail "Expected installer validation to canonicalize leading-zero CODEX_WEBVIEW_PORT"
    [ ! -s "$canonical_stderr" ] || fail "Expected installer leading-zero canonicalization to be quiet, got: $(cat "$canonical_stderr")"

    cat > "$start_script" <<'SCRIPT'
#!/bin/bash
set -euo pipefail
CODEX_LINUX_APP_ID=codex-desktop
CODEX_LINUX_APP_DISPLAY_NAME=Codex
CODEX_LINUX_WEBVIEW_PORT=${CODEX_WEBVIEW_PORT:-5175}
SCRIPT
    cat "$REPO_DIR/launcher/start.sh.template" >> "$start_script"
    chmod +x "$start_script"

    set +e
    CODEX_WEBVIEW_PORT="$huge_port" bash "$start_script" --help >"$launcher_stdout" 2>"$launcher_stderr"
    rc=$?
    set -e
    [ "$rc" -ne 0 ] || fail "Expected launcher validation to reject oversized CODEX_WEBVIEW_PORT"
    assert_contains "$launcher_stderr" "CODEX_WEBVIEW_PORT must be between 1 and 65535"
    assert_not_contains "$launcher_stderr" "integer expected"

    XDG_CONFIG_HOME="$workspace/help-config" bash "$start_script" --help >"$launcher_stdout" 2>"$launcher_stderr"
    assert_contains "$launcher_stdout" "electron-flags.conf"
    assert_file_not_exists "$workspace/help-config/codex-desktop/electron-flags.conf"

    cat > "$launcher_probe_script" <<'SCRIPT'
#!/bin/bash
set -euo pipefail
CODEX_LINUX_WEBVIEW_PORT=${CODEX_WEBVIEW_PORT:-5175}
SCRIPT
    awk '
        /^normalize_tcp_port\(\) \{/ { emit = 1 }
        /^launcher_port_is_open\(\) \{/ { exit }
        emit { print }
    ' "$REPO_DIR/launcher/start.sh.template" >> "$launcher_probe_script"
    cat >> "$launcher_probe_script" <<'SCRIPT'
printf '%s\n' "$CODEX_LINUX_WEBVIEW_PORT"
SCRIPT
    chmod +x "$launcher_probe_script"
    CODEX_WEBVIEW_PORT=00080 bash "$launcher_probe_script" >"$launcher_stdout" 2>"$launcher_stderr"
    [ "$(tail -n 1 "$launcher_stdout")" = "80" ] || fail "Expected launcher validation to canonicalize leading-zero CODEX_WEBVIEW_PORT"
    [ ! -s "$launcher_stderr" ] || fail "Expected launcher leading-zero canonicalization to be quiet, got: $(cat "$launcher_stderr")"
}

test_managed_node_runtime_source_install() {
    info "Checking managed Node.js runtime source install"
    local workspace="$TMP_DIR/managed-node-runtime"
    local source_dir="$workspace/source"
    local install_dir="$workspace/install"

    mkdir -p "$source_dir/bin" "$install_dir/resources"
    for binary in node npm npx; do
        cat > "$source_dir/bin/$binary" <<'SCRIPT'
#!/usr/bin/env bash
case "$(basename "$0")" in
    node)
        case "${1:-}" in
            -e) printf '%s' 'codex-node-runtime-ok:22.22.2' ;;
            -v) echo v22.22.2 ;;
            *) echo v22.22.2 ;;
        esac
        ;;
    *) echo 10.9.7 ;;
esac
SCRIPT
        chmod +x "$source_dir/bin/$binary"
    done

    (
        SCRIPT_DIR="$REPO_DIR"
        WORK_DIR="$workspace/work"
        ARCH="x86_64"
        CODEX_MANAGED_NODE_SOURCE="$source_dir"
        mkdir -p "$WORK_DIR"
        info() { echo "[INFO] $*" >&2; }
        warn() { echo "[WARN] $*" >&2; }
        error() { echo "[ERROR] $*" >&2; exit 1; }
        # shellcheck disable=SC1091
        source "$REPO_DIR/scripts/lib/node-runtime.sh"
        ensure_managed_node_runtime "$install_dir/resources/node-runtime"
        command -v node
        node -v
    ) > "$workspace/output.log" 2>&1

    assert_file_exists "$install_dir/resources/node-runtime/bin/node"
    assert_contains "$workspace/output.log" "$install_dir/resources/node-runtime/bin/node"
    assert_contains "$workspace/output.log" "v22.22.2"
}

test_managed_node_runtime_rejects_version_only_stub() {
    info "Checking managed Node.js runtime rejects version-only stubs"
    local workspace="$TMP_DIR/managed-node-runtime-stub"
    local source_dir="$workspace/source"
    local install_dir="$workspace/install"

    mkdir -p "$source_dir/bin" "$install_dir/resources/node-runtime/bin"
    for binary in node npm npx; do
        cat > "$install_dir/resources/node-runtime/bin/$binary" <<'SCRIPT'
#!/usr/bin/env bash
case "$(basename "$0")" in
    node) echo v22.22.2 ;;
    *) echo 10.9.7 ;;
esac
SCRIPT
        chmod +x "$install_dir/resources/node-runtime/bin/$binary"
    done

    for binary in node npm npx; do
        cat > "$source_dir/bin/$binary" <<'SCRIPT'
#!/usr/bin/env bash
case "$(basename "$0")" in
    node)
        case "${1:-}" in
            -e) printf '%s' 'codex-node-runtime-ok:22.22.2' ;;
            -v) echo v22.22.2 ;;
            *) echo v22.22.2 ;;
        esac
        ;;
    *) echo 10.9.7 ;;
esac
SCRIPT
        chmod +x "$source_dir/bin/$binary"
    done

    (
        SCRIPT_DIR="$REPO_DIR"
        WORK_DIR="$workspace/work"
        ARCH="x86_64"
        CODEX_MANAGED_NODE_SOURCE="$source_dir"
        mkdir -p "$WORK_DIR"
        info() { echo "[INFO] $*" >&2; }
        warn() { echo "[WARN] $*" >&2; }
        error() { echo "[ERROR] $*" >&2; exit 1; }
        # shellcheck disable=SC1091
        source "$REPO_DIR/scripts/lib/node-runtime.sh"
        ensure_managed_node_runtime "$install_dir/resources/node-runtime"
        command -v node
        node -v
    ) > "$workspace/output.log" 2>&1

    assert_contains "$workspace/output.log" "Managed Node.js runtime copied from $source_dir"
    assert_contains "$workspace/output.log" "$install_dir/resources/node-runtime/bin/node"
    assert_contains "$workspace/output.log" "v22.22.2"
}

test_better_sqlite3_electron_42_source_patch() {
    info "Checking better-sqlite3 Electron 42 source patch"
    local workspace="$TMP_DIR/better-sqlite3-electron-42"
    local module_dir="$workspace/node_modules/better-sqlite3"
    local output_log="$workspace/output.log"

    mkdir -p "$module_dir/src/util"
    cat > "$module_dir/src/better_sqlite3.cpp" <<'CPP'
void init(v8::Isolate* isolate, Addon* addon) {
	v8::Local<v8::External> data = v8::External::New(isolate, addon);
}
CPP
    cat > "$module_dir/src/util/macros.cpp" <<'CPP'
#define EasyIsolate v8::Isolate* isolate = v8::Isolate::GetCurrent()
#define OnlyIsolate info.GetIsolate()
#define OnlyContext isolate->GetCurrentContext()
#define OnlyAddon static_cast<Addon*>(info.Data().As<v8::External>()->Value())
CPP
    cat > "$module_dir/src/util/helpers.cpp" <<'CPP'
void SetPrototypeGetter() {
	recv->InstanceTemplate()->SetNativeDataProperty(
		InternalizedFromLatin1(isolate, name),
		func,
		0,
		data
	);
}
CPP

    (
        ELECTRON_VERSION="42.0.1"
        info() { echo "[INFO] $*" >&2; }
        warn() { echo "[WARN] $*" >&2; }
        error() { echo "[ERROR] $*" >&2; exit 1; }
        # shellcheck disable=SC1091
        source "$REPO_DIR/scripts/lib/native-modules.sh"
        patch_better_sqlite3_for_v8_external_pointer_api "$module_dir"
        patch_better_sqlite3_for_v8_external_pointer_api "$module_dir"
    ) > "$output_log" 2>&1

    assert_contains "$module_dir/src/better_sqlite3.cpp" "BETTER_SQLITE3_EXTERNAL_NEW(isolate, addon)"
    assert_contains "$module_dir/src/util/macros.cpp" "BETTER_SQLITE3_EXTERNAL_POINTER_TAG"
    assert_contains "$module_dir/src/util/macros.cpp" "BETTER_SQLITE3_EXTERNAL_VALUE(info.Data().As<v8::External>())"
    assert_contains "$module_dir/src/util/helpers.cpp" "nullptr"
    assert_contains "$output_log" "Patched better-sqlite3 source for V8 external pointer API"
    assert_contains "$output_log" "already applied"
}

test_v8_nullptr_workaround_skips_when_included_probe_succeeds() {
    info "Checking V8 nullptr_t workaround probe stays inactive when not needed"
    local workspace="$TMP_DIR/v8-nullptr-workaround-skip"
    local fake_bin="$workspace/bin"
    local cxx_log="$workspace/cxx.log"
    local cxx_state="$workspace/cxx-state.log"
    local output_log="$workspace/output.log"

    mkdir -p "$fake_bin" "$workspace/work"
    cat > "$fake_bin/c++" <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
printf 'argv:%s\n' "$*" >> "$NATIVE_CXX_LOG"
for arg in "$@"; do
    if [ -f "$arg" ]; then
        cat "$arg" >> "$NATIVE_CXX_LOG"
    fi
done
exit 0
SCRIPT
    chmod +x "$fake_bin/c++"

    (
        CXX="$fake_bin/c++"
        NATIVE_CXX_LOG="$cxx_log"
        export CXX NATIVE_CXX_LOG
        info() { echo "[INFO] $*" >&2; }
        warn() { echo "[WARN] $*" >&2; }
        error() { echo "[ERROR] $*" >&2; exit 1; }
        # shellcheck disable=SC1091
        source "$REPO_DIR/scripts/lib/native-modules.sh"
        apply_v8_nullptr_t_workaround_if_needed "$workspace/work"
        printf 'CXX=%s\n' "$CXX" > "$cxx_state"
    ) > "$output_log" 2>&1

    assert_contains "$cxx_log" "#include <cstddef>"
    assert_contains "$cxx_log" "nullptr_t x = nullptr;"
    assert_contains "$cxx_state" "CXX=$fake_bin/c++"
    assert_not_contains "$output_log" "Applied GCC 16+ nullptr_t compatibility workaround"
}

test_v8_nullptr_workaround_wraps_when_included_probe_fails() {
    info "Checking V8 nullptr_t workaround wraps CXX only when needed"
    local workspace="$TMP_DIR/v8-nullptr-workaround-wrap"
    local fake_bin="$workspace/bin"
    local cxx_log="$workspace/cxx.log"
    local cxx_state="$workspace/cxx-state.log"
    local output_log="$workspace/output.log"

    mkdir -p "$fake_bin" "$workspace/work"
    cat > "$fake_bin/c++" <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
printf 'argv:%s\n' "$*" >> "$NATIVE_CXX_LOG"
for arg in "$@"; do
    if [ -f "$arg" ]; then
        cat "$arg" >> "$NATIVE_CXX_LOG"
    fi
    case "$arg" in
        *.v8-nullptr-probe.cc) exit 1 ;;
    esac
done
exit 0
SCRIPT
    chmod +x "$fake_bin/c++"
    printf '%s\n' 'int main() { return 0; }' > "$workspace/dummy.cc"

    (
        CXX="$fake_bin/c++"
        NATIVE_CXX_LOG="$cxx_log"
        export CXX NATIVE_CXX_LOG
        info() { echo "[INFO] $*" >&2; }
        warn() { echo "[WARN] $*" >&2; }
        error() { echo "[ERROR] $*" >&2; exit 1; }
        # shellcheck disable=SC1091
        source "$REPO_DIR/scripts/lib/native-modules.sh"
        apply_v8_nullptr_t_workaround_if_needed "$workspace/work"
        "$CXX" -x c++ -fsyntax-only "$workspace/dummy.cc"
        printf 'CXX=%s\n' "$CXX" > "$cxx_state"
    ) > "$output_log" 2>&1

    assert_file_exists "$workspace/work/.v8-nullptr-fix.h"
    assert_file_exists "$workspace/work/.cxx-v8-nullptr"
    assert_contains "$workspace/work/.v8-nullptr-fix.h" "using std::nullptr_t;"
    assert_contains "$cxx_state" "CXX=$workspace/work/.cxx-v8-nullptr"
    assert_contains "$cxx_log" "-include"
    assert_contains "$cxx_log" ".v8-nullptr-fix.h"
    assert_contains "$output_log" "Applied GCC 16+ nullptr_t compatibility workaround"
}

test_native_module_rebuild_uses_local_electron_rebuild_toolchain() {
    info "Checking native module rebuild uses local Electron rebuild toolchain"
    local workspace="$TMP_DIR/native-module-rebuild-toolchain"
    local app_dir="$workspace/app-extracted"
    local fake_bin="$workspace/bin"
    local toolchain_log="$workspace/toolchain.log"
    local output_log="$workspace/output.log"

    mkdir -p "$app_dir/node_modules/better-sqlite3" "$app_dir/node_modules/node-pty" "$fake_bin"
    printf '%s\n' '{"version":"12.9.0"}' > "$app_dir/node_modules/better-sqlite3/package.json"
    printf '%s\n' '{"version":"1.1.0"}' > "$app_dir/node_modules/node-pty/package.json"

    cat > "$fake_bin/npm" <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail

printf 'npm %s\n' "$*" >> "$NATIVE_TOOLCHAIN_LOG"
args=" $* "

case "$args" in
    *" @electron/rebuild@4.0.4 "*)
        mkdir -p node_modules/@electron/rebuild/lib
        cat > node_modules/@electron/rebuild/lib/cli.js <<'REBUILD'
#!/usr/bin/env node
const fs = require("fs");
fs.appendFileSync(process.env.NATIVE_TOOLCHAIN_LOG, `electron-rebuild ${process.argv.slice(2).join(" ")}\n`);
fs.appendFileSync(process.env.NATIVE_TOOLCHAIN_LOG, `electron-rebuild-env jobs=${process.env.npm_config_jobs || ""} makeflags=${process.env.MAKEFLAGS || ""}\n`);
fs.mkdirSync("node_modules/better-sqlite3/build/Release", { recursive: true });
fs.mkdirSync("node_modules/node-pty/build/Release", { recursive: true });
fs.closeSync(fs.openSync("node_modules/better-sqlite3/build/Release/better_sqlite3.node", "w"));
fs.closeSync(fs.openSync("node_modules/node-pty/build/Release/pty.node", "w"));
REBUILD
        ;;
esac

case "$args" in
    *" better-sqlite3@12.9.0 "*)
        mkdir -p node_modules/better-sqlite3/src/util
        printf '%s\n' '{"version":"12.9.0"}' > node_modules/better-sqlite3/package.json
        cat > node_modules/better-sqlite3/src/better_sqlite3.cpp <<'CPP'
void init(v8::Isolate* isolate, Addon* addon) {
	v8::Local<v8::External> data = v8::External::New(isolate, addon);
}
CPP
        cat > node_modules/better-sqlite3/src/util/macros.cpp <<'CPP'
#define EasyIsolate v8::Isolate* isolate = v8::Isolate::GetCurrent()
#define OnlyIsolate info.GetIsolate()
#define OnlyContext isolate->GetCurrentContext()
#define OnlyAddon static_cast<Addon*>(info.Data().As<v8::External>()->Value())
CPP
        cat > node_modules/better-sqlite3/src/util/helpers.cpp <<'CPP'
void SetPrototypeGetter() {
	recv->InstanceTemplate()->SetNativeDataProperty(
		InternalizedFromLatin1(isolate, name),
		func,
		0,
		data
	);
}
CPP
        ;;
esac

case "$args" in
    *" node-pty@1.1.0 "*)
        mkdir -p node_modules/node-pty
        printf '%s\n' '{"version":"1.1.0"}' > node_modules/node-pty/package.json
        ;;
esac
SCRIPT
    chmod +x "$fake_bin/npm"

    cat > "$fake_bin/npx" <<'SCRIPT'
#!/usr/bin/env bash
echo "npx should not be used for electron-rebuild" >&2
exit 99
SCRIPT
    chmod +x "$fake_bin/npx"

    (
        PATH="$fake_bin:$PATH"
        export PATH
        NATIVE_TOOLCHAIN_LOG="$toolchain_log"
        export NATIVE_TOOLCHAIN_LOG
        MAX_BUILD_THREADS=4
        MAKEFLAGS="-j12 -l8"
        export MAX_BUILD_THREADS
        export MAKEFLAGS
        WORK_DIR="$workspace/work"
        ELECTRON_VERSION="42.0.1"
        ELECTRON_HEADERS_URL="https://example.invalid/electron"
        mkdir -p "$WORK_DIR"
        info() { echo "[INFO] $*" >&2; }
        warn() { echo "[WARN] $*" >&2; }
        error() { echo "[ERROR] $*" >&2; exit 1; }
        # shellcheck disable=SC1091
        source "$REPO_DIR/scripts/lib/native-modules.sh"
        build_native_modules "$app_dir"
    ) > "$output_log" 2>&1

    assert_contains "$toolchain_log" "@electron/rebuild@4.0.4"
    assert_contains "$toolchain_log" "node-abi@^4.31.0"
    assert_contains "$toolchain_log" "electron-rebuild -v 42.0.1 --force --dist-url https://example.invalid/electron --sequential"
    assert_contains "$toolchain_log" "electron-rebuild-env jobs=4 makeflags=-j4"
    assert_contains "$output_log" "Native modules built successfully"
    assert_file_exists "$app_dir/node_modules/better-sqlite3/build/Release/better_sqlite3.node"
    assert_file_exists "$app_dir/node_modules/node-pty/build/Release/pty.node"
}

test_native_module_rebuild_accepts_prebuilt_source() {
    info "Checking native module rebuild accepts prebuilt source"
    local workspace="$TMP_DIR/native-module-prebuilt-source"
    local app_dir="$workspace/app-extracted"
    local source_dir="$workspace/prebuilt"
    local output_log="$workspace/output.log"

    mkdir -p \
        "$app_dir/node_modules/better-sqlite3" \
        "$app_dir/node_modules/node-pty" \
        "$source_dir/better-sqlite3/build/Release" \
        "$source_dir/node-pty/build/Release"
    printf '%s\n' '{"version":"12.9.0"}' > "$app_dir/node_modules/better-sqlite3/package.json"
    printf '%s\n' '{"version":"1.1.0"}' > "$app_dir/node_modules/node-pty/package.json"
    printf '%s\n' stale > "$app_dir/node_modules/better-sqlite3/old.txt"

    printf '%s\n' '{"version":"12.9.0"}' > "$source_dir/better-sqlite3/package.json"
    printf '%s\n' '{"version":"1.1.0"}' > "$source_dir/node-pty/package.json"
    : > "$source_dir/better-sqlite3/build/Release/better_sqlite3.node"
    : > "$source_dir/better-sqlite3/build/Release/junk.o"
    : > "$source_dir/node-pty/build/Release/pty.node"
    : > "$source_dir/node-pty/build/Release/junk.o"

    (
        WORK_DIR="$workspace/work"
        ELECTRON_VERSION="42.0.1"
        CODEX_NATIVE_MODULES_SOURCE="$source_dir"
        mkdir -p "$WORK_DIR"
        info() { echo "[INFO] $*" >&2; }
        warn() { echo "[WARN] $*" >&2; }
        error() { echo "[ERROR] $*" >&2; exit 1; }
        # shellcheck disable=SC1091
        source "$REPO_DIR/scripts/lib/native-modules.sh"
        build_native_modules "$app_dir"
    ) > "$output_log" 2>&1

    assert_contains "$output_log" "Using prebuilt native modules from $source_dir"
    assert_file_exists "$app_dir/node_modules/better-sqlite3/build/Release/better_sqlite3.node"
    assert_file_exists "$app_dir/node_modules/node-pty/build/Release/pty.node"
    [ ! -f "$app_dir/node_modules/better-sqlite3/old.txt" ] || fail "Expected stale better-sqlite3 module to be replaced"
    [ ! -f "$app_dir/node_modules/better-sqlite3/build/Release/junk.o" ] || fail "Expected better-sqlite3 build junk to be pruned"
    [ ! -f "$app_dir/node_modules/node-pty/build/Release/junk.o" ] || fail "Expected node-pty build junk to be pruned"
}

test_bundled_plugin_builders_accept_prebuilt_binaries() {
    info "Checking bundled plugin builders accept prebuilt binaries"
    local workspace="$TMP_DIR/bundled-plugin-prebuilt-binaries"
    local backend="$workspace/codex-computer-use-linux"
    local cosmic="$workspace/codex-computer-use-cosmic"
    local host="$workspace/codex-chrome-extension-host"
    local output_log="$workspace/output.log"

    mkdir -p "$workspace"
    printf '#!/usr/bin/env bash\n' > "$backend"
    printf '#!/usr/bin/env bash\n' > "$cosmic"
    printf '#!/usr/bin/env bash\n' > "$host"
    chmod +x "$backend" "$cosmic" "$host"

    (
        SCRIPT_DIR="$REPO_DIR"
        CODEX_LINUX_COMPUTER_USE_BACKEND_SOURCE="$backend"
        CODEX_LINUX_COMPUTER_USE_COSMIC_SOURCE="$cosmic"
        CODEX_CHROME_EXTENSION_HOST_SOURCE="$host"
        info() { echo "[INFO] $*" >&2; }
        warn() { echo "[WARN] $*" >&2; }
        error() { echo "[ERROR] $*" >&2; exit 1; }
        # shellcheck disable=SC1091
        source "$REPO_DIR/scripts/lib/bundled-plugins.sh"
        build_linux_computer_use_backend
        build_chrome_extension_host
    ) > "$output_log" 2>&1

    assert_contains "$output_log" "Using prebuilt Linux Computer Use backend"
    assert_contains "$output_log" "Using prebuilt Chrome extension host"
    assert_contains "$output_log" "$backend"
    assert_contains "$output_log" "$cosmic"
    assert_contains "$output_log" "$host"
}

test_launcher_template_sanity() {
    info "Checking launcher template markers"
    assert_contains "$REPO_DIR/install.sh" 'DEFAULT_CODEX_WEBVIEW_PORT=5175'
    assert_contains "$REPO_DIR/install.sh" "inspect_rebuild_candidate"
    assert_contains "$REPO_DIR/scripts/lib/install-helpers.sh" "--inspect"
    assert_contains "$REPO_DIR/scripts/lib/install-helpers.sh" "--report-dir"
    assert_contains "$REPO_DIR/scripts/lib/asar-patch.sh" "CODEX_PATCH_REPORT_JSON"
    assert_contains "$REPO_DIR/scripts/lib/rebuild-report.sh" "write_rebuild_report_json"
    assert_contains "$REPO_DIR/install.sh" "MIN_BETTER_SQLITE3_VERSION_FOR_ELECTRON_41=\"12.9.0\""
    assert_contains "$REPO_DIR/scripts/lib/native-modules.sh" "better_sqlite3_build_version"
    assert_contains "$REPO_DIR/scripts/lib/native-modules.sh" "patch_better_sqlite3_for_v8_external_pointer_api"
    assert_contains "$REPO_DIR/scripts/lib/native-modules.sh" "@electron/rebuild@4.0.4"
    assert_contains "$REPO_DIR/scripts/lib/native-modules.sh" "node-abi@^4.31.0"
    assert_contains "$REPO_DIR/scripts/lib/native-modules.sh" 'node_modules/@electron/rebuild/lib/cli.js'
    assert_not_contains "$REPO_DIR/scripts/lib/native-modules.sh" "npx --yes @electron/rebuild"
    assert_contains "$REPO_DIR/scripts/lib/native-modules.sh" "prune_native_module_build_artifacts"
    assert_contains "$REPO_DIR/scripts/lib/native-modules.sh" 'find "$build_dir" -type f ! -name'
    assert_contains "$REPO_DIR/scripts/lib/native-modules.sh" 'find "$module_dir" -type f -name'
    assert_contains "$REPO_DIR/scripts/lib/native-modules.sh" "CODEX_ELECTRON_CACHE_DIR"
    assert_contains "$REPO_DIR/scripts/lib/native-modules.sh" "--continue-at -"
    assert_file_exists "$REPO_DIR/launcher/webview-server.py"
    assert_contains "$REPO_DIR/launcher/webview-server.py" "Cache-Control"
    assert_contains "$REPO_DIR/launcher/webview-server.py" "If-Modified-Since"
    assert_contains "$REPO_DIR/install.sh" "webview-server.py"
    assert_contains "$REPO_DIR/launcher/start.sh.template" 'python3 "$SCRIPT_DIR/.codex-linux/webview-server.py" "$CODEX_LINUX_WEBVIEW_PORT" --bind 127.0.0.1'
    assert_contains "$REPO_DIR/launcher/start.sh.template" "WEBVIEW_PID_FILE"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "owned_webview_server_pid"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "discover_webview_server_pid"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "Adopted existing webview server"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "reconcile_runtime_state"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "detect_warm_start"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "send_warm_start_launch_action"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "CODEX_DESKTOP_LAUNCH_ACTION_SOCKET"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "APP_SETTINGS_FILE"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "linux_setting_enabled"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "register_url_scheme_handlers"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "xdg-mime default"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "x-scheme-handler/"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "codex-browser-sidebar"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "codex-linux-warm-start-enabled"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "--new-instance"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "CODEX_MULTI_LAUNCH"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "CODEX_MULTI_LAUNCH_PORT_RANGE"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "choose_multi_launch_port"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "configure_multi_launch_instance"
    assert_contains "$REPO_DIR/launcher/start.sh.template" 'launcher-$CODEX_LINUX_INSTANCE_ID.log'
    assert_contains "$REPO_DIR/launcher/start.sh.template" "ADOPTED_WEBVIEW_PID"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "Reusing webview server pid="
    assert_contains "$REPO_DIR/launcher/start.sh.template" "run_cold_start_hooks"
    assert_contains "$REPO_DIR/linux-features/remote-mobile-control/feature.json" '"stageHook": "./stage.sh"'
    assert_contains "$REPO_DIR/linux-features/remote-mobile-control/stage.sh" "cold-start.d"
    assert_contains "$REPO_DIR/linux-features/remote-mobile-control/stage.sh" "remote-mobile-control"
    assert_contains "$REPO_DIR/linux-features/remote-mobile-control/stage.sh" "cold-start-hook.sh"
    assert_contains "$REPO_DIR/linux-features/remote-mobile-control/cold-start-hook.sh" "remote-control start"
    assert_contains "$REPO_DIR/linux-features/remote-mobile-control/cold-start-hook.sh" "/run/current-system/sw/bin"
    assert_contains "$REPO_DIR/linux-features/remote-mobile-control/cold-start-hook.sh" "codex-remote-control.service"
    assert_contains "$REPO_DIR/linux-features/remote-mobile-control/cold-start-hook.sh" "continuing best-effort in the background"
    assert_contains "$REPO_DIR/flake.nix" "homeManagerModules"
    assert_contains "$REPO_DIR/flake.nix" "nixosModules"
    assert_contains "$REPO_DIR/nix/home-manager-module.nix" "codex-remote-control"
    assert_contains "$REPO_DIR/nix/home-manager-module.nix" "--remote-control"
    assert_contains "$REPO_DIR/nix/home-manager-module.nix" "CODEX_REMOTE_CONTROL_DAEMON_AUTOSTART_DISABLED"
    assert_contains "$REPO_DIR/nix/nixos-module.nix" "codex-remote-control"
    assert_contains "$REPO_DIR/nix/nixos-module.nix" "--remote-control"
    assert_contains "$REPO_DIR/nix/nixos-module.nix" "CODEX_REMOTE_CONTROL_DAEMON_AUTOSTART_DISABLED"
    python3 - "$REPO_DIR/launcher/start.sh.template" <<'PY'
import re
import sys

source = open(sys.argv[1], encoding="utf-8").read()
detect_body = source.split("detect_warm_start() {", 1)[1].split("send_warm_start_launch_action() {", 1)[0]
launch_body = source.split("launch_electron() {", 1)[1].split("load_packaged_runtime_helper", 1)[0]
runtime_body = source.split("trap cleanup_launcher EXIT", 1)[1].split("launch_electron", 1)[0]
webview_probe_body = source.split("webview_port_is_open() {", 1)[1].split("wait_for_webview_server() {", 1)[0]
wait_body = source.split("wait_for_webview_server() {", 1)[1].split("verify_webview_origin() {", 1)[0]
prelaunch_hooks_body = source.split("run_feature_prelaunch_hooks() {", 1)[1].split("bundled_plugin_version() {", 1)[0]
cold_start_hooks_body = source.split("run_cold_start_hooks() {", 1)[1].split("run_cli_preflight() {", 1)[0]
stop_body = source.split("stop_owned_webview_server() {", 1)[1].split("owned_webview_server_pid() {", 1)[0]
stale_body = source.split("pid_is_stale_webview_server() {", 1)[1].split("stop_owned_webview_server() {", 1)[0]
multi_body = source.split("configure_multi_launch_instance() {", 1)[1].split('WEBVIEW_ORIGIN="http://127.0.0.1:$CODEX_LINUX_WEBVIEW_PORT"', 1)[0]
adopt_body = source.split("adopt_existing_webview_server() {", 1)[1].split("ensure_webview_server() {", 1)[0]
ensure_body = source.split("ensure_webview_server() {", 1)[1].split("wait_for_webview_server", 1)[0]
reconcile_body = source.split("reconcile_runtime_state() {", 1)[1].split("set_electron_defaults() {", 1)[0]
if 'LAUNCHER_ARGS=()' not in source:
    raise SystemExit("launcher must keep a sanitized argv for launcher-only flags")
if 'CODEX_LINUX_FEATURES_DIR="$SCRIPT_DIR/.codex-linux/features"' not in source:
    raise SystemExit("launcher must expose the app-local Linux feature resource directory")
if 'export CODEX_HOME CODEX_LINUX_APP_ID CODEX_LINUX_APP_DISPLAY_NAME CODEX_LINUX_WEBVIEW_PORT CODEX_LINUX_SETTINGS_FILE CODEX_LINUX_FEATURES_DIR' not in source:
    raise SystemExit("launcher must export CODEX_HOME and Linux feature resource directory")
if 'configure_multi_launch_instance "$@"' not in source:
    raise SystemExit("launcher must configure multi-launch before deriving WEBVIEW_ORIGIN")
if 'unset CODEX_LINUX_MULTI_LAUNCH' not in source.split('parse_launcher_args() {', 1)[0]:
    raise SystemExit("launcher must clear inherited internal multi-launch markers before parsing args")
if '$((CODEX_LINUX_WEBVIEW_PORT + 4))' not in source:
    raise SystemExit("multi-launch default range must cap the default at five ports")
if '( trap - EXIT\n      exec 3<>/dev/tcp/127.0.0.1/"$CODEX_LINUX_WEBVIEW_PORT" || exit 1\n      exec 3>&- 3<&-\n      exit 0 )' not in webview_probe_body:
    raise SystemExit("webview port probe must not inherit the launcher EXIT cleanup trap")
if '( trap - EXIT\n      sleep 0.2' not in webview_probe_body:
    raise SystemExit("webview port probe watchdog must not inherit the launcher EXIT cleanup trap")
if "webview_origin_is_reachable_fast" not in wait_body or "webview_port_is_open" in wait_body:
    raise SystemExit("wait_for_webview_server must use the HTTP origin as the readiness signal")
if "if webview_origin_is_reachable;" not in wait_body:
    raise SystemExit("wait_for_webview_server must fall back to full origin verification before failing")
if 'CODEX_LINUX_INSTANCE_ID="port-$CODEX_LINUX_WEBVIEW_PORT"' not in multi_body:
    raise SystemExit("multi-launch must derive a stable instance id from the allocated port")
if 'CODEX_LINUX_MULTI_LAUNCH=1' not in multi_body:
    raise SystemExit("multi-launch must export an app-visible multi-launch marker")
if 'export CODEX_ELECTRON_USER_DATA_DIR CODEX_LINUX_INSTANCE_ID CODEX_LINUX_MULTI_LAUNCH CODEX_LINUX_WEBVIEW_PORT' not in multi_body:
    raise SystemExit("multi-launch must export instance identity for Electron")
if 'APP_STATE_DIR="$base_state_dir/instances/$CODEX_LINUX_INSTANCE_ID"' not in multi_body:
    raise SystemExit("multi-launch must isolate app pid/webview state per allocated port")
if 'LAUNCH_ACTION_RUNTIME_DIR="$XDG_RUNTIME_DIR/$CODEX_LINUX_APP_ID/instances/$CODEX_LINUX_INSTANCE_ID"' not in multi_body:
    raise SystemExit("multi-launch must isolate warm-start sockets per allocated port")
if 'CODEX_ELECTRON_USER_DATA_DIR="$APP_STATE_DIR/electron-user-data"' not in multi_body:
    raise SystemExit("multi-launch must force a per-instance Electron user-data dir")
if 'send_warm_start_launch_action "${LAUNCHER_ARGS[@]}"' not in source:
    raise SystemExit("warm-start handoff must not receive launcher-only multi-launch flags")
if 'launch_electron "${LAUNCHER_ARGS[@]}"' not in source:
    raise SystemExit("Electron launch must receive sanitized launcher args")
if 'Adopted concurrently-started verified webview server' not in source:
    raise SystemExit("launcher must tolerate a concurrent verified webview server winning the bind race")
if 'RUNNING_APP_PID="$(find_running_app_pid)"' not in detect_body:
    raise SystemExit("detect_warm_start must record a pid-file running app even when warm start is disabled")
if '[ -S "$LAUNCH_ACTION_SOCKET" ] && RUNNING_APP_PID="$(discover_running_app_pid)"' not in detect_body:
    raise SystemExit("detect_warm_start must only use the expensive running-app scan when the launch socket exists")
if not re.search(r'if ! linux_setting_enabled "codex-linux-warm-start-enabled" 1; then.*?return 0', detect_body, re.S):
    raise SystemExit("detect_warm_start must not fail when warm start is disabled")
if "preserving liveness marker for second-instance handoff" not in detect_body:
    raise SystemExit("detect_warm_start must preserve the live app liveness marker")
if launch_body.count("unset ELECTRON_RUN_AS_NODE") != 2:
    raise SystemExit("launch_electron must clear ELECTRON_RUN_AS_NODE before both Electron launch paths")
if 'pid_matches_executable "$RUNNING_APP_PID" "$SCRIPT_DIR/electron"' not in launch_body:
    raise SystemExit("launch_electron must not overwrite APP_PID_FILE for second-instance handoff")
if 'echo "$ELECTRON_PID" > "$APP_PID_FILE"' not in launch_body:
    raise SystemExit("launch_electron must still write APP_PID_FILE for normal cold launches")
electron_launch = '"$SCRIPT_DIR/electron" "${ELECTRON_LAUNCH_ARGS[@]}" "${ELECTRON_ARGS[@]}"'
warm_log = 'echo "Electron warm-start handoff:'
normal_log = 'echo "Electron launch mode:'
warm_log_pos = launch_body.index(warm_log)
warm_unset_pos = launch_body.index("unset ELECTRON_RUN_AS_NODE", warm_log_pos)
warm_launch_pos = launch_body.index(electron_launch, warm_unset_pos)
normal_log_pos = launch_body.index(normal_log)
normal_unset_pos = launch_body.index("unset ELECTRON_RUN_AS_NODE", normal_log_pos)
normal_launch_pos = launch_body.index(electron_launch + " &", normal_unset_pos)
if not (warm_log_pos < warm_unset_pos < warm_launch_pos < normal_log_pos < normal_unset_pos < normal_launch_pos):
    raise SystemExit("launch_electron must clear ELECTRON_RUN_AS_NODE immediately before each Electron launch")
if "using_second_instance_handoff" not in source or "needs_cold_start" not in source:
    raise SystemExit("launcher must have an explicit second-instance handoff mode")
if "second_instance_handoff_ready" not in runtime_body:
    raise SystemExit("second-instance handoff must skip cold-start setup")
if "clear_bundled_marketplace_tmp_cache\nmonitor_bundled_marketplace_tmp_permissions\nreconcile_runtime_state" in runtime_body:
    raise SystemExit("warm-start path must not clear bundled marketplace temp cache")
if not re.search(r'if needs_cold_start; then\s+clear_bundled_marketplace_tmp_cache\s+# The runtime marketplace is populated asynchronously.*?monitor_bundled_marketplace_tmp_permissions\s+sync_browser_use_bundled_plugin_cache\s+sync_chrome_bundled_plugin_cache\s+sync_computer_use_bundled_plugin_cache\s+sync_read_aloud_bundled_plugin_cache\s+run_cold_start_hooks\s+fi', runtime_body, re.S):
    raise SystemExit("bundled marketplace cleanup, plugin sync, and cold-start hooks must run only on cold start")
if 'if needs_cold_start && [ -z "${CODEX_CLI_PATH:-}" ]; then' not in runtime_body:
    raise SystemExit("second-instance handoff must skip CLI lookup")
if 'if needs_cold_start && [ -z "$CODEX_CLI_PATH" ]; then' not in runtime_body:
    raise SystemExit("second-instance handoff must skip missing-CLI failure")
if '"$HOME/.bun/bin/codex"' not in source:
    raise SystemExit("CLI lookup must include bun global install path")
if "if needs_cold_start;" not in runtime_body:
    raise SystemExit("second-instance handoff must skip CLI preflight")
if 'run_cold_start_hooks' not in runtime_body:
    raise SystemExit("cold start must run feature-staged hooks before Electron launches")
for name, body in (("prelaunch", prelaunch_hooks_body), ("cold-start", cold_start_hooks_body)):
    if 'CODEX_HOME="$CODEX_HOME"' not in body:
        raise SystemExit(f"launcher {name} hooks must receive resolved CODEX_HOME")
    if 'CODEX_LINUX_FEATURES_DIR="$CODEX_LINUX_FEATURES_DIR"' not in body:
        raise SystemExit(f"launcher {name} hooks must receive the app-local Linux feature resource directory")
if 'COLD_START_HOOK_DIR' not in cold_start_hooks_body or '"$hook" "$SCRIPT_DIR" "$APP_STATE_DIR" "$LOG_DIR"' not in cold_start_hooks_body:
    raise SystemExit("launcher cold-start hook runner must be generic and pass standard paths")
if '>>"$LOG_FILE" 2>&1 &' not in cold_start_hooks_body:
    raise SystemExit("launcher cold-start hooks must be non-blocking")
if 'remote_mobile_control_main' in source:
    raise SystemExit("remote mobile daemon startup must live in the remote-mobile-control feature hook, not the main launcher")
if "running_app_is_active" not in stop_body or "Preserving webview server" not in stop_body:
    raise SystemExit("stop_owned_webview_server must not stop the live app webview server")
if "stale_webview_server_pid" not in source or "stop_stale_webview_server" not in source:
    raise SystemExit("launcher must detect stale deleted webview servers left behind by previous installs")
if 'current_webview_dir="$(canonical_path "$WEBVIEW_DIR")"' not in stale_body:
    raise SystemExit("stale webview detection must compare against the current bundle path")
if '[ "$cwd" != "$current_webview_dir" ]' not in stale_body:
    raise SystemExit("stale webview detection must catch servers moved into backup bundle directories")
if 'ADOPTED_WEBVIEW_PID="$pid"' not in adopt_body:
    raise SystemExit("adopt_existing_webview_server must not mark a running app server as started by this launcher")
if 'STARTED_WEBVIEW_PID="$pid"' not in adopt_body:
    raise SystemExit("adopt_existing_webview_server must still own orphaned servers when no live app is running")
if "running_app_is_active" not in adopt_body:
    raise SystemExit("adopt_existing_webview_server must detect live-app reuse before cleanup")
if "if adopt_existing_webview_server; then" not in ensure_body:
    raise SystemExit("ensure_webview_server must split adoption from origin verification")
if "stop_stale_webview_server" not in ensure_body:
    raise SystemExit("ensure_webview_server must clear stale deleted webview servers before treating the port as foreign")
if ensure_body.find("stop_stale_webview_server") > ensure_body.find("is already serving Codex content"):
    raise SystemExit("ensure_webview_server must try stale-server cleanup before foreign reachable-port failure")
if "Keeping the live app untouched" not in ensure_body:
    raise SystemExit("ensure_webview_server must not stop a live app server when validation fails")
if 'if live_app_pid="$(find_running_app_pid)" || { [ -S "$LAUNCH_ACTION_SOCKET" ] && live_app_pid="$(discover_running_app_pid)"; }; then' not in reconcile_body:
    raise SystemExit("reconcile_runtime_state must preserve runtime markers when a live app still exists")
if 'rm -f "$LAUNCH_ACTION_SOCKET"' not in reconcile_body:
    raise SystemExit("reconcile_runtime_state must clear a stale launch-action socket when no live app exists")
if 'clear_stale_pid_file' not in reconcile_body:
    raise SystemExit("reconcile_runtime_state must still clear stale app.pid markers")
if 'if [ -z "$webview_pid" ] || { ! pid_is_webview_server "$webview_pid" && ! pid_is_stale_webview_server "$webview_pid"; }; then' not in reconcile_body:
    raise SystemExit("reconcile_runtime_state must clear stale launcher webview ownership markers without touching valid orphaned servers")
PY
    local launcher_probe
    local output
    launcher_probe="$TMP_DIR/launcher-rendering-probe.sh"
    python3 - "$REPO_DIR/launcher/start.sh.template" "$launcher_probe" <<'PY'
import sys

source_path, output_path = sys.argv[1:3]
source = open(source_path, encoding="utf-8").read()
start = source.index("is_wsl_environment() {")
end = source.index("configure_side_by_side_app_env() {")
probe = "#!/usr/bin/env bash\n" + source[start:end] + r'''
set -Eeuo pipefail

CODEX_LINUX_APP_ID="${CODEX_LINUX_APP_ID:-codex-desktop}"
APP_STATE_DIR="${APP_STATE_DIR:-/tmp/codex-launcher-probe-state}"
APP_CONFIG_DIR="${APP_CONFIG_DIR:-/tmp/codex-launcher-probe-config.$$}"
USER_ELECTRON_FLAGS_FILE="${USER_ELECTRON_FLAGS_FILE:-$APP_CONFIG_DIR/electron-flags.conf}"
FEATURE_ELECTRON_ARGS_DIR="${FEATURE_ELECTRON_ARGS_DIR:-}"

print_state() {
    printf 'mode=%s wslg=%s ozone_platform=%s ozone_hint=%s gpu=%s gpu_arg=%s comp=%s gl_added=%s renderer_accessibility=%s launch=' \
        "$ELECTRON_RENDERING_MODE" \
        "$ELECTRON_WSLG_DETECTED" \
        "${ELECTRON_OZONE_PLATFORM:-}" \
        "${ELECTRON_OZONE_HINT:-}" \
        "$ELECTRON_GPU_ENABLED" \
        "$ELECTRON_GPU_DISABLE_SWITCH_IN_ARGS" \
        "$ELECTRON_GPU_COMPOSITING_DISABLED" \
        "$ELECTRON_GL_SWITCH_ADDED" \
        "$ELECTRON_RENDERER_ACCESSIBILITY_FORCED"
    for arg in "${ELECTRON_LAUNCH_ARGS[@]}"; do
        printf '<%s>' "$arg"
    done
    printf ' electron='
    for arg in "${ELECTRON_ARGS[@]}"; do
        printf '<%s>' "$arg"
    done
    printf '\n'
}

case "${1:-}" in
    probe)
        shift
        load_feature_electron_args
        load_user_electron_flags
        set_electron_defaults "${FEATURE_ELECTRON_ARGS[@]}" "${USER_ELECTRON_FLAGS[@]}" "$@"
        build_electron_launch_args
        print_state
        ;;
    ensure-template)
        ensure_user_electron_flags_file
        ;;
    *)
        echo "Usage: $0 probe [launcher args...]" >&2
        exit 2
        ;;
esac
'''
open(output_path, "w", encoding="utf-8").write(probe)
PY
    chmod +x "$launcher_probe"

    output="$(env -i PATH="$PATH" HOME="$HOME" CODEX_LINUX_RENDERING_MODE=default "$launcher_probe" probe --x11 -- --use-gl=angle)"
    [[ "$output" == *"electron=<--use-gl=angle>"* ]] || fail "launcher must pass Electron args after -- without the separator: $output"
    [[ "$output" != *"electron=<--><--use-gl=angle>"* ]] || fail "launcher must not pass the -- separator to Electron: $output"
    [[ "$output" == *"<--ozone-platform=x11>"* ]] || fail "launcher --x11 must still set the Electron ozone platform: $output"
    [[ "$output" == *"comp=0"* && "$output" != *"<--disable-gpu-compositing>"* ]] || fail "default Linux profile must keep GPU compositing enabled: $output"
    [[ "$output" == *"renderer_accessibility=1"* && "$output" == *"<--force-renderer-accessibility>"* ]] || fail "default Linux profile must still force renderer accessibility: $output"

    output="$(env -i PATH="$PATH" HOME="$HOME" CODEX_LINUX_RENDERING_MODE=default "$launcher_probe" probe -- --ozone-platform=x11)"
    [[ "$output" == *"electron=<--ozone-platform=x11>"* ]] || fail "pass-through ozone platform must reach Electron: $output"
    [[ "$output" != *"<--ozone-platform-hint=auto>"* ]] || fail "launcher must not add ozone hint when pass-through supplies an ozone platform: $output"

    local user_flags_dir="$TMP_DIR/user-electron-flags"
    local user_flags_file="$user_flags_dir/electron-flags.conf"
    mkdir -p "$user_flags_dir"
    printf '%s\n' \
        '# --disable-gpu' \
        '' \
        '--ozone-platform=x11' \
        '--enable-wayland-ime' \
        '--use-gl=angle' > "$user_flags_file"

    output="$(env -i PATH="$PATH" HOME="$HOME" APP_CONFIG_DIR="$user_flags_dir" USER_ELECTRON_FLAGS_FILE="$user_flags_file" CODEX_LINUX_RENDERING_MODE=default "$launcher_probe" probe)"
    [[ "$output" == *"<--ozone-platform=x11>"* ]] || fail "persistent flags file must set the Electron ozone platform: $output"
    [[ "$output" != *"<--ozone-platform-hint=auto>"* ]] || fail "persistent ozone platform must suppress the default ozone hint: $output"
    [[ "$output" == *"electron=<--enable-wayland-ime><--use-gl=angle>"* ]] || fail "persistent flags file must pass non-launcher Electron args in order: $output"
    [[ "$output" != *"<--disable-gpu>"* ]] || fail "commented persistent flags must be ignored: $output"

    output="$(env -i PATH="$PATH" HOME="$HOME" APP_CONFIG_DIR="$user_flags_dir" USER_ELECTRON_FLAGS_FILE="$user_flags_file" CODEX_LINUX_RENDERING_MODE=default "$launcher_probe" probe -- --use-gl=desktop)"
    [[ "$output" == *"electron=<--enable-wayland-ime><--use-gl=angle><--use-gl=desktop>"* ]] || fail "explicit CLI Electron args must follow persistent file args: $output"

    local feature_args_dir="$TMP_DIR/feature-electron-args"
    mkdir -p "$feature_args_dir"
    printf '%s\n' '--ozone-platform=wayland' '--use-angle=gl' > "$feature_args_dir/feature"
    output="$(env -i PATH="$PATH" HOME="$HOME" APP_CONFIG_DIR="$user_flags_dir" USER_ELECTRON_FLAGS_FILE="$user_flags_file" FEATURE_ELECTRON_ARGS_DIR="$feature_args_dir" CODEX_LINUX_RENDERING_MODE=default "$launcher_probe" probe)"
    [[ "$output" == *"<--ozone-platform=x11>"* ]] || fail "persistent flags file must override feature Electron platform args: $output"
    [[ "$output" != *"<--ozone-platform=wayland>"* ]] || fail "feature Electron platform args must not survive after user override: $output"
    [[ "$output" == *"electron=<--use-angle=gl><--enable-wayland-ime><--use-gl=angle>"* ]] || fail "feature, user, and CLI-independent Electron args must keep precedence order: $output"

    local template_dir="$TMP_DIR/user-electron-template"
    local template_file="$template_dir/electron-flags.conf"
    env -i PATH="$PATH" HOME="$HOME" APP_CONFIG_DIR="$template_dir" USER_ELECTRON_FLAGS_FILE="$template_file" "$launcher_probe" ensure-template >/dev/null
    assert_file_exists "$template_file"
    assert_contains "$template_file" "--x11"
    assert_contains "$template_file" "--enable-wayland-ime"
    printf '%s\n' '--wayland' > "$template_file"
    env -i PATH="$PATH" HOME="$HOME" APP_CONFIG_DIR="$template_dir" USER_ELECTRON_FLAGS_FILE="$template_file" "$launcher_probe" ensure-template >/dev/null
    [ "$(cat "$template_file")" = "--wayland" ] || fail "persistent flags template must not overwrite an existing file"

    output="$(env -i PATH="$PATH" HOME="$HOME" CODEX_LINUX_RENDERING_MODE=wayland-gpu "$launcher_probe" probe)"
    [[ "$output" == *"mode=wayland-gpu"* && "$output" == *"ozone_platform=wayland"* && "$output" == *"gpu=1"* ]] || fail "wayland-gpu profile must force native Wayland with GPU enabled: $output"
    [[ "$output" == *"comp=0"* && "$output" != *"<--disable-gpu-compositing>"* ]] || fail "wayland-gpu profile must keep GPU compositing enabled: $output"
    [[ "$output" == *"<--ozone-platform=wayland>"* && "$output" == *"<--enable-features=WaylandWindowDecorations>"* ]] || fail "wayland-gpu profile must add Wayland launch args: $output"
    [[ "$output" == *"renderer_accessibility=0"* && "$output" != *"<--force-renderer-accessibility>"* ]] || fail "wayland-gpu profile must skip renderer accessibility by default: $output"

    output="$(env -i PATH="$PATH" HOME="$HOME" CODEX_LINUX_RENDERING_MODE=wayland-gpu CODEX_FORCE_RENDERER_ACCESSIBILITY=1 "$launcher_probe" probe)"
    [[ "$output" == *"renderer_accessibility=1"* && "$output" == *"<--force-renderer-accessibility>"* ]] || fail "CODEX_FORCE_RENDERER_ACCESSIBILITY=1 must force renderer accessibility under wayland-gpu: $output"

    output="$(env -i PATH="$PATH" HOME="$HOME" CODEX_LINUX_RENDERING_MODE=wayland-gpu "$launcher_probe" probe --x11)"
    [[ "$output" == *"mode=wayland-gpu"* && "$output" == *"ozone_platform=x11"* ]] || fail "explicit --x11 must override the wayland-gpu platform: $output"
    [[ "$output" == *"renderer_accessibility=1"* && "$output" == *"<--force-renderer-accessibility>"* ]] || fail "wayland-gpu with explicit --x11 must keep X11 renderer accessibility default: $output"

    output="$(env -i PATH="$PATH" HOME="$HOME" CODEX_LINUX_RENDERING_MODE=wayland-gpu "$launcher_probe" probe --safe-mode)"
    [[ "$output" == *"mode=wayland-gpu"* && "$output" == *"ozone_platform=x11"* && "$output" == *"gpu=0"* ]] || fail "safe-mode must override wayland-gpu to X11 software rendering: $output"
    [[ "$output" == *"renderer_accessibility=1"* && "$output" == *"<--force-renderer-accessibility>"* ]] || fail "wayland-gpu with safe-mode must keep X11 renderer accessibility default: $output"

    output="$(env -i PATH="$PATH" HOME="$HOME" CODEX_LINUX_RENDERING_MODE=wayland-gpu "$launcher_probe" probe -- --ozone-platform=x11)"
    [[ "$output" == *"electron=<--ozone-platform=x11>"* && "$output" != *"<--ozone-platform-hint=auto>"* ]] || fail "pass-through X11 platform must override wayland-gpu hinting: $output"
    [[ "$output" == *"renderer_accessibility=1"* && "$output" == *"<--force-renderer-accessibility>"* ]] || fail "wayland-gpu with pass-through X11 platform must keep X11 renderer accessibility default: $output"

    output="$(env -i PATH="$PATH" HOME="$HOME" CODEX_LINUX_RENDERING_MODE=wslg "$launcher_probe" probe)"
    [[ "$output" == *"mode=wslg"* && "$output" == *"comp=0"* && "$output" == *"gl_added=1"* ]] || fail "forced WSLg profile must keep GPU compositing enabled and add ANGLE: $output"
    [[ "$output" == *"<--ozone-platform=x11>"* && "$output" == *"electron=<--use-gl=angle>"* ]] || fail "forced WSLg profile must use X11 and ANGLE by default: $output"
    [[ "$output" != *"<--disable-gpu-compositing>"* ]] || fail "forced WSLg profile must not add disable-gpu-compositing by default: $output"
    [[ "$output" == *"renderer_accessibility=0"* && "$output" != *"<--force-renderer-accessibility>"* ]] || fail "forced WSLg profile must skip renderer accessibility by default: $output"

    output="$(env -i PATH="$PATH" HOME="$HOME" CODEX_LINUX_RENDERING_MODE=wslg CODEX_FORCE_RENDERER_ACCESSIBILITY=1 "$launcher_probe" probe)"
    [[ "$output" == *"renderer_accessibility=1"* && "$output" == *"<--force-renderer-accessibility>"* ]] || fail "CODEX_FORCE_RENDERER_ACCESSIBILITY=1 must force renderer accessibility under WSLg: $output"

    output="$(env -i PATH="$PATH" HOME="$HOME" CODEX_LINUX_RENDERING_MODE=default CODEX_FORCE_RENDERER_ACCESSIBILITY=0 "$launcher_probe" probe)"
    [[ "$output" == *"renderer_accessibility=0"* && "$output" != *"<--force-renderer-accessibility>"* ]] || fail "CODEX_FORCE_RENDERER_ACCESSIBILITY=0 must disable renderer accessibility under default Linux: $output"

    output="$(env -i PATH="$PATH" HOME="$HOME" CODEX_LINUX_RENDERING_MODE=wslg "$launcher_probe" probe --wayland --use-gl=desktop)"
    [[ "$output" == *"<--ozone-platform=wayland>"* && "$output" == *"electron=<--use-gl=desktop>"* ]] || fail "explicit rendering args must override WSLg defaults: $output"
    [[ "$output" == *"gl_added=0"* && "$output" != *"<--use-gl=angle>"* ]] || fail "WSLg profile must not add ANGLE when a GL switch was supplied: $output"

    output="$(env -i PATH="$PATH" HOME="$HOME" CODEX_LINUX_RENDERING_MODE=wslg "$launcher_probe" probe -- --disable-gpu)"
    [[ "$output" == *"gpu=1"* && "$output" == *"gpu_arg=1"* && "$output" == *"gl_added=0"* ]] || fail "pass-through --disable-gpu must suppress WSLg ANGLE without becoming a launcher GPU toggle: $output"
    [[ "$output" == *"electron=<--disable-gpu>"* && "$output" != *"<--disable-features=Vulkan>"* ]] || fail "pass-through --disable-gpu must not add launcher-only Vulkan flags: $output"

    output="$(env -i PATH="$PATH" HOME="$HOME" CODEX_LINUX_RENDERING_MODE=wslg CODEX_ELECTRON_DISABLE_GPU_COMPOSITING=1 "$launcher_probe" probe)"
    [[ "$output" == *"comp=1"* && "$output" == *"<--disable-gpu-compositing>"* ]] || fail "CODEX_ELECTRON_DISABLE_GPU_COMPOSITING=1 must force the compositor flag: $output"

    output="$(env -i PATH="$PATH" HOME="$HOME" CODEX_LINUX_RENDERING_MODE=default CODEX_ELECTRON_DISABLE_GPU_COMPOSITING=1 "$launcher_probe" probe)"
    [[ "$output" == *"comp=1"* && "$output" == *"<--disable-gpu-compositing>"* ]] || fail "CODEX_ELECTRON_DISABLE_GPU_COMPOSITING=1 must force the compositor flag under default Linux: $output"

    output="$(env -i PATH="$PATH" HOME="$HOME" CODEX_LINUX_RENDERING_MODE=default CODEX_ELECTRON_DISABLE_GPU_COMPOSITING=0 "$launcher_probe" probe)"
    [[ "$output" == *"comp=0"* && "$output" != *"<--disable-gpu-compositing>"* ]] || fail "CODEX_ELECTRON_DISABLE_GPU_COMPOSITING=0 must suppress the compositor flag: $output"

    output="$(env -i PATH="$PATH" HOME="$HOME" WSL_INTEROP=/tmp/codex-wsl WAYLAND_DISPLAY=wayland-0 "$launcher_probe" probe)"
    [[ "$output" == *"mode=wslg"* && "$output" == *"wslg=1"* ]] || fail "auto rendering mode must detect WSLg from WSL and GUI markers: $output"

    assert_contains "$REPO_DIR/launcher/start.sh.template" "warm_start_ipc_sent"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "launcher_phase"
    assert_contains "$REPO_DIR/launcher/start.sh.template" 'date +%s%N'
    assert_contains "$REPO_DIR/launcher/start.sh.template" '10#$nanos / 1000000'
    assert_contains "$REPO_DIR/launcher/start.sh.template" "CODEX_SYNC_CLI_PREFLIGHT"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "wait_for_webview_server"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "verify_webview_origin"
    # Probe-shape invariants: shell-native bash /dev/tcp + curl, with the
    # bounded-execution defenses preserved (0.2 s watchdog + 2 s curl cap).
    assert_contains "$REPO_DIR/launcher/start.sh.template" '/dev/tcp/127.0.0.1/"$CODEX_LINUX_WEBVIEW_PORT"'
    assert_contains "$REPO_DIR/launcher/start.sh.template" "kill -9 \"\$probe_pid\""
    assert_contains "$REPO_DIR/launcher/start.sh.template" 'curl --disable --noproxy 127.0.0.1,localhost --silent --show-error --fail --max-time 2'
    assert_contains "$REPO_DIR/launcher/start.sh.template" "webview_origin_is_reachable_fast"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "for attempt in \$(seq 1 20)"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "sleep 0.05"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "Webview origin verified."
    assert_contains "$REPO_DIR/launcher/start.sh.template" "hydrate_graphical_session_env"
    assert_not_contains "$REPO_DIR/install.sh" "pkill -f \"http.server 5175\""
    assert_contains "$REPO_DIR/launcher/start.sh.template" "CODEX_WEBVIEW_PORT"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "CODEX_LINUX_PIN_RENDERER_URL"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "CODEX_LINUX_ALLOW_RENDERER_URL_OVERRIDE"
    assert_contains "$REPO_DIR/launcher/start.sh.template" 'export ELECTRON_RENDERER_URL="$WEBVIEW_ORIGIN/"'
    assert_contains "$REPO_DIR/launcher/start.sh.template" 'ELECTRON_RENDERER_URL="${ELECTRON_RENDERER_URL:-$WEBVIEW_ORIGIN/}"'
    assert_contains "$REPO_DIR/launcher/start.sh.template" '--app-id="$CODEX_LINUX_APP_ID"'
    assert_contains "$REPO_DIR/scripts/lib/process-detection.sh" "CODEX_APP_ID"
    assert_contains "$REPO_DIR/launcher/start.sh.template" 'ELECTRON_OZONE_HINT="auto"'
    assert_contains "$REPO_DIR/launcher/start.sh.template" "CODEX_LINUX_RENDERING_MODE=auto|default|wslg|wayland-gpu"
    assert_contains "$REPO_DIR/launcher/start.sh.template" '--ozone-platform-hint="$ELECTRON_OZONE_HINT"'
    assert_contains "$REPO_DIR/launcher/start.sh.template" "--disable-gpu-sandbox"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "--force-renderer-accessibility"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "CODEX_FORCE_RENDERER_ACCESSIBILITY=auto|0|1"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "PACKAGED_RUNTIME_HELPER"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "--allow-install-missing"
    assert_contains "$REPO_DIR/scripts/lib/process-detection.sh" "CODEX_INSTALL_ALLOW_RUNNING"
    assert_contains "$REPO_DIR/scripts/lib/process-detection.sh" "assert_install_target_not_running"
    assert_contains "$REPO_DIR/scripts/lib/process-detection.sh" "find_running_install_target_pid"
    assert_contains "$REPO_DIR/scripts/lib/process-detection.sh" "Codex Desktop is currently running from"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "prompt_install_missing_cli"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "prompt-install-cli"
    assert_contains "$REPO_DIR/launcher/start.sh.template" '.npm-global/bin/codex'
    assert_contains "$REPO_DIR/launcher/start.sh.template" '.config}/nvm/versions/node'
    assert_contains "$REPO_DIR/launcher/start.sh.template" "CODEX_UPDATE_MANAGER_PATH"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "resolve_update_manager_path"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "run_update_manager"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "sync_browser_use_bundled_plugin_cache"
    assert_contains "$REPO_DIR/launcher/start.sh.template" 'source_plugin="$SCRIPT_DIR/resources/plugins/openai-bundled/plugins/browser"'
    assert_contains "$REPO_DIR/launcher/start.sh.template" 'marketplace_plugin_link="$marketplace_root/plugins/$plugin_dir_name"'
    assert_contains "$REPO_DIR/launcher/start.sh.template" "sync_chrome_bundled_plugin_cache"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "sync_read_aloud_bundled_plugin_cache"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "make_tree_owner_writable"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "clear_bundled_marketplace_tmp_cache"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "monitor_bundled_marketplace_tmp_permissions"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "extension-id.json"
    assert_contains "$REPO_DIR/launcher/start.sh.template" ".config/BraveSoftware/Brave-Browser/NativeMessagingHosts"
    assert_contains "$REPO_DIR/launcher/start.sh.template" ".config/chromium/NativeMessagingHosts"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "scripts/check-extension-installed.js"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "scripts/chrome-is-running.js"
    assert_contains "$REPO_DIR/launcher/start.sh.template" ".tmp/bundled-marketplaces/openai-bundled"
    assert_contains "$REPO_DIR/launcher/start.sh.template" ".agents/plugins/marketplace.json"
    assert_contains "$REPO_DIR/scripts/lib/bundled-plugins.sh" "stage_chrome_plugin_from_upstream"
    assert_contains "$REPO_DIR/scripts/lib/patch-chrome-plugin.js" "Linux native host manifest location"
    assert_contains "$REPO_DIR/computer-use-linux/src/bin/codex-chrome-extension-host.rs" "CODEX_BROWSER_USE_SOCKET_DIR"
    assert_contains "$REPO_DIR/flake.nix" "Browser Use bundled marketplace metadata"
    assert_contains "$REPO_DIR/flake.nix" ".tmp/bundled-marketplaces/openai-bundled"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "Install it now? \\[Y/n\\]"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "is_interactive_terminal"
    assert_contains "$REPO_DIR/updater/src/app.rs" "kdialog"
    assert_contains "$REPO_DIR/updater/src/app.rs" "zenity"
    assert_contains "$REPO_DIR/packaging/linux/codex-packaged-runtime.sh" "CHROME_DESKTOP"
    assert_contains "$REPO_DIR/packaging/linux/codex-packaged-runtime.sh" "codex-update-manager-launch-check"
    assert_contains "$REPO_DIR/packaging/linux/codex-packaged-runtime.sh" "codex-update-manager check-now --if-stale"
    assert_not_contains "$REPO_DIR/packaging/linux/codex-packaged-runtime.sh" "restart codex-update-manager.service"
    assert_contains "$REPO_DIR/scripts/install-deps.sh" 'NODEJS_MAJOR="${NODEJS_MAJOR:-22}"'
    assert_contains "$REPO_DIR/scripts/install-deps.sh" "apt_nodejs_candidate_major"
    assert_contains "$REPO_DIR/scripts/install-deps.sh" "Installing distro Node.js/npm candidate"
    assert_contains "$REPO_DIR/scripts/install-deps.sh" "/etc/apt/keyrings/nodesource.gpg"
    assert_contains "$REPO_DIR/scripts/install-deps.sh" "signed-by="
    assert_contains "$REPO_DIR/scripts/install-deps.sh" "https://deb.nodesource.com/node_"
    assert_not_contains "$REPO_DIR/packaging/linux/control" "Depends:.*nodejs"
    assert_not_contains "$REPO_DIR/packaging/linux/control" "Depends:.*npm"
    assert_not_contains "$REPO_DIR/packaging/linux/codex-desktop.spec" "Requires:.*nodejs"
    assert_not_contains "$REPO_DIR/packaging/linux/codex-desktop.spec" "Requires:.*npm"
    assert_not_contains "$REPO_DIR/packaging/linux/PKGBUILD.template" "'nodejs>=20'"
    assert_contains "$REPO_DIR/packaging/linux/PKGBUILD.template" "optional override for the bundled managed Node.js runtime"
    assert_contains "$REPO_DIR/scripts/lib/node-runtime.sh" "MANAGED_NODE_VERSION"
    assert_contains "$REPO_DIR/scripts/lib/package-common.sh" "node-runtime"
    assert_contains "$REPO_DIR/tests/fixtures/create-packaged-app-fixture.sh" "resources/node-runtime/bin"
    assert_contains "$REPO_DIR/.github/workflows/ci.yml" "tests/fixtures/create-packaged-app-fixture.sh codex-app"
    assert_contains "$REPO_DIR/.github/workflows/ci.yml" "for file in scripts/patches/"
    assert_contains "$REPO_DIR/scripts/ci/container-entrypoint.sh" "for file in scripts/patches/"
    assert_contains "$REPO_DIR/flake.nix" "rewriteCratesIoDownloadUrl"
    assert_contains "$REPO_DIR/flake.nix" "https://static.crates.io/crates/"
    assert_contains "$REPO_DIR/flake.nix" "api/v1/crates/"
    assert_contains "$REPO_DIR/launcher/start.sh.template" "MANAGED_NODE_BIN_DIR"
    assert_contains "$REPO_DIR/updater/src/builder.rs" "managed_node_bin_dirs"
    assert_contains "$REPO_DIR/scripts/build-rpm.sh" "stage_common_package_files"
    assert_contains "$REPO_DIR/scripts/build-rpm.sh" "PACKAGED_RUNTIME_SOURCE"
    assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "BAMF_DESKTOP_FILE_HINT"
    assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "/usr/bin/codex-desktop %u"
    assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "MimeType=x-scheme-handler/codex;x-scheme-handler/codex-browser-sidebar;"
    assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "StartupWMClass=codex-desktop"
    assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "X-GNOME-WMClass=codex-desktop"
    assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "Actions=new-window;CheckForUpdates;InstallReadyUpdate;"
    assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "[Desktop Action new-window]"
    assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "CODEX_MULTI_LAUNCH=1 /usr/bin/codex-desktop --new-instance"
    assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "codex-update-manager check-now"
    assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "codex-update-manager install-ready"
    assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop" "BAMF_DESKTOP_FILE_HINT=@HOME@/.local/share/applications/codex-desktop.desktop"
    assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop" "@HOME@/.local/bin/codex-desktop %U"
    assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop" "MimeType=x-scheme-handler/codex;x-scheme-handler/codex-browser-sidebar;"
    assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop" "Actions=new-window;"
    assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop" "CODEX_MULTI_LAUNCH=1 @HOME@/.local/bin/codex-desktop --new-instance"
    assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/bin/codex-desktop" "CODEX_USER_LOCAL_OZONE_PLATFORM"
    assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/bin/codex-desktop" 'exec "${APP_DIR}/start.sh" --x11 "$@"'
    assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/bin/codex-desktop" 'exec "${APP_DIR}/start.sh" --wayland "$@"'
    assert_contains "$REPO_DIR/contrib/user-local-install/install-user-local.sh" "--force-x11"
    assert_contains "$REPO_DIR/contrib/user-local-install/install-user-local.sh" "user-local.env"
    assert_contains "$REPO_DIR/contrib/user-local-install/README.md" "--force-x11"

    node - "$REPO_DIR/launcher/start.sh.template" <<'NODE' || fail "Bundled backend plugin cache syncs must expose marketplace plugin links"
const fs = require("node:fs");
const launcher = fs.readFileSync(process.argv[2], "utf8");

function functionBody(name, nextName) {
  const pattern = new RegExp(`${name}\\(\\) \\{([\\s\\S]*?)\\n\\}\\n\\n${nextName}\\(\\) \\{`, "u");
  const match = launcher.match(pattern);
  if (match == null) {
    throw new Error(`missing ${name}`);
  }
  return match[1];
}

function assertCacheLinks({ body, plugin }) {
  for (const required of [
    `marketplace_plugin_link="$marketplace_root/plugins/${plugin}"`,
    'ln -sfn "$version" "$cache_root/latest"',
    'ln -sfn "$cache_root/latest" "$marketplace_plugin_link"',
  ]) {
    if (!body.includes(required)) {
      throw new Error(`${plugin} sync missing ${required}`);
    }
  }
  if (!body.includes("needs_copy=0")) {
    throw new Error(`${plugin} sync must refresh links on cache hits`);
  }
}

assertCacheLinks({
  body: functionBody("sync_computer_use_bundled_plugin_cache", "sync_read_aloud_bundled_plugin_cache"),
  plugin: "computer-use",
});
assertCacheLinks({
  body: functionBody("sync_read_aloud_bundled_plugin_cache", "resolve_browser_use_runtime_env"),
  plugin: "read-aloud",
});
NODE
}

test_side_by_side_launcher_identity() {
    info "Checking side-by-side launcher identity"
    local workspace="$TMP_DIR/side-by-side-launcher"
    local app_dir="$workspace/codex-cua-lab-app"
    local bin_dir="$workspace/bin"
    local help_log="$workspace/help.log"
    local symlink_help_log="$workspace/symlink-help.log"

    mkdir -p "$app_dir" "$bin_dir"

    CODEX_INSTALLER_SOURCE_ONLY=1 \
    CODEX_APP_ID="codex-cua-lab" \
    CODEX_APP_DISPLAY_NAME="Codex CUA Lab" \
    CODEX_INSTALL_DIR="$app_dir" \
    bash -c 'source "$1"; validate_app_identity; create_start_script' _ "$REPO_DIR/install.sh"

    assert_file_exists "$app_dir/start.sh"
    assert_file_exists "$app_dir/.codex-linux/webview-server.py"
    assert_contains "$app_dir/start.sh" "CODEX_LINUX_APP_ID=codex-cua-lab"
    assert_contains "$app_dir/start.sh" "CODEX_LINUX_APP_DISPLAY_NAME=Codex\\\\ CUA\\\\ Lab"
    assert_contains "$app_dir/start.sh" 'CODEX_LINUX_WEBVIEW_PORT=${CODEX_WEBVIEW_PORT:-5176}'
    assert_contains "$app_dir/start.sh" 'CODEX_LINUX_SETTINGS_FILE="$APP_SETTINGS_FILE"'
    assert_contains "$app_dir/start.sh" 'export CODEX_HOME CODEX_LINUX_APP_ID CODEX_LINUX_APP_DISPLAY_NAME CODEX_LINUX_WEBVIEW_PORT CODEX_LINUX_SETTINGS_FILE CODEX_LINUX_FEATURES_DIR'
    assert_contains "$app_dir/start.sh" 'WEBVIEW_ORIGIN="http://127.0.0.1:$CODEX_LINUX_WEBVIEW_PORT"'
    assert_contains "$app_dir/start.sh" "CODEX_LINUX_PIN_RENDERER_URL"
    assert_contains "$app_dir/start.sh" "CODEX_LINUX_ALLOW_RENDERER_URL_OVERRIDE"
    assert_contains "$app_dir/start.sh" 'export ELECTRON_RENDERER_URL="$WEBVIEW_ORIGIN/"'
    assert_contains "$app_dir/start.sh" 'ELECTRON_RENDERER_URL="${ELECTRON_RENDERER_URL:-$WEBVIEW_ORIGIN/}"'
    assert_contains "$app_dir/start.sh" "resolve_script_dir"
    assert_contains "$app_dir/start.sh" "configure_side_by_side_app_env"
    assert_contains "$app_dir/start.sh" 'XDG_CONFIG_HOME="${CODEX_XDG_CONFIG_HOME:-$APP_STATE_DIR/xdg-config}"'
    assert_contains "$app_dir/start.sh" '--class="$CODEX_LINUX_APP_ID"'
    assert_contains "$app_dir/start.sh" '--app-id="$CODEX_LINUX_APP_ID"'
    assert_contains "$app_dir/start.sh" '--user-data-dir="${CODEX_ELECTRON_USER_DATA_DIR:-$APP_STATE_DIR/electron-user-data}"'
    assert_contains "$app_dir/start.sh" "--force-renderer-accessibility"
    assert_contains "$app_dir/start.sh" 'LOG_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/$CODEX_LINUX_APP_ID"'
    XDG_CACHE_HOME="$workspace/cache" XDG_STATE_HOME="$workspace/state" XDG_RUNTIME_DIR="$workspace/runtime" bash "$app_dir/start.sh" --help >"$help_log"
    assert_contains "$help_log" "Launches the Codex CUA Lab app."
    assert_contains "$help_log" "codex-cua-lab/launcher.log"

    ln -s "$app_dir/start.sh" "$bin_dir/codex-cua-lab"
    XDG_CACHE_HOME="$workspace/cache" XDG_STATE_HOME="$workspace/state" XDG_RUNTIME_DIR="$workspace/runtime" bash "$bin_dir/codex-cua-lab" --help >"$symlink_help_log"
    assert_contains "$symlink_help_log" "Launches the Codex CUA Lab app."
}

test_browser_use_node_repl_fallback_runtime() {
    info "Checking Browser Use node_repl fallback runtime"
    if [ "$(uname -m)" != "x86_64" ]; then
        info "Skipping x86_64-only Browser Use fallback runtime test"
        return 0
    fi

    local workspace="$TMP_DIR/browser-use-node-repl-fallback"
    local app_dir="$workspace/Codex.app"
    local install_dir="$workspace/install"
    local archive_root="$workspace/archive-root"
    local archive="$workspace/runtime.tar.xz"
    local output_log="$workspace/output.log"
    local archive_sha
    local true_bin

    mkdir -p "$workspace" "$install_dir/resources" "$archive_root/codex-primary-runtime/dependencies/bin"
    make_fake_browser_upstream_app "$app_dir"

    # Simulate the current upstream DMG shape: node_repl exists, but it is not a Linux ELF.
    printf '\xfe\xed\xfa\xcf' > "$app_dir/Contents/Resources/node_repl"
    chmod +x "$app_dir/Contents/Resources/node_repl"

    true_bin="$(type -P true)"
    cp "$true_bin" "$archive_root/codex-primary-runtime/dependencies/bin/node_repl"
    chmod 0755 "$archive_root/codex-primary-runtime/dependencies/bin/node_repl"
    tar -cJf "$archive" -C "$archive_root" codex-primary-runtime
    archive_sha="$(sha256sum "$archive" | awk '{print $1}')"

    (
        SCRIPT_DIR="$REPO_DIR"
        INSTALL_DIR="$install_dir"
        WORK_DIR="$workspace/work"
        ARCH="$(uname -m)"
        ICON_SOURCE="$workspace/missing-icon.png"
        CODEX_APP_ID="codex-desktop"
        XDG_CACHE_HOME="$workspace/xdg-cache"
        CODEX_NODE_REPL_PATH=
        CODEX_LINUX_NODE_REPL_SOURCE=
        CODEX_BROWSER_USE_RUNTIME_CACHE_DIR="$workspace/cache"
        CODEX_BROWSER_USE_NODE_REPL_RUNTIME_URL="file://$archive"
        CODEX_BROWSER_USE_NODE_REPL_RUNTIME_SHA256="$archive_sha"
        mkdir -p "$WORK_DIR"
        warn() { echo "[WARN] $*" >&2; }
        info() { echo "[INFO] $*" >&2; }
        # shellcheck disable=SC1091
        source "$REPO_DIR/scripts/lib/bundled-plugins.sh"
        stage_linux_computer_use_plugin() { return 1; }
        build_chrome_extension_host() {
            local fake_host="$workspace/codex-chrome-extension-host"
            printf '#!/bin/sh\n' > "$fake_host"
            chmod +x "$fake_host"
            printf '%s\n' "$fake_host"
        }
        install_bundled_plugin_resources "$app_dir"
    ) >"$output_log" 2>&1

    assert_file_exists "$install_dir/resources/node_repl"
    assert_file_exists "$install_dir/resources/plugins/openai-bundled/plugins/browser/scripts/browser-client.mjs"
    cmp -s "$true_bin" "$install_dir/resources/node_repl" || fail "Expected fallback node_repl to come from the runtime archive"
    assert_contains "$install_dir/resources/plugins/openai-bundled/plugins/browser/scripts/browser-client.mjs" 'globalThis.nodeRepl?.env?.\[e\]'
    assert_not_contains "$install_dir/resources/plugins/openai-bundled/plugins/browser/scripts/browser-client.mjs" 'globalThis.nodeRepl?.env\[e\]'
    assert_contains "$install_dir/resources/plugins/openai-bundled/plugins/browser/scripts/browser-client.mjs" "codexLinuxSiteStatusAllowlistFallback"
    assert_contains "$install_dir/resources/plugins/openai-bundled/plugins/browser/scripts/browser-client.mjs" "codexLinuxFileUrlPolicy"
    assert_contains "$output_log" "Browser Use node_repl runtime is not a Linux executable for x86_64; skipping"
    assert_not_contains "$output_log" "WARN.*Browser Use node_repl runtime is not a Linux executable"
    assert_contains "$output_log" "Downloading Browser Use node_repl fallback runtime"
}

test_browser_use_file_url_policy_patch_behavior() {
    info "Checking Browser Use file URL policy patch behavior"
    local workspace="$TMP_DIR/browser-file-url-policy"
    local client="$workspace/browser-client.mjs"
    local output_log="$workspace/output.log"

    mkdir -p "$workspace"
    cat > "$client" <<'JS'
var I2=new Set(["about:blank"]);function Gb(e){if(I2.has(e))return!0;let t;try{t=new URL(e)}catch{return!1}return t.protocol==="http:"||t.protocol==="https:"}
JS

    (
        warn() { echo "[WARN] $*" >&2; }
        info() { echo "[INFO] $*" >&2; }
        # shellcheck disable=SC1091
        source "$REPO_DIR/scripts/lib/bundled-plugins.sh"
        patch_browser_use_file_url_policy "$client"
    ) >"$output_log" 2>&1

    assert_contains "$client" "codexLinuxFileUrlPolicy"
    assert_contains "$client" 'protocol==="file:"'
    assert_not_contains "$client" 'protocol==="data:"'
    assert_not_contains "$output_log" "Could not find Browser Use URL policy insertion point"

    node - "$client" <<'NODE'
const fs = require("fs");
const vm = require("vm");

const client = process.argv[2];
const source = fs.readFileSync(client, "utf8");
const context = { URL };
vm.createContext(context);
vm.runInContext(
  `${source}
this.results = {
  aboutBlank: Gb("about:blank"),
  http: Gb("http://example.com/"),
  https: Gb("https://example.com/"),
  localFile: Gb("file:///tmp/codex-browser-file-policy.html"),
  localhostFile: Gb("file://localhost/tmp/codex-browser-file-policy.html"),
  remoteFile: Gb("file://example.com/tmp/codex-browser-file-policy.html"),
  data: Gb("data:text/html,hello"),
  javascript: Gb("javascript:alert(1)"),
  ftp: Gb("ftp://example.com/"),
  invalid: Gb("not a url"),
};`,
  context,
);

const expected = {
  aboutBlank: true,
  http: true,
  https: true,
  localFile: true,
  localhostFile: true,
  remoteFile: false,
  data: false,
  javascript: false,
  ftp: false,
  invalid: false,
};

for (const [key, value] of Object.entries(expected)) {
  if (context.results[key] !== value) {
    throw new Error(`${key}: expected ${value}, got ${context.results[key]}`);
  }
}
NODE
}

test_browser_plugin_renamed_upstream_staging() {
    info "Checking Browser plugin staging from renamed upstream resources"
    local workspace="$TMP_DIR/browser-plugin-renamed"
    local app_dir="$workspace/Codex.app"
    local install_dir="$workspace/install"
    local output_log="$workspace/output.log"
    local browser_dir="$install_dir/resources/plugins/openai-bundled/plugins/browser"
    local marketplace="$install_dir/resources/plugins/openai-bundled/.agents/plugins/marketplace.json"

    mkdir -p "$workspace" "$install_dir/resources"
    make_fake_browser_upstream_app "$app_dir"

    (
        SCRIPT_DIR="$REPO_DIR"
        INSTALL_DIR="$install_dir"
        WORK_DIR="$workspace/work"
        ARCH="x86_64"
        ICON_SOURCE="$workspace/missing-icon.png"
        CODEX_APP_ID="codex-desktop"
        mkdir -p "$WORK_DIR"
        warn() { echo "[WARN] $*" >&2; }
        info() { echo "[INFO] $*" >&2; }
        # shellcheck disable=SC1091
        source "$REPO_DIR/scripts/lib/bundled-plugins.sh"
        stage_linux_computer_use_plugin() { return 1; }
        install_browser_use_node_repl_resource() { return 0; }
        install_bundled_plugin_resources "$app_dir"
    ) >"$output_log" 2>&1

    assert_file_exists "$browser_dir/scripts/browser-client.mjs"
    assert_contains "$browser_dir/.codex-plugin/plugin.json" '"name":"browser"'
    assert_contains "$browser_dir/scripts/browser-client.mjs" 'globalThis.nodeRepl?.env?.\[e\]'
    assert_not_contains "$browser_dir/scripts/browser-client.mjs" 'globalThis.nodeRepl?.env\[e\]'
    assert_contains "$browser_dir/scripts/browser-client.mjs" "nativePipe??import.meta.__codexNativePipe"
    assert_not_contains "$browser_dir/scripts/browser-client.mjs" "let e=import.meta.__codexNativePipe;return"
    assert_contains "$browser_dir/scripts/browser-client.mjs" "codexLinuxSiteStatusAllowlistFallback"
    assert_contains "$browser_dir/scripts/browser-client.mjs" "codexLinuxFileUrlPolicy"
    assert_contains "$browser_dir/scripts/browser-client.mjs" 'protocol==="file:"'
    assert_not_contains "$browser_dir/scripts/browser-client.mjs" 'protocol==="data:"'
    assert_contains "$marketplace" '"name": "browser"'
    assert_contains "$marketplace" '"path": "./plugins/browser"'
    assert_contains "$output_log" "Browser plugin staged from upstream DMG"
    assert_not_contains "$output_log" "Browser bundled plugin resources not present"
}

test_browser_use_node_repl_glibc_pidfd_patch_static() {
    info "Checking Browser Use node_repl glibc pidfd patch scope"
    assert_contains "$REPO_DIR/scripts/lib/bundled-plugins.sh" "patch_browser_use_node_repl_glibc_pidfd_symbols"
    assert_contains "$REPO_DIR/scripts/lib/bundled-plugins.sh" "is_browser_use_node_repl_ldd_output_compatible"
    assert_contains "$REPO_DIR/scripts/lib/bundled-plugins.sh" "install_browser_use_node_repl_executable_resource"
    assert_contains "$REPO_DIR/scripts/lib/bundled-plugins.sh" "pidfd_spawnp"
    assert_contains "$REPO_DIR/scripts/lib/bundled-plugins.sh" "pidfd_getpid"
    assert_contains "$REPO_DIR/scripts/lib/bundled-plugins.sh" "GLIBC_2.39"
    assert_contains "$REPO_DIR/scripts/lib/bundled-plugins.sh" "GLIBC_2.34"
    assert_contains "$REPO_DIR/scripts/lib/bundled-plugins.sh" "non-pidfd GLIBC_2.39 references remain"
    assert_contains "$REPO_DIR/scripts/lib/bundled-plugins.sh" 'ldd "$destination"'
}

test_browser_use_node_repl_ldd_output_compatibility() {
    info "Checking Browser Use node_repl ldd output compatibility gate"
    # shellcheck disable=SC1091
    source "$REPO_DIR/scripts/lib/bundled-plugins.sh"

    if is_browser_use_node_repl_ldd_output_compatible "/node_repl: /lib/x86_64-linux-gnu/libc.so.6: version 'GLIBC_2.39' not found (required by /node_repl)"; then
        fail "Expected ldd GLIBC version errors to be rejected"
    fi

    if is_browser_use_node_repl_ldd_output_compatible "libmissing.so => not found"; then
        fail "Expected unresolved ldd libraries to be rejected"
    fi

    is_browser_use_node_repl_ldd_output_compatible "libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6" \
        || fail "Expected ordinary ldd output to be accepted"
}

make_fake_chrome_upstream_app() {
    local app_dir="$1"
    local resources_dir="$app_dir/Contents/Resources"
    local chrome_dir="$resources_dir/plugins/openai-bundled/plugins/chrome"

    mkdir -p \
        "$resources_dir/plugins/openai-bundled/.agents/plugins" \
        "$chrome_dir/.codex-plugin" \
        "$chrome_dir/scripts"

    cat > "$resources_dir/plugins/openai-bundled/.agents/plugins/marketplace.json" <<'JSON'
{"plugins":[{"name":"chrome","source":{"source":"local","path":"./plugins/chrome"},"policy":{"installation":"AVAILABLE"}}]}
JSON
    cat > "$chrome_dir/.codex-plugin/plugin.json" <<'JSON'
{"name":"chrome","version":"0.1.7"}
JSON
    cat > "$chrome_dir/scripts/installManifest.mjs" <<'JS'
var n={extensionId:"hehggadaopoacecdllhhajmbjkdcmajg",extensionHostName:"com.openai.codexextension"};var p=o=>{let t=`${o.extensionHostName}.json`,r={darwin:["Library/Application Support/Google/Chrome/NativeMessagingHosts"],linux:[".config/google-chrome/NativeMessagingHosts"],win32:["AppData/Local/OpenAI/extension"]}[m.platform()];return r.map(s=>l.resolve(m.homedir(),s,t))};
JS
    cat > "$chrome_dir/scripts/extension-id.json" <<'JSON'
{"extensionId":"hehggadaopoacecdllhhajmbjkdcmajg","extensionHostName":"com.openai.codexextension"}
JSON
    cat > "$chrome_dir/scripts/browser-client.mjs" <<'JS'
import{resolve as GF}from"path";import{homedir as VF,platform as WF}from"os";var Tc=GF(VF(),WF()==="win32"?"AppData\\Local\\Google\\Chrome\\User Data":"Library/Application Support/Google/Chrome");import{ClassicLevel as KF}from"./node_modules/classic-level.mjs";import{resolve as Gf}from"path";import{tmpdir as YF}from"os";import{cp as ZF,mkdtemp as JF,rm as kS}from"fs/promises";import{existsSync as XF}from"fs";var IS=async(t,e)=>{let r=Gf(Tc,t,"Local Extension Settings",e);if(!XF(r))return null;let n=await JF(Gf(QF(),"codex"));await ZF(r,n,{recursive:!0}),await kS(Gf(n,"LOCK"));let o=new KF(n,{createIfMissing:!1,keyEncoding:"utf8",valueEncoding:"utf8"});try{await o.open();let i=await o.get("extensionInstanceId");if(!i)return null;let s=JSON.parse(i);return typeof s!="string"?null:s}finally{await o.close(),await kS(n,{force:!0,recursive:!0})}},QF=()=>"nodeRepl"in globalThis&&globalThis.nodeRepl?globalThis.nodeRepl.tmpDir:YF();var AS=async t=>{if(t.type!=="extension"||!t.metadata?.extensionInstanceId||!t.metadata.extensionId)return t;let e=await rO(t.metadata.extensionId,t.metadata.extensionInstanceId);return e?{...t,metadata:{...t.metadata,profileName:e.name,profileIsLastUsed:e.isLastUsed.toString(),profileOrdering:e.orderingIndex.toString()}}:t},rO=async(t,e)=>(await nO(t)).find(o=>o.instanceId===e)||null,nO=async t=>{let e=await oO();return await Promise.all(e.map(async r=>({...r,instanceId:await IS(r.id,t).catch(n=>(ee(n),null))})))},oO=async()=>{let t=tO(Tc,"Local State"),e=JSON.parse(await eO(t,"utf8"));return e.profile.profiles_order.map((r,n)=>{let o=e.profile.info_cache[r];return o?{id:r,name:o.name,isLastUsed:e.profile.last_used===r,orderingIndex:n,avatarUrl:o.avatar_icon}:null}).filter(r=>!!r)};
var oh=Vb(l9.platform()),d9=async e=>{let t=ST(),r=e.filter(o=>o.info.type==="iab"),n=p9(r,t);return await Promise.all(r.filter(o=>!n.includes(o)).map(async({api:o})=>o.close())),[...e.filter(o=>o.info.type!=="iab"),...n]},p9=(e,t)=>t==null?[]:e.filter(r=>r.info.metadata?.codexSessionId===t);
function lu(e){let t=globalThis.nodeRepl?.env[e];return typeof t=="string"?t:void 0}
import{platform as yT}from"node:os";function eh(){return"privileged native pipe bridge is not available; browser-client is not trusted"}function th(){let e=globalThis.nodeRepl?.nativePipe;return e==null||typeof e.createConnection!="function"?null:e}var ml=class e{constructor(t){this.socket=t}static async create(t){let r=th();if(r!=null){let n=await r.createConnection(t);return new e(n)}throw new Error(eh())}};
async fetchBlocked(e){let r=await bS(e.endpoint,{method:"GET"});if(!r.ok)throw new Error(ae(`Browser Use cannot determine if ${e.displayUrl} is allowed. Please try again later or use another source.`));let n=await r.json();return TF(n)}
JS
    cat > "$chrome_dir/scripts/check-native-host-manifest.js" <<'JS'
function getNativeHostManifestLocation() {
  if (process.platform === "win32") {
    const registryKey = `${WINDOWS_NATIVE_HOST_REGISTRY_KEY_PREFIX}\\${expectedHostName}`;
    const registryManifestPath = readWindowsRegistryDefaultValue(registryKey);

    return {
      manifestPath: registryManifestPath || getDefaultWindowsManifestPath(),
      registryKey,
      registryManifestPath,
      registryKeyExists: registryManifestPath != null,
    };
  }

  throw new Error(
    `Unsupported platform for native host manifest check: ${process.platform}. This script supports macOS and Windows.`,
  );
}
JS
    cat > "$chrome_dir/scripts/installed-browsers.js" <<'JS'
const KNOWN_BROWSERS = [
  {
    name: "Google Chrome",
    bundleIds: ["com.google.Chrome"],
    appNames: ["Google Chrome.app"],
    commands: ["google-chrome", "chrome"],
    windowsExecutable: "chrome.exe",
  },
];
JS
    cat > "$chrome_dir/scripts/chrome-is-running.js" <<'JS'
const CHROME_PROCESS_NAMES_BY_PLATFORM = {
  darwin: new Set(["Google Chrome", "Google Chrome Helper"]),
  win32: new Set(["chrome.exe"]),
};
JS
    cat > "$chrome_dir/scripts/check-extension-installed.js" <<'JS'
function resolveChromeUserDataDirectory() {
  return path.join(os.homedir(), ".config", "google-chrome");
}

function resolveChromeProfileDirectory(userDataDirectory) {
  const localStateProfile =
    resolveChromeProfileDirectoryFromLocalState(userDataDirectory);
  if (localStateProfile) return localStateProfile;

  const latestProfile = findLatestChromeProfile(userDataDirectory);
  if (latestProfile) return latestProfile;

  throw new Error(`No Chrome profile found in ${userDataDirectory}`);
}

function resolveChromeProfileDirectoryFromLocalState(userDataDirectory) {
  return null;
}

function findLatestChromeProfile(userDataDirectory) {
  return "Default";
}

function isUsableChromeProfile(userDataDirectory, profileDirectory) {
  return profileDirectory.length > 0;
}
JS
    cat > "$chrome_dir/scripts/open-chrome-window.js" <<'JS'
function resolveChromeUserDataDirectory() {
  return path.join(os.homedir(), ".config", "google-chrome");
}

function resolveChromeProfileDirectory(userDataDirectory) {
  const localStateProfile =
    resolveChromeProfileDirectoryFromLocalState(userDataDirectory);
  if (localStateProfile) return localStateProfile;

  const latestProfile = findLatestChromeProfile(userDataDirectory);
  if (latestProfile) return latestProfile;

  throw new Error(`No Chrome profile found in ${userDataDirectory}`);
}

function resolveChromeProfileDirectoryFromLocalState(userDataDirectory) {
  return null;
}

function findLatestChromeProfile(userDataDirectory) {
  return "Default";
}

function isUsableChromeProfile(userDataDirectory, profileDirectory) {
  return profileDirectory.length > 0;
}

function getOpenChromeCommand(profileDirectory) {
  const chromeArgs = [
    `--profile-directory=${profileDirectory}`,
    "--new-window",
    ABOUT_BLANK_URL,
  ];

  return {
    command: "google-chrome",
    args: chromeArgs,
  };
}
JS
}

test_chrome_plugin_staging() {
    info "Checking Chrome plugin staging"
    local workspace="$TMP_DIR/chrome-plugin"
    local app_dir="$workspace/Codex.app"
    local install_dir="$workspace/install"
    local output_log="$workspace/output.log"
    local chrome_dir="$install_dir/resources/plugins/openai-bundled/plugins/chrome"
    local host="$chrome_dir/extension-host/linux/x64/extension-host"

    mkdir -p "$workspace" "$install_dir/resources"
    make_fake_chrome_upstream_app "$app_dir"

    (
        SCRIPT_DIR="$REPO_DIR"
        INSTALL_DIR="$install_dir"
        WORK_DIR="$workspace/work"
        ARCH="x86_64"
        ICON_SOURCE="$workspace/missing-icon.png"
        CODEX_APP_ID="codex-desktop"
        mkdir -p "$WORK_DIR"
        warn() { echo "[WARN] $*" >&2; }
        info() { echo "[INFO] $*" >&2; }
        # shellcheck disable=SC1091
        source "$REPO_DIR/scripts/lib/bundled-plugins.sh"
        stage_linux_computer_use_plugin() { return 1; }
        build_chrome_extension_host() {
            local fake_host="$workspace/codex-chrome-extension-host"
            printf '#!/bin/sh\n' > "$fake_host"
            chmod +x "$fake_host"
            printf '%s\n' "$fake_host"
        }
        install_bundled_plugin_resources "$app_dir"
    ) >"$output_log" 2>&1

    assert_file_exists "$host"
    [ -x "$host" ] || fail "Expected Chrome extension host to be executable: $host"
    assert_contains "$chrome_dir/scripts/installManifest.mjs" "BraveSoftware/Brave-Browser/NativeMessagingHosts"
    assert_contains "$chrome_dir/scripts/installManifest.mjs" ".config/chromium/NativeMessagingHosts"
    assert_contains "$chrome_dir/scripts/installed-browsers.js" "Brave Browser"
    assert_contains "$chrome_dir/scripts/installed-browsers.js" "Chromium"
    assert_contains "$chrome_dir/scripts/chrome-is-running.js" "brave-browser"
    assert_contains "$chrome_dir/scripts/chrome-is-running.js" "chromium-browser"
    assert_contains "$chrome_dir/scripts/check-native-host-manifest.js" 'process.platform === "linux"'
    assert_contains "$chrome_dir/scripts/check-native-host-manifest.js" "BraveSoftware"
    assert_contains "$chrome_dir/scripts/check-native-host-manifest.js" "chromium"
    assert_contains "$chrome_dir/scripts/check-extension-installed.js" "linuxBraveUserDataDirectory"
    assert_contains "$chrome_dir/scripts/check-extension-installed.js" "linuxChromiumUserDataDirectory"
    assert_contains "$chrome_dir/scripts/check-extension-installed.js" "linuxCandidateWithInstalledExtension"
    assert_contains "$chrome_dir/scripts/check-extension-installed.js" "resolveChromeProfileDirectoryFromRunningProcess"
    assert_contains "$chrome_dir/scripts/check-extension-installed.js" "defaultLinuxUserDataDirectoryForCommand"
    assert_contains "$chrome_dir/scripts/open-chrome-window.js" "brave-browser"
    assert_contains "$chrome_dir/scripts/open-chrome-window.js" "chromium"
    assert_contains "$chrome_dir/scripts/open-chrome-window.js" "defaultBrowser ==="
    assert_contains "$chrome_dir/scripts/open-chrome-window.js" "resolveChromeProfileDirectoryFromRunningProcess"
    assert_contains "$chrome_dir/scripts/open-chrome-window.js" "defaultLinuxUserDataDirectoryForCommand"
    assert_contains "$chrome_dir/scripts/browser-client.mjs" "codexLinuxChromeUserDataDirectories"
    assert_contains "$chrome_dir/scripts/browser-client.mjs" '"BraveSoftware","Brave-Browser"'
    assert_contains "$chrome_dir/scripts/browser-client.mjs" '".config","chromium"'
    assert_contains "$chrome_dir/scripts/browser-client.mjs" "instanceId:await IS(o.id,t,r)"
    assert_contains "$chrome_dir/scripts/browser-client.mjs" "codexLinuxRankBrowserBackends"
    assert_contains "$chrome_dir/scripts/browser-client.mjs" "getUserTabs()"
    assert_contains "$chrome_dir/scripts/browser-client.mjs" 'globalThis.nodeRepl?.env?.\[e\]'
    assert_not_contains "$chrome_dir/scripts/browser-client.mjs" 'globalThis.nodeRepl?.env\[e\]'
    assert_contains "$chrome_dir/scripts/browser-client.mjs" "nativePipe??import.meta.__codexNativePipe"
    assert_not_contains "$chrome_dir/scripts/browser-client.mjs" "codexLinuxNativePipeFallback"
    assert_not_contains "$chrome_dir/scripts/browser-client.mjs" 'await import("node:net")'
    assert_contains "$chrome_dir/scripts/browser-client.mjs" "codexLinuxSiteStatusAllowlistFallback"
    assert_contains "$install_dir/resources/plugins/openai-bundled/.agents/plugins/marketplace.json" '"name": "chrome"'
    assert_contains "$output_log" "Chrome plugin staged from upstream DMG"
}

test_chrome_browser_client_profile_root_variants() {
    info "Checking Chrome browser-client profile root variants"
    local workspace="$TMP_DIR/chrome-browser-client-profile-roots"
    local chrome_dir="$workspace/chrome"
    local browser_client="$chrome_dir/scripts/browser-client.mjs"

    mkdir -p "$chrome_dir/scripts"

    cat > "$browser_client" <<'JS'
import{resolve as GF}from"path";import{homedir as VF,platform as WF}from"os";var Tc=GF(VF(),WF()==="win32"?"AppData\\Local\\Google\\Chrome\\User Data":"Library/Application Support/Google/Chrome");
JS
    node "$REPO_DIR/scripts/lib/patch-chrome-plugin.js" "$chrome_dir" >/dev/null 2>&1
    assert_contains "$browser_client" "codexLinuxChromeUserDataDirectories"
    assert_contains "$browser_client" '"BraveSoftware","Brave-Browser"'
    assert_contains "$browser_client" '".config","chromium"'

    cat > "$browser_client" <<'JS'
import{resolve as eO}from"path";import{homedir as tO,platform as rO}from"os";var Ic=eO(tO(),rO()==="win32"?"AppData\\Local\\Google\\Chrome\\User Data":"Library/Application Support/Google/Chrome");
JS
    node "$REPO_DIR/scripts/lib/patch-chrome-plugin.js" "$chrome_dir" >/dev/null 2>&1
    assert_contains "$browser_client" "codexLinuxChromeUserDataDirectories"
    assert_contains "$browser_client" '"BraveSoftware","Brave-Browser"'
    assert_contains "$browser_client" '".config","chromium"'

    cat > "$browser_client" <<'JS'
import{resolve as Y5}from"path";import{homedir as Z5,platform as X5}from"os";var hl=Y5(Z5(),X5()==="win32"?"AppData\\Local\\Google\\Chrome\\User Data":"Library/Application Support/Google/Chrome");import{ClassicLevel as Q5}from"./node_modules/classic-level.mjs";import{resolve as rh}from"path";import{tmpdir as o9}from"os";import{cp as t9,mkdtemp as r9,rm as fT}from"fs/promises";import{existsSync as n9}from"fs";var mT=async(e,t)=>{let r=rh(hl,e,"Local Extension Settings",t);if(!n9(r))return null;let n=await r9(rh(o9(),"codex"));await t9(r,n,{recursive:!0}),await fT(rh(n,"LOCK"));let o=new Q5(n,{createIfMissing:!1,keyEncoding:"utf8",valueEncoding:"utf8"});try{await o.open();let i=await o.get("extensionInstanceId");if(!i)return null;let s=JSON.parse(i);return typeof s!="string"?null:s}finally{await o.close(),await fT(n,{force:!0,recursive:!0})}};var a9=async(e,t)=>(await u9(e)).find(o=>o.instanceId===t)||null,u9=async e=>{let t=await c9();return await Promise.all(t.map(async r=>({...r,instanceId:await mT(r.id,e).catch(n=>(ne(n),null))})))},c9=async()=>{let e=s9(hl,"Local State"),t=JSON.parse(await i9(e,"utf8"));return t.profile.profiles_order.map((r,n)=>{let o=t.profile.info_cache[r];return o?{id:r,name:o.name,isLastUsed:t.profile.last_used===r,orderingIndex:n,avatarUrl:o.avatar_icon}:null}).filter(r=>!!r)}
JS
    node "$REPO_DIR/scripts/lib/patch-chrome-plugin.js" "$chrome_dir" >/dev/null 2>&1
    assert_contains "$browser_client" "codexLinuxChromeUserDataDirectories"
    assert_contains "$browser_client" '"BraveSoftware","Brave-Browser"'
    assert_contains "$browser_client" '".config","chromium"'
    assert_contains "$browser_client" "async(e,t,r=hl)"
    assert_contains "$browser_client" "instanceId:await mT(o.id,e,r)"

    cat > "$browser_client" <<'JS'
import{readFile as $j}from"fs/promises";import{resolve as zj}from"path";import{resolve as Nj}from"path";import{homedir as Oj,platform as Mj}from"os";var $c=Nj(Oj(),Mj()==="win32"?"AppData\\Local\\Google\\Chrome\\User Data":"Library/Application Support/Google/Chrome");import{ClassicLevel as Fj}from"./node_modules/classic-level.mjs";import{resolve as Ih}from"path";import{tmpdir as Bj}from"os";import{cp as Lj,mkdtemp as Uj,rm as gk}from"fs/promises";import{existsSync as jj}from"fs";var bk=async(e,t)=>{let r=Ih($c,e,"Local Extension Settings",t);if(!jj(r))return null;let n=await Uj(Ih(qj(),"codex"));await Lj(r,n,{recursive:!0}),await gk(Ih(n,"LOCK"));let o=new Fj(n,{createIfMissing:!1,keyEncoding:"utf8",valueEncoding:"utf8"});try{await o.open();let i=await o.get("extensionInstanceId");if(!i)return null;let s=JSON.parse(i);return typeof s!="string"?null:s}finally{await o.close(),await gk(n,{force:!0,recursive:!0})}},qj=()=>"nodeRepl"in globalThis&&globalThis.nodeRepl?globalThis.nodeRepl.tmpDir:Bj();var yk=async e=>{if(e.type!=="extension"||!e.metadata?.extensionInstanceId||!e.metadata.extensionId)return e;let t=await Wj(e.metadata.extensionId,e.metadata.extensionInstanceId);return t?{...e,metadata:{...e.metadata,profileName:t.name,profileIsLastUsed:t.isLastUsed.toString(),profileOrdering:t.orderingIndex.toString()}}:e},Wj=async(e,t)=>(await Hj(e)).find(o=>o.instanceId===t)||null,Hj=async e=>{let t=await Vj();return await Promise.all(t.map(async r=>({...r,instanceId:await bk(r.id,e).catch(n=>(ue(n),null))})))},Vj=async()=>{let e=zj($c,"Local State"),t=JSON.parse(await $j(e,"utf8"));return t.profile.profiles_order.map((r,n)=>{let o=t.profile.info_cache[r];return o?{id:r,name:o.name,isLastUsed:t.profile.last_used===r,orderingIndex:n,avatarUrl:o.avatar_icon}:null}).filter(r=>!!r)};var Ph=Iy(Gj.platform()),Kj=async(e,{codexSessionId:t})=>{let r=ap(Ey),n=e.filter(i=>i.info.type==="iab"),o=Jj(n,t,r);return await Promise.all(n.filter(i=>!o.includes(i)).map(async({api:i})=>i.close())),[...e.filter(i=>i.info.type!=="iab"),...o]},Jj=(e,t,r)=>t==null?[]:e.filter(n=>n.info.metadata?.codexSessionId===t&&(r==null||n.info.metadata.codexAppBuildFlavor===r));var vk=async e=>[];
JS
    node "$REPO_DIR/scripts/lib/patch-chrome-plugin.js" "$chrome_dir" >/dev/null 2>&1
    assert_contains "$browser_client" "codexLinuxChromeUserDataDirectories"
    assert_contains "$browser_client" '"BraveSoftware","Brave-Browser"'
    assert_contains "$browser_client" '".config","chromium"'
    assert_contains "$browser_client" 'async(e,t,r=$c)'
    assert_contains "$browser_client" "instanceId:await bk(o.id,e,r)"
    assert_contains "$browser_client" "codexLinuxRankBrowserBackends"
}

test_chrome_marketplace_fallback_synthesis() {
    info "Checking Chrome marketplace fallback synthesis when upstream omits chrome"
    local workspace="$TMP_DIR/chrome-marketplace-fallback"
    local app_dir="$workspace/Codex.app"
    local install_dir="$workspace/install"
    local output_log="$workspace/output.log"
    local marketplace="$install_dir/resources/plugins/openai-bundled/.agents/plugins/marketplace.json"

    mkdir -p "$workspace" "$install_dir/resources"
    make_fake_chrome_upstream_app "$app_dir"

    # Upstream marketplace.json lists no chrome entry — exercises the
    # synthesized-fallback path in write_bundled_plugins_marketplace.
    cat > "$app_dir/Contents/Resources/plugins/openai-bundled/.agents/plugins/marketplace.json" <<'JSON'
{"plugins":[{"name":"browser","source":{"source":"local","path":"./plugins/browser"},"policy":{"installation":"AVAILABLE"}}]}
JSON

    # Distinctive name + category prove the synthesized entry actually
    # reads the staged plugin.json rather than reusing hardcoded values.
    cat > "$app_dir/Contents/Resources/plugins/openai-bundled/plugins/chrome/.codex-plugin/plugin.json" <<'JSON'
{"name":"chrome-fallback-test","version":"9.9.9","interface":{"category":"FallbackCategory"}}
JSON

    (
        SCRIPT_DIR="$REPO_DIR"
        INSTALL_DIR="$install_dir"
        WORK_DIR="$workspace/work"
        ARCH="x86_64"
        ICON_SOURCE="$workspace/missing-icon.png"
        CODEX_APP_ID="codex-desktop"
        mkdir -p "$WORK_DIR"
        warn() { echo "[WARN] $*" >&2; }
        info() { echo "[INFO] $*" >&2; }
        # shellcheck disable=SC1091
        source "$REPO_DIR/scripts/lib/bundled-plugins.sh"
        stage_linux_computer_use_plugin() { return 1; }
        build_chrome_extension_host() {
            local fake_host="$workspace/codex-chrome-extension-host"
            printf '#!/bin/sh\n' > "$fake_host"
            chmod +x "$fake_host"
            printf '%s\n' "$fake_host"
        }
        install_bundled_plugin_resources "$app_dir"
    ) >"$output_log" 2>&1

    assert_file_exists "$marketplace"
    assert_contains "$marketplace" '"name": "chrome-fallback-test"'
    assert_contains "$marketplace" '"category": "FallbackCategory"'
    assert_contains "$marketplace" '"path": "./plugins/chrome"'
    assert_contains "$marketplace" '"installation": "AVAILABLE"'
    assert_contains "$marketplace" '"authentication": "ON_INSTALL"'
    assert_not_contains "$marketplace" "Bundled marketplace does not contain chrome plugin"
}

test_chrome_native_host_manifest_writer() {
    info "Checking Chrome native host manifest writer"
    local workspace="$TMP_DIR/chrome-native-host-manifest"
    local plugin_dir="$workspace/plugin"
    local home_dir="$workspace/home"
    local app_dir="$workspace/app"
    local host_path="$workspace/extension-host"
    local manifest_path

    mkdir -p "$plugin_dir/scripts" "$home_dir" "$app_dir/.codex-linux" "$(dirname "$host_path")"
    printf '#!/bin/sh\n' > "$host_path"
    chmod +x "$host_path"
    cat > "$plugin_dir/scripts/extension-id.json" <<'JSON'
{"extensionId":"abcdefghijklmnopabcdefghijklmnop","extensionHostName":"com.example.codextest"}
JSON
    printf '%s\n' ".config/example-browser/NativeMessagingHosts" > "$app_dir/.codex-linux/chrome-native-host-manifest-paths"

    python3 - "$REPO_DIR/launcher/start.sh.template" "$host_path" "$home_dir" "$plugin_dir" "$app_dir" <<'PY'
import subprocess
import sys
from pathlib import Path

source = Path(sys.argv[1]).read_text(encoding="utf-8")
marker = "python3 - \"$host_path\" \"$HOME\" \"$plugin_dir\" \"$SCRIPT_DIR\" <<'PY'\n"
start = source.index(marker) + len(marker)
end = source.index("\nPY\n", start)
script = source[start:end]
subprocess.run(
    ["python3", "-", sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5]],
    input=script,
    text=True,
    check=True,
)
PY

    for relative in \
        ".config/google-chrome/NativeMessagingHosts" \
        ".config/BraveSoftware/Brave-Browser/NativeMessagingHosts" \
        ".config/chromium/NativeMessagingHosts" \
        ".config/example-browser/NativeMessagingHosts"; do
        manifest_path="$home_dir/$relative/com.example.codextest.json"
        assert_file_exists "$manifest_path"
        assert_contains "$manifest_path" "com.example.codextest"
        assert_contains "$manifest_path" "chrome-extension://abcdefghijklmnopabcdefghijklmnop/"
        assert_contains "$manifest_path" "$host_path"
    done
}

make_fake_extracted_asar() {
    local root="$1"
    local bundle_body="$2"
    local settings_body="${3:-}"
    local index_body="${4:-}"

    mkdir -p "$root/webview/assets" "$root/.vite/build"
    printf 'png' > "$root/webview/assets/app-test.png"
    printf 'export{s as t};\n' > "$root/webview/assets/chunk-test.js"
    printf 'import{t as e}from"./chunk-test.js";Symbol.for(`react.transitional.element`);export{e as t};\n' > "$root/webview/assets/react-test.js"
    printf 'import{t as e}from"./chunk-test.js";Symbol.for(`react.transitional.element`);export{e as t};\n' > "$root/webview/assets/jsx-runtime-test.js"
    printf 'let marker=`vscode://codex`;async function n(){return{}}export{n};\n' > "$root/webview/assets/vscode-api-test.js"
    cat > "$root/webview/assets/app-server-manager-signals-test.js" <<'JS'
function j(e){return e}function B(e){if(e==null||typeof e==`string`)return null;let t=Mi(e);return t==null?null:Ni(t)}function Mi(e){return`subAgent`in e?e.subAgent:null}function Ni(e){return typeof e==`string`?Pi():`thread_spawn`in e?{parentThreadId:j(e.thread_spawn.parent_thread_id),depth:e.thread_spawn.depth,agentNickname:e.thread_spawn.agent_nickname,agentRole:e.thread_spawn.agent_role}:Pi()}function Pi(){return{parentThreadId:null,depth:null,agentNickname:null,agentRole:null}}function Xl(e){return e==null?null:Zl(e.agentNickname)??Zl(B(e.source)?.agentNickname)}function Zl(e){if(e==null)return null;let t=e.trim();return t.length===0?null:t}
JS
    printf 'let marker=`hotkey-window-hotkey-state`;function i(){}export{i};\n' > "$root/webview/assets/general-settings-hotkey-test.js"
    printf 'function t(){}export{t};\n' > "$root/webview/assets/toggle-test.js"
    printf 'function n(){}export{n};\n' > "$root/webview/assets/settings-row-test.js"
    printf 'function r(){}function n(){}function t(){}export{r,n,t};\n' > "$root/webview/assets/settings-content-layout-test.js"
    if [ -n "$settings_body" ]; then
        printf '%s\n' "$settings_body" > "$root/webview/assets/general-settings-test.js"
    fi
    if [ -n "$index_body" ]; then
        printf '%s\n' "$index_body" > "$root/webview/assets/index-test.js"
    fi
    cat > "$root/package.json" <<'JSON'
{}
JSON
    printf '%s\n' "$bundle_body" > "$root/.vite/build/main-test.js"
}

test_linux_file_manager_patch_smoke() {
    info "Checking Linux file manager patch behavior"
    local workspace="$TMP_DIR/file-manager-patch"
    local extracted="$workspace/extracted"
    local output_log="$workspace/output.log"

    mkdir -p "$workspace"
    make_fake_extracted_asar "$extracted" 'let D={removeMenu(){},setMenuBarVisibility(){},setIcon(){},once(){}};let n=require(`electron`),t=require(`node:path`),a=require(`node:fs`);...process.platform===`win32`?{autoHideMenuBar:!0}:{},process.platform===`win32`&&D.removeMenu(),foo)}),D.once(`ready-to-show`,()=>{var sa=Mi({id:`fileManager`,label:`Finder`,icon:`apps/finder.png`,kind:`fileManager`,darwin:{detect:()=>`open`,args:e=>ai(e)},win32:{label:`File Explorer`,icon:`apps/file-explorer.png`,detect:ca,args:e=>ai(e),open:async({path:e})=>la(e)}});function ca(){let e=1;return e}async function la(e){let t=ua(e);if(t&&(0,a.statSync)(t).isFile()){n.shell.showItemInFolder(t);return}let r=t??e,i=await n.shell.openPath(r);if(i)throw Error(i)}function ua(e){return e}var Ua=Mi({id:`systemDefault`,label:`System Default App`,icon:`apps/file-explorer.png`,kind:`systemDefault`,hidden:!0,darwin:{icon:`apps/finder.png`,detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)},win32:{detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)},linux:{detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)}});async function Wa(e){return e}'

    node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_contains "$extracted/.vite/build/main-test.js" 'detect:()=>`linux-file-manager`'
    assert_contains "$extracted/.vite/build/main-test.js" 'linux:{label:`File Manager`'
    assert_contains "$extracted/.vite/build/main-test.js" 'process.platform===`linux`&&D.setMenuBarVisibility(!1),'
    assert_contains "$extracted/.vite/build/main-test.js" '&&D.setIcon('
    assert_contains "$extracted/webview/assets/app-server-manager-signals-test.js" '`subAgent`in e?e.subAgent:`subagent`in e?e.subagent:null'
    assert_contains "$extracted/webview/assets/app-server-manager-signals-test.js" 'Zl(e.agentNickname)??Zl(e.agent_nickname)??Zl(B(e.source)?.agentNickname)'
    assert_not_contains "$output_log" 'Failed to apply Linux File Manager Patch'

    node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_occurrence_count "$extracted/webview/assets/app-server-manager-signals-test.js" '`subagent`in e?e.subagent' '1'
    assert_occurrence_count "$extracted/webview/assets/app-server-manager-signals-test.js" 'Zl(e.agent_nickname)' '1'
    assert_not_contains "$output_log" 'Failed to apply Linux File Manager Patch'
}

test_linux_translucent_sidebar_default_patch_smoke() {
    info "Checking Linux translucent sidebar default patch behavior"
    local workspace="$TMP_DIR/translucent-sidebar-patch"
    local extracted="$workspace/extracted"
    local output_log="$workspace/output.log"

    mkdir -p "$workspace"
    make_fake_extracted_asar \
        "$extracted" \
        'let D={removeMenu(){},setMenuBarVisibility(){},setIcon(){},once(){}};let n=require(`electron`),t=require(`node:path`),a=require(`node:fs`);...process.platform===`win32`?{autoHideMenuBar:!0}:{},process.platform===`win32`&&D.removeMenu(),foo)}),D.once(`ready-to-show`,()=>{var sa=Mi({id:`fileManager`,label:`Finder`,icon:`apps/finder.png`,kind:`fileManager`,darwin:{detect:()=>`open`,args:e=>ai(e)},win32:{label:`File Explorer`,icon:`apps/file-explorer.png`,detect:ca,args:e=>ai(e),open:async({path:e})=>la(e)}});function ca(){let e=1;return e}async function la(e){let t=ua(e);if(t&&(0,a.statSync)(t).isFile()){n.shell.showItemInFolder(t);return}let r=t??e,i=await n.shell.openPath(r);if(i)throw Error(i)}function ua(e){return e}var Ua=Mi({id:`systemDefault`,label:`System Default App`,icon:`apps/file-explorer.png`,kind:`systemDefault`,hidden:!0,darwin:{icon:`apps/finder.png`,detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)},win32:{detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)},linux:{detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)}});async function Wa(e){return e}' \
        'function settings(){let d=ot(r,e),f=at(e),p={codeThemeId:tt(a,e).id,theme:d},x=`settings.general.appearance.chromeTheme.translucentSidebar`;return {p,x}}' \
        'function runtime(){let o=`light`,a=`electron`,l=null,f=null,C=fl(l,`light`),w=fl(f,`dark`);let T=o===`light`?C:w,E;if(T.opaqueWindows&&!XZ()){document.body.classList.add(`electron-opaque`);return E}return E}'
    cat > "$extracted/webview/assets/app-main-test.js" <<'JS'
let{data:c}=Qc(y.APPEARANCE_LIGHT_CHROME_THEME,s),l;let{data:u}=Qc(y.APPEARANCE_DARK_CHROME_THEME,l),d;let x=b,S;let C=o===`light`?x:S,w;if(C.opaqueWindows&&!ba()){e.classList.add(`electron-opaque`)}
JS
    cat > "$extracted/webview/assets/diff-view-mode-test.js" <<'JS'
function oe(e,t){let n=o[t];return{accent:p(e?.accent)??n.accent,contrast:se(e?.contrast,n.contrast),fonts:le(e?.fonts),ink:p(e?.ink)??n.ink,opaqueWindows:e?.opaqueWindows??n.opaqueWindows,semanticColors:ue(e?.semanticColors,n.semanticColors),surface:p(e?.surface)??n.surface}}
JS

    node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_contains "$extracted/webview/assets/general-settings-test.js" 'navigator.userAgent.includes(`Linux`)&&r?.opaqueWindows==null&&(d={...d,opaqueWindows:!0})'
    assert_contains "$extracted/webview/assets/index-test.js" 'document.documentElement.dataset.codexOs===`linux`&&((o===`light`?l:f)?.opaqueWindows==null&&(T={...T,opaqueWindows:!0}))'
    assert_contains "$extracted/webview/assets/app-main-test.js" 'document.documentElement.dataset.codexOs===`linux`&&((o===`light`?c:u)?.opaqueWindows==null&&(C={...C,opaqueWindows:!0}))'
    assert_contains "$extracted/webview/assets/diff-view-mode-test.js" 'opaqueWindows:e?.opaqueWindows??(typeof navigator<`u`&&((navigator.userAgentData?.platform??navigator.platform??navigator.userAgent).toLowerCase().includes(`linux`))?!0:n.opaqueWindows)'
    assert_occurrence_count "$extracted/webview/assets/general-settings-test.js" 'navigator.userAgent.includes(`Linux`)' '1'
    assert_occurrence_count "$extracted/webview/assets/index-test.js" 'dataset.codexOs===`linux`' '1'
    assert_occurrence_count "$extracted/webview/assets/app-main-test.js" 'dataset.codexOs===`linux`' '1'
    assert_occurrence_count "$extracted/webview/assets/diff-view-mode-test.js" 'toLowerCase().includes(`linux`)' '1'
    assert_not_contains "$output_log" 'Could not find Linux opaque window default insertion point'

    node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_occurrence_count "$extracted/webview/assets/general-settings-test.js" 'navigator.userAgent.includes(`Linux`)' '1'
    assert_occurrence_count "$extracted/webview/assets/index-test.js" 'dataset.codexOs===`linux`' '1'
    assert_occurrence_count "$extracted/webview/assets/app-main-test.js" 'dataset.codexOs===`linux`' '1'
    assert_occurrence_count "$extracted/webview/assets/diff-view-mode-test.js" 'toLowerCase().includes(`linux`)' '1'
    assert_not_contains "$output_log" 'Could not find Linux opaque window default insertion point'
}

test_linux_tray_patch_smoke() {
    info "Checking Linux tray patch behavior"
    local workspace="$TMP_DIR/tray-patch"
    local extracted="$workspace/extracted"
    local output_log="$workspace/output.log"
    local bundle_body

    mkdir -p "$workspace"
    bundle_body="$(cat <<'JS'
let D={removeMenu(){},setMenuBarVisibility(){},setIcon(){},once(){}};
let n=require(`electron`),i=require(`node:path`),a=require(`node:fs`);
let t={join(){},C:{Prod:`prod`},A(){}};
let k={hide(){},isDestroyed(){return false}};
let f=`local`;
...process.platform===`win32`?{autoHideMenuBar:!0}:{},process.platform===`win32`&&D.removeMenu(),foo)}),D.once(`ready-to-show`,()=>{
var sa=Mi({id:`fileManager`,label:`Finder`,icon:`apps/finder.png`,kind:`fileManager`,darwin:{detect:()=>`open`,args:e=>ai(e)},win32:{label:`File Explorer`,icon:`apps/file-explorer.png`,detect:ca,args:e=>ai(e),open:async({path:e})=>la(e)}});
function ca(){let e=1;return e}
async function la(e){let t=ua(e);if(t&&(0,a.statSync)(t).isFile()){n.shell.showItemInFolder(t);return}let r=t??e,i=await n.shell.openPath(r);if(i)throw Error(i)}
function ua(e){return e}
var Ua=Mi({id:`systemDefault`,label:`System Default App`,icon:`apps/file-explorer.png`,kind:`systemDefault`,hidden:!0,darwin:{icon:`apps/finder.png`,detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)},win32:{detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)},linux:{detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)}});
async function Wa(e){return e}
function Nw(e,n){return `icon`}
async function Hw(e){return process.platform!==`win32`&&process.platform!==`darwin`?null:(zw=!0,Lw??Rw??(Rw=(async()=>{let r=await Ww(e.buildFlavor,e.repoRoot),i=new n.Tray(r.defaultIcon);return i})()))}
async function Ww(e,t){if(process.platform===`darwin`){return null}let r=process.platform===`win32`?`.ico`:`.png`,a=Nw(e,process.platform),o=[...n.app.isPackaged?[(0,i.join)(process.resourcesPath,`${a}${r}`)]:[],(0,i.join)(t,`electron`,`src`,`icons`,`${a}${r}`)];for(let e of o){let t=n.nativeImage.createFromPath(e);if(!t.isEmpty())return{defaultIcon:t,chronicleRunningIcon:null}}return{defaultIcon:await n.app.getFileIcon(process.execPath,{size:process.platform===`win32`?`small`:`normal`}),chronicleRunningIcon:null}}
var pb=class{trayMenuThreads={runningThreads:[],unreadThreads:[],pinnedThreads:[],recentThreads:[],usageLimits:[]};constructor(){this.tray={on(){},setContextMenu(){},popUpContextMenu(){}};this.onTrayButtonClick=()=>{};this.tray.on(`click`,()=>{this.onTrayButtonClick()}),this.tray.on(`right-click`,()=>{this.openNativeTrayMenu()})}async handleMessage(e){switch(e.type){case`tray-menu-threads-changed`:this.trayMenuThreads=e.trayMenuThreads;return}}openNativeTrayMenu(){this.updateChronicleTrayIcon();let e=n.Menu.buildFromTemplate(this.getNativeTrayMenuItems());e.once(`menu-will-show`,()=>{this.isNativeTrayMenuOpen=!0}),e.once(`menu-will-close`,()=>{this.isNativeTrayMenuOpen=!1,this.handleNativeTrayMenuClosed()}),this.tray.popUpContextMenu(e)}updateChronicleTrayIcon(){}getNativeTrayMenuItems(){return[]}}
v&&k.on(`close`,e=>{this.persistPrimaryWindowBounds(k,f);let t=this.getPrimaryWindows(f).some(e=>e!==k);if(process.platform===`win32`&&!this.isAppQuitting&&this.options.canHideLastLocalWindowToTray?.()===!0&&!t){e.preventDefault(),k.hide();return}if(process.platform===`darwin`&&!this.isAppQuitting&&!t){e.preventDefault(),k.hide()}});
let E=process.platform===`win32`;
let oe=async()=>{};
let se=async e=>{};
E&&oe();let ce=Hr({});
JS
)"
    make_fake_extracted_asar "$extracted" "$bundle_body"

    node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_contains "$extracted/.vite/build/main-test.js" 'process.platform!==`win32`&&process.platform!==`darwin`&&process.platform!==`linux`?null:'
    assert_contains "$extracted/.vite/build/main-test.js" 'nativeImage.createFromPath(process.resourcesPath+`/../content/webview/assets/app-test.png`)'
    assert_contains "$extracted/.vite/build/main-test.js" '(process.platform===`win32`||process.platform===`linux`)&&!this.isAppQuitting'
    assert_contains "$extracted/.vite/build/main-test.js" '!this.isAppQuitting&&!(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress())'
    assert_contains "$extracted/.vite/build/main-test.js" 'setLinuxTrayContextMenu(){let e=n.Menu.buildFromTemplate(this.getNativeTrayMenuItems())'
    assert_contains "$extracted/.vite/build/main-test.js" 'process.platform===`linux`&&this.setLinuxTrayContextMenu(),this.tray.on(`click`'
    assert_contains "$extracted/.vite/build/main-test.js" 'process.platform===`linux`?this.openNativeTrayMenu():this.onTrayButtonClick()'
    assert_contains "$extracted/.vite/build/main-test.js" 'openNativeTrayMenu(){if(process.platform===`linux`&&(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress()))return;'
    assert_contains "$extracted/.vite/build/main-test.js" 'let e=process.platform===`linux`&&this.setLinuxTrayContextMenu?this.setLinuxTrayContextMenu():n.Menu.buildFromTemplate'
    assert_contains "$extracted/.vite/build/main-test.js" 'if(process.platform===`linux`)return;e.once(`menu-will-show`'
    assert_contains "$extracted/.vite/build/main-test.js" 'this.trayMenuThreads=e.trayMenuThreads,process.platform===`linux`&&!(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress())&&this.setLinuxTrayContextMenu?.()'
    assert_contains "$extracted/.vite/build/main-test.js" '(E||process.platform===`linux`&&(typeof codexLinuxIsTrayEnabled!==`function`||codexLinuxIsTrayEnabled()))&&oe();'
    assert_not_contains "$extracted/.vite/build/main-test.js" 'process.platform===`linux`&&this.tray.setContextMenu?.(e),this.tray.popUpContextMenu(e)'
    assert_not_contains "$output_log" 'WARN: Could not find tray'

    node - "$extracted/.vite/build/main-test.js" <<'NODE'
const fs = require("fs");

const source = fs.readFileSync(process.argv[2], "utf8");
const closeSnippet = source.match(/v&&k\.on\(`close`,e=>\{.*?\}\);/)?.[0];
if (!closeSnippet) {
  throw new Error("Could not extract patched Linux close handler");
}

function registerCloseHandler({ quitInProgress = false, isAppQuitting = false, trayEnabled = true } = {}) {
  const state = { hideCalls: 0 };
  const controller = {
    isAppQuitting,
    options: { canHideLastLocalWindowToTray: () => trayEnabled },
    persistPrimaryWindowBounds() {},
    getPrimaryWindows() {
      return [];
    },
  };
  const factory = new Function(
    "process",
    "codexLinuxIsQuitInProgress",
    "state",
    `return function(){const v=true;const f=\`local\`;const k={handlers:{},on(event,handler){this.handlers[event]=handler},hide(){state.hideCalls+=1}};${closeSnippet};return k.handlers.close;};`,
  );
  const makeHandler = factory({ platform: "linux" }, () => quitInProgress, state);
  const handler = makeHandler.call(controller);
  return { handler, state };
}

function runCloseWithoutHelper({ trayEnabled = true, isAppQuitting = false } = {}) {
  const event = {
    prevented: false,
    preventDefault() {
      this.prevented = true;
    },
  };
  const state = { hideCalls: 0 };
  const controller = {
    isAppQuitting,
    options: { canHideLastLocalWindowToTray: () => trayEnabled },
    persistPrimaryWindowBounds() {},
    getPrimaryWindows() {
      return [];
    },
  };
  const factory = new Function(
    "process",
    "state",
    `return function(){const v=true;const f=\`local\`;const k={handlers:{},on(event,handler){this.handlers[event]=handler},hide(){state.hideCalls+=1}};${closeSnippet};return k.handlers.close;};`,
  );
  const handler = factory({ platform: "linux" }, state).call(controller);
  handler(event);
  return { event, state };
}

function runClose(options) {
  const event = {
    prevented: false,
    preventDefault() {
      this.prevented = true;
    },
  };
  const { handler, state } = registerCloseHandler(options);
  handler(event);
  return { event, state };
}

let result = runClose({ trayEnabled: true, quitInProgress: false, isAppQuitting: false });
if (!result.event.prevented || result.state.hideCalls !== 1) {
  throw new Error("normal Linux close should still hide to tray");
}

result = runClose({ trayEnabled: true, quitInProgress: true, isAppQuitting: false });
if (result.event.prevented || result.state.hideCalls !== 0) {
  throw new Error("quit-in-progress Linux close should not hide to tray");
}

result = runClose({ trayEnabled: true, quitInProgress: false, isAppQuitting: true });
if (result.event.prevented || result.state.hideCalls !== 0) {
  throw new Error("app.quit close should not hide to tray when upstream quit flag is already set");
}

result = runCloseWithoutHelper({ trayEnabled: true, isAppQuitting: false });
if (!result.event.prevented || result.state.hideCalls !== 1) {
  throw new Error("Linux close should still hide to tray when the quit helper is unavailable");
}
NODE

    node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'process.platform!==`linux`' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'nativeImage.createFromPath(process.resourcesPath' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'process.platform===`linux`)&&!this.isAppQuitting' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'setLinuxTrayContextMenu(){' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'process.platform===`linux`&&this.setLinuxTrayContextMenu(),this.tray.on(`click`' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'process.platform===`linux`?this.openNativeTrayMenu():this.onTrayButtonClick()' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress()' '3'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'openNativeTrayMenu(){if(process.platform===`linux`&&(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress()))return;' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'let e=process.platform===`linux`&&this.setLinuxTrayContextMenu?this.setLinuxTrayContextMenu():n.Menu.buildFromTemplate' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'if(process.platform===`linux`)return;e.once(`menu-will-show`' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'process.platform===`linux`&&!(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress())&&this.setLinuxTrayContextMenu?.()' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'process.platform===`linux`&&(typeof codexLinuxIsTrayEnabled!==`function`||codexLinuxIsTrayEnabled()))&&oe' '1'
}

test_linux_explicit_quit_patch_smoke() {
    info "Checking Linux explicit quit patch behavior"
    local workspace="$TMP_DIR/explicit-quit-patch"
    local extracted="$workspace/extracted"
    local output_log="$workspace/output.log"
    local bundle_body

    mkdir -p "$workspace"
    bundle_body="$(cat <<'JS'
let n=require(`electron`),i=require(`node:path`),a=require(`node:fs`);
var pb=class{getNativeTrayMenuItems(){return[{label:rB(this.appName),click:()=>{n.app.quit()}}]}};
function qB(r,o){if(o.type===`quit-app`){n.app.quit();return}return o}
n.app.on(`before-quit`,o=>{let s=BI(),c=t.sr().some(e=>e.status===`ACTIVE`);if(e||i.canQuitWithoutPrompt()||r||!s&&!c){g=!0,a.markAppQuitting();return}let l=n.app.getName();if(n.dialog.showMessageBoxSync({type:`warning`,buttons:[`Quit`,`Cancel`],defaultId:0,cancelId:1,noLink:!0,title:`Quit ${l}?`,message:`Quit ${l}?`,detail:vB({hasInProgressLocalConversation:s,hasEnabledAutomations:c})})!==0){o.preventDefault();return}i.markQuitApproved(),g=!0,a.markAppQuitting()});
n.app.on(`will-quit`,e=>{if(g=!0,!h){if(i.shouldSkipDrainBeforeQuit()){mB({hotkeyWindowLifecycleManager:c,globalDictationLifecycleManager:l,flushAndDisposeContexts:d,disposables:f});return}e.preventDefault(),h=!0,c.dispose(),l.dispose(),Promise.all([...u.values()].map(e=>e.flush())).finally(()=>{d(),f.dispose(),n.app.quit()})}});
JS
)"
    make_fake_extracted_asar "$extracted" "$bundle_body"

    node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxPrepareForExplicitQuit=()=>{codexLinuxExplicitQuitApproved=!0,codexLinuxMarkQuitInProgress()}'
    assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxShouldBypassQuitPrompt=()=>codexLinuxExplicitQuitApproved===!0'
    assert_contains "$extracted/.vite/build/main-test.js" '{label:rB(this.appName),click:()=>{typeof codexLinuxPrepareForExplicitQuit===`function`?codexLinuxPrepareForExplicitQuit():typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress(),n.app.quit()}}'
    assert_contains "$extracted/.vite/build/main-test.js" 'if(o.type===`quit-app`){typeof codexLinuxPrepareForExplicitQuit===`function`?codexLinuxPrepareForExplicitQuit():typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress(),n.app.quit();return}'
    assert_contains "$extracted/.vite/build/main-test.js" 'if((typeof codexLinuxShouldBypassQuitPrompt===`function`&&codexLinuxShouldBypassQuitPrompt())||e||i.canQuitWithoutPrompt()||r||!s&&!c){g=!0,a.markAppQuitting();return}'
    assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxFinalizeQuit=()=>{d(),f.dispose(),n.app.quit()},codexLinuxDrainPromise=Promise.all('
    assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxExplicitQuitDrainTimeoutMs'
    assert_contains "$extracted/.vite/build/main-test.js" 'setTimeout(e,typeof codexLinuxExplicitQuitDrainTimeoutMs'
    assert_not_contains "$extracted/.vite/build/main-test.js" '\`number\`'
    assert_not_contains "$output_log" 'WARN: Could not find tray quit menu handler'
    assert_not_contains "$output_log" 'WARN: Could not find quit-app IPC handler'
    assert_not_contains "$output_log" 'WARN: Could not find before-quit confirmation guard'
    assert_not_contains "$output_log" 'WARN: Could not find will-quit drain sequence'

    node - "$extracted/.vite/build/main-test.js" <<'NODE'
const fs = require("fs");

const source = fs.readFileSync(process.argv[2], "utf8");
const helperSnippet = source.match(/let codexLinuxQuitInProgress=!1,[^;]*codexLinuxShouldBypassQuitPrompt=\(\)=>codexLinuxExplicitQuitApproved===!0,[^;]*codexLinuxIsQuitInProgress=\(\)=>codexLinuxQuitInProgress===!0;/)?.[0];
const traySnippet = source.match(/\{label:rB\(this\.appName\),click:\(\)=>\{typeof codexLinuxPrepareForExplicitQuit===`function`\?codexLinuxPrepareForExplicitQuit\(\):typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress\(\),n\.app\.quit\(\)\}\}/)?.[0];
const quitAppSnippet = source.match(/if\(o\.type===`quit-app`\)\{typeof codexLinuxPrepareForExplicitQuit===`function`\?codexLinuxPrepareForExplicitQuit\(\):typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress\(\),n\.app\.quit\(\);return\}/)?.[0];
const beforeQuitSnippet = source.match(/if\(\(typeof codexLinuxShouldBypassQuitPrompt===`function`&&codexLinuxShouldBypassQuitPrompt\(\)\)\|\|e\|\|i\.canQuitWithoutPrompt\(\)\|\|r\|\|!s&&!c\)\{g=!0,a\.markAppQuitting\(\);return\}/)?.[0];
if (!helperSnippet || !traySnippet || !quitAppSnippet || !beforeQuitSnippet) {
  throw new Error("Could not extract explicit quit snippets");
}

function runTrayQuit({ withHelper = true } = {}) {
  const state = { markCalls: 0, prepareCalls: 0, quitCalls: 0 };
  const app = { quit() { state.quitCalls += 1; } };
  const mark = () => { state.markCalls += 1; };
  const prepare = withHelper ? () => { state.prepareCalls += 1; mark(); } : undefined;
  const factory = new Function(
    "n",
    "rB",
    "codexLinuxPrepareForExplicitQuit",
    "codexLinuxMarkQuitInProgress",
    `return (${traySnippet}).click;`,
  );
  const click = factory({ app }, () => "Quit", prepare, mark);
  click();
  return state;
}

function runQuitApp({ withHelper = true } = {}) {
  const state = { markCalls: 0, prepareCalls: 0, quitCalls: 0 };
  const app = { quit() { state.quitCalls += 1; } };
  const mark = () => { state.markCalls += 1; };
  const prepare = withHelper ? () => { state.prepareCalls += 1; mark(); } : undefined;
  const handler = new Function(
    "n",
    "codexLinuxPrepareForExplicitQuit",
    "codexLinuxMarkQuitInProgress",
    "o",
    `${quitAppSnippet};return null;`,
  );
  handler({ app }, prepare, mark, { type: "quit-app" });
  return state;
}

function runBeforeQuitBypass() {
  const state = { markCalls: 0 };
  const scope = new Function(
    "BI",
    "t",
    `${helperSnippet}return {runBeforeQuitCheck(e,i,r,a){let s=BI(),c=t.sr().some(e=>e.status===\`ACTIVE\`);${beforeQuitSnippet}return \`prompt\`;},prepare:codexLinuxPrepareForExplicitQuit,bypass:codexLinuxShouldBypassQuitPrompt};`,
  )(
    () => true,
    { sr: () => [{ status: "ACTIVE" }] },
  );
  const controller = {
    canQuitWithoutPrompt() { return false; },
    markQuitApproved() {},
  };
  const appQuitting = { markAppQuitting() { state.markCalls += 1; } };
  scope.prepare();
  const bypassed = scope.runBeforeQuitCheck(false, controller, false, appQuitting);
  return { state, bypassed, shouldBypass: scope.bypass() };
}

let state = runTrayQuit();
if (state.prepareCalls !== 1 || state.markCalls !== 1 || state.quitCalls !== 1) {
  throw new Error("tray quit should prepare explicit quit before quitting");
}

state = runQuitApp();
if (state.prepareCalls !== 1 || state.markCalls !== 1 || state.quitCalls !== 1) {
  throw new Error("quit-app IPC should prepare explicit quit before quitting");
}

state = runTrayQuit({ withHelper: false });
if (state.prepareCalls !== 0 || state.markCalls !== 1 || state.quitCalls !== 1) {
  throw new Error("tray quit should still fall back to the quit-in-progress marker");
}

state = runQuitApp({ withHelper: false });
if (state.prepareCalls !== 0 || state.markCalls !== 1 || state.quitCalls !== 1) {
  throw new Error("quit-app IPC should still fall back to the quit-in-progress marker");
}

state = runBeforeQuitBypass();
if (!state.shouldBypass || state.bypassed !== undefined || state.state.markCalls !== 1) {
  throw new Error("before-quit should bypass the Linux quit confirmation after an explicit quit");
}
NODE

    node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxPrepareForExplicitQuit=()=>{codexLinuxExplicitQuitApproved=!0,codexLinuxMarkQuitInProgress()}' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxShouldBypassQuitPrompt=()=>codexLinuxExplicitQuitApproved===!0' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'typeof codexLinuxPrepareForExplicitQuit===`function`?codexLinuxPrepareForExplicitQuit():typeof codexLinuxMarkQuitInProgress===`function`&&codexLinuxMarkQuitInProgress()' '2'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'typeof codexLinuxShouldBypassQuitPrompt===`function`&&codexLinuxShouldBypassQuitPrompt()' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxDrainPromise=Promise.all(' '1'
}

test_keybinds_settings_tab_patch_smoke() {
    info "Checking Keybinds settings tab patch behavior"
    local workspace="$TMP_DIR/keybinds-settings-patch"
    local extracted="$workspace/extracted"
    local output_log="$workspace/output.log"

    mkdir -p "$workspace"
    make_fake_extracted_asar "$extracted" 'let D={removeMenu(){},setMenuBarVisibility(){},setIcon(){},once(){}};let t={join(){}};let a={existsSync(){return true},statSync(){return {isFile(){return false}}}};let n={shell:{openPath(){return ""},showItemInFolder(){}}};...process.platform===`win32`?{autoHideMenuBar:!0}:{},process.platform===`win32`&&D.removeMenu(),foo)}),D.once(`ready-to-show`,()=>{var sa=Mi({id:`fileManager`,label:`Finder`,icon:`apps/finder.png`,kind:`fileManager`,darwin:{detect:()=>`open`,args:e=>ai(e)},win32:{label:`File Explorer`,icon:`apps/file-explorer.png`,detect:ca,args:e=>ai(e),open:async({path:e})=>la(e)}});function ca(){let e=1;return e}async function la(e){let t=ua(e);if(t&&(0,a.statSync)(t).isFile()){n.shell.showItemInFolder(t);return}let r=t??e,i=await n.shell.openPath(r);if(i)throw Error(i)}function ua(e){return e}var Ua=Mi({id:`systemDefault`,label:`System Default App`,icon:`apps/file-explorer.png`,kind:`systemDefault`,hidden:!0,darwin:{icon:`apps/finder.png`,detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)},win32:{detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)},linux:{detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)}});async function Wa(e){return e}'

    cat > "$extracted/webview/assets/settings-sections-test.js" <<'JS'
var e=`general-settings`,t=`mcp-settings`,n=[{slug:e},{slug:`appearance`},{slug:`git-settings`},{slug:`connections`},{slug:`local-environments`},{slug:`worktrees`},{slug:`agent`},{slug:`personalization`},{slug:`usage`},{slug:`browser-use`},{slug:`computer-use`},{slug:t},{slug:`plugins-settings`},{slug:`skills-settings`},{slug:`data-controls`}],r=t;export{n,t as r,e as t};
JS
    cat > "$extracted/webview/assets/settings-shared-test.js" <<'JS'
import{t as d}from"./jsx-runtime-ebkFq_df.js";var c={"general-settings":{id:`settings.nav.general-settings`,defaultMessage:`General`,description:`Title for general settings section`},appearance:{id:`settings.nav.appearance`,defaultMessage:`Appearance`,description:`Title for appearance settings section`}};function m(e){let t=(0,u.c)(17),{slug:r}=e;switch(r){case`appearance`:{let e;return t[1]===Symbol.for(`react.memo_cache_sentinel`)?(e=(0,d.jsx)(n,{id:`settings.section.appearance`,defaultMessage:`Appearance`,description:`Title for appearance settings section`}),t[1]=e):e=t[1],e}case`general-settings`:{let e;return t[2]===Symbol.for(`react.memo_cache_sentinel`)?(e=(0,d.jsx)(n,{id:`settings.section.general-settings`,defaultMessage:`General`,description:`Title for general settings section`}),t[2]=e):e=t[2],e}}}
JS
    cat > "$extracted/webview/assets/index-test.js" <<'JS'
var Xge={"general-settings":xh,appearance:Pf,agent:gU},H7={},Zge=[`general-settings`,`appearance`,`agent`,`personalization`,`mcp-settings`,`connections`,`git-settings`,`local-environments`,`worktrees`,`browser-use`,`computer-use`,`data-controls`],Qge=[{key:`app`,heading:H7.appHeading,slugs:[`general-settings`,`appearance`,`connections`,`git-settings`,`usage`]}];function n_e(){let l=`electron`,e=e=>{switch(e.slug){case`appearance`:case`git-settings`:case`worktrees`:case`local-environments`:case`data-controls`:case`environments`:return l===`electron`;case`account`:case`general-settings`:case`agent`:case`personalization`:case`mcp-settings`:return!0}};if(O)bb0:switch(D.slug){case`usage`:k=g;break bb0;case`appearance`:case`general-settings`:case`agent`:case`git-settings`:case`account`:case`data-controls`:case`personalization`:k=!1;break bb0;}}function s_e(e){let{slug:n}=e,r=c_e[n];return (0,$.jsx)(r,{})}var c_e={"general-settings":(0,Z.lazy)(()=>s(()=>import(`./general-settings-DZbwMmWz.js`).then(e=>({default:e.GeneralSettings})),__vite__mapDeps([4]),import.meta.url)),appearance:(0,Z.lazy)(()=>s(()=>import(`./appearance-settings-D4xYjo5o.js`).then(e=>({default:e.AppearanceSettings})),__vite__mapDeps([56]),import.meta.url)),agent:(0,Z.lazy)(()=>Promise.resolve({default:l_e}))};
JS

    node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_file_exists "$extracted/webview/assets/keybinds-settings-linux.js"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "function KeybindsSettings"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "HotkeyWindowHotkeyRow"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "DEFAULT_SHORTCUTS"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "codex-linux-keybind-overrides"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "function ShortcutInput"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "data-codex-keybind-input"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "newThread"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "openFolder"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "toggleTerminal"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "toggleDiffPanel"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "thread9"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "CmdOrCtrl+Alt+N"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "CmdOrCtrl+Shift+O"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "CmdOrCtrl+G"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "CmdOrCtrl+J"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "CmdOrCtrl+Alt+Shift+C"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "navigateBack"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "navigateForward"
    assert_not_contains "$extracted/webview/assets/keybinds-settings-linux.js" "CmdOrCtrl+Shift+K"
    assert_not_contains "$extracted/webview/assets/keybinds-settings-linux.js" "Ctrl+\`"
    assert_not_contains "$extracted/webview/assets/keybinds-settings-linux.js" "newWindow"
    assert_not_contains "$extracted/webview/assets/keybinds-settings-linux.js" "openThreadOverlay"
    assert_not_contains "$extracted/webview/assets/keybinds-settings-linux.js" "openAvatarOverlay"
    assert_not_contains "$extracted/webview/assets/keybinds-settings-linux.js" "toggleTraceRecording"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "codex-linux-system-tray-enabled"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "codex-linux-warm-start-enabled"
    assert_contains "$extracted/webview/assets/keybinds-settings-linux.js" "codex-linux-prompt-window-enabled"
    assert_contains "$extracted/webview/assets/settings-sections-test.js" 'slug:`keybinds`'
    assert_contains "$extracted/webview/assets/settings-shared-test.js" "settings.nav.keybinds"
    assert_contains "$extracted/webview/assets/settings-shared-test.js" "settings.section.keybinds"
    assert_contains "$extracted/webview/assets/index-test.js" "keybinds-settings-linux.js"
    assert_contains "$extracted/webview/assets/index-test.js" "keybinds:xh"
    assert_contains "$extracted/webview/assets/index-test.js" 'Zge=\[`general-settings`,`keybinds`'
    assert_contains "$extracted/webview/assets/index-test.js" 'slugs:\[`general-settings`,`keybinds`'
    assert_contains "$extracted/webview/assets/index-test.js" 'case`keybinds`:return l===`electron`'
    assert_contains "$extracted/webview/assets/index-test.js" "codexLinuxKeybindOverridesRuntime"
    assert_contains "$extracted/webview/assets/index-test.js" "codex-linux-keybind-overrides"
    assert_contains "$extracted/webview/assets/index-test.js" "go-to-thread-index"
    assert_contains "$extracted/webview/assets/index-test.js" "newThreadAlt"
    assert_contains "$extracted/webview/assets/index-test.js" "new-chat"
    assert_contains "$extracted/webview/assets/index-test.js" "toggle-terminal"
    assert_contains "$extracted/webview/assets/index-test.js" "toggle-diff-panel"
    assert_contains "$extracted/webview/assets/index-test.js" "isShortcutCaptureTarget"
    assert_contains "$extracted/webview/assets/index-test.js" "data-codex-keybind-input"
    assert_not_contains "$extracted/webview/assets/index-test.js" "isEditableTarget(event))return"
    assert_not_contains "$extracted/webview/assets/index-test.js" "ac(id)"

    node - "$extracted/webview/assets/index-test.js" <<'NODE'
const fs = require("fs");
const vm = require("vm");
const file = process.argv[2];
const source = fs.readFileSync(file, "utf8");
const marker = ";function codexLinuxKeybindOverridesRuntime()";
const start = source.indexOf(marker);
if (start === -1) throw new Error("missing runtime patch");
const runtime = source
  .slice(start)
  .replace("codexLinuxKeybindOverridesRuntime();", "globalThis.codexLinuxKeybindOverridesRuntime=codexLinuxKeybindOverridesRuntime;");
const listeners = {};
const calls = [];
class FakeElement {
  constructor(isKeybindInput = false) {
    this.isKeybindInput = isKeybindInput;
  }
  closest(selector) {
    return selector === "[data-codex-keybind-input]" && this.isKeybindInput ? this : null;
  }
}
const context = {
  window: { addEventListener: (event, fn) => (listeners[event] ??= []).push(fn) },
  Element: FakeElement,
  navigator: { platform: "Linux x86_64" },
  localStorage: { getItem: () => JSON.stringify({ toggleFileTreePanel: "Ctrl+E" }) },
  Ct: { toggleFileTreePanel: "Command+Shift+E" },
  E: {
    dispatchHostMessage: (message) => calls.push(message),
    dispatchMessage: () => {},
  },
  globalThis: null,
};
context.globalThis = context;
vm.runInNewContext(runtime, context);
context.codexLinuxKeybindOverridesRuntime();
const makeEvent = (target) => ({
  defaultPrevented: false,
  repeat: false,
  target,
  ctrlKey: true,
  altKey: false,
  shiftKey: false,
  metaKey: false,
  key: "e",
  preventDefault() {
    this.defaultPrevented = true;
  },
  stopPropagation() {
    this.stopped = true;
  },
});
const composerEvent = makeEvent(new FakeElement(false));
listeners.keydown[0](composerEvent);
if (calls.length !== 1 || calls[0].type !== "toggle-file-tree-panel" || !composerEvent.defaultPrevented) {
  throw new Error("Ctrl+E override did not dispatch from composer-like target");
}
const keybindInputEvent = makeEvent(new FakeElement(true));
listeners.keydown[0](keybindInputEvent);
if (calls.length !== 1 || keybindInputEvent.defaultPrevented) {
  throw new Error("keybind capture input should not dispatch runtime override");
}
NODE

    node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_occurrence_count "$extracted/webview/assets/settings-sections-test.js" 'slug:`keybinds`' '1'
    assert_occurrence_count "$extracted/webview/assets/settings-shared-test.js" "settings.nav.keybinds" '1'
    assert_occurrence_count "$extracted/webview/assets/settings-shared-test.js" "settings.section.keybinds" '1'
    assert_occurrence_count "$extracted/webview/assets/index-test.js" "keybinds-settings-linux.js" '1'
    assert_occurrence_count "$extracted/webview/assets/index-test.js" "keybinds:xh" '1'
    assert_occurrence_count "$extracted/webview/assets/index-test.js" "function codexLinuxKeybindOverridesRuntime" '1'
}

test_keybinds_settings_patch_warns_on_bundle_shape_miss() {
    info "Checking Keybinds settings bundle-shape warning"
    local workspace="$TMP_DIR/keybinds-settings-shape-warning"
    local extracted="$workspace/extracted"
    local output_log="$workspace/output.log"

    mkdir -p "$workspace"
    make_fake_extracted_asar "$extracted" 'let D={removeMenu(){},setMenuBarVisibility(){},setIcon(){},once(){}};let t={join(){}};let a={existsSync(){return true},statSync(){return {isFile(){return false}}}};let n={shell:{openPath(){return ""},showItemInFolder(){}}};...process.platform===`win32`?{autoHideMenuBar:!0}:{},process.platform===`win32`&&D.removeMenu(),foo)}),D.once(`ready-to-show`,()=>{var sa=Mi({id:`fileManager`,label:`Finder`,icon:`apps/finder.png`,kind:`fileManager`,darwin:{detect:()=>`open`,args:e=>ai(e)},win32:{label:`File Explorer`,icon:`apps/file-explorer.png`,detect:ca,args:e=>ai(e),open:async({path:e})=>la(e)}});function ca(){let e=1;return e}async function la(e){let t=ua(e);if(t&&(0,a.statSync)(t).isFile()){n.shell.showItemInFolder(t);return}let r=t??e,i=await n.shell.openPath(r);if(i)throw Error(i)}function ua(e){return e}var Ua=Mi({id:`systemDefault`,label:`System Default App`,icon:`apps/file-explorer.png`,kind:`systemDefault`,hidden:!0,darwin:{icon:`apps/finder.png`,detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)},win32:{detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)},linux:{detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)}});async function Wa(e){return e}'
    rm "$extracted/webview/assets/settings-row-test.js"
    cat > "$extracted/webview/assets/settings-sections-test.js" <<'JS'
var e=`general-settings`,t=`mcp-settings`,n=[{slug:e},{slug:`appearance`}],r=t;export{n,t as r,e as t};
JS
    cat > "$extracted/webview/assets/settings-shared-test.js" <<'JS'
var c={"general-settings":{id:`settings.nav.general-settings`,defaultMessage:`General`,description:`Title for general settings section`}};function m(e){let t=(0,u.c)(17),{slug:r}=e;switch(r){case`general-settings`:{let e;return t[2]===Symbol.for(`react.memo_cache_sentinel`)?(e=(0,d.jsx)(n,{id:`settings.section.general-settings`,defaultMessage:`General`,description:`Title for general settings section`}),t[2]=e):e=t[2],e}}}
JS
    cat > "$extracted/webview/assets/index-test.js" <<'JS'
var Xge={"general-settings":xh,appearance:Pf},H7={},Zge=[`general-settings`,`appearance`],Qge=[{key:`app`,heading:H7.appHeading,slugs:[`general-settings`,`appearance`,`connections`,`git-settings`,`usage`]}];function n_e(){let l=`electron`,e=e=>{switch(e.slug){case`appearance`:case`git-settings`:case`worktrees`:case`local-environments`:case`data-controls`:case`environments`:return l===`electron`;case`account`:case`general-settings`:return!0}};if(O)bb0:switch(D.slug){case`appearance`:case`general-settings`:k=!1;break bb0;}}var c_e={"general-settings":(0,Z.lazy)(()=>s(()=>import(`./general-settings-DZbwMmWz.js`).then(e=>({default:e.GeneralSettings})),__vite__mapDeps([4]),import.meta.url))};
JS

    node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_contains "$output_log" "WARN: Keybinds settings patch skipped"
    assert_contains "$output_log" "could not find settings row asset"
    [ ! -f "$extracted/webview/assets/keybinds-settings-linux.js" ] || fail "Keybinds asset should not be written when bundle shape is missing"
    assert_not_contains "$extracted/webview/assets/settings-sections-test.js" 'slug:`keybinds`'
    assert_not_contains "$extracted/webview/assets/index-test.js" "keybinds-settings-linux.js"
}

test_browser_annotation_screenshot_patch_smoke() {
    info "Checking browser annotation screenshot patch behavior"
    local workspace="$TMP_DIR/browser-annotation-patch"
    local extracted="$workspace/extracted"
    local output_log="$workspace/output.log"

    mkdir -p "$workspace"
    make_fake_extracted_asar "$extracted" 'let D={removeMenu(){},setMenuBarVisibility(){},setIcon(){},once(){}};let n=require(`electron`),t=require(`node:path`),a=require(`node:fs`);...process.platform===`win32`?{autoHideMenuBar:!0}:{},process.platform===`win32`&&D.removeMenu(),foo)}),D.once(`ready-to-show`,()=>{})'
    cat > "$extracted/.vite/build/comment-preload.js" <<'JS'
if(ve&&M?.anchor.kind===`element`){let e=hl(M,y.current)??null,t=e==null?null:El(e);ke=t?.rect??Rl(M.anchor),je=t?.borderRadius,Ae=Xl(M.anchor,ke,_.width,_.height)}
Se=(!ve&&xe!=null?k.filter(e=>e.id!==xe.id):k).flatMap
JS

    node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_contains "$extracted/.vite/build/comment-preload.js" 'if(ve&&M?.anchor.kind===`element`){ke=Rl(M.anchor),je=void 0,Ae=Xl(M.anchor,ke,_.width,_.height)}'
    assert_contains "$extracted/.vite/build/comment-preload.js" 'Se=(ve?_e:!ve&&xe!=null?k.filter(e=>e.id!==xe.id):k).flatMap'
    assert_not_contains "$extracted/.vite/build/comment-preload.js" 'hl(M,y.current)'

    node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_occurrence_count "$extracted/.vite/build/comment-preload.js" 'ke=Rl(M.anchor)' '1'
    assert_occurrence_count "$extracted/.vite/build/comment-preload.js" 'Se=(ve?_e' '1'
}

test_linux_single_instance_patch_smoke() {
    info "Checking Linux single-instance patch behavior"
    local workspace="$TMP_DIR/single-instance-patch"
    local extracted="$workspace/extracted"
    local output_log="$workspace/output.log"
    local bundle_body

    mkdir -p "$workspace"
    bundle_body="$(cat <<'JS'
let S=globalThis.__codexSmoke;
let n={app:{whenReady(){return Promise.resolve()},quit(){S.quitCount++},requestSingleInstanceLock(){S.lockCount++;return true},on(e,t){S.appHandlers[e]=t},off(e,t){S.offHandlers[e]=t}}};
let t={Er(){return {info(){}}},jn:class{add(e){S.disposables.push(e)}},y(){return{setSecondInstanceArgsHandler:e=>{S.initialHandler=e}}},g(e){return e},t(e){return Array.isArray(e)&&e.includes(`--open-project`)}};
let i={default:{dirname(e){S.dirnameCalls.push(e);return `/tmp`}}},o={mkdirSync(...e){S.mkdirSyncCalls.push(e)},rmSync(...e){S.rmSyncCalls.push(e)}},u={default:{createServer(e){S.createServerCalls++;S.socketConnectionHandler=e;return S.socketServer}}};
async function uT(){let{setSecondInstanceArgsHandler:l}=t.y(),k=new t.jn;k.add(()=>{}),t.Er().info(`Launching app`,{safe:{agentRunId:process.env.CODEX_ELECTRON_AGENT_RUN_ID?.trim()||null}});let A=Date.now();await n.app.whenReady();let w=(...e)=>{S.traceCalls.push(e)},M={globalState:S.globalState,repoRoot:`/tmp/codex-smoke`},z=`local`,R={deepLinks:{queueProcessArgs(e){S.queueArgs.push(e);return Array.isArray(e)&&e.some(e=>{let t=String(e);return t.startsWith(`codex://`)||t.startsWith(`codex-browser-sidebar://`)})},flushPendingDeepLinks(){S.flushPendingDeepLinksCalls++;return Promise.resolve()}},navigateToRoute(e,t){S.navigateCalls.push({windowId:e.id,path:t})}},P={windowManager:{sendMessageToWindow(e,t){S.messages.push({windowId:e.id,message:t})}},hotkeyWindowLifecycleManager:{hide(){S.hideCalls++},show(){S.showCalls++;return S.hotkeyWindowShowResult},ensureHotkeyWindowController(){S.ensureHotkeyWindowControllerCalls++;return S.hotkeyWindowController}},getPrimaryWindow(){return S.primaryWindow},createFreshLocalWindow(e){S.createFreshLocalWindowCalls.push(e);return S.createdWindow},ensureHostWindow(e){S.ensureHostWindowCalls.push(e);return S.primaryWindow??S.createdWindow}},g={reportNonFatal(e,t){S.errors.push({error:String(e),meta:t})}},re=e=>{S.focusCalls.push(e.id);e.isMinimized()&&e.restore(),e.show(),e.focus()},ie=async()=>{S.ieCalls++;try{P.hotkeyWindowLifecycleManager.hide();let e=P.getPrimaryWindow()??await P.createFreshLocalWindow(`/`);if(e==null)return;re(e)}catch(e){g.reportNonFatal(e instanceof Error?e:`Failed to open window on second instance`,{kind:`second-instance-open-window-failed`})}};l(e=>{let n=t.t(t.g(e));if(R.deepLinks.queueProcessArgs(e)){n&&ie();return}if(n){ie();return}ie()});let ae=async(e,t)=>{P.hotkeyWindowLifecycleManager.hide();let n=P.getPrimaryWindow(),r=n??await P.createFreshLocalWindow(e);r!=null&&(n!=null&&t.navigateExistingWindow&&R.navigateToRoute(r,e),re(r))},oe=async()=>{S.trayStartupCalls++};let E=process.platform===`win32`;E&&oe();let me=await P.ensureHostWindow(z);me&&re(me),w(`local window ensured`,A,{hostId:z,localWindowVisible:me?.isVisible()??!1}),A=Date.now(),await R.deepLinks.flushPendingDeepLinks()}
JS
)"
    make_fake_extracted_asar "$extracted" "$bundle_body"

    node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_contains "$extracted/.vite/build/main-test.js" 'process.platform===`linux`&&process.env.CODEX_LINUX_MULTI_LAUNCH!==`1`&&!n.app.requestSingleInstanceLock()'
    assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxHandleLaunchActionArgs'
    assert_contains "$extracted/.vite/build/main-test.js" 'e.includes(`--new-chat`)'
    assert_contains "$extracted/.vite/build/main-test.js" 'e.includes(`--quick-chat`)'
    assert_contains "$extracted/.vite/build/main-test.js" 'e.includes(`--prompt-chat`)'
    assert_contains "$extracted/.vite/build/main-test.js" 'e.includes(`--hotkey-window`)'
    assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxHasDeepLink'
    assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxShowHotkeyWindow'
    assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxGetHotkeyWindowController'
    assert_contains "$extracted/.vite/build/main-test.js" 'ensureHotkeyWindowController'
    assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxPrewarmHotkeyWindow'
    assert_contains "$extracted/.vite/build/main-test.js" 'codexLinuxStartLaunchActionSocket'
    assert_contains "$extracted/.vite/build/main-test.js" 'CODEX_DESKTOP_LAUNCH_ACTION_SOCKET'
    assert_contains "$extracted/.vite/build/main-test.js" 'e.openHome'
    assert_contains "$extracted/.vite/build/main-test.js" 'e.prewarm'
    assert_contains "$extracted/.vite/build/main-test.js" 'type:`new-quick-chat`'

    node - "$extracted/.vite/build/main-test.js" <<'NODE'
const fs = require("fs");
const vm = require("vm");

const source = fs.readFileSync(process.argv[2], "utf8");
let state = makeState();

function makeState(settings = {}) {
  const next = {
    appHandlers: Object.create(null),
    offHandlers: Object.create(null),
    disposables: [],
    initialHandler: null,
    lockCount: 0,
    quitCount: 0,
    globalStateGetKeys: [],
    linuxSettings: {
      promptChatEnabled: true,
      warmStartEnabled: true,
      trayEnabled: true,
      ...settings,
    },
  };

  next.globalState = {
    get(key) {
      next.globalStateGetKeys.push(String(key));
      return linuxSettingForKey(next, key);
    },
  };

  return next;
}

function linuxSettingsAtom(settings) {
  return {
    "settings.keybinds.promptChatEnabled": settings.promptChatEnabled,
    "settings.keybinds.promptChat": settings.promptChatEnabled,
    "settings.keybinds.hotkeyWindowEnabled": settings.promptChatEnabled,
    "settings.keybinds.warmStartEnabled": settings.warmStartEnabled,
    "settings.keybinds.warmStart": settings.warmStartEnabled,
    "settings.keybinds.launchActionSocketEnabled": settings.warmStartEnabled,
    "settings.keybinds.trayEnabled": settings.trayEnabled,
    "settings.keybinds.tray": settings.trayEnabled,
    "settings.linux.promptChatEnabled": settings.promptChatEnabled,
    "settings.linux.warmStartEnabled": settings.warmStartEnabled,
    "settings.linux.trayEnabled": settings.trayEnabled,
  };
}

function linuxSettingForKey(next, key) {
  const keyText = String(key).toLowerCase();
  const settings = next.linuxSettings;

  if (keyText.includes("persisted") || keyText === "electron-persisted-atom-state") {
    return linuxSettingsAtom(settings);
  }

  if (keyText.includes("keybind") && !keyText.includes("prompt") && !keyText.includes("hotkey") && !keyText.includes("warm") && !keyText.includes("launch") && !keyText.includes("socket") && !keyText.includes("tray")) {
    return {
      promptChatEnabled: settings.promptChatEnabled,
      hotkeyWindowEnabled: settings.promptChatEnabled,
      warmStartEnabled: settings.warmStartEnabled,
      launchActionSocketEnabled: settings.warmStartEnabled,
      trayEnabled: settings.trayEnabled,
    };
  }

  if (keyText.includes("prompt") || keyText.includes("hotkey")) {
    return settings.promptChatEnabled;
  }

  if (keyText.includes("warm") || keyText.includes("socket") || keyText.includes("launch")) {
    return settings.warmStartEnabled;
  }

  if (keyText.includes("tray")) {
    return settings.trayEnabled;
  }

  return null;
}

function makeWindow(id) {
  return {
    id,
    isMinimized() {
      state.windowCalls.push(`${id}:isMinimized`);
      return false;
    },
    isVisible() {
      state.windowCalls.push(`${id}:isVisible`);
      return true;
    },
    restore() {
      state.windowCalls.push(`${id}:restore`);
    },
    show() {
      state.windowCalls.push(`${id}:show`);
    },
    focus() {
      state.windowCalls.push(`${id}:focus`);
    },
  };
}

function resetCalls() {
  const existingCreateServerCalls = state.createServerCalls ?? 0;
  const existingSocketConnectionHandler = state.socketConnectionHandler ?? null;
  const existingSocketListenCalls = state.socketListenCalls ?? [];
  const existingSocketServerHandlers = state.socketServerHandlers ?? Object.create(null);
  state.queueArgs = [];
  state.navigateCalls = [];
  state.messages = [];
  state.hideCalls = 0;
  state.showCalls = 0;
  state.controllerShowCalls = 0;
  state.hotkeyWindowShowResult = true;
  state.openHomeCalls = 0;
  state.hotkeyWindowOpenHomeResult = undefined;
  state.prewarmCalls = 0;
  state.prewarmThrows = false;
  state.ensureHotkeyWindowControllerCalls = 0;
  state.hotkeyWindowController = {
    show() {
      state.controllerShowCalls++;
      return state.hotkeyWindowShowResult;
    },
    openHome() {
      state.openHomeCalls++;
      return state.hotkeyWindowOpenHomeResult;
    },
    prewarm() {
      state.prewarmCalls++;
      if (state.prewarmThrows) {
        throw new Error("prewarm failed");
      }
    },
  };
  state.ensureHostWindowCalls = [];
  state.createFreshLocalWindowCalls = [];
  state.focusCalls = [];
  state.windowCalls = [];
  state.errors = [];
  state.ieCalls = 0;
  state.traceCalls = [];
  state.flushPendingDeepLinksCalls = 0;
  state.trayStartupCalls = 0;
  state.primaryWindow = null;
  state.createdWindow = makeWindow("created");
  state.dirnameCalls = [];
  state.mkdirSyncCalls = [];
  state.rmSyncCalls = [];
  state.createServerCalls = existingCreateServerCalls;
  state.socketConnectionHandler = existingSocketConnectionHandler;
  state.socketListenCalls = existingSocketListenCalls;
  state.socketCloseCalls = 0;
  state.socketServer = {
    listen(path) {
      state.socketListenCalls.push(path);
    },
    close() {
      state.socketCloseCalls += 1;
    },
    on(event, handler) {
      state.socketServerHandlers[event] = handler;
      return this;
    },
  };
  state.socketServerHandlers = existingSocketServerHandlers;
}

function assert(condition, message) {
  if (!condition) {
    throw new Error(message);
  }
}

async function flushAsyncHandlers() {
  await new Promise((resolve) => setImmediate(resolve));
  await new Promise((resolve) => setImmediate(resolve));
}

async function boot(settings = {}, env = { CODEX_DESKTOP_LAUNCH_ACTION_SOCKET: "/tmp/codex-smoke.sock" }) {
  state = makeState(settings);
  resetCalls();
  state.primary = makeWindow("primary");

  const context = {
    console,
    process: { platform: "linux", env },
    __codexSmoke: state,
  };
  context.globalThis = context;

  vm.runInNewContext(`${source}\nglobalThis.__codexSmokeRun = uT;`, context, {
    filename: "main-test.js",
  });

  await context.__codexSmokeRun();
  return context;
}

(async () => {
  await boot();
  assert(typeof state.initialHandler === "function", "setSecondInstanceArgsHandler callback was not registered");
  assert(state.createServerCalls === 1, "warm-start launch action socket server was not created");
  assert(state.socketListenCalls.length === 1 && state.socketListenCalls[0] === "/tmp/codex-smoke.sock", "warm-start launch action socket did not listen on the configured path");
  assert(typeof state.socketConnectionHandler === "function", "warm-start launch action socket connection handler was not registered");
  assert(state.mkdirSyncCalls.length === 1, "warm-start launch action socket should create its parent runtime directory");
  assert(state.rmSyncCalls.length === 1 && state.rmSyncCalls[0][0] === "/tmp/codex-smoke.sock", "warm-start launch action socket should remove a stale socket before listening");
  assert(state.prewarmCalls === 1, "startup should prewarm the compact hotkey prompt window");
  assert(state.ensureHotkeyWindowControllerCalls === 1, "startup prewarm should use the real hotkey window controller");
  assert(state.flushPendingDeepLinksCalls === 1, "startup should still flush pending deeplinks after prewarm");
  assert(state.trayStartupCalls === 1, "startup should initialize the Linux tray when the tray gate is enabled");

  async function runSecondInstance(args) {
    state.initialHandler(args);
    await flushAsyncHandlers();
  }

  async function runInitialArgs(args) {
    state.initialHandler(args);
    await flushAsyncHandlers();
  }

  function makeSocket() {
    const handlers = Object.create(null);
    return {
      destroyed: false,
      encoding: null,
      outputs: [],
      setEncoding(encoding) {
        this.encoding = encoding;
      },
      on(event, handler) {
        handlers[event] = handler;
        return this;
      },
      emit(event, payload) {
        if (handlers[event]) {
          handlers[event](payload);
        }
      },
      end(output) {
        this.outputs.push(output);
      },
      destroy() {
        this.destroyed = true;
      },
    };
  }

  async function runSocketArgs(args) {
    const socket = makeSocket();
    state.socketConnectionHandler(socket);
    socket.emit("data", `${JSON.stringify({ argv: args })}\n`);
    await flushAsyncHandlers();
    return socket;
  }

  resetCalls();
  state.primaryWindow = state.primary;
  await runSecondInstance(["codex-desktop", "--new-chat"]);
  assert(state.queueArgs.length === 0, "--new-chat without a deeplink should not be consumed by deeplink routing");
  assert(state.createFreshLocalWindowCalls.length === 0, "--new-chat should reuse the warm primary window");
  assert(state.focusCalls.length === 1 && state.focusCalls[0] === "primary", "--new-chat should focus the warm primary window");
  assert(state.navigateCalls.length === 1 && state.navigateCalls[0].path === "/", "--new-chat should navigate the warm primary window to /");
  assert(state.messages.length === 0, "--new-chat should not send a quick-chat message");

  resetCalls();
  state.primaryWindow = state.primary;
  await runSecondInstance(["codex-desktop", "--quick-chat"]);
  assert(state.queueArgs.length === 0, "--quick-chat without a deeplink should not be consumed by deeplink routing");
  assert(state.createFreshLocalWindowCalls.length === 0, "--quick-chat should reuse the warm primary window");
  assert(state.focusCalls.length === 1 && state.focusCalls[0] === "primary", "--quick-chat should focus the warm primary window");
  assert(state.messages.length === 1 && state.messages[0].windowId === "primary" && state.messages[0].message.type === "new-quick-chat", "--quick-chat should send new-quick-chat to the warm primary window");
  assert(state.navigateCalls.length === 0, "--quick-chat should not navigate by route");

  resetCalls();
  state.primaryWindow = state.primary;
  await runSecondInstance(["codex-desktop", "--prompt-chat"]);
  assert(state.queueArgs.length === 0, "--prompt-chat without a deeplink should not be consumed by deeplink routing");
  assert(state.openHomeCalls === 1, "--prompt-chat should open the compact hotkey prompt on the new-chat home surface");
  assert(state.ensureHotkeyWindowControllerCalls === 1, "--prompt-chat should use the real hotkey window controller");
  assert(state.showCalls === 0, "--prompt-chat should not reopen the last hotkey surface");
  assert(state.controllerShowCalls === 0, "--prompt-chat should not call the controller show fallback");
  assert(state.ensureHostWindowCalls.length === 0, "--prompt-chat should not open the main window when the hotkey prompt shows");
  assert(state.hideCalls === 0, "--prompt-chat should not hide the hotkey window before showing it");
  assert(state.focusCalls.length === 0, "--prompt-chat should not focus the main window");

  resetCalls();
  state.primaryWindow = state.primary;
  await runSecondInstance(["codex-desktop", "--hotkey-window"]);
  assert(state.openHomeCalls === 1, "--hotkey-window should open the compact hotkey prompt on the new-chat home surface");
  assert(state.ensureHotkeyWindowControllerCalls === 1, "--hotkey-window should use the real hotkey window controller");
  assert(state.ensureHostWindowCalls.length === 0, "--hotkey-window should not open the main window when the compact prompt shows");

  resetCalls();
  state.primaryWindow = state.primary;
  let socket = await runSocketArgs(["codex-desktop", "--prompt-chat"]);
  assert(socket.outputs[0] === "ok\n", "warm-start socket should acknowledge handled prompt args");
  assert(state.openHomeCalls === 1, "warm-start socket should open the compact prompt on the new-chat home surface");
  assert(state.ensureHotkeyWindowControllerCalls === 1, "warm-start socket prompt should use the real hotkey window controller");
  assert(state.focusCalls.length === 0, "warm-start socket prompt should not focus the main window");

  resetCalls();
  state.primaryWindow = state.primary;
  socket = await runSocketArgs(["codex://thread/abc", "--prompt-chat"]);
  assert(socket.outputs[0] === "ok\n", "warm-start socket should acknowledge deeplink args");
  assert(state.queueArgs.length === 1, "warm-start socket should check deeplinks before prompt flags");
  assert(state.openHomeCalls === 0, "warm-start socket should not open the prompt when a deeplink is present");

  resetCalls();
  socket = await runSocketArgs(["codex-desktop"]);
  assert(socket.outputs[0] === "ok\n", "warm-start socket should acknowledge fallback focus args");
  assert(state.ieCalls === 1, "warm-start socket should use the focus fallback for args without launch flags");

  resetCalls();
  state.primaryWindow = state.primary;
  await runSecondInstance(["codex://thread/abc", "--quick-chat"]);
  assert(state.queueArgs.length === 1, "deeplink+flag should check deeplinks");
  assert(state.messages.length === 0, "deeplink+flag should not open quick chat");
  assert(state.navigateCalls.length === 0, "deeplink+flag should not navigate to /");
  assert(state.ieCalls === 0, "deeplink+flag should not fall back to focus");

  resetCalls();
  state.primaryWindow = state.primary;
  await runSecondInstance(["codex-browser-sidebar://open", "--quick-chat"]);
  assert(state.queueArgs.length === 1, "browser-sidebar deeplink+flag should check deeplinks");
  assert(state.messages.length === 0, "browser-sidebar deeplink+flag should not open quick chat");
  assert(state.navigateCalls.length === 0, "browser-sidebar deeplink+flag should not navigate to /");
  assert(state.ieCalls === 0, "browser-sidebar deeplink+flag should not fall back to focus");

  resetCalls();
  state.primaryWindow = state.primary;
  await runSecondInstance(["codex://thread/abc", "--prompt-chat"]);
  assert(state.queueArgs.length === 1, "deeplink+prompt flag should check deeplinks first");
  assert(state.openHomeCalls === 0, "deeplink+prompt flag should not open the compact prompt");
  assert(state.showCalls === 0, "deeplink+prompt flag should not show the compact prompt");
  assert(state.ensureHostWindowCalls.length === 0, "deeplink+prompt flag should not fall back to the host window");

  resetCalls();
  await runSecondInstance(["codex-desktop"]);
  assert(state.queueArgs.length === 0, "no-flag args without a deeplink should not be consumed by deeplink routing");
  assert(state.ieCalls === 1, "no-flag args should use the focus fallback");
  assert(state.createFreshLocalWindowCalls.length === 1 && state.createFreshLocalWindowCalls[0] === "/", "fallback should create the default window");

  resetCalls();
  state.primaryWindow = state.primary;
  await runInitialArgs(["codex-desktop", "--quick-chat"]);
  assert(state.createFreshLocalWindowCalls.length === 0, "initial argv handler should reuse an existing primary window");
  assert(state.messages.length === 1 && state.messages[0].windowId === "primary" && state.messages[0].message.type === "new-quick-chat", "initial argv handler should open quick chat in the existing primary window");

  resetCalls();
  state.primaryWindow = state.primary;
  await runInitialArgs(["codex-desktop", "--prompt-chat"]);
  assert(state.openHomeCalls === 1, "initial argv handler should open the compact prompt on the new-chat home surface");
  assert(state.ensureHotkeyWindowControllerCalls === 1, "initial argv handler should use the real hotkey window controller");
  assert(state.showCalls === 0, "initial argv handler should not reopen the last hotkey surface");
  assert(state.ensureHostWindowCalls.length === 0, "initial argv handler should not open the main window when the compact prompt shows");

  resetCalls();
  await runInitialArgs(["codex-desktop", "--quick-chat"]);
  assert(state.createFreshLocalWindowCalls.length === 1 && state.createFreshLocalWindowCalls[0] === "/", "initial argv handler should create a window when no primary exists");
  assert(state.messages.length === 1 && state.messages[0].windowId === "created" && state.messages[0].message.type === "new-quick-chat", "initial argv handler should open quick chat in the created window when no primary exists");

  resetCalls();
  state.primaryWindow = state.primary;
  await runInitialArgs(["codex-desktop", "--new-chat"]);
  assert(state.createFreshLocalWindowCalls.length === 0, "initial --new-chat should reuse a warm primary window");
  assert(state.navigateCalls.length === 1 && state.navigateCalls[0].path === "/", "initial --new-chat should navigate an existing window to /");
  assert(state.focusCalls.length === 1 && state.focusCalls[0] === "primary", "initial --new-chat should focus the main window");

  await boot({ promptChatEnabled: false });
  resetCalls();
  state.primaryWindow = state.primary;
  await runSecondInstance(["codex://thread/abc", "--prompt-chat"]);
  assert(state.queueArgs.length === 1, "deeplink priority should still win when the prompt-chat gate is disabled");
  assert(state.openHomeCalls === 0, "disabled prompt-chat gate should not open the compact prompt for deeplink args");
  assert(state.ieCalls === 0, "deeplink args should not fall back to main-window focus when the prompt-chat gate is disabled");

  resetCalls();
  state.primaryWindow = state.primary;
  await runSecondInstance(["codex-desktop", "--prompt-chat"]);
  assert(state.queueArgs.length === 0, "disabled prompt-chat args without a deeplink should not be consumed by deeplink routing");
  assert(state.openHomeCalls === 0, "disabled prompt-chat gate should not open the compact prompt");
  assert(state.ensureHotkeyWindowControllerCalls === 0, "disabled prompt-chat gate should not create the hotkey window controller");
  assert(state.ieCalls === 1, "disabled prompt-chat gate should fall back to main-window focus");
  assert(state.focusCalls.length === 1 && state.focusCalls[0] === "primary", "disabled prompt-chat fallback should focus the warm primary window");

  resetCalls();
  state.primaryWindow = state.primary;
  await runSecondInstance(["codex-desktop", "--hotkey-window"]);
  assert(state.openHomeCalls === 0, "disabled prompt-chat gate should also block --hotkey-window prompt opening");
  assert(state.ensureHotkeyWindowControllerCalls === 0, "disabled prompt-chat gate should not create a controller for --hotkey-window");
  assert(state.ieCalls === 1, "disabled --hotkey-window should fall back to main-window focus");

  await boot({ warmStartEnabled: false }, { CODEX_DESKTOP_LAUNCH_ACTION_SOCKET: "/tmp/codex-disabled.sock" });
  assert(state.createServerCalls === 0, "disabled warm-start gate should not create the launch-action socket server");
  assert(state.socketListenCalls.length === 0, "disabled warm-start gate should not listen on the launch-action socket");
  assert(state.socketConnectionHandler == null, "disabled warm-start gate should not register a socket connection handler");

  await boot({ trayEnabled: false });
  assert(state.trayStartupCalls === 0, "disabled tray gate should not start the Linux tray during startup");
})().catch((error) => {
  console.error(error.stack || error);
  process.exit(1);
});
NODE

    node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_occurrence_count "$extracted/.vite/build/main-test.js" '!n.app.requestSingleInstanceLock()' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxQuitInProgress=!1' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxIsQuitInProgress=()=>codexLinuxQuitInProgress===!0' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxHandleLaunchActionArgs=' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxHandleLaunchActionArgs=async e=>(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress())?!0:' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxHandleLaunchActionArgsFallback=(e,t)=>{if(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress())return;' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'e.includes(`--new-chat`)' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'e.includes(`--quick-chat`)' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'e.includes(`--prompt-chat`)' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'e.includes(`--hotkey-window`)' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxShowHotkeyWindow=' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxGetHotkeyWindowController=' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxPrewarmHotkeyWindow=' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxStartLaunchActionSocket=' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxOpenQuickChat=' '1'
    assert_occurrence_count "$extracted/.vite/build/main-test.js" 'codexLinuxPrewarmHotkeyWindow()' '1'
}

test_linux_computer_use_gate_patch_smoke() {
    info "Checking Linux Computer Use plugin gate patch behavior"
    local workspace="$TMP_DIR/computer-use-gate-patch"
    local extracted="$workspace/extracted"
    local output_log="$workspace/output.log"
    local bundle_body

    mkdir -p "$workspace"
    bundle_body="$(cat <<'JS'
let n={app:{whenReady(){},quit(){},requestSingleInstanceLock(){},on(){},off(){}}};
let Qt=`openai-bundled`,$t=`browser-use`,en=`chrome-internal`,tn=`computer-use`,nn=`latex-tectonic`;
var $n=[{forceReload:!0,installWhenMissing:!0,name:$t,isEnabled:({features:e})=>e.browserAgentAvailable,migrate:cn},{name:en,isEnabled:({buildFlavor:e})=>rn(e)},{name:tn,isEnabled:({features:e,platform:t})=>t===`darwin`&&e.computerUse,migrate:wn},{name:nn,isEnabled:()=>!0}];
JS
)"
    make_fake_extracted_asar "$extracted" "$bundle_body"

    node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_contains "$extracted/.vite/build/main-test.js" '(t===`darwin`||t===`linux`)&&e.computerUse'
    assert_not_contains "$extracted/.vite/build/main-test.js" 't===`darwin`&&e.computerUse'

    node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_occurrence_count "$extracted/.vite/build/main-test.js" '(t===`darwin`||t===`linux`)&&e.computerUse' '1'
}

test_linux_computer_use_ui_opt_in_smoke() {
    info "Checking Linux Computer Use UI opt-in gating"
    local workspace="$TMP_DIR/computer-use-ui-opt-in"
    local extracted="$workspace/extracted"
    local fake_home="$workspace/home"
    local output_log="$workspace/output.log"
    local main_bundle="$extracted/.vite/build/main-test.js"
    local renderer_asset="$extracted/webview/assets/use-model-settings-test.js"
    local install_flow_asset="$extracted/webview/assets/use-plugin-install-flow-test.js"
    local bundle_body
    local renderer_body
    local install_flow_body

    mkdir -p "$workspace" "$fake_home/.config/codex-desktop"

    bundle_body="$(cat <<'JS'
let n={app:{whenReady(){},quit(){},requestSingleInstanceLock(){},on(){},off(){}}};
let Qt=`openai-bundled`,$t=`browser-use`,en=`chrome-internal`,tn=`computer-use`,nn=`latex-tectonic`;
var $n=[{name:tn,isEnabled:({features:e,platform:t})=>t===`darwin`&&e.computerUse,migrate:wn}];
function me(e,{env:t=process.env,platform:n=process.platform}={}){return n!==`win32`||t.CODEX_ELECTRON_ENABLE_WINDOWS_COMPUTER_USE!==`1`?e:{...e,computerUse:!0,computerUseNodeRepl:!0}}
JS
)"
    renderer_body="$(cat <<'JS'
function hae(e){return e===`macOS`||e===`windows`}
function RS(e){let t=(0,q.c)(8),{enabled:n,hostId:r,isHostLocal:i}=e,a=n===void 0?!0:n,o=r===void 0?R:r,s=Kn(),{isLoading:c,platform:l}=Hr(),u=Vn(`1506311413`),d;t[0]===o?d=t[1]:(d={featureName:`computer_use`,hostId:o},t[0]=o,t[1]=d);let f=LS(d),p;t[2]===l?p=t[3]:(p=hae(l),t[2]=l,t[3]=p);let m=a&&i&&s===`electron`&&u&&(c||p),h=m&&!c&&f.enabled&&!f.isLoading,g=m&&f.isLoading,_=m&&(c||f.isLoading),v;return v}
JS
)"
    install_flow_body='function Qe({forceReloadPlugins:e,hostId:t}){let ne=f({featureName:`computer_use`,hostId:t}),re=!ne.isLoading&&ne.enabled,[L,R]=(0,Z.useState)({});return re}'

    make_fake_extracted_asar "$extracted" "$bundle_body"
    printf '%s\n' "$renderer_body" > "$renderer_asset"
    printf '%s\n' "$install_flow_body" > "$install_flow_asset"

    # Branch 1: no env var, no settings.json — only the plugin manifest gate runs.
    HOME="$fake_home" XDG_CONFIG_HOME= unset_env_value="" \
        env -u CODEX_LINUX_ENABLE_COMPUTER_USE_UI HOME="$fake_home" XDG_CONFIG_HOME="$fake_home/.config" \
        node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_contains "$main_bundle" '(t===`darwin`||t===`linux`)&&e.computerUse'
    assert_not_contains "$main_bundle" 'return n===`linux`?{...e,computerUse:!0,computerUseNodeRepl:!0}'
    assert_not_contains "$renderer_asset" 'function hae(e){return e===`macOS`||e===`windows`||e===`linux`}'
    assert_not_contains "$install_flow_asset" 'navigator.userAgent.includes(`Linux`)'

    # Branch 2: env var opts in — all four patches apply.
    rm "$main_bundle" "$renderer_asset" "$install_flow_asset"
    printf '%s\n' "$bundle_body" > "$main_bundle"
    printf '%s\n' "$renderer_body" > "$renderer_asset"
    printf '%s\n' "$install_flow_body" > "$install_flow_asset"

    env CODEX_LINUX_ENABLE_COMPUTER_USE_UI=1 HOME="$fake_home" XDG_CONFIG_HOME="$fake_home/.config" \
        node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_contains "$main_bundle" '(t===`darwin`||t===`linux`)&&e.computerUse'
    assert_contains "$main_bundle" 'return n===`linux`?{...e,computerUse:!0,computerUseNodeRepl:!0}'
    assert_contains "$renderer_asset" 'function hae(e){return e===`macOS`||e===`windows`||e===`linux`}'
    assert_contains "$install_flow_asset" 'navigator.userAgent.includes(`Linux`)'

    # Branch 3: settings.json flag opts in even without env var.
    rm "$main_bundle" "$renderer_asset" "$install_flow_asset"
    printf '%s\n' "$bundle_body" > "$main_bundle"
    printf '%s\n' "$renderer_body" > "$renderer_asset"
    printf '%s\n' "$install_flow_body" > "$install_flow_asset"
    printf '%s\n' '{"codex-linux-computer-use-ui-enabled": true}' > "$fake_home/.config/codex-desktop/settings.json"

    env -u CODEX_LINUX_ENABLE_COMPUTER_USE_UI HOME="$fake_home" XDG_CONFIG_HOME="$fake_home/.config" \
        node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_contains "$main_bundle" 'return n===`linux`?{...e,computerUse:!0,computerUseNodeRepl:!0}'
    assert_contains "$renderer_asset" 'function hae(e){return e===`macOS`||e===`windows`||e===`linux`}'
    assert_contains "$install_flow_asset" 'navigator.userAgent.includes(`Linux`)'
}

test_linux_file_manager_patch_fails_soft() {
    info "Checking Linux file manager patch fallback"
    local workspace="$TMP_DIR/file-manager-patch-fallback"
    local extracted="$workspace/extracted"
    local output_log="$workspace/output.log"

    mkdir -p "$workspace"
    make_fake_extracted_asar "$extracted" 'let D={removeMenu(){},setMenuBarVisibility(){},setIcon(){},once(){}};let t={join(){}};...process.platform===`win32`?{autoHideMenuBar:!0}:{},process.platform===`win32`&&D.removeMenu(),foo)}),D.once(`ready-to-show`,()=>{var brokenFileManager=Mi({id:`fileManager`,label:`Finder`,icon:`apps/finder.png`,kind:`fileManager`});var Ua=Mi({id:`systemDefault`,label:`System Default App`,icon:`apps/file-explorer.png`,kind:`systemDefault`,hidden:!0,darwin:{icon:`apps/finder.png`,detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)},win32:{detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)},linux:{detect:()=>`system-default`,iconPath:()=>null,args:e=>[e],open:async({path:e})=>Wa(e)}});async function Wa(e){return e}'

    node "$REPO_DIR/scripts/patch-linux-window-ui.js" "$extracted" >"$output_log" 2>&1
    assert_contains "$output_log" 'Failed to apply Linux File Manager Patch'
}

test_webview_probe_equivalence() {
    info "Checking webview probe behavioral equivalence (bash + curl vs python3 reference)"
    # The harness extracts webview_port_is_open and verify_webview_origin from
    # the live launcher template, runs them against a controlled localhost
    # python3 http.server fixture, and asserts the verdicts match the
    # python3 reference implementation across every input class (open/closed
    # port, marker-OK, 404, wrong title, missing loader, dead port) plus
    # confirms the watchdog cap still fires within its 150-500 ms window.
    bash "$REPO_DIR/tests/webview_probe_equivalence.sh" \
        || fail "webview probe equivalence harness reported a verdict mismatch or unbounded watchdog"
}

test_user_local_prepare_build_repo_overlays_committed_local_changes() {
    info "Checking user-local managed checkout preserves committed local overlay changes"
    local workspace="$TMP_DIR/user-local-overlay"
    local origin_repo="$workspace/origin.git"
    local source_repo="$workspace/source"
    local upstream_repo="$workspace/upstream"
    local managed_repo="$workspace/xdg-data/codex-desktop-linux/managed-repo"
    local install_env="$workspace/install.env"

    mkdir -p "$workspace"
    git init --bare --initial-branch=main "$origin_repo" >/dev/null
    git clone "$origin_repo" "$source_repo" >/dev/null 2>&1
    git -C "$source_repo" config user.name "Smoke Test"
    git -C "$source_repo" config user.email "smoke@example.com"

    cat > "$source_repo/tracked.txt" <<'EOF'
base
EOF
    cat > "$source_repo/upstream.txt" <<'EOF'
upstream-base
EOF
    git -C "$source_repo" add tracked.txt upstream.txt
    git -C "$source_repo" commit -m "base" >/dev/null
    git -C "$source_repo" push -u origin main >/dev/null
    git -C "$source_repo" remote set-head origin -a >/dev/null 2>&1 || true

    cat > "$source_repo/tracked.txt" <<'EOF'
local-overlay
EOF
    git -C "$source_repo" commit -am "local overlay" >/dev/null

    git clone "$origin_repo" "$upstream_repo" >/dev/null 2>&1
    git -C "$upstream_repo" config user.name "Smoke Test"
    git -C "$upstream_repo" config user.email "smoke@example.com"
    cat > "$upstream_repo/upstream.txt" <<'EOF'
upstream-advanced
EOF
    cat > "$upstream_repo/remote-only.txt" <<'EOF'
remote-only
EOF
    git -C "$upstream_repo" add upstream.txt remote-only.txt
    git -C "$upstream_repo" commit -m "upstream advance" >/dev/null
    git -C "$upstream_repo" push origin main >/dev/null

    (
        export HOME="$workspace/home"
        export XDG_DATA_HOME="$workspace/xdg-data"
        export XDG_STATE_HOME="$workspace/xdg-state"
        mkdir -p "$HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME"

        # shellcheck disable=SC1091
        source "$REPO_DIR/contrib/user-local-install/files/.local/lib/codex-desktop-linux/common.sh"

        INSTALL_CONFIG_FILE="$install_env"
        cat > "$INSTALL_CONFIG_FILE" <<EOF
SOURCE_REPO_DIR=$(printf '%q' "$source_repo")
MANAGED_REPO_DIR=$(printf '%q' "$managed_repo")
REPO_ORIGIN_URL=$(printf '%q' "$origin_repo")
REPO_DEFAULT_BRANCH=$(printf '%q' "main")
OPT_ROOT=$(printf '%q' "$workspace/opt")
EOF

        prepare_build_repo

        [ "$(git -C "$MANAGED_REPO_DIR" rev-parse HEAD)" = "$(git -C "$upstream_repo" rev-parse HEAD)" ] \
            || fail "Expected managed checkout to reset to latest upstream commit"
        [ "$(cat "$MANAGED_REPO_DIR/tracked.txt")" = "local-overlay" ] \
            || fail "Expected committed local overlay change to be copied into managed checkout"
        [ "$(cat "$MANAGED_REPO_DIR/upstream.txt")" = "upstream-advanced" ] \
            || fail "Expected upstream-only change to remain intact in managed checkout"
        [ "$(cat "$MANAGED_REPO_DIR/remote-only.txt")" = "remote-only" ] \
            || fail "Expected upstream-only added file to remain in managed checkout"
        [ -n "$(source_repo_overlay_signature)" ] \
            || fail "Expected committed local overlay to produce a non-empty overlay signature"
    )
}

test_user_local_prepare_build_repo_detects_default_branch_without_recorded_branch() {
    info "Checking user-local managed checkout detects remote default branch when metadata leaves it empty"
    local workspace="$TMP_DIR/user-local-branch-detect"
    local origin_repo="$workspace/origin.git"
    local source_repo="$workspace/source"
    local unmanaged_source="$workspace/source-without-git"
    local managed_repo="$workspace/xdg-data/codex-desktop-linux/managed-repo"
    local install_env="$workspace/install.env"

    mkdir -p "$workspace" "$unmanaged_source"
    git init --bare --initial-branch=master "$origin_repo" >/dev/null
    git clone "$origin_repo" "$source_repo" >/dev/null 2>&1
    git -C "$source_repo" config user.name "Smoke Test"
    git -C "$source_repo" config user.email "smoke@example.com"
    cat > "$source_repo/branch.txt" <<'EOF'
master-branch
EOF
    git -C "$source_repo" add branch.txt
    git -C "$source_repo" commit -m "base" >/dev/null
    git -C "$source_repo" push -u origin master >/dev/null
    git -C "$source_repo" remote set-head origin -a >/dev/null 2>&1 || true

    (
        export HOME="$workspace/home"
        export XDG_DATA_HOME="$workspace/xdg-data"
        export XDG_STATE_HOME="$workspace/xdg-state"
        mkdir -p "$HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME"

        # shellcheck disable=SC1091
        source "$REPO_DIR/contrib/user-local-install/files/.local/lib/codex-desktop-linux/common.sh"

        INSTALL_CONFIG_FILE="$install_env"
        cat > "$INSTALL_CONFIG_FILE" <<EOF
SOURCE_REPO_DIR=$(printf '%q' "$unmanaged_source")
MANAGED_REPO_DIR=$(printf '%q' "$managed_repo")
REPO_ORIGIN_URL=$(printf '%q' "$origin_repo")
REPO_DEFAULT_BRANCH=$(printf '%q' "")
OPT_ROOT=$(printf '%q' "$workspace/opt")
EOF

        prepare_build_repo

        [ "$(repo_default_branch)" = "master" ] \
            || fail "Expected default branch detection to resolve to the remote master branch"
        [ "$(git -C "$MANAGED_REPO_DIR" rev-parse --abbrev-ref HEAD)" = "master" ] \
            || fail "Expected managed checkout to land on the detected master branch"
        [ "$(cat "$MANAGED_REPO_DIR/branch.txt")" = "master-branch" ] \
            || fail "Expected managed checkout contents from the detected master branch"
    )
}

test_user_local_prepare_build_repo_ignores_stale_recorded_default_branch() {
    info "Checking user-local managed checkout ignores a stale recorded default branch"
    local workspace="$TMP_DIR/user-local-stale-branch"
    local origin_repo="$workspace/origin.git"
    local source_repo="$workspace/source"
    local unmanaged_source="$workspace/source-without-git"
    local managed_repo="$workspace/xdg-data/codex-desktop-linux/managed-repo"
    local install_env="$workspace/install.env"

    mkdir -p "$workspace" "$unmanaged_source"
    git init --bare --initial-branch=main "$origin_repo" >/dev/null
    git clone "$origin_repo" "$source_repo" >/dev/null 2>&1
    git -C "$source_repo" config user.name "Smoke Test"
    git -C "$source_repo" config user.email "smoke@example.com"
    cat > "$source_repo/branch.txt" <<'EOF'
main-branch
EOF
    git -C "$source_repo" add branch.txt
    git -C "$source_repo" commit -m "base" >/dev/null
    git -C "$source_repo" push -u origin main >/dev/null
    git -C "$source_repo" remote set-head origin -a >/dev/null 2>&1 || true

    (
        export HOME="$workspace/home"
        export XDG_DATA_HOME="$workspace/xdg-data"
        export XDG_STATE_HOME="$workspace/xdg-state"
        mkdir -p "$HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME"

        # shellcheck disable=SC1091
        source "$REPO_DIR/contrib/user-local-install/files/.local/lib/codex-desktop-linux/common.sh"

        INSTALL_CONFIG_FILE="$install_env"
        cat > "$INSTALL_CONFIG_FILE" <<EOF
SOURCE_REPO_DIR=$(printf '%q' "$unmanaged_source")
MANAGED_REPO_DIR=$(printf '%q' "$managed_repo")
REPO_ORIGIN_URL=$(printf '%q' "$origin_repo")
REPO_DEFAULT_BRANCH=$(printf '%q' "master")
OPT_ROOT=$(printf '%q' "$workspace/opt")
EOF

        prepare_build_repo

        [ "$(repo_default_branch)" = "main" ] \
            || fail "Expected stale recorded branch to fall back to the remote default branch"
        [ "$(git -C "$MANAGED_REPO_DIR" rev-parse --abbrev-ref HEAD)" = "main" ] \
            || fail "Expected managed checkout to land on the recovered main branch"
        [ "$(cat "$MANAGED_REPO_DIR/branch.txt")" = "main-branch" ] \
            || fail "Expected managed checkout contents from the recovered main branch"
    )
}

test_user_local_prepare_build_repo_ignores_stale_source_origin_head() {
    info "Checking user-local managed checkout ignores a stale source origin/HEAD ref"
    local workspace="$TMP_DIR/user-local-stale-origin-head"
    local origin_repo="$workspace/origin.git"
    local source_repo="$workspace/source"
    local managed_repo="$workspace/xdg-data/codex-desktop-linux/managed-repo"
    local install_env="$workspace/install.env"

    mkdir -p "$workspace"
    git init --bare --initial-branch=main "$origin_repo" >/dev/null
    git clone "$origin_repo" "$source_repo" >/dev/null 2>&1
    git -C "$source_repo" config user.name "Smoke Test"
    git -C "$source_repo" config user.email "smoke@example.com"
    cat > "$source_repo/branch.txt" <<'EOF'
main-branch
EOF
    git -C "$source_repo" add branch.txt
    git -C "$source_repo" commit -m "base" >/dev/null
    git -C "$source_repo" push -u origin main >/dev/null
    git -C "$source_repo" remote set-head origin -a >/dev/null 2>&1 || true
    git -C "$source_repo" symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/master

    (
        export HOME="$workspace/home"
        export XDG_DATA_HOME="$workspace/xdg-data"
        export XDG_STATE_HOME="$workspace/xdg-state"
        mkdir -p "$HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME"

        # shellcheck disable=SC1091
        source "$REPO_DIR/contrib/user-local-install/files/.local/lib/codex-desktop-linux/common.sh"

        INSTALL_CONFIG_FILE="$install_env"
        cat > "$INSTALL_CONFIG_FILE" <<EOF
SOURCE_REPO_DIR=$(printf '%q' "$source_repo")
MANAGED_REPO_DIR=$(printf '%q' "$managed_repo")
REPO_ORIGIN_URL=$(printf '%q' "$origin_repo")
REPO_DEFAULT_BRANCH=$(printf '%q' "")
OPT_ROOT=$(printf '%q' "$workspace/opt")
EOF

        prepare_build_repo

        [ "$(repo_default_branch)" = "main" ] \
            || fail "Expected stale source origin/HEAD to fall back to the real remote default branch"
        [ "$(git -C "$MANAGED_REPO_DIR" rev-parse --abbrev-ref HEAD)" = "main" ] \
            || fail "Expected managed checkout to land on the recovered main branch"
        [ "$(cat "$MANAGED_REPO_DIR/branch.txt")" = "main-branch" ] \
            || fail "Expected managed checkout contents from the recovered main branch"
    )
}

test_user_local_prepare_build_repo_handles_relative_origin_url() {
    info "Checking user-local managed checkout handles relative origin URLs"
    local workspace="$TMP_DIR/user-local-relative-origin"
    local origin_repo="$workspace/origin.git"
    local source_repo="$workspace/source"
    local moved_source_repo="$workspace/source-moved"
    local updater_repo="$workspace/updater"
    local managed_repo="$workspace/xdg-data/codex-desktop-linux/managed-repo"
    local install_env="$workspace/install.env"

    mkdir -p "$workspace"
    git init --bare --initial-branch=main "$origin_repo" >/dev/null
    git clone "$origin_repo" "$source_repo" >/dev/null 2>&1
    git -C "$source_repo" config user.name "Smoke Test"
    git -C "$source_repo" config user.email "smoke@example.com"
    cat > "$source_repo/relative.txt" <<'EOF'
relative-origin
EOF
    git -C "$source_repo" add relative.txt
    git -C "$source_repo" commit -m "base" >/dev/null
    git -C "$source_repo" push -u origin main >/dev/null
    git -C "$source_repo" remote set-head origin -a >/dev/null 2>&1 || true
    git -C "$source_repo" remote set-url origin ../origin.git

    (
        export HOME="$workspace/home"
        export XDG_DATA_HOME="$workspace/xdg-data"
        export XDG_STATE_HOME="$workspace/xdg-state"
        mkdir -p "$HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME"

        # shellcheck disable=SC1091
        source "$REPO_DIR/contrib/user-local-install/files/.local/lib/codex-desktop-linux/common.sh"

        INSTALL_CONFIG_FILE="$install_env"
        cat > "$INSTALL_CONFIG_FILE" <<EOF
SOURCE_REPO_DIR=$(printf '%q' "$source_repo")
MANAGED_REPO_DIR=$(printf '%q' "$managed_repo")
REPO_ORIGIN_URL=$(printf '%q' "../origin.git")
REPO_DEFAULT_BRANCH=$(printf '%q' "main")
OPT_ROOT=$(printf '%q' "$workspace/opt")
EOF

        prepare_build_repo

        [ "$(cat "$MANAGED_REPO_DIR/relative.txt")" = "relative-origin" ] \
            || fail "Expected managed checkout contents from relative origin URL"
        [ "$(git -C "$MANAGED_REPO_DIR" remote get-url origin)" = "$origin_repo" ] \
            || fail "Expected first relative-origin checkout to store an absolute managed origin URL"

        mv "$source_repo" "$moved_source_repo"
        git clone "$origin_repo" "$updater_repo" >/dev/null 2>&1
        git -C "$updater_repo" config user.name "Smoke Test"
        git -C "$updater_repo" config user.email "smoke@example.com"
        cat > "$updater_repo/relative.txt" <<'EOF'
relative-origin-updated
EOF
        git -C "$updater_repo" commit -am "advance remote" >/dev/null
        git -C "$updater_repo" push origin main >/dev/null

        prepare_build_repo

        [ "$(cat "$MANAGED_REPO_DIR/relative.txt")" = "relative-origin-updated" ] \
            || fail "Expected managed checkout to update after source checkout moved away"
        [ "$(git -C "$MANAGED_REPO_DIR" remote get-url origin)" = "$origin_repo" ] \
            || fail "Expected moved-source update to keep using the absolute managed origin URL"
    )
}

test_desktop_entry_doctor_repairs_only_legacy_generated_entries() {
    info "Checking desktop-entry doctor only backs up legacy generated entries"
    local workspace="$TMP_DIR/desktop-entry-doctor"
    local desktop_dir="$workspace/applications"
    local template="$REPO_DIR/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop"
    local stale_entry="$desktop_dir/stale.desktop"
    local current_entry="$desktop_dir/current.desktop"
    local custom_entry="$desktop_dir/custom.desktop"

    mkdir -p "$desktop_dir"

    cat > "$stale_entry" <<'EOF'
[Desktop Entry]
Type=Application
Name=Codex Desktop
Exec=/home/tester/.local/bin/codex-desktop %U
TryExec=/home/tester/.local/bin/codex-desktop
Terminal=false
Icon=codex-desktop
Actions=NewInstance;

[Desktop Action NewInstance]
Name=Open New Instance
Exec=env CODEX_MULTI_LAUNCH=1 /home/tester/.local/bin/codex-desktop --new-instance
EOF

    cat > "$custom_entry" <<'EOF'
[Desktop Entry]
Type=Application
Name=My Custom App
Exec=/usr/bin/custom-app
Icon=custom-app
EOF

    (
        # shellcheck disable=SC1091
        . "$REPO_DIR/packaging/linux/codex-desktop-entry-doctor.sh"
        codex_desktop_write_user_local_entry "$template" "$current_entry" "/home/tester"
        codex_desktop_repair_shadow_entry "$stale_entry"
        if codex_desktop_repair_shadow_entry "$current_entry"; then
            exit 1
        fi
        if codex_desktop_repair_shadow_entry "$custom_entry"; then
            exit 1
        fi
        if codex_desktop_repair_shadow_entry "$stale_entry"; then
            exit 1
        fi
    )

    assert_file_not_exists "$stale_entry"
    assert_file_exists "$stale_entry.bak"
    assert_contains "$stale_entry.bak" "Actions=NewInstance;"
    assert_file_exists "$current_entry"
    assert_contains "$current_entry" "Actions=new-window;"
    assert_contains "$current_entry" "x-scheme-handler/codex-browser-sidebar"
    assert_file_exists "$custom_entry"
    assert_not_contains "$custom_entry" "codex-browser-sidebar"
    assert_file_not_exists "$stale_entry.bak.1"
}

test_user_local_install_from_update_defers_record_only_metadata() {
    info "Checking user-local helper refresh does not record metadata before update success"
    local workspace="$TMP_DIR/user-local-from-update-record-only"
    local fake_bin="$workspace/bin"
    local home="$workspace/home"
    local marker="$workspace/record-only-attempted"

    mkdir -p "$fake_bin"
    cat > "$fake_bin/7z" <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
: "${RECORD_ONLY_MARKER:?}"
mkdir -p "$(dirname "$RECORD_ONLY_MARKER")"
printf '%s\n' "attempted" > "$RECORD_ONLY_MARKER"
exit 1
SCRIPT
    printf '#!/usr/bin/env bash\nexit 0\n' > "$fake_bin/systemctl"
    printf '#!/usr/bin/env bash\nexit 0\n' > "$fake_bin/update-desktop-database"
    chmod +x "$fake_bin/7z" "$fake_bin/systemctl" "$fake_bin/update-desktop-database"

    PATH="$fake_bin:$PATH" \
        HOME="$home" \
        XDG_DATA_HOME="$workspace/data" \
        XDG_STATE_HOME="$workspace/state" \
        RECORD_ONLY_MARKER="$marker" \
        CODEX_USER_LOCAL_SOURCE_REPO_DIR="$REPO_DIR" \
        bash "$REPO_DIR/contrib/user-local-install/install-user-local.sh" --from-update >/dev/null
    assert_file_not_exists "$marker"

    PATH="$fake_bin:$PATH" \
        HOME="$home" \
        XDG_DATA_HOME="$workspace/data" \
        XDG_STATE_HOME="$workspace/state" \
        RECORD_ONLY_MARKER="$marker" \
        CODEX_USER_LOCAL_SOURCE_REPO_DIR="$REPO_DIR" \
        bash "$REPO_DIR/contrib/user-local-install/install-user-local.sh" >/dev/null
    assert_file_exists "$marker"
}

test_user_local_install_preserves_persisted_x11_preference_on_refresh() {
    info "Checking user-local X11 fallback preference persists across helper refreshes"
    local workspace="$TMP_DIR/user-local-x11-preference"
    local stub_bin="$workspace/bin"
    local home="$workspace/home"
    local config_home="$workspace/config"
    local preference_file="$config_home/codex-desktop-linux/user-local.env"

    mkdir -p "$stub_bin"
    printf '#!/usr/bin/env bash\nexit 0\n' > "$stub_bin/7z"
    printf '#!/usr/bin/env bash\nexit 0\n' > "$stub_bin/systemctl"
    printf '#!/usr/bin/env bash\nexit 0\n' > "$stub_bin/update-desktop-database"
    chmod +x "$stub_bin/7z" "$stub_bin/systemctl" "$stub_bin/update-desktop-database"

    PATH="$stub_bin:$PATH" \
        HOME="$home" \
        XDG_CONFIG_HOME="$config_home" \
        XDG_DATA_HOME="$workspace/data" \
        XDG_STATE_HOME="$workspace/state" \
        CODEX_USER_LOCAL_SOURCE_REPO_DIR="$REPO_DIR" \
        bash "$REPO_DIR/contrib/user-local-install/install-user-local.sh" --force-x11 >/dev/null
    assert_file_exists "$preference_file"
    assert_contains "$preference_file" "CODEX_USER_LOCAL_OZONE_PLATFORM=x11"

    PATH="$stub_bin:$PATH" \
        HOME="$home" \
        XDG_CONFIG_HOME="$config_home" \
        XDG_DATA_HOME="$workspace/data" \
        XDG_STATE_HOME="$workspace/state" \
        CODEX_USER_LOCAL_SOURCE_REPO_DIR="$REPO_DIR" \
        bash "$REPO_DIR/contrib/user-local-install/install-user-local.sh" --from-update >/dev/null
    assert_contains "$preference_file" "CODEX_USER_LOCAL_OZONE_PLATFORM=x11"

    PATH="$stub_bin:$PATH" \
        HOME="$home" \
        XDG_CONFIG_HOME="$config_home" \
        XDG_DATA_HOME="$workspace/data" \
        XDG_STATE_HOME="$workspace/state" \
        CODEX_USER_LOCAL_SOURCE_REPO_DIR="$REPO_DIR" \
        bash "$REPO_DIR/contrib/user-local-install/install-user-local.sh" --no-force-x11 >/dev/null
    assert_contains "$preference_file" "CODEX_USER_LOCAL_OZONE_PLATFORM=auto"
}

test_user_local_prepare_build_repo_copies_enabled_local_features() {
    info "Checking user-local managed checkout stages enabled local features"
    local workspace="$TMP_DIR/user-local-local-features"
    local origin_repo="$workspace/origin.git"
    local source_repo="$workspace/source"
    local managed_repo="$workspace/xdg-data/codex-desktop-linux/managed-repo"
    local install_env="$workspace/install.env"
    local feature_config="$workspace/linux-features.json"
    local staged_local_feature="$managed_repo/linux-features/local/local-tool"

    mkdir -p "$workspace"
    git init --bare --initial-branch=main "$origin_repo" >/dev/null
    git clone "$origin_repo" "$source_repo" >/dev/null 2>&1
    git -C "$source_repo" config user.name "Smoke Test"
    git -C "$source_repo" config user.email "smoke@example.com"

    mkdir -p "$source_repo/linux-features/repo-feature"
    printf '%s\n' '# Linux Features' > "$source_repo/linux-features/README.md"
    printf '%s\n' '{"enabled":[]}' > "$source_repo/linux-features/features.example.json"
    printf '%s\n' '{"id":"repo-feature","title":"Repo Feature"}' \
        > "$source_repo/linux-features/repo-feature/feature.json"
    printf '%s\n' '# Repo Feature' > "$source_repo/linux-features/repo-feature/README.md"
    git -C "$source_repo" add linux-features
    git -C "$source_repo" commit -m "base" >/dev/null
    git -C "$source_repo" push -u origin main >/dev/null

    mkdir -p "$source_repo/linux-features/local/local-tool/nested"
    mkdir -p "$source_repo/linux-features/local/repo-feature"
    printf '%s\n' '{"id":"local-tool","title":"Local Tool"}' \
        > "$source_repo/linux-features/local/local-tool/feature.json"
    printf '%s\n' '# Local Tool' > "$source_repo/linux-features/local/local-tool/README.md"
    printf '%s\n' 'payload' > "$source_repo/linux-features/local/local-tool/nested/payload.txt"
    ln -s nested/payload.txt "$source_repo/linux-features/local/local-tool/payload-link"
    printf '%s\n' '{"id":"repo-feature","title":"Local Repo Feature"}' \
        > "$source_repo/linux-features/local/repo-feature/feature.json"
    cat > "$feature_config" <<'JSON'
{
  "enabled": [
    "local-tool",
    "repo-feature",
    "missing-local",
    "bad id"
  ]
}
JSON

    (
        export HOME="$workspace/home"
        export XDG_DATA_HOME="$workspace/xdg-data"
        export XDG_STATE_HOME="$workspace/xdg-state"
        export CODEX_LINUX_FEATURES_CONFIG="$feature_config"
        mkdir -p "$HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME"

        # shellcheck disable=SC1091
        source "$REPO_DIR/contrib/user-local-install/files/.local/lib/codex-desktop-linux/common.sh"

        INSTALL_CONFIG_FILE="$install_env"
        cat > "$INSTALL_CONFIG_FILE" <<EOF
SOURCE_REPO_DIR=$(printf '%q' "$source_repo")
MANAGED_REPO_DIR=$(printf '%q' "$managed_repo")
REPO_ORIGIN_URL=$(printf '%q' "$origin_repo")
REPO_DEFAULT_BRANCH=$(printf '%q' "main")
OPT_ROOT=$(printf '%q' "$workspace/opt")
EOF

        prepare_build_repo
    )

    assert_file_exists "$staged_local_feature/feature.json"
    [ "$(cat "$staged_local_feature/nested/payload.txt")" = "payload" ] \
        || fail "Expected local feature nested payload to be copied"
    [ -L "$staged_local_feature/payload-link" ] \
        || fail "Expected local feature symlink to be preserved"
    [ "$(readlink "$staged_local_feature/payload-link")" = "nested/payload.txt" ] \
        || fail "Expected local feature symlink target to be preserved"
    assert_file_not_exists "$managed_repo/linux-features/local/repo-feature/feature.json"
    assert_file_not_exists "$managed_repo/linux-features/local/missing-local/feature.json"
    assert_file_exists "$managed_repo/linux-features/repo-feature/feature.json"
}

test_user_local_prepare_build_repo_updates_existing_single_branch_fetch_refspec() {
    info "Checking user-local managed checkout can switch branches after a single-branch clone"
    local workspace="$TMP_DIR/user-local-single-branch-refspec"
    local origin_repo="$workspace/origin.git"
    local upstream_repo="$workspace/upstream"
    local unmanaged_source="$workspace/source-without-git"
    local managed_repo="$workspace/xdg-data/codex-desktop-linux/managed-repo"
    local install_env="$workspace/install.env"

    mkdir -p "$workspace" "$unmanaged_source"
    git init --bare --initial-branch=main "$origin_repo" >/dev/null
    git clone "$origin_repo" "$upstream_repo" >/dev/null 2>&1
    git -C "$upstream_repo" config user.name "Smoke Test"
    git -C "$upstream_repo" config user.email "smoke@example.com"
    cat > "$upstream_repo/branch.txt" <<'EOF'
main-branch
EOF
    git -C "$upstream_repo" add branch.txt
    git -C "$upstream_repo" commit -m "base" >/dev/null
    git -C "$upstream_repo" push -u origin main >/dev/null

    (
        export HOME="$workspace/home"
        export XDG_DATA_HOME="$workspace/xdg-data"
        export XDG_STATE_HOME="$workspace/xdg-state"
        mkdir -p "$HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME"

        # shellcheck disable=SC1091
        source "$REPO_DIR/contrib/user-local-install/files/.local/lib/codex-desktop-linux/common.sh"

        INSTALL_CONFIG_FILE="$install_env"
        cat > "$INSTALL_CONFIG_FILE" <<EOF
SOURCE_REPO_DIR=$(printf '%q' "$unmanaged_source")
MANAGED_REPO_DIR=$(printf '%q' "$managed_repo")
REPO_ORIGIN_URL=$(printf '%q' "$origin_repo")
REPO_DEFAULT_BRANCH=$(printf '%q' "main")
OPT_ROOT=$(printf '%q' "$workspace/opt")
EOF

        prepare_build_repo

        [ "$(git -C "$MANAGED_REPO_DIR" rev-parse --abbrev-ref HEAD)" = "main" ] \
            || fail "Expected managed checkout to start on main"
        [ "$(git -C "$MANAGED_REPO_DIR" config --get-all remote.origin.fetch)" = "+refs/heads/*:refs/remotes/origin/*" ] \
            || fail "Expected managed checkout fetch refspec to include all branches"
    )

    git -C "$upstream_repo" checkout -q -b master
    cat > "$upstream_repo/branch.txt" <<'EOF'
master-branch
EOF
    git -C "$upstream_repo" commit -am "master branch" >/dev/null
    git -C "$upstream_repo" push -u origin master >/dev/null
    git --git-dir="$origin_repo" symbolic-ref HEAD refs/heads/master

    (
        export HOME="$workspace/home"
        export XDG_DATA_HOME="$workspace/xdg-data"
        export XDG_STATE_HOME="$workspace/xdg-state"
        mkdir -p "$HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME"

        # shellcheck disable=SC1091
        source "$REPO_DIR/contrib/user-local-install/files/.local/lib/codex-desktop-linux/common.sh"

        INSTALL_CONFIG_FILE="$install_env"
        cat > "$INSTALL_CONFIG_FILE" <<EOF
SOURCE_REPO_DIR=$(printf '%q' "$unmanaged_source")
MANAGED_REPO_DIR=$(printf '%q' "$managed_repo")
REPO_ORIGIN_URL=$(printf '%q' "$origin_repo")
REPO_DEFAULT_BRANCH=$(printf '%q' "master")
OPT_ROOT=$(printf '%q' "$workspace/opt")
EOF

        prepare_build_repo

        [ "$(git -C "$MANAGED_REPO_DIR" rev-parse --abbrev-ref HEAD)" = "master" ] \
            || fail "Expected managed checkout to switch to master"
        [ "$(cat "$MANAGED_REPO_DIR/branch.txt")" = "master-branch" ] \
            || fail "Expected managed checkout contents from the newly selected branch"
    )
}

test_user_local_prepare_build_repo_handles_deleted_overlay_paths() {
    info "Checking user-local managed checkout tolerates overlay paths deleted in the worktree"
    local workspace="$TMP_DIR/user-local-deleted-overlay"
    local origin_repo="$workspace/origin.git"
    local source_repo="$workspace/source"
    local managed_repo="$workspace/xdg-data/codex-desktop-linux/managed-repo"
    local install_env="$workspace/install.env"

    mkdir -p "$workspace"
    git init --bare --initial-branch=main "$origin_repo" >/dev/null
    git clone "$origin_repo" "$source_repo" >/dev/null 2>&1
    git -C "$source_repo" config user.name "Smoke Test"
    git -C "$source_repo" config user.email "smoke@example.com"

    cat > "$source_repo/overlay.txt" <<'EOF'
base
EOF
    git -C "$source_repo" add overlay.txt
    git -C "$source_repo" commit -m "base" >/dev/null
    git -C "$source_repo" push -u origin main >/dev/null
    git -C "$source_repo" remote set-head origin -a >/dev/null 2>&1 || true

    cat > "$source_repo/overlay.txt" <<'EOF'
committed-overlay
EOF
    git -C "$source_repo" commit -am "overlay commit" >/dev/null
    rm -f "$source_repo/overlay.txt"

    (
        export HOME="$workspace/home"
        export XDG_DATA_HOME="$workspace/xdg-data"
        export XDG_STATE_HOME="$workspace/xdg-state"
        mkdir -p "$HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME"

        # shellcheck disable=SC1091
        source "$REPO_DIR/contrib/user-local-install/files/.local/lib/codex-desktop-linux/common.sh"

        INSTALL_CONFIG_FILE="$install_env"
        cat > "$INSTALL_CONFIG_FILE" <<EOF
SOURCE_REPO_DIR=$(printf '%q' "$source_repo")
MANAGED_REPO_DIR=$(printf '%q' "$managed_repo")
REPO_ORIGIN_URL=$(printf '%q' "$origin_repo")
REPO_DEFAULT_BRANCH=$(printf '%q' "main")
OPT_ROOT=$(printf '%q' "$workspace/opt")
EOF

        prepare_build_repo

        [ ! -e "$MANAGED_REPO_DIR/overlay.txt" ] \
            || fail "Expected deleted overlay path to be removed from managed checkout"
    )
}

test_user_local_prepare_build_repo_removes_rename_source_paths() {
    info "Checking user-local managed checkout removes rename source paths"
    local workspace="$TMP_DIR/user-local-rename-overlay"
    local origin_repo="$workspace/origin.git"
    local source_repo="$workspace/source"
    local managed_repo="$workspace/xdg-data/codex-desktop-linux/managed-repo"
    local install_env="$workspace/install.env"

    mkdir -p "$workspace"
    git init --bare --initial-branch=main "$origin_repo" >/dev/null
    git clone "$origin_repo" "$source_repo" >/dev/null 2>&1
    git -C "$source_repo" config user.name "Smoke Test"
    git -C "$source_repo" config user.email "smoke@example.com"

    cat > "$source_repo/old-name.txt" <<'EOF'
base
EOF
    git -C "$source_repo" add old-name.txt
    git -C "$source_repo" commit -m "base" >/dev/null
    git -C "$source_repo" push -u origin main >/dev/null
    git -C "$source_repo" remote set-head origin -a >/dev/null 2>&1 || true

    git -C "$source_repo" mv old-name.txt new-name.txt
    git -C "$source_repo" commit -m "rename overlay file" >/dev/null

    (
        export HOME="$workspace/home"
        export XDG_DATA_HOME="$workspace/xdg-data"
        export XDG_STATE_HOME="$workspace/xdg-state"
        mkdir -p "$HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME"

        # shellcheck disable=SC1091
        source "$REPO_DIR/contrib/user-local-install/files/.local/lib/codex-desktop-linux/common.sh"

        INSTALL_CONFIG_FILE="$install_env"
        cat > "$INSTALL_CONFIG_FILE" <<EOF
SOURCE_REPO_DIR=$(printf '%q' "$source_repo")
MANAGED_REPO_DIR=$(printf '%q' "$managed_repo")
REPO_ORIGIN_URL=$(printf '%q' "$origin_repo")
REPO_DEFAULT_BRANCH=$(printf '%q' "main")
OPT_ROOT=$(printf '%q' "$workspace/opt")
EOF

        prepare_build_repo

        [ ! -e "$MANAGED_REPO_DIR/old-name.txt" ] \
            || fail "Expected rename source path to be removed from managed checkout"
        [ "$(cat "$MANAGED_REPO_DIR/new-name.txt")" = "base" ] \
            || fail "Expected rename destination path to be present in managed checkout"
    )
}

test_user_local_prepare_build_repo_skips_unmerged_overlay_paths() {
    info "Checking user-local managed checkout skips unmerged overlay paths"
    local workspace="$TMP_DIR/user-local-unmerged-overlay"
    local origin_repo="$workspace/origin.git"
    local source_repo="$workspace/source"
    local managed_repo="$workspace/xdg-data/codex-desktop-linux/managed-repo"
    local install_env="$workspace/install.env"

    mkdir -p "$workspace"
    git init --bare --initial-branch=main "$origin_repo" >/dev/null
    git clone "$origin_repo" "$source_repo" >/dev/null 2>&1
    git -C "$source_repo" config user.name "Smoke Test"
    git -C "$source_repo" config user.email "smoke@example.com"

    cat > "$source_repo/conflict.txt" <<'EOF'
base
EOF
    git -C "$source_repo" add conflict.txt
    git -C "$source_repo" commit -m "base" >/dev/null
    git -C "$source_repo" push -u origin main >/dev/null
    git -C "$source_repo" remote set-head origin -a >/dev/null 2>&1 || true

    git -C "$source_repo" checkout -q -b feature
    cat > "$source_repo/conflict.txt" <<'EOF'
feature-change
EOF
    git -C "$source_repo" commit -am "feature change" >/dev/null
    git -C "$source_repo" checkout -q main
    cat > "$source_repo/conflict.txt" <<'EOF'
main-change
EOF
    git -C "$source_repo" commit -am "main change" >/dev/null
    if git -C "$source_repo" merge feature >/dev/null 2>&1; then
        fail "Expected merge to conflict in unmerged overlay smoke test"
    fi
    assert_contains "$source_repo/conflict.txt" "<<<<<<<"

    (
        export HOME="$workspace/home"
        export XDG_DATA_HOME="$workspace/xdg-data"
        export XDG_STATE_HOME="$workspace/xdg-state"
        mkdir -p "$HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME"

        # shellcheck disable=SC1091
        source "$REPO_DIR/contrib/user-local-install/files/.local/lib/codex-desktop-linux/common.sh"

        INSTALL_CONFIG_FILE="$install_env"
        cat > "$INSTALL_CONFIG_FILE" <<EOF
SOURCE_REPO_DIR=$(printf '%q' "$source_repo")
MANAGED_REPO_DIR=$(printf '%q' "$managed_repo")
REPO_ORIGIN_URL=$(printf '%q' "$origin_repo")
REPO_DEFAULT_BRANCH=$(printf '%q' "main")
OPT_ROOT=$(printf '%q' "$workspace/opt")
EOF

        prepare_build_repo

        [ "$(cat "$MANAGED_REPO_DIR/conflict.txt")" = "base" ] \
            || fail "Expected managed checkout to keep clean upstream content for unmerged overlay paths"
        assert_not_contains "$MANAGED_REPO_DIR/conflict.txt" "<<<<<<<"
    )
}

main() {
    test_common_helper_sourcing
    test_package_payload_permission_normalization
    test_deb_builder_smoke
    test_deb_builder_rebuilds_deleted_updater_source
    test_update_builder_preserves_enabled_linux_features_config
    test_update_builder_source_info_survives_without_git_checkout
    test_linux_feature_package_hook_discovery_failure_blocks_build
    test_deb_builder_respects_package_identity
    test_deb_builder_without_updater
    test_no_updater_cleanup_helper_removes_inactive_user_enablement
    test_rpm_builder_smoke
    test_pacman_builder_without_updater_transition_hook
    test_appimage_builder_smoke
    test_missing_input_failure
    test_make_install_reports_missing_native_packages
    test_make_build_app_uses_installer_download_flow_by_default
    test_make_build_app_fresh_uses_installer_fresh_flow
    test_installer_refreshes_stale_cached_dmg_metadata
    test_fresh_install_removes_cached_dmg_metadata
    test_rebuild_candidate_uses_validated_default_dmg
    test_native_shortcut_targets_compose_existing_flows
    test_fedora_dependency_bootstrap_installs_rpmbuild
    test_setup_native_wizard_noninteractive_feature_writer
    test_setup_native_wizard_rejects_invalid_feature_ids
    test_setup_native_wizard_rejects_features_without_readme
    test_setup_native_wizard_rejects_conflicting_feature_ids
    test_setup_native_wizard_disable_is_non_destructive
    test_setup_native_wizard_accepts_numbered_feature_selection
    test_setup_native_wizard_rejects_out_of_range_feature_numbers
    test_setup_native_wizard_summary_keeps_existing_config
    test_setup_native_wizard_lists_local_features
    test_setup_native_wizard_uses_package_name_for_installed_state
    test_setup_native_wizard_portal_summary_survives_busctl_sigpipe
    test_setup_native_wizard_warns_when_conversation_mode_lacks_read_aloud
    test_setup_native_wizard_dry_runs_deps_and_install_native
    test_setup_native_wizard_prints_deep_readiness_guidance
    test_setup_native_wizard_uinput_stat_is_bounded
    test_setup_native_wizard_read_aloud_paths_match_runtime_defaults
    test_setup_native_wizard_sway_hint_is_conservative
    test_setup_native_wizard_cleanup_requires_interactive_confirmation
    test_setup_native_wizard_dry_run_cleanup_allows_noninteractive_preview
    test_setup_native_wizard_blank_interactive_cleanup_ids_skip_cleanup
    test_setup_native_wizard_dry_run_cleanup_does_not_delete_confirmed_paths
    test_setup_native_wizard_cleanup_deletes_only_confirmed_paths
    test_upstream_build_app_workflow_tracks_dmg_metadata
    test_installer_detects_electron_version_from_plist
    test_installer_keeps_electron_fallback_for_bad_metadata
    test_port_validation_rejects_oversized_numeric_values
    test_managed_node_runtime_source_install
    test_managed_node_runtime_rejects_version_only_stub
    test_better_sqlite3_electron_42_source_patch
    test_v8_nullptr_workaround_skips_when_included_probe_succeeds
    test_v8_nullptr_workaround_wraps_when_included_probe_fails
    test_native_module_rebuild_uses_local_electron_rebuild_toolchain
    test_native_module_rebuild_accepts_prebuilt_source
    test_bundled_plugin_builders_accept_prebuilt_binaries
    test_browser_use_node_repl_fallback_runtime
    test_browser_use_file_url_policy_patch_behavior
    test_browser_plugin_renamed_upstream_staging
    test_browser_use_node_repl_glibc_pidfd_patch_static
    test_browser_use_node_repl_ldd_output_compatibility
    test_chrome_plugin_staging
    test_chrome_browser_client_profile_root_variants
    test_chrome_marketplace_fallback_synthesis
    test_chrome_native_host_manifest_writer
    test_launcher_template_sanity
    test_webview_probe_equivalence
    test_side_by_side_launcher_identity
    test_linux_file_manager_patch_smoke
    test_linux_translucent_sidebar_default_patch_smoke
    test_keybinds_settings_tab_patch_smoke
    test_keybinds_settings_patch_warns_on_bundle_shape_miss
    test_linux_tray_patch_smoke
    test_linux_explicit_quit_patch_smoke
    test_browser_annotation_screenshot_patch_smoke
    test_linux_single_instance_patch_smoke
    test_linux_computer_use_gate_patch_smoke
    test_linux_computer_use_ui_opt_in_smoke
    test_linux_file_manager_patch_fails_soft
    test_user_local_prepare_build_repo_overlays_committed_local_changes
    test_user_local_prepare_build_repo_detects_default_branch_without_recorded_branch
    test_user_local_prepare_build_repo_ignores_stale_recorded_default_branch
    test_user_local_prepare_build_repo_ignores_stale_source_origin_head
    test_user_local_prepare_build_repo_handles_relative_origin_url
    test_desktop_entry_doctor_repairs_only_legacy_generated_entries
    test_user_local_install_from_update_defers_record_only_metadata
    test_user_local_install_preserves_persisted_x11_preference_on_refresh
    test_user_local_prepare_build_repo_copies_enabled_local_features
    test_user_local_prepare_build_repo_updates_existing_single_branch_fetch_refspec
    test_user_local_prepare_build_repo_handles_deleted_overlay_paths
    test_user_local_prepare_build_repo_removes_rename_source_paths
    test_user_local_prepare_build_repo_skips_unmerged_overlay_paths
    info "All script smoke tests passed"
}

main "$@"
