#include "runtime/command_line.h" #include #include "compat/guest_abi.h" #include #include #include #include #include #include namespace mocktail { namespace runtime { namespace { TEST(CommandLineTest, ShortVrAliasSurvivesReexecAndLastOverrideWins) { const char* arguments[] = {"mocktail", "-vr"}; const auto parsed = ParseCommandLine(2, arguments); ASSERT_TRUE(parsed); ASSERT_TRUE(parsed.options.vr_enabled.has_value()); EXPECT_TRUE(*parsed.options.vr_enabled); std::vector reexec; std::string error; ASSERT_TRUE(BuildCommandLineReexecArguments(parsed.options, 2, arguments, &reexec, &error)); EXPECT_EQ(reexec, (std::vector{"-vr"})); const char* disabled[] = {"mocktail", "-vr", "--no-vr"}; EXPECT_FALSE(*ParseCommandLine(3, disabled).options.vr_enabled); } TEST(CommandLineTest, VrOverrideSurvivesReexecAndCanDisableEnvironmentDefault) { const std::array arguments = {"mocktail", "--vr", "--headless", "--no-vr"}; const auto parsed = ParseCommandLine(arguments.size(), arguments.data()); ASSERT_TRUE(parsed) << parsed.error; ASSERT_TRUE(parsed.options.vr_enabled.has_value()); EXPECT_FALSE(*parsed.options.vr_enabled); std::vector reexec; std::string error; ASSERT_TRUE(BuildCommandLineReexecArguments( parsed.options, arguments.size(), arguments.data(), &reexec, &error)) << error; EXPECT_EQ(reexec, (std::vector{"--vr", "--headless", "--no-vr"})); const char* previous = std::getenv("MOCKTAIL_VR_ENABLED"); const std::optional saved = previous ? std::optional(previous) : std::nullopt; const char* previous_headless = std::getenv("MOCKTAIL_HEADLESS"); const std::optional saved_headless = previous_headless ? std::optional(previous_headless) : std::nullopt; setenv("MOCKTAIL_VR_ENABLED", "true", 1); EXPECT_TRUE(ApplyCommandLineEnvironment(parsed.options, &error)) << error; EXPECT_STREQ(std::getenv("MOCKTAIL_VR_ENABLED"), "0"); if (saved) setenv("MOCKTAIL_VR_ENABLED", saved->c_str(), 1); else unsetenv("MOCKTAIL_VR_ENABLED"); if (saved_headless) setenv("MOCKTAIL_HEADLESS", saved_headless->c_str(), 1); else unsetenv("MOCKTAIL_HEADLESS"); } TEST(CommandLineTest, ParsesExplicitVrAndKeepsUnspecifiedDistinctFromFalse) { const char* enabled[] = {"mocktail", "--vr"}; const auto parsed = ParseCommandLine(2, enabled); ASSERT_TRUE(parsed) << parsed.error; EXPECT_EQ(parsed.options.vr_enabled, std::optional(true)); const char* normal[] = {"mocktail"}; EXPECT_FALSE(ParseCommandLine(1, normal).options.vr_enabled.has_value()); const char* incompatible[] = {"mocktail", "--force-run-latest", "--vr"}; EXPECT_FALSE(ParseCommandLine(3, incompatible)); } TEST(CommandLineTest, ParsesCompleteRunContractOnce) { const std::array arguments = { "mocktail", "--roblox-lib", "/payload/libroblox.so", "--headless", "--graphics", "direct-vulkan", "--windowed", "--allow-unverified-build", "--launch-uri", "roblox://experiences/start?placeId=17580461965", nullptr}; const CommandLineParseResult result = ParseCommandLine(10, arguments.data()); ASSERT_TRUE(result); EXPECT_EQ(result.options.mode, CommandMode::kRun); EXPECT_EQ(result.options.roblox_library_path, "/payload/libroblox.so"); EXPECT_EQ(result.options.graphics_backend, "direct-vulkan"); EXPECT_EQ(result.options.window_mode, WindowMode::kWindowed); EXPECT_TRUE(result.options.allow_unverified_build); EXPECT_EQ(result.options.raw_launch_argument, "roblox://experiences/start?placeId=17580461965"); EXPECT_FALSE(result.options.launch_request_json.empty()); EXPECT_EQ(result.options.launch_argument_index, 9); } TEST(CommandLineTest, PreservesDesktopLaunchUriAsOneOpaqueArgument) { constexpr char kLaunchUri[] = "roblox://experiences/start?placeId=1962086868&" "gameInstanceId=97ea-49e4&launchData=%257B%2522key%2522%253A%2522a%" "252Bb%2522%257D"; const std::array arguments = {"mocktail", "--launch-uri", kLaunchUri}; const CommandLineParseResult result = ParseCommandLine(arguments.size(), arguments.data()); ASSERT_TRUE(result) << result.error; EXPECT_EQ(result.options.mode, CommandMode::kRun); EXPECT_EQ(result.options.raw_launch_argument, kLaunchUri); EXPECT_FALSE(result.options.launch_request_json.empty()); EXPECT_EQ(result.options.launch_argument_index, 2); } TEST(CommandLineTest, AcceptsDesktopUriWithoutBreakingMenuLaunch) { constexpr char kUri[] = "roblox://experiences/start?placeId=17580461965&" "gameInstanceId=instance%2Bserver"; const std::array arguments = {"mocktail", kUri}; const CommandLineParseResult result = ParseCommandLine(arguments.size(), arguments.data()); ASSERT_TRUE(result) << result.error; EXPECT_EQ(result.options.raw_launch_argument, kUri); EXPECT_FALSE(result.options.launch_request_json.empty()); EXPECT_EQ(result.options.launch_argument_index, 1); std::vector reexec_arguments; std::string reexec_error; ASSERT_TRUE(BuildCommandLineReexecArguments(result.options, arguments.size(), arguments.data(), &reexec_arguments, &reexec_error)) << reexec_error; ASSERT_EQ(reexec_arguments.size(), 2u); EXPECT_EQ(reexec_arguments[0], "--launch-request-json"); EXPECT_EQ(reexec_arguments[1], result.options.launch_request_json); const std::array menu_launch = {"mocktail"}; const CommandLineParseResult menu_result = ParseCommandLine(menu_launch.size(), menu_launch.data()); ASSERT_TRUE(menu_result) << menu_result.error; EXPECT_TRUE(menu_result.options.raw_launch_argument.empty()); EXPECT_TRUE(menu_result.options.launch_request_json.empty()); } TEST(CommandLineTest, RedactsBrowserTicketAcrossCgroupReexec) { constexpr char kSecret[] = "SECRET-BROWSER-GAMEINFO-TICKET"; constexpr char kLaunchUri[] = "roblox-player:1+launchmode:play+" "gameinfo:SECRET-BROWSER-GAMEINFO-TICKET+" "placelauncherurl:https%3A%2F%2Fwww.roblox.com%2FGame%2F" "PlaceLauncher.ashx%3Frequest%3DRequestGame%26placeId%3D17580461965"; const std::array arguments = {"mocktail", "--launch-uri", kLaunchUri}; const CommandLineParseResult result = ParseCommandLine(arguments.size(), arguments.data()); ASSERT_TRUE(result) << result.error; ASSERT_EQ(result.options.launch_request_json.find(kSecret), std::string::npos); std::vector reexec_arguments; std::string error; ASSERT_TRUE(BuildCommandLineReexecArguments(result.options, arguments.size(), arguments.data(), &reexec_arguments, &error)) << error; ASSERT_EQ(reexec_arguments.size(), 2u); EXPECT_EQ(reexec_arguments[0], "--launch-request-json"); EXPECT_EQ(reexec_arguments[1], result.options.launch_request_json); EXPECT_EQ(reexec_arguments[1].find(kSecret), std::string::npos); const std::array reexec = { "mocktail", reexec_arguments[0].c_str(), reexec_arguments[1].c_str()}; const CommandLineParseResult reparsed = ParseCommandLine(reexec.size(), reexec.data()); ASSERT_TRUE(reparsed) << reparsed.error; EXPECT_FALSE(reparsed.options.raw_launch_argument.empty()); EXPECT_EQ(reparsed.options.launch_argument_index, 2); EXPECT_EQ(reparsed.options.launch_request_json, result.options.launch_request_json); } TEST(CommandLineTest, DoesNotEchoInvalidInternalLaunchDocument) { constexpr char kSecret[] = "SECRET-INTERNAL-LAUNCH-DOCUMENT"; const std::array arguments = { "mocktail", "--launch-request-json", kSecret}; const CommandLineParseResult result = ParseCommandLine(arguments.size(), arguments.data()); EXPECT_FALSE(result); EXPECT_EQ(result.error, "invalid controlled launch request"); EXPECT_EQ(result.error.find(kSecret), std::string::npos); } TEST(CommandLineTest, ScrubsInternalLaunchDocumentFromOwnedAndProcessArgv) { char program[] = "mocktail"; char option[] = "--launch-request-json"; char document[] = R"({"placeId":17580461965,"accessCode":"private-access-code"})"; char* mutable_arguments[] = {program, option, document}; const char* parse_arguments[] = {program, option, document}; CommandLineParseResult result = ParseCommandLine(3, parse_arguments); ASSERT_TRUE(result) << result.error; ASSERT_FALSE(result.options.raw_launch_argument.empty()); ASSERT_FALSE(result.options.launch_request_json.empty()); ScrubCommandLineLaunchArguments(&result.options, 3, mutable_arguments); EXPECT_TRUE(result.options.raw_launch_argument.empty()); EXPECT_TRUE(result.options.launch_request_json.empty()); EXPECT_EQ(result.options.launch_argument_index, -1); EXPECT_TRUE(std::all_of(std::begin(document), std::end(document) - 1, [](char byte) { return byte == '\0'; })); } TEST(CommandLineTest, ParsesProfileTracePath) { const std::array arguments = {"mocktail", "--profile", "/traces/session.json"}; const CommandLineParseResult result = ParseCommandLine(arguments.size(), arguments.data()); ASSERT_TRUE(result) << result.error; EXPECT_EQ(result.options.profile_trace_path, "/traces/session.json"); } TEST(CommandLineTest, AppliesProfileTraceAsAbsolutePath) { unsetenv("MOCKTAIL_PROFILE_TRACE"); CommandLineOptions options; options.profile_trace_path = "session.json"; std::string error; ASSERT_TRUE(ApplyCommandLineEnvironment(options, &error)) << error; const char* applied = std::getenv("MOCKTAIL_PROFILE_TRACE"); ASSERT_NE(applied, nullptr); EXPECT_EQ(std::string(applied), (std::filesystem::current_path() / "session.json").string()); unsetenv("MOCKTAIL_PROFILE_TRACE"); } TEST(CommandLineTest, LeavesProfileTraceUnsetWithoutOption) { unsetenv("MOCKTAIL_PROFILE_TRACE"); std::string error; ASSERT_TRUE(ApplyCommandLineEnvironment(CommandLineOptions{}, &error)) << error; EXPECT_EQ(std::getenv("MOCKTAIL_PROFILE_TRACE"), nullptr); } TEST(CommandLineTest, RejectsUnsupportedPositionalArgument) { const std::array arguments = {"mocktail", "/tmp/payload"}; const CommandLineParseResult result = ParseCommandLine(arguments.size(), arguments.data()); EXPECT_FALSE(result); EXPECT_EQ(result.error, "unknown argument"); } TEST(CommandLineTest, RejectsUnknownOptionBeforeRuntimeWork) { const std::array arguments = {"mocktail", "--unknown"}; const CommandLineParseResult result = ParseCommandLine(arguments.size(), arguments.data()); EXPECT_FALSE(result); EXPECT_EQ(result.error, "unknown option: --unknown"); } TEST(CommandLineTest, RejectsEveryMissingValue) { for (const char* option : {"--roblox-lib", "--graphics", "--launch-uri", "--profile"}) { const std::array arguments = {"mocktail", option}; const CommandLineParseResult result = ParseCommandLine(arguments.size(), arguments.data()); EXPECT_FALSE(result) << option; EXPECT_EQ(result.error, std::string("missing value for ") + option) << option; } } TEST(CommandLineTest, RejectsDuplicateLaunchUri) { const std::array arguments = { "mocktail", "--launch-uri", "roblox://experiences/start?placeId=17580461965", "--launch-uri", "roblox://experiences/start?placeId=1962086868"}; const CommandLineParseResult result = ParseCommandLine(arguments.size(), arguments.data()); EXPECT_FALSE(result); EXPECT_NE(result.error.find("--launch-uri"), std::string::npos); } TEST(CommandLineTest, DoesNotEchoASecondRawLaunchTicket) { constexpr char kSecret[] = "SECRET-BROWSER-TICKET"; const std::array arguments = { "mocktail", "roblox://experiences/start?placeId=17580461965", "roblox-player:1+launchmode:play+gameinfo:SECRET-BROWSER-TICKET"}; const CommandLineParseResult result = ParseCommandLine(arguments.size(), arguments.data()); EXPECT_FALSE(result); EXPECT_EQ(result.error, "unknown argument"); EXPECT_EQ(result.error.find(kSecret), std::string::npos); } TEST(CommandLineTest, RejectsInvalidLaunchUriBeforeRuntimeWork) { const std::array arguments = { "mocktail", "--launch-uri", "https://www.roblox.com/experiences/start?placeId=17580461965"}; const CommandLineParseResult result = ParseCommandLine(arguments.size(), arguments.data()); EXPECT_FALSE(result); EXPECT_TRUE(result.options.raw_launch_argument.empty()); EXPECT_NE(result.error.find("invalid Roblox launch URI"), std::string::npos); } TEST(CommandLineTest, RejectsRemovedStandaloneLoginOptions) { for (const char* option : {"--login", "--import-cookie"}) { const std::array arguments = {"mocktail", option}; const CommandLineParseResult result = ParseCommandLine(arguments.size(), arguments.data()); EXPECT_FALSE(result) << option; EXPECT_EQ(result.error, std::string("unknown option: ") + option); } } TEST(CommandLineTest, RejectsAnotherOptionAsMissingValue) { const std::array arguments = {"mocktail", "--roblox-lib", "--headless"}; const CommandLineParseResult result = ParseCommandLine(arguments.size(), arguments.data()); EXPECT_FALSE(result); EXPECT_EQ(result.error, "missing value for --roblox-lib"); } TEST(CommandLineTest, HelpIsATypedAuxiliaryMode) { const std::array help = {"mocktail", "--help"}; const CommandLineParseResult help_result = ParseCommandLine(help.size(), help.data()); ASSERT_TRUE(help_result); EXPECT_EQ(help_result.options.mode, CommandMode::kHelp); } TEST(CommandLineTest, UsageContainsEverySupportedOption) { const std::string usage = CommandLineUsage("mocktail-test"); EXPECT_NE(usage.find("mocktail-test"), std::string::npos); for (const char* option : {"--roblox-lib", "--headless", "--windowed", "--graphics", "--allow-unverified-build", "--force-run-latest", "--launch-uri", "--profile", "--help"}) { EXPECT_NE(usage.find(option), std::string::npos) << option; } EXPECT_EQ(usage.find("--login"), std::string::npos); EXPECT_EQ(usage.find("--import-cookie"), std::string::npos); EXPECT_NE(usage.find("native sign-in flow"), std::string::npos); EXPECT_NE( usage.find("verified managed " + std::string(mocktail::compat::kGuestAbi) + " Roblox payload"), std::string::npos); EXPECT_EQ(usage.find("Normal startup uses rbx_bin/libroblox.so"), std::string::npos); EXPECT_EQ(usage.find("--launch-request-json"), std::string::npos); } } // namespace } // namespace runtime } // namespace mocktail