Files
InstanceBrick--nightcap/tests/auto_update_orchestration_test.sh
T
2026-08-16 03:25:01 +03:00

1332 lines
56 KiB
Bash
Executable File

#!/usr/bin/env bash
# Copyright 2026 Mocktail Project Authors
# Licensed under the Apache License, Version 2.0.
set -Eeuo pipefail
AUTO_UPDATE="${1:?auto-update script is required}"
TEMP_DIR="$(mktemp -d)"
LOCK_HOLDER_PID=""
CleanupTest() {
if [[ -n "${LOCK_HOLDER_PID}" ]]; then
kill "${LOCK_HOLDER_PID}" 2>/dev/null || true
wait "${LOCK_HOLDER_PID}" 2>/dev/null || true
fi
rm -rf -- "${TEMP_DIR}"
}
trap CleanupTest EXIT
mkdir -p "${TEMP_DIR}/bin" "${TEMP_DIR}/bundle"
touch "${TEMP_DIR}/bundle/base.apk" "${TEMP_DIR}/bundle/split_config.x86_64.apk"
cat > "${TEMP_DIR}/bin/fetch" <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
source_name=""
staging_root=""
requested_version=""
while (( $# > 0 )); do
case "$1" in
--source) source_name="$2"; shift 2 ;;
--staging-root) staging_root="$2"; shift 2 ;;
--version) requested_version="$2"; shift 2 ;;
*) echo "unexpected fetch argument: $1" >&2; exit 1 ;;
esac
done
printf 'fetch\n' >> "${FAKE_FETCH_CALLS}"
printf '%s|%s\n' "${source_name}" "${requested_version}" >> "${FAKE_FETCH_ARGUMENTS}"
if [[ -n "${FAKE_FETCH_LAST_VERSION:-}" ]]; then
printf '%s\n' "${requested_version}" > "${FAKE_FETCH_LAST_VERSION}"
fi
bundle_dir="${staging_root}/com.roblox.client-9999-111111111111-222222222222"
mkdir -p "${bundle_dir}"
touch "${bundle_dir}/base.apk" "${bundle_dir}/split_config.x86_64.apk"
printf '%s\n' "${bundle_dir}"
EOF
cat > "${TEMP_DIR}/bin/validator" <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
[[ "${FAKE_VALIDATOR_FAIL:-false}" != true ]] || exit 43
data_root=""
while (( $# > 0 )); do
if [[ "$1" == --store-root ]]; then
data_root="$2"
shift 2
else
shift
fi
done
version_code="${FAKE_VERSION_CODE}"
build_id="${FAKE_BUILD_ID}"
if [[ "${FAKE_UNKNOWN_EXACT_THEN_FALLBACK:-false}" == true ]]; then
requested_version="$(cat "${FAKE_FETCH_LAST_VERSION}")"
case "${requested_version}" in
failure-candidate)
version_code=8888
build_id=abababababababababababababababababababab
;;
2.725.1142)
version_code=2546
build_id=d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21
;;
*) exit 44 ;;
esac
fi
payload_id="${version_code}-${build_id}"
payload_dir="${data_root}/payloads/${payload_id}"
mkdir -p "${payload_dir}/assets/content"
touch "${payload_dir}/libroblox.so"
jq -n --arg build_id "${build_id}" \
--argjson version_code "${version_code}" \
--arg version_name "2.999.${version_code}" \
'{schema_version:1, version_name:$version_name,
version_code:$version_code, elf_build_id:$build_id}' \
> "${payload_dir}/roblox_payload.json"
printf '%s\n' "${payload_id}"
EOF
cat > "${TEMP_DIR}/bin/store" <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
root=""
compatibility=""
expected_current=""
expected_fingerprint=""
expected_rollback_target=""
candidate_profile=""
candidate_compatibility=""
runtime_fingerprint=""
runtime_build_id=""
canary_attestations=()
command_name=""
payload_id=""
while (( $# > 0 )); do
case "$1" in
--root) root="$2"; shift 2 ;;
--compatibility) compatibility="$2"; shift 2 ;;
--expected-current) expected_current="$2"; shift 2 ;;
--expected-payload-fingerprint) expected_fingerprint="$2"; shift 2 ;;
--expected-rollback-target) expected_rollback_target="$2"; shift 2 ;;
--candidate-profile) candidate_profile="$2"; shift 2 ;;
--candidate-compatibility) candidate_compatibility="$2"; shift 2 ;;
--runtime-fingerprint) runtime_fingerprint="$2"; shift 2 ;;
--runtime-build-id) runtime_build_id="$2"; shift 2 ;;
--canary-attestation) canary_attestations+=("$2"); shift 2 ;;
promote|promote-probation|fingerprint)
command_name="$1"; payload_id="$2"; shift 2 ;;
verify-current) command_name="$1"; shift ;;
rollback) command_name="$1"; shift ;;
*) echo "unexpected store argument: $1" >&2; exit 2 ;;
esac
done
fingerprint=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
if [[ "${command_name}" == fingerprint ]]; then
[[ -f "${root}/payloads/${payload_id}/libroblox.so" ]] || exit 1
printf '%s\n' "${fingerprint}"
exit 0
fi
actual_current=none
if [[ -f "${root}/current.json" ]]; then
actual_current="$(jq -r '.payload_id // empty' "${root}/current.json")"
fi
if [[ "${command_name}" == verify-current ]]; then
if [[ "${actual_current}" == none ]]; then
printf 'none\n'
exit 0
fi
[[ "${actual_current}" =~ ^[0-9]+-[0-9a-f]{40}$ &&
"$(jq -r '.payload_path // empty' "${root}/current.json")" == \
"payloads/${actual_current}" &&
-f "${root}/payloads/${actual_current}/libroblox.so" &&
! -L "${root}/payloads/${actual_current}/libroblox.so" &&
-f "${root}/payloads/${actual_current}/roblox_payload.json" ]] || exit 81
approval_path="$(jq -r '.approval_path // empty' "${root}/current.json")"
profile_path="$(jq -r '.host_abi_profile_path // empty' "${root}/current.json")"
compatibility_path="$(jq -r '.compatibility_manifest_path // empty' "${root}/current.json")"
if [[ -n "${approval_path}${profile_path}${compatibility_path}" ]]; then
generation=""
if [[ "${approval_path}" =~ ^approvals/${actual_current}-([0-9a-f]{40})\.json$ ]]; then
generation="${BASH_REMATCH[1]}"
fi
[[ -n "${generation}" &&
"${profile_path}" == "host_abi_profiles/${actual_current}-${generation}.json" &&
"${compatibility_path}" == "compatibility_profiles/${actual_current}-${generation}.json" &&
-f "${root}/${approval_path}" &&
-f "${root}/${profile_path}" &&
-f "${root}/${compatibility_path}" &&
-f "${root}/approvals/${actual_current}-${generation}.canary-1.json" &&
-f "${root}/approvals/${actual_current}-${generation}.canary-2.json" ]] || exit 82
fi
printf '%s\n' "${actual_current}"
exit 0
fi
[[ -z "${expected_current}" || "${expected_current}" == "${actual_current}" ]] || exit 75
if [[ "${command_name}" == rollback ]]; then
[[ "${FAKE_STORE_MODE:-pass}" != \
fail_after_commit_and_rollback ]] || exit 80
[[ -f "${root}/previous_good.json" ]] || exit 1
[[ -z "${expected_rollback_target}" ||
"$(jq -r .payload_id "${root}/previous_good.json")" == \
"${expected_rollback_target}" ]] || exit 78
cp -- "${root}/previous_good.json" "${root}/current.json"
printf 'rollback:%s\n' "$(jq -r .payload_id "${root}/current.json")" >> "${FAKE_CALLS}"
exit 0
fi
[[ ( "${command_name}" == promote ||
"${command_name}" == promote-probation ) &&
"${expected_fingerprint}" == "${fingerprint}" ]] || exit 74
if [[ "${command_name}" == promote-probation ]]; then
[[ -f "${candidate_profile}" && -f "${candidate_compatibility}" &&
"${runtime_fingerprint}" =~ ^[0-9a-f]{64}$ &&
"${runtime_build_id}" =~ ^[0-9a-f]{40}$ &&
"${#canary_attestations[@]}" -eq 2 &&
-f "${canary_attestations[0]}" &&
-f "${canary_attestations[1]}" ]] || exit 79
[[ "$(jq -r .run "${canary_attestations[0]}")" == 1 &&
"$(jq -r .run "${canary_attestations[1]}")" == 2 ]] || exit 79
printf 'promote-probation:%s\n' "${payload_id}" >> "${FAKE_CALLS}"
else
printf 'promote:%s\n' "${payload_id}" >> "${FAKE_CALLS}"
fi
if [[ "${actual_current}" == "${payload_id}" &&
"${command_name}" != promote-probation ]]; then
jq -n --arg payload_id "${payload_id}" \
--arg payload_path "payloads/${payload_id}" \
'{schema_version:1,payload_id:$payload_id,payload_path:$payload_path}' \
> "${root}/current.json"
exit 0
fi
[[ "${FAKE_STORE_MODE:-pass}" != fail_before_commit ]] || exit 76
mkdir -p "${root}"
if [[ "${actual_current}" != none &&
"${actual_current}" != "${payload_id}" ]]; then
cp -- "${root}/current.json" "${root}/previous_good.json"
fi
if [[ "${command_name}" == promote-probation ]]; then
mkdir -p "${root}/approvals" "${root}/host_abi_profiles" \
"${root}/compatibility_profiles"
canary_one_sha256="$(sha256sum "${canary_attestations[0]}" | awk '{print $1}')"
canary_two_sha256="$(sha256sum "${canary_attestations[1]}" | awk '{print $1}')"
generation="$(printf '%s\n' \
"runtime_build_id=${runtime_build_id}" \
"runtime_sha256=${runtime_fingerprint}" \
"canary_1=${canary_one_sha256}" \
"canary_2=${canary_two_sha256}" |
sha256sum | awk '{print substr($1, 1, 40)}')"
cp -- "${candidate_profile}" \
"${root}/host_abi_profiles/${payload_id}-${generation}.json"
cp -- "${candidate_compatibility}" \
"${root}/compatibility_profiles/${payload_id}-${generation}.json"
cp -- "${canary_attestations[0]}" \
"${root}/approvals/${payload_id}-${generation}.canary-1.json"
cp -- "${canary_attestations[1]}" \
"${root}/approvals/${payload_id}-${generation}.canary-2.json"
jq -n --arg runtime_build_id "${runtime_build_id}" \
--arg runtime_sha256 "${runtime_fingerprint}" \
'{runtime_build_id:$runtime_build_id,
runtime_sha256:$runtime_sha256,
canary_runtime_sha256:$runtime_sha256}' \
> "${root}/approvals/${payload_id}-${generation}.json"
jq -n --arg payload_id "${payload_id}" \
--arg payload_path "payloads/${payload_id}" \
--arg approval_path "approvals/${payload_id}-${generation}.json" \
--arg profile_path "host_abi_profiles/${payload_id}-${generation}.json" \
--arg compatibility_path "compatibility_profiles/${payload_id}-${generation}.json" \
'{schema_version:1,payload_id:$payload_id,payload_path:$payload_path,
approval_path:$approval_path,host_abi_profile_path:$profile_path,
compatibility_manifest_path:$compatibility_path}' \
> "${root}/current.json"
else
jq -n --arg payload_id "${payload_id}" \
--arg payload_path "payloads/${payload_id}" \
'{schema_version:1, payload_id:$payload_id, payload_path:$payload_path}' \
> "${root}/current.json"
fi
[[ "${FAKE_STORE_MODE:-pass}" != fail_after_commit &&
"${FAKE_STORE_MODE:-pass}" != fail_after_commit_and_rollback ]] || exit 77
EOF
cat > "${TEMP_DIR}/bin/smoke" <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
[[ "${MOCKTAIL_SKIP_UPDATE_CHECK:-0}" == 1 ]] || exit 1
[[ -f "${ROBLOX_LIB_PATH:-}" && ! -L "${ROBLOX_LIB_PATH}" ]] || exit 1
[[ -d "${MOCKTAIL_ASSET_ROOT:-}" && ! -L "${MOCKTAIL_ASSET_ROOT}" ]] || exit 1
[[ -d "${MOCKTAIL_ASSET_PATH:-}" && ! -L "${MOCKTAIL_ASSET_PATH}" ]] || exit 1
[[ "${MOCKTAIL_ASSET_PATH}" == "${MOCKTAIL_ASSET_ROOT}/content" ]] || exit 1
if [[ "${MOCKTAIL_ISOLATED_CANARY:-0}" == 1 ]]; then
[[ "${MOCKTAIL_AUTO_EXIT_AFTER_PRESENT_MS:-}" == 5000 ]] || exit 1
else
[[ "${MOCKTAIL_AUTO_EXIT_AFTER_PRESENT_MS:-}" == 0 ]] || exit 1
fi
if [[ "${MOCKTAIL_HOST_ABI_CANARY:-0}" == 1 ]]; then
[[ "${MOCKTAIL_ALLOW_CANDIDATE_HOST_ABI:-0}" == 1 &&
-f "${MOCKTAIL_HOST_ABI_PROFILE_FILE:-}" &&
"${MOCKTAIL_COMPATIBILITY_MANIFEST:-}" != \
"${FAKE_EXPECTED_COMPATIBILITY_PATH:?}" ]] || exit 1
[[ " $* " == *" --allow-unverified-build "* ]] || exit 1
elif [[ -n "${MOCKTAIL_HOST_ABI_APPROVAL_RECEIPT:-}" ]]; then
[[ -f "${MOCKTAIL_HOST_ABI_APPROVAL_RECEIPT}" &&
-f "${MOCKTAIL_HOST_ABI_PROFILE_FILE:-}" &&
-f "${MOCKTAIL_COMPATIBILITY_MANIFEST:-}" &&
"${MOCKTAIL_COMPATIBILITY_MANIFEST}" != \
"${FAKE_EXPECTED_COMPATIBILITY_PATH:?}" &&
-z "${MOCKTAIL_ALLOW_CANDIDATE_HOST_ABI+x}" ]] || exit 1
else
[[ "${MOCKTAIL_COMPATIBILITY_MANIFEST:-}" == \
"${FAKE_EXPECTED_COMPATIBILITY_PATH:?}" ]] || exit 1
[[ -z "${MOCKTAIL_ALLOW_CANDIDATE_HOST_ABI+x}" ]] || exit 1
fi
[[ "${MOCKTAIL_SKIP_LIBROBLOX_CTORS:-}" == 0 ]] || exit 1
[[ "${MOCKTAIL_HOST_JNI_SINGLETON_SEED:-}" == 0 ]] || exit 1
[[ -z "${MOCKTAIL_HOST_ALLOCATOR_BRIDGES+x}" ]] || exit 1
[[ -z "${MOCKTAIL_PATCH_HOSTILE_CANARY+x}" ]] || exit 1
[[ -z "${MOCKTAIL_ROBLOX_COOKIES+x}" ]] || exit 1
if [[ "${MOCKTAIL_ISOLATED_CANARY:-0}" == 1 ]]; then
[[ -z "${MOCKTAIL_COOKIE_FILE+x}" &&
"${MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP:-0}" == 1 &&
"${MOCKTAIL_IGNORE_WINDOW_CLOSE:-0}" == 1 ]] || exit 1
fi
[[ "${VK_DRIVER_FILES:-}" == /test/mocktail-vk.json &&
"${VK_ICD_FILENAMES:-}" == /test/mocktail-icd.json &&
"${MESA_LOADER_DRIVER_OVERRIDE:-}" == mocktail-test-driver &&
"${DRI_PRIME:-}" == 1 &&
"${__NV_PRIME_RENDER_OFFLOAD:-}" == 1 &&
"${__VK_LAYER_NV_optimus:-}" == NVIDIA_only &&
"${__GLX_VENDOR_LIBRARY_NAME:-}" == nvidia ]] || exit 1
if [[ "${MOCKTAIL_ISOLATED_CANARY:-0}" == 1 ]]; then
[[ -z "${BASH_ENV+x}" && -z "${LD_PRELOAD+x}" &&
-z "${LD_AUDIT+x}" && -z "${RIPGREP_CONFIG_PATH+x}" ]] || exit 1
[[ "$(type -t mocktail_hostile_canary || true)" != function ]] || exit 1
[[ -n "${FAKE_LIVE_DATA_ROOT:-}" &&
"${MOCKTAIL_DATA_ROOT:-}" != "${FAKE_LIVE_DATA_ROOT}" ]] || exit 1
[[ "${HOME}" != "${FAKE_ORIGINAL_HOME:?}" &&
"${XDG_DATA_HOME}" == "${MOCKTAIL_DATA_ROOT}" &&
"${XDG_CACHE_HOME}" == "${MOCKTAIL_CACHE_ROOT}" &&
"${XDG_STATE_HOME}" == "${MOCKTAIL_STATE_ROOT}" &&
"${XDG_CONFIG_HOME}" == "${MOCKTAIL_CONFIG_ROOT}" ]] || exit 1
mkdir -p "${MOCKTAIL_DATA_ROOT}"
printf 'isolated\n' > "${MOCKTAIL_DATA_ROOT}/canary-marker"
else
[[ "${MOCKTAIL_DATA_ROOT:-}" == "${FAKE_LIVE_DATA_ROOT:?}" ]] || exit 1
fi
MOCKTAIL_LOG_DIR="${MOCKTAIL_LOG_DIR:-${FAKE_LIVE_DATA_ROOT}/launch-logs}"
mkdir -p "${MOCKTAIL_LOG_DIR}"
mode="${FAKE_SMOKE_MODE:-pass}"
if [[ "${mode}" != no_log ]]; then
printf '%s\n' \
'[window] graphics backend=direct-vulkan video=x11 egl=system gles=system' \
'[window] first Roblox Vulkan frame presented' \
'[input] typed production input ready: mouse=1 touch=1 keyboard=1' \
'[main] Roblox lifecycle shutdown: Stopped' \
> "${MOCKTAIL_LOG_DIR}/tierC_fake.log"
fi
printf 'smoke:%s\n' "${mode}" >> "${FAKE_CALLS}"
case "${mode}" in
pass|no_log) ;;
fail) exit 42 ;;
exit3) exit 3 ;;
crash)
ulimit -c 0
kill -SEGV "$$"
;;
hang)
printf '%s\n' "$$" > "${FAKE_HANG_PID_PATH}"
exec sleep 60
;;
race)
jq -n \
--arg payload_id 7777-cccccccccccccccccccccccccccccccccccccccc \
--arg payload_path payloads/7777-cccccccccccccccccccccccccccccccccccccccc \
'{schema_version:1,payload_id:$payload_id,payload_path:$payload_path}' \
> "${FAKE_LIVE_DATA_ROOT}/current.json"
;;
*) exit 2 ;;
esac
EOF
cat > "${TEMP_DIR}/bin/check" <<'EOF'
#!/usr/bin/env bash
[[ "${FAKE_CHECK_FAIL:-false}" != true ]] || exit 42
if [[ -n "${FAKE_CHECK_CALLS:-}" ]]; then
printf '%s\n' "$1" >> "${FAKE_CHECK_CALLS}"
fi
jq -nc --argjson version_code "${FAKE_REMOTE_VERSION}" \
'{version_code:$version_code,version_name:"test"}'
EOF
cat > "${TEMP_DIR}/bin/derive.py" <<'EOF'
#!/usr/bin/env python3
import argparse
import hashlib
import json
import os
parser = argparse.ArgumentParser()
parser.add_argument("--reference-lib", required=True)
parser.add_argument("--reference-profile", required=True)
parser.add_argument("--candidate-lib", required=True)
parser.add_argument("--payload-metadata", required=True)
parser.add_argument("--output", required=True)
parser.add_argument("--compatibility-output", required=True)
args = parser.parse_args()
with open(args.payload_metadata, "r", encoding="utf-8") as source:
metadata = json.load(source)
build_id = metadata["elf_build_id"].lower()
version_code = metadata["version_code"]
payload_id = f"{version_code}-{build_id}"
with open(args.candidate_lib, "rb") as source:
payload_sha256 = hashlib.sha256(source.read()).hexdigest()
profile = {
"schema_version": 1,
"payload_id": payload_id,
"payload_path": f"payloads/{payload_id}",
"elf_build_id": build_id,
"payload_sha256": payload_sha256,
"reference": {"elf_build_id": "1" * 40, "payload_sha256": "2" * 64},
"profile": {},
}
compatibility = {
"schema_version": 1,
"profiles": [{
"version_name": metadata["version_name"],
"version_code": version_code,
"elf_build_id": build_id,
"status": "experimental",
"default_allowed": True,
"allow_legacy_binary_patches": False,
"allow_host_abi_bridges": True,
"allow_host_constructor_replay": True,
}],
}
with open(args.output, "w", encoding="utf-8") as output:
json.dump(profile, output)
with open(args.compatibility_output, "w", encoding="utf-8") as output:
json.dump(compatibility, output)
with open(os.environ["FAKE_DERIVER_CALLS"], "a", encoding="utf-8") as output:
output.write(f"{args.reference_profile}|{payload_id}\n")
EOF
chmod +x "${TEMP_DIR}/bin/"*
export MOCKTAIL_DATA_ROOT="${TEMP_DIR}/data"
export MOCKTAIL_CACHE_ROOT="${TEMP_DIR}/cache"
export MOCKTAIL_STATE_ROOT="${TEMP_DIR}/state"
export MOCKTAIL_UPDATE_FETCH_SCRIPT="${TEMP_DIR}/bin/fetch"
export MOCKTAIL_UPDATE_VALIDATOR_SCRIPT="${TEMP_DIR}/bin/validator"
export MOCKTAIL_UPDATE_STORE_SCRIPT="${TEMP_DIR}/bin/store"
export MOCKTAIL_UPDATE_SMOKE_SCRIPT="${TEMP_DIR}/bin/smoke"
export MOCKTAIL_UPDATE_CHECK_SCRIPT="${TEMP_DIR}/bin/check"
export MOCKTAIL_CONFIG_FILE="${TEMP_DIR}/config.yaml"
export MOCKTAIL_UPDATE_COMPATIBILITY_PATH="${TEMP_DIR}/compatibility.json"
export MOCKTAIL_BOOTSTRAP_SOURCES_PATH="${TEMP_DIR}/bootstrap-sources.json"
export FAKE_BUNDLE_DIR="${TEMP_DIR}/bundle"
export FAKE_CALLS="${TEMP_DIR}/calls"
export FAKE_FETCH_CALLS="${TEMP_DIR}/fetch-calls"
export FAKE_FETCH_ARGUMENTS="${TEMP_DIR}/fetch-arguments"
export FAKE_FETCH_LAST_VERSION="${TEMP_DIR}/fetch-last-version"
export FAKE_HANG_PID_PATH="${TEMP_DIR}/hang-pid"
export FAKE_DERIVER_CALLS="${TEMP_DIR}/deriver-calls"
export FAKE_LIVE_DATA_ROOT="${MOCKTAIL_DATA_ROOT}"
export FAKE_EXPECTED_COMPATIBILITY_PATH="${MOCKTAIL_UPDATE_COMPATIBILITY_PATH}"
export MOCKTAIL_UPDATE_TEST_ENV_PASSTHROUGH=1
export FAKE_ORIGINAL_HOME="${HOME}"
export MOCKTAIL_HOST_ALLOCATOR_BRIDGES=1
export MOCKTAIL_PATCH_HOSTILE_CANARY=1
export MOCKTAIL_SKIP_LIBROBLOX_CTORS=1
export MOCKTAIL_COMPATIBILITY_MANIFEST="${TEMP_DIR}/hostile-compatibility.json"
export MOCKTAIL_ROBLOX_COOKIES=hostile-secret-cookie
export BASH_ENV=/dev/null
export LD_PRELOAD=
export LD_AUDIT=
export RIPGREP_CONFIG_PATH=/dev/null
export VK_DRIVER_FILES=/test/mocktail-vk.json
export VK_ICD_FILENAMES=/test/mocktail-icd.json
export MESA_LOADER_DRIVER_OVERRIDE=mocktail-test-driver
export DRI_PRIME=1
export __NV_PRIME_RENDER_OFFLOAD=1
export __VK_LAYER_NV_optimus=NVIDIA_only
export __GLX_VENDOR_LIBRARY_NAME=nvidia
mocktail_hostile_canary() { :; }
export -f mocktail_hostile_canary
printf '%s\n' 'version: 1' > "${MOCKTAIL_CONFIG_FILE}"
jq -n '{schema_version:1,sources:[
{package:"com.roblox.client",version_name:"2.725.1142",version_code:2546,
provider:"uptodown"}
]}' > "${MOCKTAIL_BOOTSTRAP_SOURCES_PATH}"
jq -n \
--arg current d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21 \
--arg seed 1686400865ae0e408cd7bd67de7a439625c6fd13 \
--arg failure bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb \
--arg candidate aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
'{schema_version:1, profiles:[
{version_name:"2.725.1142",version_code:2546,elf_build_id:$current,
status:"supported",default_allowed:true,
allow_legacy_binary_patches:false},
{version_name:"2.727.1199",version_code:2628,elf_build_id:$seed,
status:"supported",default_allowed:true,
allow_legacy_binary_patches:false},
{version_name:"failure-candidate",version_code:8888,elf_build_id:$failure,
status:"supported",default_allowed:true,
allow_legacy_binary_patches:false},
{version_name:"candidate",version_code:9999,elf_build_id:$candidate,
status:"experimental",default_allowed:false,
allow_legacy_binary_patches:false}
]}' > "${MOCKTAIL_UPDATE_COMPATIBILITY_PATH}"
# MOCKTAIL_CONFIG_ROOT is the runtime override used before path exports.
# The updater must read the same file instead of silently falling back to the
# default XDG path.
saved_config_file="${MOCKTAIL_CONFIG_FILE}"
unset MOCKTAIL_CONFIG_FILE
export MOCKTAIL_CONFIG_ROOT="${TEMP_DIR}/typed-config-root"
export FAKE_CHECK_CALLS="${TEMP_DIR}/config-root-check-calls"
mkdir -p "${MOCKTAIL_CONFIG_ROOT}"
cat > "${MOCKTAIL_CONFIG_ROOT}/config.yaml" <<'EOF'
version: 1
updates:
automatic: false
EOF
"${AUTO_UPDATE}" --skip-build --scheduled
[[ ! -e "${FAKE_CHECK_CALLS}" ]] || {
echo "scheduled updater ignored MOCKTAIL_CONFIG_ROOT" >&2
exit 1
}
unset MOCKTAIL_CONFIG_ROOT FAKE_CHECK_CALLS
export MOCKTAIL_CONFIG_FILE="${saved_config_file}"
# A fresh installation downloads and stages provider latest before considering
# a supported fallback. This fixture makes that latest download resolve to an
# exact-supported payload, so no fallback mirror request is necessary.
export MOCKTAIL_DATA_ROOT="${TEMP_DIR}/bootstrap-data"
export FAKE_LIVE_DATA_ROOT="${MOCKTAIL_DATA_ROOT}"
export FAKE_VERSION_CODE=2628
export FAKE_BUILD_ID=1686400865ae0e408cd7bd67de7a439625c6fd13
export FAKE_REMOTE_VERSION=2628
export MOCKTAIL_UPDATE_LOCAL_VERSION=none
progress_packets="${TEMP_DIR}/first-run-progress"
exec 7>"${progress_packets}"
MOCKTAIL_UPDATE_PROGRESS_FD=7 \
"${AUTO_UPDATE}" --skip-build --startup-preflight
exec 7>&-
[[ "$(cat "${progress_packets}")" == \
'PDownloading Roblox...PInstalling Roblox...PTesting Roblox...PInstalling Roblox...' ]] || {
echo "first-run progress did not follow updater stages" >&2
exit 1
}
[[ "$(cat "${FAKE_FETCH_ARGUMENTS}")" == 'apk-pure|' ]] || {
echo "first-run did not request provider latest before fallback" >&2
exit 1
}
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/current.json")" == \
"2628-${FAKE_BUILD_ID}" ]]
if find "${MOCKTAIL_CACHE_ROOT}/downloads/apk-staging" -mindepth 1 \
-maxdepth 1 -type d -print -quit | grep -q .; then
echo "first-run bootstrap retained a duplicate downloaded APK bundle" >&2
exit 1
fi
# Once a runnable local payload exists, interactive startup does not contact
# the provider or repeat candidate canaries. The user timer owns remote work.
startup_fetch_calls="$(wc -l < "${FAKE_FETCH_CALLS}")"
export FAKE_CHECK_CALLS="${TEMP_DIR}/startup-check-calls"
export FAKE_REMOTE_VERSION=9999
rm -f -- "${FAKE_CHECK_CALLS}" "${FAKE_CALLS}"
"${AUTO_UPDATE}" --skip-build --startup-preflight
[[ ! -e "${FAKE_CHECK_CALLS}" && ! -e "${FAKE_CALLS}" ]]
[[ "$(wc -l < "${FAKE_FETCH_CALLS}")" -eq "${startup_fetch_calls}" ]]
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/current.json")" == \
"2628-${FAKE_BUILD_ID}" ]]
unset FAKE_CHECK_CALLS
# A second installation can share the same managed payload root while the
# first one is still updating it. Interactive startup waits for that updater
# and then validates its result instead of reporting a false crash and
# collecting a useless lock-contention support bundle.
lock_ready="${TEMP_DIR}/startup-lock-ready"
lock_release="${TEMP_DIR}/startup-lock-release"
[[ ! -e "${MOCKTAIL_STATE_ROOT}/support" ]]
(
exec 9>"${MOCKTAIL_DATA_ROOT}/.auto-update.lock"
flock 9
touch "${lock_ready}"
while [[ ! -e "${lock_release}" ]]; do
sleep 0.01
done
) &
LOCK_HOLDER_PID=$!
for _ in {1..500}; do
[[ ! -e "${lock_ready}" ]] || break
sleep 0.01
done
[[ -e "${lock_ready}" ]]
waiting_progress="${TEMP_DIR}/waiting-progress"
waiting_stderr="${TEMP_DIR}/waiting-stderr"
exec 7>"${waiting_progress}"
MOCKTAIL_UPDATE_PROGRESS_FD=7 \
"${AUTO_UPDATE}" --skip-build --startup-preflight \
>"${TEMP_DIR}/waiting-stdout" 2>"${waiting_stderr}" &
waiting_updater_pid=$!
for _ in {1..500}; do
grep -Fq 'PWaiting for Roblox update...' "${waiting_progress}" && break
kill -0 "${waiting_updater_pid}" 2>/dev/null
sleep 0.01
done
grep -Fq 'PWaiting for Roblox update...' "${waiting_progress}"
kill -0 "${waiting_updater_pid}" 2>/dev/null
touch "${lock_release}"
wait "${LOCK_HOLDER_PID}"
LOCK_HOLDER_PID=""
wait "${waiting_updater_pid}"
exec 7>&-
grep -Fq \
'another Roblox update is already running; waiting for its result' \
"${waiting_stderr}"
grep -Fq \
'the running Roblox update finished; validating its result' \
"${waiting_stderr}"
[[ ! -e "${MOCKTAIL_STATE_ROOT}/support" ]]
# Existing short configs predate updates.*. The documented automatic=true
# default still enables the timer instead of silently disabling background
# updates for those users.
export FAKE_CHECK_CALLS="${TEMP_DIR}/default-scheduled-check-calls"
export FAKE_REMOTE_VERSION=2628
export MOCKTAIL_UPDATE_LOCAL_VERSION=2628
"${AUTO_UPDATE}" --skip-build --scheduled
[[ -s "${FAKE_CHECK_CALLS}" ]]
unset FAKE_CHECK_CALLS
export MOCKTAIL_UPDATE_LOCAL_VERSION=none
# A temporary metadata outage on a new installation still bootstraps the
# newest explicitly supported profile instead of downloading unknown latest
# bytes that cannot pass the compatibility gate.
export MOCKTAIL_DATA_ROOT="${TEMP_DIR}/offline-bootstrap-data"
export FAKE_LIVE_DATA_ROOT="${MOCKTAIL_DATA_ROOT}"
export FAKE_VERSION_CODE=8888
export FAKE_BUILD_ID=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
export FAKE_CHECK_FAIL=true
rm -f -- "${FAKE_CALLS}" "${FAKE_FETCH_CALLS}" "${FAKE_FETCH_ARGUMENTS}"
"${AUTO_UPDATE}" --skip-build
[[ "$(cat "${FAKE_FETCH_ARGUMENTS}")" == 'apk-pure|failure-candidate' ]] || {
echo "offline first-run bootstrap did not request the newest supported profile" >&2
exit 1
}
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/current.json")" == \
"8888-${FAKE_BUILD_ID}" ]]
unset FAKE_CHECK_FAIL
# A fresh install must not choose one of two supported Build IDs that share
# the newest fallback versionCode. It skips that ambiguous version and uses
# the next unique exact profile.
PRIMARY_COMPATIBILITY_PATH="${MOCKTAIL_UPDATE_COMPATIBILITY_PATH}"
AMBIGUOUS_BOOTSTRAP_BUILD=cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd
jq --arg build_id "${AMBIGUOUS_BOOTSTRAP_BUILD}" '
.profiles += [{version_name:"failure-candidate-alt",version_code:8888,
elf_build_id:$build_id,status:"supported",default_allowed:true,
allow_legacy_binary_patches:false}]
' "${PRIMARY_COMPATIBILITY_PATH}" > "${TEMP_DIR}/compatibility.bootstrap-ambiguous.json"
export MOCKTAIL_UPDATE_COMPATIBILITY_PATH="${TEMP_DIR}/compatibility.bootstrap-ambiguous.json"
export FAKE_EXPECTED_COMPATIBILITY_PATH="${MOCKTAIL_UPDATE_COMPATIBILITY_PATH}"
export MOCKTAIL_DATA_ROOT="${TEMP_DIR}/ambiguous-bootstrap-data"
export FAKE_LIVE_DATA_ROOT="${MOCKTAIL_DATA_ROOT}"
for ambiguous_build in \
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb \
"${AMBIGUOUS_BOOTSTRAP_BUILD}"; do
ambiguous_dir="${MOCKTAIL_DATA_ROOT}/payloads/8888-${ambiguous_build}"
mkdir -p "${ambiguous_dir}/assets/content"
touch "${ambiguous_dir}/libroblox.so"
jq -n --arg build_id "${ambiguous_build}" \
'{schema_version:1,version_code:8888,elf_build_id:$build_id}' \
> "${ambiguous_dir}/roblox_payload.json"
done
export FAKE_VERSION_CODE=2546
export FAKE_BUILD_ID=d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21
export FAKE_REMOTE_VERSION=9999
rm -f -- "${FAKE_CALLS}" "${FAKE_FETCH_CALLS}" "${FAKE_FETCH_ARGUMENTS}"
"${AUTO_UPDATE}" --skip-build
mapfile -t ambiguous_bootstrap_fetches < "${FAKE_FETCH_ARGUMENTS}"
[[ "${ambiguous_bootstrap_fetches[0]}" == 'apk-pure|' &&
"${ambiguous_bootstrap_fetches[1]}" == 'apk-pure|2.727.1199' &&
"${#ambiguous_bootstrap_fetches[@]}" -eq 2 ]] || {
echo "fresh install did not bind provider payloads to fallback versions" >&2
exit 1
}
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/current.json")" == \
"2546-${FAKE_BUILD_ID}" ]]
# A provider can reuse a versionCode for a different ELF Build ID. If that
# happens on first run, keep the unknown bytes staged and retry one lower
# unique compatibility profile instead of leaving the installation inert.
export MOCKTAIL_UPDATE_COMPATIBILITY_PATH="${PRIMARY_COMPATIBILITY_PATH}"
export FAKE_EXPECTED_COMPATIBILITY_PATH="${MOCKTAIL_UPDATE_COMPATIBILITY_PATH}"
export MOCKTAIL_DATA_ROOT="${TEMP_DIR}/unknown-exact-bootstrap-data"
export FAKE_LIVE_DATA_ROOT="${MOCKTAIL_DATA_ROOT}"
export FAKE_REMOTE_VERSION=8888
export MOCKTAIL_UPDATE_LOCAL_VERSION=none
export FAKE_UNKNOWN_EXACT_THEN_FALLBACK=true
rm -f -- "${FAKE_CALLS}" "${FAKE_FETCH_CALLS}" \
"${FAKE_FETCH_ARGUMENTS}" "${FAKE_FETCH_LAST_VERSION}"
"${AUTO_UPDATE}" --skip-build
mapfile -t fallback_fetches < "${FAKE_FETCH_ARGUMENTS}"
[[ "${fallback_fetches[0]}" == 'apk-pure|' ]]
[[ "${fallback_fetches[1]}" == 'apk-pure|failure-candidate' ]]
[[ "${fallback_fetches[2]}" == 'apk-pure|2.727.1199' ]]
[[ "${fallback_fetches[3]}" == 'uptodown|2.725.1142' ]]
[[ "${#fallback_fetches[@]}" -eq 4 ]]
[[ -d "${MOCKTAIL_DATA_ROOT}/payloads/8888-abababababababababababababababababababab" ]]
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/current.json")" == \
2546-d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21 ]]
unset FAKE_UNKNOWN_EXACT_THEN_FALLBACK
export MOCKTAIL_DATA_ROOT="${TEMP_DIR}/data"
export FAKE_LIVE_DATA_ROOT="${MOCKTAIL_DATA_ROOT}"
rm -f -- "${FAKE_CALLS}" "${FAKE_FETCH_CALLS}" "${FAKE_FETCH_ARGUMENTS}"
export FAKE_VERSION_CODE=2546
export FAKE_BUILD_ID=d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21
export FAKE_REMOTE_VERSION=2546
export MOCKTAIL_UPDATE_LOCAL_VERSION=none
export FAKE_STORE_MODE=fail_after_commit
"${AUTO_UPDATE}" --skip-build
unset FAKE_STORE_MODE
[[ "$(sed -n '1p' "${FAKE_CALLS}")" == smoke:pass ]]
[[ "$(sed -n '2p' "${FAKE_CALLS}")" == \
"promote:2546-${FAKE_BUILD_ID}" ]]
[[ "$(wc -l < "${FAKE_CALLS}")" -eq 2 ]]
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/current.json")" == \
"2546-${FAKE_BUILD_ID}" ]]
# A distinct supported update exercises the actual 2546 -> candidate path.
# A regular readiness failure, native crash, or watchdog timeout must preserve
# both live manifests byte-for-byte and must not invoke payload promotion.
PREVIOUS_ID=2400-eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
previous_payload_dir="${MOCKTAIL_DATA_ROOT}/payloads/${PREVIOUS_ID}"
mkdir -p "${previous_payload_dir}/assets/content"
touch "${previous_payload_dir}/libroblox.so"
jq -n '{schema_version:1,version_name:"2.700.2400",version_code:2400,
elf_build_id:"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"}' \
> "${previous_payload_dir}/roblox_payload.json"
jq '.profiles += [{version_name:"2.700.2400",version_code:2400,
elf_build_id:"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
status:"supported",default_allowed:true,
allow_legacy_binary_patches:false}]' \
"${MOCKTAIL_UPDATE_COMPATIBILITY_PATH}" > "${TEMP_DIR}/compatibility.previous.json"
mv -- "${TEMP_DIR}/compatibility.previous.json" \
"${MOCKTAIL_UPDATE_COMPATIBILITY_PATH}"
jq -n --arg payload_id "${PREVIOUS_ID}" \
--arg payload_path "payloads/${PREVIOUS_ID}" \
'{schema_version:1,payload_id:$payload_id,payload_path:$payload_path}' \
> "${MOCKTAIL_DATA_ROOT}/previous_good.json"
cp -- "${MOCKTAIL_DATA_ROOT}/current.json" "${TEMP_DIR}/current.before-canary"
cp -- "${MOCKTAIL_DATA_ROOT}/previous_good.json" \
"${TEMP_DIR}/previous.before-canary"
export FAKE_VERSION_CODE=8888
export FAKE_BUILD_ID=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
export FAKE_REMOTE_VERSION=8888
export MOCKTAIL_UPDATE_LOCAL_VERSION=2546
for smoke_mode in fail exit3 crash hang; do
rm -f -- "${FAKE_CALLS}" "${FAKE_HANG_PID_PATH}"
export FAKE_SMOKE_MODE="${smoke_mode}"
started_at="${SECONDS}"
set +e
if [[ "${smoke_mode}" == hang ]]; then
MOCKTAIL_UPDATE_CANARY_TIMEOUT_S=1 \
"${AUTO_UPDATE}" --skip-build >"${TEMP_DIR}/${smoke_mode}.stdout" \
2>"${TEMP_DIR}/${smoke_mode}.stderr"
else
"${AUTO_UPDATE}" --skip-build >"${TEMP_DIR}/${smoke_mode}.stdout" \
2>"${TEMP_DIR}/${smoke_mode}.stderr"
fi
canary_status=$?
set -e
[[ "${canary_status}" -eq 0 ]] || {
echo "${smoke_mode} canary prevented the working payload from continuing" >&2
exit 1
}
[[ "$(cat "${FAKE_CALLS}")" == "smoke:${smoke_mode}" ]] || {
echo "${smoke_mode} canary reached payload promotion" >&2
exit 1
}
cmp --silent "${TEMP_DIR}/current.before-canary" \
"${MOCKTAIL_DATA_ROOT}/current.json" || {
echo "${smoke_mode} canary changed the active payload manifest" >&2
exit 1
}
cmp --silent "${TEMP_DIR}/previous.before-canary" \
"${MOCKTAIL_DATA_ROOT}/previous_good.json" || {
echo "${smoke_mode} canary changed previous-good before promotion" >&2
exit 1
}
[[ ! -e "${MOCKTAIL_DATA_ROOT}/canary-marker" ]] || {
echo "${smoke_mode} canary used the live data root" >&2
exit 1
}
if compgen -G "${MOCKTAIL_CACHE_ROOT}/.update-canary.*" >/dev/null; then
echo "${smoke_mode} canary left its isolated directory behind" >&2
exit 1
fi
if [[ "${smoke_mode}" == hang ]]; then
[[ $((SECONDS - started_at)) -lt 10 ]]
hang_pid="$(cat "${FAKE_HANG_PID_PATH}")"
if kill -0 "${hang_pid}" 2>/dev/null; then
echo "timed-out canary left an orphan process ${hang_pid}" >&2
exit 1
fi
fi
if [[ "${smoke_mode}" == fail ]]; then
support_archive="$(find "${MOCKTAIL_STATE_ROOT}/support" -maxdepth 1 \
-type f -name 'mocktail-support-*.tar.gz' -print -quit)"
[[ -n "${support_archive}" ]] || {
echo "failed canary did not retain a support bundle" >&2
exit 1
}
support_extract="${TEMP_DIR}/canary-support"
rm -rf -- "${support_extract}"
mkdir -p -- "${support_extract}"
tar -C "${support_extract}" -xzf "${support_archive}"
grep -Fxq 'context=updater' \
"${support_extract}/mocktail-support/runtime.txt"
grep -Fxq 'reason=canary-failed' \
"${support_extract}/mocktail-support/runtime.txt"
grep -Fxq 'exit_code=42' \
"${support_extract}/mocktail-support/runtime.txt"
grep -Fxq 'status=candidate' \
"${support_extract}/mocktail-support/payload.txt"
grep -Fxq 'version_code=8888' \
"${support_extract}/mocktail-support/payload.txt"
grep -Fxq "elf_build_id=${FAKE_BUILD_ID}" \
"${support_extract}/mocktail-support/payload.txt"
grep -Fxq 'actual_video_driver=x11' \
"${support_extract}/mocktail-support/markers.txt"
fi
done
unset FAKE_SMOKE_MODE
# A successful smoke followed by a store failure still preserves both
# manifests. If the store reports failure after its commit point, the updater
# observes candidate=current and performs a conditional rollback to 2546.
for store_mode in fail_before_commit fail_after_commit; do
rm -f -- "${FAKE_CALLS}"
export FAKE_STORE_MODE="${store_mode}"
set +e
"${AUTO_UPDATE}" --skip-build >"${TEMP_DIR}/${store_mode}.stdout" \
2>"${TEMP_DIR}/${store_mode}.stderr"
update_status=$?
set -e
[[ "${update_status}" -eq 0 ]]
cmp --silent "${TEMP_DIR}/current.before-canary" \
"${MOCKTAIL_DATA_ROOT}/current.json"
[[ "$(sed -n '1p' "${FAKE_CALLS}")" == smoke:pass ]]
[[ "$(sed -n '2p' "${FAKE_CALLS}")" == \
"promote:8888-${FAKE_BUILD_ID}" ]]
! grep -Fq '[auto-update] support bundle:' \
"${TEMP_DIR}/${store_mode}.stderr"
promotion_archive="$(find "${MOCKTAIL_STATE_ROOT}/support" -maxdepth 1 \
-type f -name 'mocktail-support-*.tar.gz' -printf '%T@ %p\n' | \
sort -n | tail -n 1 | cut -d' ' -f2-)"
[[ -f "${promotion_archive}" ]] || {
echo "${store_mode} did not retain a promotion support bundle" >&2
exit 1
}
promotion_extract="${TEMP_DIR}/promotion-support-${store_mode}"
mkdir -p -- "${promotion_extract}"
tar -C "${promotion_extract}" -xzf "${promotion_archive}"
grep -Fxq 'reason=updater-promotion-failed' \
"${promotion_extract}/mocktail-support/runtime.txt"
grep -Fxq 'actual_video_driver=x11' \
"${promotion_extract}/mocktail-support/markers.txt"
if [[ "${store_mode}" == fail_before_commit ]]; then
[[ "$(wc -l < "${FAKE_CALLS}")" -eq 2 ]]
cmp --silent "${TEMP_DIR}/previous.before-canary" \
"${MOCKTAIL_DATA_ROOT}/previous_good.json"
else
[[ "$(wc -l < "${FAKE_CALLS}")" -eq 3 ]]
[[ "$(sed -n '3p' "${FAKE_CALLS}")" == \
"rollback:2546-d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21" ]]
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/previous_good.json")" == \
2546-d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21 ]]
fi
done
# If both the post-commit promotion result and its conditional rollback fail,
# current is uncertain. The updater must return a hard failure instead of
# treating the candidate as the previous working payload.
rm -f -- "${FAKE_CALLS}"
export FAKE_STORE_MODE=fail_after_commit_and_rollback
set +e
"${AUTO_UPDATE}" --skip-build \
>"${TEMP_DIR}/rollback-failure.stdout" \
2>"${TEMP_DIR}/rollback-failure.stderr"
rollback_failure_status=$?
set -e
[[ "${rollback_failure_status}" -ne 0 ]]
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/current.json")" == \
"8888-${FAKE_BUILD_ID}" ]]
grep -Fq 'current payload is uncertain' \
"${TEMP_DIR}/rollback-failure.stderr"
cp -- "${TEMP_DIR}/current.before-canary" \
"${MOCKTAIL_DATA_ROOT}/current.json"
cp -- "${TEMP_DIR}/previous.before-canary" \
"${MOCKTAIL_DATA_ROOT}/previous_good.json"
unset FAKE_STORE_MODE
cp -- "${TEMP_DIR}/previous.before-canary" \
"${MOCKTAIL_DATA_ROOT}/previous_good.json"
# A concurrent writer changing current during canary wins the CAS race; the
# updater must not overwrite that manifest with its candidate.
rm -f -- "${FAKE_CALLS}"
export FAKE_SMOKE_MODE=race
set +e
"${AUTO_UPDATE}" --skip-build >"${TEMP_DIR}/race.stdout" \
2>"${TEMP_DIR}/race.stderr"
race_status=$?
set -e
[[ "${race_status}" -ne 0 ]]
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/current.json")" == \
7777-cccccccccccccccccccccccccccccccccccccccc ]]
[[ "$(cat "${FAKE_CALLS}")" == smoke:race ]]
cp -- "${TEMP_DIR}/current.before-canary" \
"${MOCKTAIL_DATA_ROOT}/current.json"
cmp --silent "${TEMP_DIR}/previous.before-canary" \
"${MOCKTAIL_DATA_ROOT}/previous_good.json"
unset FAKE_SMOKE_MODE
export FAKE_VERSION_CODE=2546
export FAKE_BUILD_ID=d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21
export FAKE_REMOTE_VERSION=2546
current_library="${MOCKTAIL_DATA_ROOT}/payloads/2546-${FAKE_BUILD_ID}/libroblox.so"
rm -- "${current_library}"
unset MOCKTAIL_UPDATE_LOCAL_VERSION
"${AUTO_UPDATE}" --skip-build
[[ -f "${current_library}" ]] || {
echo "missing current library did not trigger payload recovery" >&2
exit 1
}
[[ "$(wc -l < "${FAKE_FETCH_CALLS}")" -eq 3 ]]
rm -f "${FAKE_CALLS}"
export FAKE_VERSION_CODE=9999
export FAKE_BUILD_ID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
export FAKE_REMOTE_VERSION=9999
export MOCKTAIL_UPDATE_LOCAL_VERSION=2546
"${AUTO_UPDATE}" --skip-build
[[ -d "${MOCKTAIL_DATA_ROOT}/payloads/9999-${FAKE_BUILD_ID}" ]] || {
echo "unknown Build ID was not retained as an isolated staged payload" >&2
exit 1
}
[[ "$(sed -n '1p' "${FAKE_CALLS}")" == smoke:pass ]]
[[ "$(sed -n '2p' "${FAKE_CALLS}")" == \
"promote:8888-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" ]]
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/current.json")" == \
"8888-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" ]]
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/previous_good.json")" == \
2546-d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21 ]]
# The same selection must work without another download when the unsupported
# remote payload was already staged by an earlier check.
cp -- "${TEMP_DIR}/current.before-canary" \
"${MOCKTAIL_DATA_ROOT}/current.json"
cp -- "${TEMP_DIR}/previous.before-canary" \
"${MOCKTAIL_DATA_ROOT}/previous_good.json"
fetch_calls_before_staged_fallback="$(wc -l < "${FAKE_FETCH_CALLS}")"
rm -f -- "${FAKE_CALLS}"
"${AUTO_UPDATE}" --skip-build
[[ "$(sed -n '1p' "${FAKE_CALLS}")" == smoke:pass ]]
[[ "$(sed -n '2p' "${FAKE_CALLS}")" == \
"promote:8888-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" ]]
[[ "$(wc -l < "${FAKE_FETCH_CALLS}")" -eq \
"${fetch_calls_before_staged_fallback}" ]] || {
echo "staged supported fallback triggered another provider download" >&2
exit 1
}
# Two exact-supported Build IDs at the same newest staged version are
# ambiguous. Neither may be selected automatically.
AMBIGUOUS_BUILD_A=1111111111111111111111111111111111111111
AMBIGUOUS_BUILD_B=2222222222222222222222222222222222222222
jq --arg first "${AMBIGUOUS_BUILD_A}" --arg second "${AMBIGUOUS_BUILD_B}" '
.profiles += [
{version_name:"ambiguous-a",version_code:9000,elf_build_id:$first,
status:"supported",default_allowed:true,
allow_legacy_binary_patches:false},
{version_name:"ambiguous-b",version_code:9000,elf_build_id:$second,
status:"supported",default_allowed:true,
allow_legacy_binary_patches:false}
]' "${MOCKTAIL_UPDATE_COMPATIBILITY_PATH}" \
> "${TEMP_DIR}/compatibility.ambiguous.json"
mv -- "${TEMP_DIR}/compatibility.ambiguous.json" \
"${MOCKTAIL_UPDATE_COMPATIBILITY_PATH}"
for ambiguous_build in "${AMBIGUOUS_BUILD_A}" "${AMBIGUOUS_BUILD_B}"; do
ambiguous_dir="${MOCKTAIL_DATA_ROOT}/payloads/9000-${ambiguous_build}"
mkdir -p "${ambiguous_dir}/assets/content"
touch "${ambiguous_dir}/libroblox.so"
jq -n --arg build_id "${ambiguous_build}" \
'{schema_version:1,version_code:9000,elf_build_id:$build_id}' \
> "${ambiguous_dir}/roblox_payload.json"
done
export MOCKTAIL_UPDATE_LOCAL_VERSION=8888
rm -f -- "${FAKE_CALLS}"
"${AUTO_UPDATE}" --skip-build
[[ "$(sed -n '1p' "${FAKE_CALLS}")" == smoke:pass ]]
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/current.json")" == \
"8888-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" ]]
# The ambiguity check must also apply when the provider remote version is the
# same version shared by both exact-supported staged Build IDs.
export FAKE_REMOTE_VERSION=9000
rm -f -- "${FAKE_CALLS}"
"${AUTO_UPDATE}" --skip-build
[[ ! -e "${FAKE_CALLS}" ]]
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/current.json")" == \
"8888-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" ]]
export FAKE_REMOTE_VERSION=9999
# Reconsider a staged candidate after its exact Build-ID profile is approved,
# without downloading the same payload again.
fetch_calls_before_approval="$(wc -l < "${FAKE_FETCH_CALLS}")"
jq '(.profiles[] | select(.version_code == 9999)) |=
(.status = "supported" | .default_allowed = true)' \
"${MOCKTAIL_UPDATE_COMPATIBILITY_PATH}" \
> "${TEMP_DIR}/compatibility.approved.json"
mv -- "${TEMP_DIR}/compatibility.approved.json" \
"${MOCKTAIL_UPDATE_COMPATIBILITY_PATH}"
rm -f -- "${FAKE_CALLS}"
export MOCKTAIL_UPDATE_LOCAL_VERSION=8888
"${AUTO_UPDATE}" --skip-build
[[ "$(sed -n '1p' "${FAKE_CALLS}")" == smoke:pass ]]
[[ "$(sed -n '2p' "${FAKE_CALLS}")" == \
"promote:9999-${FAKE_BUILD_ID}" ]]
[[ "$(wc -l < "${FAKE_FETCH_CALLS}")" -eq \
"${fetch_calls_before_approval}" ]] || {
echo "approved staged candidate was downloaded again" >&2
exit 1
}
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/current.json")" == \
"9999-${FAKE_BUILD_ID}" ]]
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/previous_good.json")" == \
8888-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ]]
# Stale provider metadata must never turn an update check into a downgrade.
export MOCKTAIL_UPDATE_LOCAL_VERSION=9999
export FAKE_REMOTE_VERSION=9000
fetch_calls_before_regression="$(wc -l < "${FAKE_FETCH_CALLS}")"
rm -f -- "${FAKE_CALLS}"
"${AUTO_UPDATE}" --skip-build
[[ ! -e "${FAKE_CALLS}" ]]
[[ "$(wc -l < "${FAKE_FETCH_CALLS}")" -eq \
"${fetch_calls_before_regression}" ]]
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/current.json")" == \
"9999-${FAKE_BUILD_ID}" ]]
# An exact derived sidecar is exercised twice in separate HOME/XDG roots. Only
# after both attestations bind the same candidate/profile/runtime fingerprint
# may the store receive promote-probation.
main_data_root="${MOCKTAIL_DATA_ROOT}"
probation_data_root="${TEMP_DIR}/probation-data"
export MOCKTAIL_DATA_ROOT="${probation_data_root}"
export FAKE_LIVE_DATA_ROOT="${MOCKTAIL_DATA_ROOT}"
seed_build_id=1686400865ae0e408cd7bd67de7a439625c6fd13
seed_payload_id="2628-${seed_build_id}"
seed_payload_dir="${MOCKTAIL_DATA_ROOT}/payloads/${seed_payload_id}"
mkdir -p "${seed_payload_dir}/assets/content"
printf 'seed\n' > "${seed_payload_dir}/libroblox.so"
jq -n --arg build_id "${seed_build_id}" \
'{schema_version:1,version_name:"2.727.1199",version_code:2628,
elf_build_id:$build_id}' > "${seed_payload_dir}/roblox_payload.json"
jq -n --arg payload_id "${seed_payload_id}" \
--arg payload_path "payloads/${seed_payload_id}" \
'{schema_version:1,payload_id:$payload_id,payload_path:$payload_path}' \
> "${MOCKTAIL_DATA_ROOT}/current.json"
printf '{}\n' > "${TEMP_DIR}/reference-profile.json"
export MOCKTAIL_UPDATE_REFERENCE_PROFILE="${TEMP_DIR}/reference-profile.json"
export MOCKTAIL_UPDATE_PROFILE_DERIVER="${TEMP_DIR}/bin/derive.py"
export MOCKTAIL_UPDATE_LOCAL_VERSION=2628
export FAKE_REMOTE_VERSION=10001
export FAKE_VERSION_CODE=10001
export FAKE_BUILD_ID=edededededededededededededededededededed
rm -f -- "${FAKE_CALLS}" "${FAKE_DERIVER_CALLS}"
"${AUTO_UPDATE}" --skip-build --launch
mapfile -t probation_calls < "${FAKE_CALLS}"
[[ "${probation_calls[0]}" == smoke:pass &&
"${probation_calls[1]}" == smoke:pass &&
"${probation_calls[2]}" == \
"promote-probation:10001-${FAKE_BUILD_ID}" &&
"${probation_calls[3]}" == smoke:pass &&
"${#probation_calls[@]}" -eq 4 ]] || {
echo "derived candidate did not run two canaries then approved launch" >&2
exit 1
}
grep -Fq "${TEMP_DIR}/reference-profile.json|10001-${FAKE_BUILD_ID}" \
"${FAKE_DERIVER_CALLS}"
[[ ! -e "${MOCKTAIL_DATA_ROOT}/canary-marker" ]]
if compgen -G "${MOCKTAIL_CACHE_ROOT}/.update-canary.*" >/dev/null; then
echo "derived candidate left an isolated canary root behind" >&2
exit 1
fi
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/current.json")" == \
"10001-${FAKE_BUILD_ID}" ]]
deriver_calls_before_refresh="$(wc -l < "${FAKE_DERIVER_CALLS}")"
cp -- "${MOCKTAIL_DATA_ROOT}/previous_good.json" \
"${TEMP_DIR}/probation-previous.before-refresh"
stale_fingerprint_approval_ref="$(jq -r .approval_path \
"${MOCKTAIL_DATA_ROOT}/current.json")"
approval_file="${MOCKTAIL_DATA_ROOT}/${stale_fingerprint_approval_ref}"
jq '.runtime_sha256 = ("0" * 64) |
.canary_runtime_sha256 = ("0" * 64)' \
"${approval_file}" > "${TEMP_DIR}/approval.wrong-runtime-sha.json"
mv -- "${TEMP_DIR}/approval.wrong-runtime-sha.json" "${approval_file}"
export MOCKTAIL_UPDATE_LOCAL_VERSION=10001
export FAKE_REMOTE_VERSION=10001
rm -f -- "${FAKE_CALLS}"
"${AUTO_UPDATE}" --skip-build --no-launch
mapfile -t fingerprint_refresh_calls < "${FAKE_CALLS}"
[[ "${fingerprint_refresh_calls[0]}" == smoke:pass &&
"${fingerprint_refresh_calls[1]}" == smoke:pass &&
"${fingerprint_refresh_calls[2]}" == \
"promote-probation:10001-${FAKE_BUILD_ID}" &&
"${#fingerprint_refresh_calls[@]}" -eq 3 ]] || {
echo "runtime SHA change did not refresh approval through two canaries" >&2
exit 1
}
old_approval_ref="$(jq -r .approval_path "${MOCKTAIL_DATA_ROOT}/current.json")"
[[ "${old_approval_ref}" != "${stale_fingerprint_approval_ref}" &&
"$(jq -r .runtime_sha256 \
"${MOCKTAIL_DATA_ROOT}/${old_approval_ref}")" != \
"$(printf '0%.0s' {1..64})" ]]
export MOCKTAIL_UPDATE_RUNTIME_BUILD_ID=fefefefefefefefefefefefefefefefefefefefe
export MOCKTAIL_UPDATE_LOCAL_VERSION=10001
export FAKE_REMOTE_VERSION=10001
rm -f -- "${FAKE_CALLS}"
"${AUTO_UPDATE}" --skip-build --no-launch
mapfile -t refresh_calls < "${FAKE_CALLS}"
[[ "${refresh_calls[0]}" == smoke:pass &&
"${refresh_calls[1]}" == smoke:pass &&
"${refresh_calls[2]}" == \
"promote-probation:10001-${FAKE_BUILD_ID}" &&
"${#refresh_calls[@]}" -eq 3 ]] || {
echo "runtime change did not refresh approval through two canaries" >&2
exit 1
}
[[ "$(wc -l < "${FAKE_DERIVER_CALLS}")" -eq \
"${deriver_calls_before_refresh}" ]] || {
echo "approval refresh derived a profile from the candidate itself" >&2
exit 1
}
new_approval_ref="$(jq -r .approval_path "${MOCKTAIL_DATA_ROOT}/current.json")"
[[ "${new_approval_ref}" != "${old_approval_ref}" &&
-f "${MOCKTAIL_DATA_ROOT}/${old_approval_ref}" &&
-f "${MOCKTAIL_DATA_ROOT}/${new_approval_ref}" ]]
[[ "$(jq -r .runtime_build_id \
"${MOCKTAIL_DATA_ROOT}/${new_approval_ref}")" == \
"${MOCKTAIL_UPDATE_RUNTIME_BUILD_ID}" ]]
cmp --silent "${TEMP_DIR}/probation-previous.before-refresh" \
"${MOCKTAIL_DATA_ROOT}/previous_good.json"
# Receipt refresh happens before the version API. If the new runtime canary
# fails, rollback to validated previous_good must complete even while that API
# is down; the stale dynamic approval is never launched.
cp -- "${MOCKTAIL_DATA_ROOT}/current.json" \
"${TEMP_DIR}/probation-current.after-refresh"
export MOCKTAIL_UPDATE_RUNTIME_BUILD_ID=abababababababababababababababababababab
export FAKE_SMOKE_MODE=fail
export FAKE_CHECK_FAIL=true
rm -f -- "${FAKE_CALLS}"
"${AUTO_UPDATE}" --skip-build --no-launch
mapfile -t stale_api_calls < "${FAKE_CALLS}"
[[ "${stale_api_calls[0]}" == smoke:fail &&
"${stale_api_calls[1]}" == "rollback:${seed_payload_id}" &&
"${#stale_api_calls[@]}" -eq 2 ]]
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/current.json")" == \
"${seed_payload_id}" ]]
# A stale dynamic previous_good is not a usable rollback target after a runtime
# change. Recover through the newest staged exact-supported payload instead.
cp -- "${TEMP_DIR}/probation-current.after-refresh" \
"${MOCKTAIL_DATA_ROOT}/current.json"
cp -- "${TEMP_DIR}/probation-current.after-refresh" \
"${MOCKTAIL_DATA_ROOT}/previous_good.json"
rm -f -- "${FAKE_CALLS}"
export MOCKTAIL_UPDATE_RUNTIME_BUILD_ID=cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd
"${AUTO_UPDATE}" --skip-build --no-launch \
>"${TEMP_DIR}/stale-supported-fallback.stdout" \
2>"${TEMP_DIR}/stale-supported-fallback.stderr"
mapfile -t supported_fallback_calls < "${FAKE_CALLS}"
[[ "${supported_fallback_calls[0]}" == smoke:fail &&
"${supported_fallback_calls[1]}" == "promote:${seed_payload_id}" &&
"${#supported_fallback_calls[@]}" -eq 2 ]]
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/current.json")" == \
"${seed_payload_id}" ]]
grep -Fq "activating exact-supported local fallback ${seed_payload_id}" \
"${TEMP_DIR}/stale-supported-fallback.stderr"
# The rejection backoff must take the same recovery path without repeating the
# failed canary on every launch.
cp -- "${TEMP_DIR}/probation-current.after-refresh" \
"${MOCKTAIL_DATA_ROOT}/current.json"
cp -- "${TEMP_DIR}/probation-current.after-refresh" \
"${MOCKTAIL_DATA_ROOT}/previous_good.json"
rm -f -- "${FAKE_CALLS}"
"${AUTO_UPDATE}" --skip-build --no-launch \
>"${TEMP_DIR}/rejected-supported-fallback.stdout" \
2>"${TEMP_DIR}/rejected-supported-fallback.stderr"
[[ "$(cat "${FAKE_CALLS}")" == "promote:${seed_payload_id}" ]]
grep -Fq 'runtime reapproval matches a recent failed canary' \
"${TEMP_DIR}/rejected-supported-fallback.stderr"
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/current.json")" == \
"${seed_payload_id}" ]]
# With neither a runtime-valid previous_good nor a staged exact-supported
# payload, the updater still fails closed.
no_fallback_data_root="${TEMP_DIR}/no-fallback-data"
dynamic_payload_id="10001-${FAKE_BUILD_ID}"
mkdir -p "${no_fallback_data_root}/payloads"
cp -a -- "${probation_data_root}/payloads/${dynamic_payload_id}" \
"${no_fallback_data_root}/payloads/"
cp -a -- "${probation_data_root}/approvals" \
"${probation_data_root}/host_abi_profiles" \
"${probation_data_root}/compatibility_profiles" \
"${no_fallback_data_root}/"
cp -- "${TEMP_DIR}/probation-current.after-refresh" \
"${no_fallback_data_root}/current.json"
export MOCKTAIL_DATA_ROOT="${no_fallback_data_root}"
export FAKE_LIVE_DATA_ROOT="${MOCKTAIL_DATA_ROOT}"
rm -f -- "${FAKE_CALLS}"
set +e
"${AUTO_UPDATE}" --skip-build --no-launch \
>"${TEMP_DIR}/stale-no-fallback.stdout" \
2>"${TEMP_DIR}/stale-no-fallback.stderr"
stale_no_fallback_status=$?
set -e
[[ "${stale_no_fallback_status}" -ne 0 ]]
[[ ! -e "${FAKE_CALLS}" ]]
grep -Fq 'rollback failed' "${TEMP_DIR}/stale-no-fallback.stderr"
unset FAKE_SMOKE_MODE FAKE_CHECK_FAIL
unset MOCKTAIL_UPDATE_RUNTIME_BUILD_ID
# An exact rejected candidate is not re-canaryed on every normal launch. The
# key includes candidate/profile/runtime/analyzer evidence, and an explicit
# override remains available for manual retry.
backoff_data_root="${TEMP_DIR}/backoff-data"
export MOCKTAIL_DATA_ROOT="${backoff_data_root}"
export FAKE_LIVE_DATA_ROOT="${MOCKTAIL_DATA_ROOT}"
backoff_seed_dir="${MOCKTAIL_DATA_ROOT}/payloads/${seed_payload_id}"
mkdir -p "${backoff_seed_dir}/assets/content"
printf 'seed\n' > "${backoff_seed_dir}/libroblox.so"
cp -- "${seed_payload_dir}/roblox_payload.json" \
"${backoff_seed_dir}/roblox_payload.json"
jq -n --arg payload_id "${seed_payload_id}" \
--arg payload_path "payloads/${seed_payload_id}" \
'{schema_version:1,payload_id:$payload_id,payload_path:$payload_path}' \
> "${MOCKTAIL_DATA_ROOT}/current.json"
export MOCKTAIL_UPDATE_RUNTIME_BUILD_ID=1212121212121212121212121212121212121212
export MOCKTAIL_UPDATE_LOCAL_VERSION=2628
export FAKE_REMOTE_VERSION=10002
export FAKE_VERSION_CODE=10002
export FAKE_BUILD_ID=1212121212121212121212121212121212121212
export FAKE_SMOKE_MODE=no_log
rm -f -- "${FAKE_CALLS}"
"${AUTO_UPDATE}" --skip-build --no-launch
[[ "$(cat "${FAKE_CALLS}")" == smoke:no_log ]]
backoff_support_count="$(find "${MOCKTAIL_STATE_ROOT}/support" -maxdepth 1 \
-type f -name 'mocktail-support-*.tar.gz' | wc -l)"
"${AUTO_UPDATE}" --skip-build --no-launch
[[ "$(cat "${FAKE_CALLS}")" == smoke:no_log ]] || {
echo "recent rejected candidate was canaryed again" >&2
exit 1
}
[[ "$(find "${MOCKTAIL_STATE_ROOT}/support" -maxdepth 1 \
-type f -name 'mocktail-support-*.tar.gz' | wc -l)" == \
"${backoff_support_count}" ]]
[[ -f "${MOCKTAIL_STATE_ROOT}/rejected_candidates/10002-${FAKE_BUILD_ID}.json" ]]
MOCKTAIL_UPDATE_RETRY_REJECTED=1 \
"${AUTO_UPDATE}" --skip-build --no-launch
[[ "$(grep -Fc smoke:no_log "${FAKE_CALLS}")" -eq 2 ]]
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/current.json")" == \
"${seed_payload_id}" ]]
unset FAKE_SMOKE_MODE MOCKTAIL_UPDATE_RUNTIME_BUILD_ID
unset MOCKTAIL_UPDATE_PROFILE_DERIVER MOCKTAIL_UPDATE_REFERENCE_PROFILE
export MOCKTAIL_DATA_ROOT="${main_data_root}"
export FAKE_LIVE_DATA_ROOT="${MOCKTAIL_DATA_ROOT}"
# The version returned by the latest API is part of the probation identity.
# A valid signed payload for a different version may remain staged, but it
# must never reach canary or replace the working current payload.
provider_mismatch_root="${TEMP_DIR}/provider-mismatch-data"
export MOCKTAIL_DATA_ROOT="${provider_mismatch_root}"
export FAKE_LIVE_DATA_ROOT="${MOCKTAIL_DATA_ROOT}"
mismatch_current_build=d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21
mismatch_current_id="2546-${mismatch_current_build}"
mismatch_current_dir="${MOCKTAIL_DATA_ROOT}/payloads/${mismatch_current_id}"
mkdir -p "${mismatch_current_dir}/assets/content"
touch "${mismatch_current_dir}/libroblox.so"
jq -n --arg build_id "${mismatch_current_build}" \
'{schema_version:1,version_name:"2.725.1142",version_code:2546,
elf_build_id:$build_id}' > "${mismatch_current_dir}/roblox_payload.json"
jq -n --arg payload_id "${mismatch_current_id}" \
--arg payload_path "payloads/${mismatch_current_id}" \
'{schema_version:1,payload_id:$payload_id,payload_path:$payload_path}' \
> "${MOCKTAIL_DATA_ROOT}/current.json"
export MOCKTAIL_UPDATE_LOCAL_VERSION=2546
export FAKE_REMOTE_VERSION=11000
export FAKE_VERSION_CODE=10999
export FAKE_BUILD_ID=3434343434343434343434343434343434343434
rm -f -- "${FAKE_CALLS}"
"${AUTO_UPDATE}" --skip-build --no-launch \
>"${TEMP_DIR}/provider-mismatch.stdout" \
2>"${TEMP_DIR}/provider-mismatch.stderr"
[[ "$(jq -r .payload_id "${MOCKTAIL_DATA_ROOT}/current.json")" == \
"${mismatch_current_id}" ]]
[[ ! -e "${FAKE_CALLS}" ]]
grep -Fq \
'provider payload versionCode 10999 does not match expected 11000' \
"${TEMP_DIR}/provider-mismatch.stderr"
export MOCKTAIL_DATA_ROOT="${main_data_root}"
export FAKE_LIVE_DATA_ROOT="${MOCKTAIL_DATA_ROOT}"
# A failure outside the canary still produces a persistent bundle with a
# structured phase reason and a canonical updater event, not an empty log.
export MOCKTAIL_DATA_ROOT="${TEMP_DIR}/validation-failure-data"
export MOCKTAIL_CACHE_ROOT="${TEMP_DIR}/validation-failure-cache"
export MOCKTAIL_STATE_ROOT="${TEMP_DIR}/validation-failure-state"
export FAKE_LIVE_DATA_ROOT="${MOCKTAIL_DATA_ROOT}"
export FAKE_VALIDATOR_FAIL=true
export MOCKTAIL_UPDATE_SUPPORT_MARKER="${MOCKTAIL_STATE_ROOT}/.update-support.123.marker"
set +e
"${AUTO_UPDATE}" --skip-build --source apk-pure \
>"${TEMP_DIR}/validation-failure.stdout" \
2>"${TEMP_DIR}/validation-failure.stderr"
validation_status=$?
set -e
unset FAKE_VALIDATOR_FAIL
[[ "${validation_status}" -eq 43 ]] || {
cat "${TEMP_DIR}/validation-failure.stderr" >&2
echo "validator status ${validation_status} was not preserved" >&2
exit 1
}
[[ -d "${MOCKTAIL_UPDATE_SUPPORT_MARKER}" ]]
unset MOCKTAIL_UPDATE_SUPPORT_MARKER
validation_archive="$(find "${MOCKTAIL_STATE_ROOT}/support" -maxdepth 1 \
-type f -name 'mocktail-support-*.tar.gz' -print -quit)"
[[ -n "${validation_archive}" ]] || {
echo "validator failure did not retain a support bundle" >&2
exit 1
}
validation_extract="${TEMP_DIR}/validation-support"
mkdir -p -- "${validation_extract}"
tar -C "${validation_extract}" -xzf "${validation_archive}"
grep -Fxq 'reason=updater-validation-failed' \
"${validation_extract}/mocktail-support/runtime.txt"
grep -Fxq '[auto-update] payload validation started' \
"${validation_extract}/mocktail-support/recent.log"
unset MOCKTAIL_HOST_ALLOCATOR_BRIDGES MOCKTAIL_PATCH_HOSTILE_CANARY \
MOCKTAIL_SKIP_LIBROBLOX_CTORS MOCKTAIL_COMPATIBILITY_MANIFEST \
BASH_ENV LD_PRELOAD LD_AUDIT RIPGREP_CONFIG_PATH VK_DRIVER_FILES \
VK_ICD_FILENAMES MESA_LOADER_DRIVER_OVERRIDE DRI_PRIME \
__NV_PRIME_RENDER_OFFLOAD __VK_LAYER_NV_optimus \
__GLX_VENDOR_LIBRARY_NAME
unset -f mocktail_hostile_canary
echo "automatic updater orchestration checks passed"