From 4b437f623fcf931e38bbddfd45b583e8949727de Mon Sep 17 00:00:00 2001 From: komaruworld Date: Mon, 17 Aug 2026 14:18:34 +0300 Subject: [PATCH] Add flag --force-run-latest #20 --- CMakeLists.txt | 1 + include/runtime/command_line.h | 3 + include/runtime/payload_update_preflight.h | 3 +- include/update/unsafe_latest_runner.h | 36 +++++ include/update/update_coordinator.h | 3 + src/main.cc | 11 ++ src/runtime/command_line.cc | 12 ++ src/runtime/payload_update_preflight.cc | 18 ++- src/update/main.cc | 12 ++ src/update/unsafe_latest_runner.cc | 162 +++++++++++++++++++++ src/update/update_coordinator.cc | 110 ++++++++++++++ tests/command_line_test.cc | 3 +- tests/native_update_helper_fixture.cc | 10 +- tests/native_update_test.cc | 61 ++++++++ tests/payload_update_preflight_test.cc | 16 ++ 15 files changed, 453 insertions(+), 8 deletions(-) create mode 100644 include/update/unsafe_latest_runner.h create mode 100644 src/update/unsafe_latest_runner.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index afc04d8..ba097e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -224,6 +224,7 @@ add_library(mocktail_update STATIC src/update/payload_integrity.cc src/update/payload_store.cc src/update/readiness_canary.cc + src/update/unsafe_latest_runner.cc src/update/update_config.cc src/update/update_coordinator.cc src/update/zip_archive.cc diff --git a/include/runtime/command_line.h b/include/runtime/command_line.h index e9f0079..8a491a9 100644 --- a/include/runtime/command_line.h +++ b/include/runtime/command_line.h @@ -30,6 +30,9 @@ struct CommandLineOptions { std::string launch_request_json; int launch_argument_index = -1; bool allow_unverified_build = false; + // Requests one explicit launch of the provider latest without promoting it + // into the managed payload state. + bool force_run_latest = false; }; struct CommandLineParseResult { diff --git a/include/runtime/payload_update_preflight.h b/include/runtime/payload_update_preflight.h index 8c4ede3..7a6d2e8 100644 --- a/include/runtime/payload_update_preflight.h +++ b/include/runtime/payload_update_preflight.h @@ -17,7 +17,8 @@ struct PayloadUpdatePreflightResult { }; PayloadUpdatePreflightResult RunPayloadUpdatePreflight( - const Environment& environment, const RuntimePaths& paths); + const Environment& environment, const RuntimePaths& paths, + bool force_run_latest = false); } // namespace runtime } // namespace mocktail diff --git a/include/update/unsafe_latest_runner.h b/include/update/unsafe_latest_runner.h new file mode 100644 index 0000000..79ec302 --- /dev/null +++ b/include/update/unsafe_latest_runner.h @@ -0,0 +1,36 @@ +#ifndef MOCKTAIL_UPDATE_UNSAFE_LATEST_RUNNER_H_ +#define MOCKTAIL_UPDATE_UNSAFE_LATEST_RUNNER_H_ + +#include +#include +#include + +namespace mocktail::update { + +// Launches a downloaded latest payload once without promoting it to the active +// store. A non-empty HostAbi profile puts the child in explicit candidate mode; +// normal startup approval is intentionally not created or modified. +struct UnsafeLatestRunOptions { + std::filesystem::path runtime_binary; + std::filesystem::path payload_directory; + std::filesystem::path compatibility_manifest; + std::filesystem::path host_abi_profile; + + // Tests may supply a deterministic environment. Production callers leave + // this empty to inherit the updater process environment. + std::vector inherited_environment; +}; + +struct UnsafeLatestRunResult { + int exit_code = -1; + std::string error; + + explicit operator bool() const { return error.empty() && exit_code == 0; } +}; + +UnsafeLatestRunResult RunUnsafeLatestCandidate( + const UnsafeLatestRunOptions& options); + +} // namespace mocktail::update + +#endif // MOCKTAIL_UPDATE_UNSAFE_LATEST_RUNNER_H_ diff --git a/include/update/update_coordinator.h b/include/update/update_coordinator.h index f5e0876..ff6f89d 100644 --- a/include/update/update_coordinator.h +++ b/include/update/update_coordinator.h @@ -24,6 +24,9 @@ struct UpdateRequest { bool startup_preflight = false; bool check_latest = true; bool run_canary = true; + // Explicit manual escape hatch. It launches the provider latest once with a + // derived candidate profile and never promotes it into current.json. + bool force_run_latest = false; CanaryGraphicsBackend canary_graphics_backend = CanaryGraphicsBackend::kDirectVulkan; int progress_fd = -1; diff --git a/src/main.cc b/src/main.cc index 650ae7c..2f58089 100644 --- a/src/main.cc +++ b/src/main.cc @@ -139,6 +139,17 @@ int main(int argc, char* argv[]) { MOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST)); const mocktail::runtime::RuntimePaths paths = mocktail::runtime::RuntimePaths::FromEnvironment(environment); + if (command_line.options.force_run_latest) { + std::cerr << "[runtime] WARNING: latest Roblox will run once without " + "approval and will not become the active payload\n"; + const mocktail::runtime::PayloadUpdatePreflightResult force_latest = + mocktail::runtime::RunPayloadUpdatePreflight(environment, paths, true); + if (!force_latest) { + std::cerr << "[FATAL] " << force_latest.error << '\n'; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; + } std::optional external_launch_request; if (!command_line.options.launch_request_json.empty()) { diff --git a/src/runtime/command_line.cc b/src/runtime/command_line.cc index e4bbc50..42dcc9d 100644 --- a/src/runtime/command_line.cc +++ b/src/runtime/command_line.cc @@ -76,6 +76,8 @@ CommandLineParseResult ParseCommandLine(int argc, const char* const argv[]) { } } else if (argument == "--allow-unverified-build") { result.options.allow_unverified_build = true; + } else if (argument == "--force-run-latest") { + result.options.force_run_latest = true; } else if (argument == "--launch-uri") { if (!result.options.launch_request_json.empty()) { result.error = "duplicate option: --launch-uri"; @@ -135,6 +137,14 @@ CommandLineParseResult ParseCommandLine(int argc, const char* const argv[]) { return result; } } + if (result.options.force_run_latest && + (!result.options.roblox_library_path.empty() || + result.options.window_mode != WindowMode::kUnspecified || + !result.options.graphics_backend.empty() || + result.options.allow_unverified_build || + !result.options.launch_request_json.empty())) { + result.error = "--force-run-latest must be used on its own"; + } return result; } @@ -242,6 +252,8 @@ std::string CommandLineUsage(const std::string& program_name) { "angle-vulkan (default: direct-vulkan)\n" << " --allow-unverified-build Run a known but unverified Build-ID " "profile\n" + << " --force-run-latest Download and run the provider latest once " + "without approval; it is not activated\n" << " --launch-uri Join from a roblox: or roblox-player: " "website link\n" << " --help, -h Show this help\n\n" diff --git a/src/runtime/payload_update_preflight.cc b/src/runtime/payload_update_preflight.cc index ab7e249..fade947 100644 --- a/src/runtime/payload_update_preflight.cc +++ b/src/runtime/payload_update_preflight.cc @@ -249,15 +249,20 @@ std::vector ChildEnvironment(const RuntimePaths& paths, } // namespace PayloadUpdatePreflightResult RunPayloadUpdatePreflight( - const Environment& environment, const RuntimePaths& paths) { + const Environment& environment, const RuntimePaths& paths, + bool force_run_latest) { PayloadUpdatePreflightResult result; - if (Enabled(environment, "MOCKTAIL_SKIP_UPDATE_CHECK") || - environment.HasNonEmpty("ROBLOX_LIB_PATH")) { + if (!force_run_latest && + (Enabled(environment, "MOCKTAIL_SKIP_UPDATE_CHECK") || + environment.HasNonEmpty("ROBLOX_LIB_PATH"))) { return result; } const std::filesystem::path helper = UpdateHelper(environment); if (helper.empty()) { + if (force_run_latest) { + result.error = "cannot locate the Mocktail update helper"; + } return result; } @@ -265,7 +270,9 @@ PayloadUpdatePreflightResult RunPayloadUpdatePreflight( std::string helper_string = helper.string(); char update[] = "update"; char startup_preflight[] = "--startup-preflight"; - char* arguments[] = {helper_string.data(), update, startup_preflight, + char force_latest[] = "--force-run-latest"; + char* arguments[] = {helper_string.data(), update, + force_run_latest ? force_latest : startup_preflight, nullptr}; UpdateProgressMonitor progress = UpdateProgressMonitor::Start(environment); std::vector child_environment = @@ -318,7 +325,8 @@ PayloadUpdatePreflightResult RunPayloadUpdatePreflight( return result; } if (!WIFEXITED(child_status) || WEXITSTATUS(child_status) != 0) { - result.error = "Roblox update preflight failed"; + result.error = force_run_latest ? "forced latest Roblox run failed" + : "Roblox update preflight failed"; } return result; } diff --git a/src/update/main.cc b/src/update/main.cc index c49fdaf..8993749 100644 --- a/src/update/main.cc +++ b/src/update/main.cc @@ -311,11 +311,23 @@ int main(int argc, char** argv) { request.check_latest = false; } else if (option == "--skip-canary") { request.run_canary = false; + } else if (option == "--force-run-latest") { + request.force_run_latest = true; } else { std::cerr << "[native-updater] unknown option: " << option << '\n'; return 2; } } + if (request.force_run_latest && + (request.startup_preflight || !request.check_latest || !request.run_canary)) { + std::cerr << "[native-updater] --force-run-latest cannot be combined with " + "--startup-preflight, --no-latest-check, or --skip-canary\n"; + return 2; + } + if (request.force_run_latest) { + std::cerr << "[native-updater] WARNING: latest Roblox will run once without " + "approval and will not become the active payload\n"; + } const mocktail::update::UpdateResult updated = mocktail::update::RunUpdate(paths, request); for (const std::string& warning : updated.warnings) { diff --git a/src/update/unsafe_latest_runner.cc b/src/update/unsafe_latest_runner.cc new file mode 100644 index 0000000..f2c2e94 --- /dev/null +++ b/src/update/unsafe_latest_runner.cc @@ -0,0 +1,162 @@ +#include "update/unsafe_latest_runner.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +extern char** environ; + +namespace mocktail::update { +namespace { + +constexpr std::array kOverriddenVariables = { + "ROBLOX_LIB_PATH", + "MOCKTAIL_ASSET_ROOT", + "MOCKTAIL_ASSET_PATH", + "MOCKTAIL_COMPATIBILITY_MANIFEST", + "MOCKTAIL_HOST_ABI_PROFILE_FILE", + "MOCKTAIL_HOST_ABI_CANARY", + "MOCKTAIL_ALLOW_CANDIDATE_HOST_ABI", + "MOCKTAIL_HOST_ABI_APPROVAL_RECEIPT", + "MOCKTAIL_SKIP_UPDATE_CHECK", + "MOCKTAIL_UNSAFE_LATEST", +}; + +bool IsEnvironmentEntry(std::string_view entry, std::string_view name) { + return entry.size() > name.size() && entry[name.size()] == '=' && + entry.substr(0, name.size()) == name; +} + +bool IsOverridden(std::string_view entry) { + for (const std::string_view name : kOverriddenVariables) { + if (IsEnvironmentEntry(entry, name)) return true; + } + return false; +} + +bool IsRegularFile(const std::filesystem::path& path) { + std::error_code error; + const std::filesystem::file_status status = + std::filesystem::symlink_status(path, error); + return !error && std::filesystem::is_regular_file(status) && + !std::filesystem::is_symlink(status); +} + +bool IsExecutableRegularFile(const std::filesystem::path& path) { + return IsRegularFile(path) && access(path.c_str(), X_OK) == 0; +} + +std::vector CurrentEnvironment() { + std::vector result; + for (char** entry = environ; entry != nullptr && *entry != nullptr; ++entry) { + result.emplace_back(*entry); + } + return result; +} + +std::vector BuildEnvironment( + const UnsafeLatestRunOptions& options) { + const std::vector inherited = + options.inherited_environment.empty() ? CurrentEnvironment() + : options.inherited_environment; + std::vector result; + result.reserve(inherited.size() + 10U); + for (const std::string& entry : inherited) { + if (entry.find('\0') == std::string::npos && !IsOverridden(entry)) { + result.push_back(entry); + } + } + const auto add = [&](std::string_view name, + const std::filesystem::path& value) { + result.push_back(std::string(name) + "=" + value.string()); + }; + add("ROBLOX_LIB_PATH", options.payload_directory / "libroblox.so"); + add("MOCKTAIL_ASSET_ROOT", options.payload_directory / "assets"); + add("MOCKTAIL_ASSET_PATH", options.payload_directory / "assets/content"); + add("MOCKTAIL_COMPATIBILITY_MANIFEST", options.compatibility_manifest); + result.push_back("MOCKTAIL_SKIP_UPDATE_CHECK=1"); + result.push_back("MOCKTAIL_UNSAFE_LATEST=1"); + if (!options.host_abi_profile.empty()) { + add("MOCKTAIL_HOST_ABI_PROFILE_FILE", options.host_abi_profile); + result.push_back("MOCKTAIL_HOST_ABI_CANARY=1"); + result.push_back("MOCKTAIL_ALLOW_CANDIDATE_HOST_ABI=1"); + } + return result; +} + +} // namespace + +UnsafeLatestRunResult RunUnsafeLatestCandidate( + const UnsafeLatestRunOptions& options) { + UnsafeLatestRunResult result; + if (!IsExecutableRegularFile(options.runtime_binary)) { + result.error = "unsafe latest runtime is not an executable regular file"; + return result; + } + if (!IsRegularFile(options.payload_directory / "libroblox.so")) { + result.error = "unsafe latest payload library is not a regular file"; + return result; + } + if (!IsRegularFile(options.compatibility_manifest)) { + result.error = "unsafe latest compatibility manifest is not a regular file"; + return result; + } + if (!options.host_abi_profile.empty() && + !IsRegularFile(options.host_abi_profile)) { + result.error = "unsafe latest HostAbi profile is not a regular file"; + return result; + } + + std::vector environment = BuildEnvironment(options); + std::vector environment_pointers; + environment_pointers.reserve(environment.size() + 1U); + for (std::string& entry : environment) + environment_pointers.push_back(entry.data()); + environment_pointers.push_back(nullptr); + + std::string binary = options.runtime_binary.string(); + char windowed[] = "--windowed"; + char allow_unverified[] = "--allow-unverified-build"; + char* arguments[] = { + binary.data(), windowed, + options.host_abi_profile.empty() ? nullptr : allow_unverified, nullptr}; + pid_t child = -1; + const int spawn_status = + posix_spawn(&child, options.runtime_binary.c_str(), nullptr, nullptr, + arguments, environment_pointers.data()); + if (spawn_status != 0) { + result.error = "cannot start unsafe latest Roblox: " + + std::string(std::strerror(spawn_status)); + return result; + } + + int status = 0; + while (waitpid(child, &status, 0) < 0) { + if (errno == EINTR) continue; + result.error = "cannot wait for unsafe latest Roblox: " + + std::string(std::strerror(errno)); + return result; + } + if (!WIFEXITED(status)) { + result.exit_code = 128 + (WIFSIGNALED(status) ? WTERMSIG(status) : 0); + result.error = "unsafe latest Roblox terminated before a normal exit"; + return result; + } + result.exit_code = WEXITSTATUS(status); + if (result.exit_code != 0) { + result.error = "unsafe latest Roblox exited with status " + + std::to_string(result.exit_code); + } + return result; +} + +} // namespace mocktail::update diff --git a/src/update/update_coordinator.cc b/src/update/update_coordinator.cc index 543bb49..796d0dc 100644 --- a/src/update/update_coordinator.cc +++ b/src/update/update_coordinator.cc @@ -22,6 +22,7 @@ #include "update/payload_integrity.h" #include "update/payload_store.h" #include "update/readiness_canary.h" +#include "update/unsafe_latest_runner.h" #include "update/update_config.h" namespace mocktail::update { @@ -277,6 +278,109 @@ ReferenceProfile ResolveReference(const UpdatePaths& paths, return result; } +UpdateResult RunUnsafeLatest( + const UpdatePaths& paths, const UpdateRequest& request, + const std::vector& profiles, + const SupportedPayloadProfile& preferred, + const PayloadStoreResult& installed, PayloadStore* store) { + UpdateResult result; + if (request.startup_preflight || !request.check_latest) { + result.error = + "unsafe latest launch requires an explicit latest-version check"; + return result; + } + + ApkPureProvider provider; + Progress(request.progress_fd, "Checking latest Roblox..."); + const ProviderVersion latest = provider.CheckLatest(); + if (!latest) { + result.error = "cannot check latest Roblox for unsafe launch: " + + latest.error; + return result; + } + + std::string workspace_error; + const std::filesystem::path workspace = UniqueDirectory( + paths.cache_root / "downloads/native-updater", &workspace_error); + if (!workspace_error.empty()) { + result.error = workspace_error; + return result; + } + std::error_code filesystem_error; + const auto cleanup = [&]() { + std::filesystem::remove_all(workspace, filesystem_error); + }; + + ExpectedPayloadIdentity identity; + identity.version_name = latest.version_name; + identity.version_code = latest.version_code; + const std::optional exact = + FindSupportedProfile(profiles, identity.version_name, identity.version_code); + if (exact.has_value()) identity.exact_build_id = exact->elf_build_id; + Candidate candidate = FindStagedCandidate(paths, identity, exact.has_value()); + if (!candidate) { + candidate = DownloadCandidate(&provider, store, identity, exact.has_value(), + paths, workspace / "candidate", + request.progress_fd); + } + if (!candidate) { + result.error = "cannot prepare latest Roblox for unsafe launch: " + + candidate.error; + cleanup(); + return result; + } + + if (!candidate.exact_supported) { + const ReferenceProfile reference = ResolveReference( + paths, installed, preferred, &provider, store, workspace, + request.progress_fd); + if (!reference) { + result.error = "cannot prepare latest Roblox for unsafe launch: " + + reference.error; + cleanup(); + return result; + } + HostAbiDerivationOptions derivation; + derivation.reference_library = reference.library; + derivation.reference_profile = reference.profile; + derivation.candidate_payload_directory = candidate.staged.payload_directory; + derivation.output_directory = workspace / "derived"; + Progress(request.progress_fd, "Deriving latest Roblox compatibility..."); + const HostAbiDerivationResult derived = DeriveHostAbiProfile(derivation); + if (!derived) { + result.error = "cannot derive latest Roblox HostAbi profile: " + + derived.error; + cleanup(); + return result; + } + candidate.profile = derived.profile; + candidate.compatibility = derived.compatibility_manifest; + } + + // Staging preserves the immutable APK for inspection, but this path must + // never promote it or alter the normal current payload. + UnsafeLatestRunOptions launch; + launch.runtime_binary = paths.runtime_binary; + launch.payload_directory = candidate.staged.payload_directory; + launch.compatibility_manifest = candidate.exact_supported + ? paths.compatibility_manifest + : candidate.compatibility; + launch.host_abi_profile = candidate.profile; + Progress(request.progress_fd, "Launching unapproved latest Roblox..."); + const UnsafeLatestRunResult launched = RunUnsafeLatestCandidate(launch); + result.payload_id = candidate.staged.payload_id; + if (!launched) { + result.error = "unsafe latest Roblox " + candidate.staged.version_name + + " did not complete: " + launched.error; + cleanup(); + return result; + } + result.message = "unsafe latest Roblox " + candidate.staged.version_name + + " exited successfully; active payload was not changed"; + cleanup(); + return result; +} + bool RunCandidateCanaries(const UpdatePaths& paths, const Candidate& candidate, bool run_canary, CanaryGraphicsBackend graphics_backend, @@ -383,6 +487,12 @@ UpdateResult RunUpdate(const UpdatePaths& paths, const UpdateRequest& request) { } else if (!current) { installed = store.InspectCurrent(); } + if (request.force_run_latest) { + UpdateResult unsafe = RunUnsafeLatest(paths, request, catalog.profiles, + *preferred, installed, &store); + unsafe.warnings = std::move(result.warnings); + return unsafe; + } if (!configured.config.automatic && request.startup_preflight) { if (current) { result.payload_id = current.payload_id; diff --git a/tests/command_line_test.cc b/tests/command_line_test.cc index 3945cca..75fdfe9 100644 --- a/tests/command_line_test.cc +++ b/tests/command_line_test.cc @@ -267,7 +267,8 @@ TEST(CommandLineTest, UsageContainsEverySupportedOption) { EXPECT_NE(usage.find("mocktail-test"), std::string::npos); for (const char* option : {"--roblox-lib", "--headless", "--windowed", "--graphics", - "--allow-unverified-build", "--launch-uri", "--help"}) { + "--allow-unverified-build", "--force-run-latest", "--launch-uri", + "--help"}) { EXPECT_NE(usage.find(option), std::string::npos) << option; } EXPECT_EQ(usage.find("--login"), std::string::npos); diff --git a/tests/native_update_helper_fixture.cc b/tests/native_update_helper_fixture.cc index a8d51c1..0fcfd6d 100644 --- a/tests/native_update_helper_fixture.cc +++ b/tests/native_update_helper_fixture.cc @@ -28,7 +28,8 @@ int IntegerEnvironment(const char* name, int fallback) { int main(int argc, char** argv) { if (argc != 3 || std::string_view(argv[1]) != "update" || - std::string_view(argv[2]) != "--startup-preflight") { + (std::string_view(argv[2]) != "--startup-preflight" && + std::string_view(argv[2]) != "--force-run-latest")) { return 64; } const char* capture = Environment("MOCKTAIL_TEST_UPDATE_CAPTURE"); @@ -40,6 +41,13 @@ int main(int argc, char** argv) { << Environment("MOCKTAIL_STATE_ROOT") << '\n'; if (!output) return 65; } + const char* argument_capture = + Environment("MOCKTAIL_TEST_UPDATE_ARGUMENT_CAPTURE"); + if (argument_capture[0] != '\0') { + std::ofstream output(argument_capture); + output << argv[2] << '\n'; + if (!output) return 67; + } const int progress = IntegerEnvironment("MOCKTAIL_UPDATE_PROGRESS_FD", -1); if (progress >= 0) { constexpr std::string_view packet = "PDownloading Roblox..."; diff --git a/tests/native_update_test.cc b/tests/native_update_test.cc index 51aa04f..6bb711c 100644 --- a/tests/native_update_test.cc +++ b/tests/native_update_test.cc @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -16,6 +17,7 @@ #include "update/payload_integrity.h" #include "update/payload_store.h" #include "update/readiness_canary.h" +#include "update/unsafe_latest_runner.h" #include "update/update_config.h" #include "update/zip_archive.h" @@ -161,6 +163,65 @@ TEST(ApkPureProviderTest, ParsesExactVersionIdentityAndTrustedUrl) { EXPECT_EQ(urls.front(), "https://download.pureapk.com/b/XAPK/roblox"); } +TEST(UnsafeLatestRunnerTest, LaunchesCandidateWithUnapprovedEnvironment) { + TemporaryDirectory temporary; + const std::filesystem::path output = temporary.root() / "environment.txt"; + const std::filesystem::path arguments = temporary.root() / "arguments.txt"; + const std::filesystem::path runtime = temporary.root() / "runtime.sh"; + Write(runtime, + "#!/bin/sh\n" + "env | sort > \"$ENV_OUTPUT\"\n" + "printf '%s\\n' \"$@\" > \"$ARGUMENT_OUTPUT\"\n"); + ASSERT_EQ(chmod(runtime.c_str(), 0700), 0); + + const std::filesystem::path payload = temporary.root() / "payload"; + Write(payload / "libroblox.so", "candidate"); + std::filesystem::create_directories(payload / "assets/content"); + const std::filesystem::path compatibility = + temporary.root() / "compatibility.json"; + const std::filesystem::path profile = temporary.root() / "profile.json"; + Write(compatibility, "{}\n"); + Write(profile, "{}\n"); + + UnsafeLatestRunOptions options; + options.runtime_binary = runtime; + options.payload_directory = payload; + options.compatibility_manifest = compatibility; + options.host_abi_profile = profile; + options.inherited_environment = { + "PATH=/usr/bin:/bin", "ENV_OUTPUT=" + output.string(), + "ARGUMENT_OUTPUT=" + arguments.string(), "UNCHANGED=value", + "ROBLOX_LIB_PATH=/stale/libroblox.so", + "MOCKTAIL_HOST_ABI_APPROVAL_RECEIPT=/stale/approval.json", + "MOCKTAIL_SKIP_UPDATE_CHECK=0", + }; + + const UnsafeLatestRunResult result = RunUnsafeLatestCandidate(options); + ASSERT_TRUE(result) << result.error; + const std::string environment = ReadFile(output); + EXPECT_NE(environment.find("UNCHANGED=value\n"), std::string::npos); + EXPECT_NE(environment.find("ROBLOX_LIB_PATH=" + + (payload / "libroblox.so").string() + "\n"), + std::string::npos); + EXPECT_NE(environment.find("MOCKTAIL_COMPATIBILITY_MANIFEST=" + + compatibility.string() + "\n"), + std::string::npos); + EXPECT_NE(environment.find("MOCKTAIL_HOST_ABI_PROFILE_FILE=" + + profile.string() + "\n"), + std::string::npos); + EXPECT_NE(environment.find("MOCKTAIL_HOST_ABI_CANARY=1\n"), + std::string::npos); + EXPECT_NE(environment.find("MOCKTAIL_ALLOW_CANDIDATE_HOST_ABI=1\n"), + std::string::npos); + EXPECT_NE(environment.find("MOCKTAIL_SKIP_UPDATE_CHECK=1\n"), + std::string::npos); + EXPECT_NE(environment.find("MOCKTAIL_UNSAFE_LATEST=1\n"), + std::string::npos); + EXPECT_EQ(environment.find("/stale/libroblox.so"), std::string::npos); + EXPECT_EQ(environment.find("/stale/approval.json"), std::string::npos); + EXPECT_EQ(ReadFile(arguments), "--windowed\n--allow-unverified-build\n"); +} + TEST(HttpDownloadPolicyTest, RejectsCredentialsAndHostSuffixTricks) { const std::vector hosts = {"pureapk.com"}; EXPECT_TRUE(IsTrustedHttpsUrl("https://download.pureapk.com/a", hosts)); diff --git a/tests/payload_update_preflight_test.cc b/tests/payload_update_preflight_test.cc index e9dd0bd..6c1f5c9 100644 --- a/tests/payload_update_preflight_test.cc +++ b/tests/payload_update_preflight_test.cc @@ -101,6 +101,22 @@ TEST(PayloadUpdatePreflightTest, RunsNativeUpdater) { EXPECT_TRUE(result.attempted); } +TEST(PayloadUpdatePreflightTest, RunsForceLatestThroughNativeUpdater) { + TemporaryDirectory temporary; + const std::filesystem::path capture = temporary.root() / "argument"; + ScopedVariable capture_variable("MOCKTAIL_TEST_UPDATE_ARGUMENT_CAPTURE", + capture.string()); + const MapEnvironment environment = NativeEnvironment(); + const RuntimePaths paths = RuntimePaths::FromEnvironment(environment); + + const auto result = RunPayloadUpdatePreflight(environment, paths, true); + + ASSERT_TRUE(result) << result.error; + EXPECT_TRUE(result.attempted); + EXPECT_EQ(ReadLines(capture), + std::vector({"--force-run-latest"})); +} + TEST(PayloadUpdatePreflightTest, RunsPackagedUpdaterFromProjectBin) { TemporaryDirectory temporary; const std::filesystem::path project_root = temporary.root() / "portable";