local function run_plugin_scenario(config) config = config or {} local recorded = { async_calls = {}, sync_calls = {}, script_messages = {}, events = {}, osd = {}, logs = {}, } local function make_mp_stub() local mp = {} function mp.get_property(name) if name == "platform" then return config.platform or "linux" end if name == "filename/no-ext" then return config.filename_no_ext or "" end if name == "filename" then return config.filename or "" end if name == "path" then return config.path or "" end if name == "media-title" then return config.media_title or "" end return "" end function mp.get_property_native(_name) return config.chapter_list or {} end function mp.get_script_directory() return "plugin/subminer" end function mp.command_native(command) recorded.sync_calls[#recorded.sync_calls + 1] = command local args = command.args or {} if args[1] == "ps" then return { status = 0, stdout = config.process_list or "", stderr = "", } end if args[1] == "curl" then local url = args[#args] or "" if type(url) == "string" and url:find("myanimelist", 1, true) then return { status = 0, stdout = config.mal_lookup_stdout or "{}", stderr = "" } end if type(url) == "string" and url:find("api.aniskip.com", 1, true) then return { status = 0, stdout = config.aniskip_stdout or "{}", stderr = "" } end return { status = 0, stdout = "{}", stderr = "" } end return { status = 0, stdout = "", stderr = "" } end function mp.command_native_async(command, callback) recorded.async_calls[#recorded.async_calls + 1] = command if callback then local args = command.args or {} if args[1] == "ps" then callback(true, { status = 0, stdout = config.process_list or "", stderr = "" }, nil) return end if args[1] == "curl" then local url = args[#args] or "" if type(url) == "string" and url:find("myanimelist", 1, true) then callback(true, { status = 0, stdout = config.mal_lookup_stdout or "{}", stderr = "" }, nil) return end if type(url) == "string" and url:find("api.aniskip.com", 1, true) then callback(true, { status = 0, stdout = config.aniskip_stdout or "{}", stderr = "" }, nil) return end end callback(true, { status = 0, stdout = "", stderr = "" }, nil) end end function mp.add_timeout(_seconds, callback) if callback then callback() end end function mp.register_script_message(name, fn) recorded.script_messages[name] = fn end function mp.add_key_binding(_keys, _name, _fn) end function mp.register_event(name, fn) if recorded.events[name] == nil then recorded.events[name] = {} end recorded.events[name][#recorded.events[name] + 1] = fn end function mp.add_hook(_name, _prio, _fn) end function mp.observe_property(_name, _kind, _fn) end function mp.osd_message(message, _duration) recorded.osd[#recorded.osd + 1] = message end function mp.set_osd_ass(...) end function mp.get_time() return 0 end function mp.commandv(...) end function mp.set_property_native(...) end function mp.get_script_name() return "subminer" end return mp end local mp = make_mp_stub() local options = {} local utils = {} function options.read_options(target, _name) for key, value in pairs(config.option_overrides or {}) do target[key] = value end end function utils.file_info(path) local exists = config.files and config.files[path] if exists then return { is_dir = false } end return nil end function utils.join_path(...) local parts = { ... } return table.concat(parts, "/") end function utils.parse_json(json) if json == "__MAL_FOUND__" then return { categories = { { items = { { id = 99, name = "Sample Show", }, }, }, }, }, nil end if json == "__ANISKIP_FOUND__" then return { found = true, results = { { skip_type = "op", interval = { start_time = 12.3, end_time = 45.6, }, }, }, }, nil end return {}, nil end package.loaded["mp"] = nil package.loaded["mp.input"] = nil package.loaded["mp.msg"] = nil package.loaded["mp.options"] = nil package.loaded["mp.utils"] = nil package.preload["mp"] = function() return mp end package.preload["mp.input"] = function() return { select = function(_) end, } end package.preload["mp.msg"] = function() return { info = function(line) recorded.logs[#recorded.logs + 1] = line end, warn = function(line) recorded.logs[#recorded.logs + 1] = line end, error = function(line) recorded.logs[#recorded.logs + 1] = line end, debug = function(line) recorded.logs[#recorded.logs + 1] = line end, } end package.preload["mp.options"] = function() return options end package.preload["mp.utils"] = function() return utils end local ok, err = pcall(dofile, "plugin/subminer/main.lua") if not ok then return nil, err, recorded end return recorded, nil, recorded end local function assert_true(condition, message) if condition then return end error(message) end local function find_start_call(async_calls) for _, call in ipairs(async_calls) do local args = call.args or {} for i = 1, #args do if args[i] == "--start" then return call end end end return nil end local function call_has_arg(call, target) local args = (call and call.args) or {} for _, value in ipairs(args) do if value == target then return true end end return false end local function has_sync_command(sync_calls, executable) for _, call in ipairs(sync_calls) do local args = call.args or {} if args[1] == executable then return true end end return false end local function has_async_command(async_calls, executable) for _, call in ipairs(async_calls) do local args = call.args or {} if args[1] == executable then return true end end return false end local function has_async_curl_for(async_calls, needle) for _, call in ipairs(async_calls) do local args = call.args or {} if args[1] == "curl" then local url = args[#args] or "" if type(url) == "string" and url:find(needle, 1, true) then return true end end end return false end local function fire_event(recorded, name) local listeners = recorded.events[name] or {} for _, listener in ipairs(listeners) do listener() end end local binary_path = "/tmp/subminer-binary" do local recorded, err = run_plugin_scenario({ process_list = "", option_overrides = { binary_path = binary_path, auto_start = "no", }, files = { [binary_path] = true, }, }) assert_true(recorded ~= nil, "plugin failed to load for cold-start scenario: " .. tostring(err)) assert_true(recorded.script_messages["subminer-start"] ~= nil, "subminer-start script message not registered") recorded.script_messages["subminer-start"]("texthooker=no") assert_true(find_start_call(recorded.async_calls) ~= nil, "expected cold-start to invoke --start command when process is absent") assert_true( not has_sync_command(recorded.sync_calls, "ps"), "expected cold-start start command to avoid synchronous process list scan" ) end do local recorded, err = run_plugin_scenario({ process_list = "", option_overrides = { binary_path = binary_path, auto_start = "no", }, media_title = "Random Movie", files = { [binary_path] = true, }, }) assert_true(recorded ~= nil, "plugin failed to load for non-subminer file-load scenario: " .. tostring(err)) fire_event(recorded, "file-loaded") assert_true(not has_sync_command(recorded.sync_calls, "ps"), "file-loaded should avoid synchronous process checks") assert_true(not has_sync_command(recorded.sync_calls, "curl"), "file-loaded should avoid synchronous AniSkip network calls") assert_true( not has_async_curl_for(recorded.async_calls, "myanimelist.net/search/prefix.json"), "file-loaded without SubMiner context should skip AniSkip MAL lookup" ) assert_true( not has_async_curl_for(recorded.async_calls, "api.aniskip.com"), "file-loaded without SubMiner context should skip AniSkip API lookup" ) end do local recorded, err = run_plugin_scenario({ process_list = "", option_overrides = { binary_path = binary_path, auto_start = "no", }, media_title = "Sample Show S01E01", mal_lookup_stdout = "__MAL_FOUND__", aniskip_stdout = "__ANISKIP_FOUND__", files = { [binary_path] = true, }, }) assert_true(recorded ~= nil, "plugin failed to load for script-message AniSkip scenario: " .. tostring(err)) assert_true(recorded.script_messages["subminer-aniskip-refresh"] ~= nil, "subminer-aniskip-refresh script message not registered") recorded.script_messages["subminer-aniskip-refresh"]() assert_true(not has_sync_command(recorded.sync_calls, "curl"), "AniSkip refresh should not perform synchronous curl calls") assert_true(has_async_command(recorded.async_calls, "curl"), "AniSkip refresh should perform async curl calls") assert_true( has_async_curl_for(recorded.async_calls, "myanimelist.net/search/prefix.json"), "AniSkip refresh should perform MAL lookup even when app is not running" ) end do local recorded, err = run_plugin_scenario({ process_list = "", option_overrides = { binary_path = binary_path, auto_start = "yes", auto_start_visible_overlay = "yes", }, media_title = "Random Movie", files = { [binary_path] = true, }, }) assert_true(recorded ~= nil, "plugin failed to load for visible auto-start scenario: " .. tostring(err)) fire_event(recorded, "file-loaded") local start_call = find_start_call(recorded.async_calls) assert_true(start_call ~= nil, "auto-start should issue --start command") assert_true( call_has_arg(start_call, "--show-visible-overlay"), "auto-start with visible overlay enabled should pass --show-visible-overlay" ) assert_true( not call_has_arg(start_call, "--hide-visible-overlay"), "auto-start with visible overlay enabled should not pass --hide-visible-overlay" ) end do local recorded, err = run_plugin_scenario({ process_list = "", option_overrides = { binary_path = binary_path, auto_start = "yes", auto_start_visible_overlay = "no", }, media_title = "Random Movie", files = { [binary_path] = true, }, }) assert_true(recorded ~= nil, "plugin failed to load for hidden auto-start scenario: " .. tostring(err)) fire_event(recorded, "file-loaded") local start_call = find_start_call(recorded.async_calls) assert_true(start_call ~= nil, "auto-start should issue --start command") assert_true( call_has_arg(start_call, "--hide-visible-overlay"), "auto-start with visible overlay disabled should pass --hide-visible-overlay" ) assert_true( not call_has_arg(start_call, "--show-visible-overlay"), "auto-start with visible overlay disabled should not pass --show-visible-overlay" ) end print("plugin start gate regression tests: OK")