From 54acc36f16e1336772354f979a618cce65061f82 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Mon, 7 Sep 2026 20:57:26 -0400 Subject: refactor: make runtime installation explicit --- tests/test_plugin_migration.py | 50 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 tests/test_plugin_migration.py (limited to 'tests') diff --git a/tests/test_plugin_migration.py b/tests/test_plugin_migration.py new file mode 100644 index 0000000..a392f85 --- /dev/null +++ b/tests/test_plugin_migration.py @@ -0,0 +1,50 @@ +import asyncio +import sys +import types +import unittest +from unittest.mock import Mock + + +class PluginMigrationTests(unittest.TestCase): + def test_migration_only_runs_decky_path_migrations(self): + decky = types.SimpleNamespace( + DECKY_HOME="/decky", + DECKY_USER_HOME="/home/deck", + migrate_logs=Mock(), + migrate_settings=Mock(), + migrate_runtime=Mock(), + logger=Mock(), + ) + previous_decky = sys.modules.get("decky") + previous_tomllib = sys.modules.get("tomllib") + sys.modules["decky"] = decky + sys.modules["tomllib"] = types.SimpleNamespace(loads=Mock()) + try: + sys.path.insert(0, "py_modules") + from lsfg_vk.plugin import Plugin + + plugin = Plugin.__new__(Plugin) + plugin.installation_service = Mock() + plugin.flatpak_service = Mock() + + asyncio.run(plugin._migration()) + + decky.migrate_logs.assert_called_once() + decky.migrate_settings.assert_called_once() + decky.migrate_runtime.assert_called_once() + plugin.installation_service.install.assert_not_called() + plugin.flatpak_service.migrate_v2.assert_not_called() + finally: + sys.path.remove("py_modules") + if previous_decky is None: + sys.modules.pop("decky", None) + else: + sys.modules["decky"] = previous_decky + if previous_tomllib is None: + sys.modules.pop("tomllib", None) + else: + sys.modules["tomllib"] = previous_tomllib + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From 8352ee74e8a437c1f4a4dadb75d63049f45b164b Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 8 Sep 2026 16:25:11 -0400 Subject: add back workarounds sections, scope out of now playing --- tests/steamLaunchOptions.test.ts | 288 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 tests/steamLaunchOptions.test.ts (limited to 'tests') diff --git a/tests/steamLaunchOptions.test.ts b/tests/steamLaunchOptions.test.ts new file mode 100644 index 0000000..8ecfa77 --- /dev/null +++ b/tests/steamLaunchOptions.test.ts @@ -0,0 +1,288 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + applyWorkaroundChange, + applyWorkaroundState, + cleanupLegacyWrapper, + getDefaultWorkaroundState, + isLegacyWrapperToken, + parseWorkaroundOptions, + readSteamLaunchOptions, + updateSteamLaunchOptions, +} from "../src/utils/steamLaunchOptions.ts"; + +test("maps the supported workarounds to current launch variables", () => { + const options = applyWorkaroundState('gamemoderun %command% --profile "high quality"', { + dxvkFrameRate: 30, + disableGamescopeWsi: true, + disableSteamdeckMode: true, + disableVkbasalt: true, + enableZink: true, + }); + + assert.equal( + options, + 'ENABLE_GAMESCOPE_WSI=0 SteamDeck=0 DISABLE_VKBASALT=1 MESA_LOADER_DRIVER_OVERRIDE=zink DXVK_CONFIG="dxvk.maxFrameRate = 30" gamemoderun %command% --profile "high quality"', + ); + assert.deepEqual(parseWorkaroundOptions(options), { + state: { + dxvkFrameRate: 30, + disableGamescopeWsi: true, + disableSteamdeckMode: true, + disableVkbasalt: true, + enableZink: true, + }, + issues: [], + }); +}); + +test("uses SteamDeck=0 before %command% without a wrapper", () => { + assert.equal( + applyWorkaroundChange("", "disableSteamdeckMode", true), + "SteamDeck=0 %command%", + ); +}); + +test("keeps WSI disable opt-in and does not add HDR assignments", () => { + const defaults = getDefaultWorkaroundState(); + assert.equal(applyWorkaroundState("%command%", defaults), "%command%"); + assert.equal(parseWorkaroundOptions("%command%").state.disableGamescopeWsi, false); + assert.equal( + applyWorkaroundChange("%command%", "disableGamescopeWsi", true), + "ENABLE_GAMESCOPE_WSI=0 %command%", + ); + assert.equal( + applyWorkaroundChange("ENABLE_GAMESCOPE_WSI=0 %command%", "disableGamescopeWsi", false), + "%command%", + ); + + const legacy = parseWorkaroundOptions("ENABLE_GAMESCOPE_WSI=0 DXVK_HDR=0 %command%"); + assert.equal(legacy.state.disableGamescopeWsi, true); + assert.deepEqual(legacy.issues, []); + assert.equal( + applyWorkaroundChange("ENABLE_GAMESCOPE_WSI=0 DXVK_HDR=0 %command%", "disableGamescopeWsi", false), + "%command%", + ); + + const invalid = parseWorkaroundOptions("ENABLE_GAMESCOPE_WSI=maybe %command%"); + assert.equal(invalid.state.disableGamescopeWsi, false); + assert.equal(invalid.issues.length, 1); + const conflicting = parseWorkaroundOptions("DISABLE_GAMESCOPE_WSI=1 ENABLE_GAMESCOPE_WSI=1 %command%"); + assert.equal(conflicting.state.disableGamescopeWsi, true); + assert.match(conflicting.issues.join(" "), /conflicting/); +}); + +test("preserves unrelated prefixes, quoted tokens, suffix arguments, and dropped variables", () => { + const options = applyWorkaroundChange( + 'PROTON_USE_WOW64=1 MANGOHUD=1 MANGOHUD_CONFIG="alpha=0.01" ENABLE_VKBASALT=1 VK_INSTANCE_LAYERS="one:two" FOO="hello world" gamemoderun %command% --flag "two words"', + "disableSteamdeckMode", + true, + ); + assert.equal( + options, + 'SteamDeck=0 PROTON_USE_WOW64=1 MANGOHUD=1 MANGOHUD_CONFIG="alpha=0.01" ENABLE_VKBASALT=1 VK_INSTANCE_LAYERS="one:two" FOO="hello world" gamemoderun %command% --flag "two words"', + ); + assert.deepEqual(parseWorkaroundOptions(options).issues, []); + + assert.equal( + applyWorkaroundChange("FOO=bar --flag", "disableSteamdeckMode", true), + "SteamDeck=0 FOO=bar %command% --flag", + ); + assert.equal( + applyWorkaroundChange("FOO=1 %command% MANGOHUD=1", "disableSteamdeckMode", false), + "FOO=1 %command% MANGOHUD=1", + ); + assert.equal( + applyWorkaroundChange('FOO=bar --literal "%command%"', "disableSteamdeckMode", true), + 'SteamDeck=0 FOO=bar %command% --literal "%command%"', + ); + assert.equal( + applyWorkaroundChange("gamemoderun SteamDeck=1 %command%", "disableSteamdeckMode", true), + "SteamDeck=0 gamemoderun SteamDeck=1 %command%", + ); + assert.equal(parseWorkaroundOptions("gamemoderun SteamDeck=0 %command%").state.disableSteamdeckMode, false); +}); + +test("uses DXVK_CONFIG for the base cap and preserves other DXVK settings", () => { + assert.equal( + applyWorkaroundChange("%command%", "dxvkFrameRate", 60), + 'DXVK_CONFIG="dxvk.maxFrameRate = 60" %command%', + ); + assert.equal(parseWorkaroundOptions('DXVK_CONFIG="dxvk.maxFrameRate = 60" %command%').state.dxvkFrameRate, 60); + assert.equal( + applyWorkaroundChange( + 'DXVK_CONFIG="dxgi.syncInterval = 0; dxvk.maxFrameRate = 30" %command%', + "dxvkFrameRate", + 0, + ), + 'DXVK_CONFIG="dxgi.syncInterval = 0" %command%', + ); + assert.equal( + applyWorkaroundChange("DXVK_FRAME_RATE=30 %command%", "dxvkFrameRate", 45), + 'DXVK_CONFIG="dxvk.maxFrameRate = 45" %command%', + ); + assert.equal( + applyWorkaroundChange("DXVK_FRAME_RATE=30 %command%", "dxvkFrameRate", 0), + "%command%", + ); + + const apiSpecific = parseWorkaroundOptions( + 'DXVK_CONFIG="dxgi.maxFrameRate = 30; d3d9.maxFrameRate = 30" %command%', + ); + assert.equal(apiSpecific.state.dxvkFrameRate, 30); + assert.deepEqual(apiSpecific.issues, []); + const partial = parseWorkaroundOptions('DXVK_CONFIG="dxgi.maxFrameRate = 30" %command%'); + assert.equal(partial.state.dxvkFrameRate, 30); + assert.match(partial.issues.join(" "), /only caps one DirectX API/); + const conflicting = parseWorkaroundOptions( + 'DXVK_CONFIG="dxgi.maxFrameRate = 30; d3d9.maxFrameRate = 60" %command%', + ); + assert.equal(conflicting.state.dxvkFrameRate, 0); + assert.match(conflicting.issues.join(" "), /conflicting/); +}); + +test("reports invalid and malformed FPS values instead of treating them as off", () => { + const invalid = parseWorkaroundOptions('DXVK_CONFIG="dxvk.maxFrameRate = 61" %command%'); + assert.equal(invalid.state.dxvkFrameRate, 0); + assert.match(invalid.issues.join(" "), /outside the supported 0-60 range/); + const malformed = parseWorkaroundOptions('DXVK_CONFIG="dxvk.maxFrameRate" %command%'); + assert.equal(malformed.state.dxvkFrameRate, 0); + assert.match(malformed.issues.join(" "), /malformed/); + const obsolete = parseWorkaroundOptions("DXVK_FRAME_RATE=wat %command%"); + assert.equal(obsolete.state.dxvkFrameRate, 0); + assert.match(obsolete.issues.join(" "), /obsolete/); + assert.throws(() => applyWorkaroundChange("%command%", "dxvkFrameRate", 61), /0 to 60/); + assert.throws(() => applyWorkaroundChange("%command%", "dxvkFrameRate", 1.5), /0 to 60/); +}); + +test("keeps vkBasalt disable mutually exclusive while preserving the dropped enable flag otherwise", () => { + assert.equal( + applyWorkaroundChange("ENABLE_VKBASALT=1 %command%", "disableSteamdeckMode", true), + "SteamDeck=0 ENABLE_VKBASALT=1 %command%", + ); + const disabled = applyWorkaroundChange("ENABLE_VKBASALT=1 %command%", "disableVkbasalt", true); + assert.equal(disabled, "DISABLE_VKBASALT=1 %command%"); + assert.equal( + applyWorkaroundChange(disabled, "disableVkbasalt", false), + "%command%", + ); + const conflict = parseWorkaroundOptions("ENABLE_VKBASALT=1 DISABLE_VKBASALT=1 %command%"); + assert.equal(conflict.state.disableVkbasalt, true); + assert.match(conflict.issues.join(" "), /conflicting/); +}); + +test("handles current and legacy Zink forms and reports partial state", () => { + const enabled = applyWorkaroundChange("%command%", "enableZink", true); + assert.equal(enabled, "MESA_LOADER_DRIVER_OVERRIDE=zink %command%"); + assert.equal(parseWorkaroundOptions(enabled).state.enableZink, true); + + const legacy = parseWorkaroundOptions( + "__GLX_VENDOR_LIBRARY_NAME=mesa MESA_LOADER_DRIVER_OVERRIDE=zink GALLIUM_DRIVER=zink %command%", + ); + assert.equal(legacy.state.enableZink, true); + assert.deepEqual(legacy.issues, []); + + const partial = parseWorkaroundOptions("__GLX_VENDOR_LIBRARY_NAME=mesa MESA_LOADER_DRIVER_OVERRIDE=zink %command%"); + assert.equal(partial.state.enableZink, true); + assert.match(partial.issues.join(" "), /partially configured/); + assert.equal( + applyWorkaroundChange( + "__GLX_VENDOR_LIBRARY_NAME=mesa MESA_LOADER_DRIVER_OVERRIDE=zink GALLIUM_DRIVER=zink %command%", + "enableZink", + false, + ), + "%command%", + ); +}); + +test("cleans only the known legacy wrapper and preserves launch options", () => { + assert.equal( + cleanupLegacyWrapper('FOO=bar ~/lsfg %command% --arg "~/lsfg"'), + 'FOO=bar %command% --arg "~/lsfg"', + ); + assert.equal(cleanupLegacyWrapper("/home/deck/lsfg %command%"), "%command%"); + assert.equal( + cleanupLegacyWrapper("DXVK_FRAME_RATE=30 LSFG_PROCESS=decky-lsfg-vk %command%"), + "DXVK_FRAME_RATE=30 LSFG_PROCESS=decky-lsfg-vk %command%", + ); + assert.equal( + cleanupLegacyWrapper("LSFG_PROCESS=decky-lsfg-vk %command%"), + "LSFG_PROCESS=decky-lsfg-vk %command%", + ); + assert.equal(isLegacyWrapperToken("/home/kurt/lsfg"), false); +}); + +test("is idempotent", () => { + const first = applyWorkaroundChange("gamemoderun %command%", "enableZink", true); + assert.equal(applyWorkaroundState(first, parseWorkaroundOptions(first).state), first); + assert.equal(applyWorkaroundChange(first, "enableZink", true), first); + const capped = applyWorkaroundChange(first, "dxvkFrameRate", 30); + assert.equal(applyWorkaroundChange(capped, "dxvkFrameRate", 30), capped); +}); + +test("reads and writes the matching Steam app-details launch-option field", async () => { + const previousWindow = (globalThis as Record).window; + const previousSteamClient = (globalThis as Record).SteamClient; + let normalOptions = "FOO=bar %command%"; + let shortcutOptions = "--windowed"; + const normalWrites: string[] = []; + const shortcutWrites: string[] = []; + const unregisters: number[] = []; + + const windowShim = { setTimeout, clearTimeout }; + const apps = { + RegisterForAppDetails(appId: number, callback: (details: SteamAppDetails) => void) { + if (appId === 42) { + callback({ strLaunchOptions: normalOptions, strShortcutLaunchOptions: "must-not-be-read" }); + } else { + callback({ + strShortcutExe: "/usr/bin/example-game", + strShortcutLaunchOptions: shortcutOptions, + strLaunchOptions: "must-not-be-read", + }); + } + return { unregister: () => unregisters.push(appId) }; + }, + SetAppLaunchOptions(appId: number, options: string) { + assert.equal(appId, 42); + normalWrites.push(options); + normalOptions = options.replaceAll(" ", " "); + }, + SetShortcutLaunchOptions(appId: number, options: string) { + assert.equal(appId, 43); + shortcutWrites.push(options); + shortcutOptions = options; + }, + }; + + (globalThis as Record).window = windowShim; + (globalThis as Record).SteamClient = { Apps: apps }; + try { + const normalBefore = await readSteamLaunchOptions(42, false); + assert.equal(normalBefore.options, "FOO=bar %command%"); + const normalAfter = await updateSteamLaunchOptions( + 42, + false, + (options) => applyWorkaroundChange(options, "disableSteamdeckMode", true), + ); + assert.equal(normalWrites.length, 1); + assert.equal(shortcutWrites.length, 0); + assert.equal(normalAfter.options, "SteamDeck=0 FOO=bar %command%"); + + const shortcutAfter = await updateSteamLaunchOptions( + 43, + true, + (options) => applyWorkaroundChange(options, "disableGamescopeWsi", true), + ); + assert.equal(shortcutWrites.length, 1); + assert.equal(shortcutWrites[0], "ENABLE_GAMESCOPE_WSI=0 %command% --windowed"); + assert.equal(shortcutAfter.options, shortcutWrites[0]); + assert.ok(unregisters.includes(42)); + assert.ok(unregisters.includes(43)); + } finally { + if (previousWindow === undefined) delete (globalThis as Record).window; + else (globalThis as Record).window = previousWindow; + if (previousSteamClient === undefined) delete (globalThis as Record).SteamClient; + else (globalThis as Record).SteamClient = previousSteamClient; + } +}); -- cgit v1.2.3 From bec26fe025c97c00d398e7a4fb571195706b9e76 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 8 Sep 2026 16:56:57 -0400 Subject: feat: more launch arg janitoring --- tests/steamLaunchOptions.test.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) (limited to 'tests') diff --git a/tests/steamLaunchOptions.test.ts b/tests/steamLaunchOptions.test.ts index 8ecfa77..0662fbb 100644 --- a/tests/steamLaunchOptions.test.ts +++ b/tests/steamLaunchOptions.test.ts @@ -3,9 +3,11 @@ import test from "node:test"; import { applyWorkaroundChange, applyWorkaroundState, + cleanupLegacyLaunchOptions, cleanupLegacyWrapper, getDefaultWorkaroundState, isLegacyWrapperToken, + normalizeLaunchOptions, parseWorkaroundOptions, readSteamLaunchOptions, updateSteamLaunchOptions, @@ -45,7 +47,7 @@ test("uses SteamDeck=0 before %command% without a wrapper", () => { test("keeps WSI disable opt-in and does not add HDR assignments", () => { const defaults = getDefaultWorkaroundState(); - assert.equal(applyWorkaroundState("%command%", defaults), "%command%"); + assert.equal(applyWorkaroundState("%command%", defaults), ""); assert.equal(parseWorkaroundOptions("%command%").state.disableGamescopeWsi, false); assert.equal( applyWorkaroundChange("%command%", "disableGamescopeWsi", true), @@ -53,7 +55,7 @@ test("keeps WSI disable opt-in and does not add HDR assignments", () => { ); assert.equal( applyWorkaroundChange("ENABLE_GAMESCOPE_WSI=0 %command%", "disableGamescopeWsi", false), - "%command%", + "", ); const legacy = parseWorkaroundOptions("ENABLE_GAMESCOPE_WSI=0 DXVK_HDR=0 %command%"); @@ -61,7 +63,7 @@ test("keeps WSI disable opt-in and does not add HDR assignments", () => { assert.deepEqual(legacy.issues, []); assert.equal( applyWorkaroundChange("ENABLE_GAMESCOPE_WSI=0 DXVK_HDR=0 %command%", "disableGamescopeWsi", false), - "%command%", + "", ); const invalid = parseWorkaroundOptions("ENABLE_GAMESCOPE_WSI=maybe %command%"); @@ -123,7 +125,7 @@ test("uses DXVK_CONFIG for the base cap and preserves other DXVK settings", () = ); assert.equal( applyWorkaroundChange("DXVK_FRAME_RATE=30 %command%", "dxvkFrameRate", 0), - "%command%", + "", ); const apiSpecific = parseWorkaroundOptions( @@ -164,7 +166,7 @@ test("keeps vkBasalt disable mutually exclusive while preserving the dropped ena assert.equal(disabled, "DISABLE_VKBASALT=1 %command%"); assert.equal( applyWorkaroundChange(disabled, "disableVkbasalt", false), - "%command%", + "", ); const conflict = parseWorkaroundOptions("ENABLE_VKBASALT=1 DISABLE_VKBASALT=1 %command%"); assert.equal(conflict.state.disableVkbasalt, true); @@ -191,7 +193,7 @@ test("handles current and legacy Zink forms and reports partial state", () => { "enableZink", false, ), - "%command%", + "", ); }); @@ -200,7 +202,7 @@ test("cleans only the known legacy wrapper and preserves launch options", () => cleanupLegacyWrapper('FOO=bar ~/lsfg %command% --arg "~/lsfg"'), 'FOO=bar %command% --arg "~/lsfg"', ); - assert.equal(cleanupLegacyWrapper("/home/deck/lsfg %command%"), "%command%"); + assert.equal(cleanupLegacyWrapper("/home/deck/lsfg %command%"), ""); assert.equal( cleanupLegacyWrapper("DXVK_FRAME_RATE=30 LSFG_PROCESS=decky-lsfg-vk %command%"), "DXVK_FRAME_RATE=30 LSFG_PROCESS=decky-lsfg-vk %command%", @@ -212,6 +214,13 @@ test("cleans only the known legacy wrapper and preserves launch options", () => assert.equal(isLegacyWrapperToken("/home/kurt/lsfg"), false); }); +test("canonicalizes a bare command token without removing real arguments", () => { + assert.equal(normalizeLaunchOptions("%command%"), ""); + assert.equal(normalizeLaunchOptions("%COMMAND%"), ""); + assert.equal(normalizeLaunchOptions("FOO=bar %command%"), "FOO=bar %command%"); + assert.equal(normalizeLaunchOptions("%command% --windowed"), "%command% --windowed"); +}); + test("is idempotent", () => { const first = applyWorkaroundChange("gamemoderun %command%", "enableZink", true); assert.equal(applyWorkaroundState(first, parseWorkaroundOptions(first).state), first); -- cgit v1.2.3 From 790132668c4421c68c32bdc8fc9792b0d6028f97 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Tue, 8 Sep 2026 22:18:13 -0400 Subject: I really dont want to but here you go little guy --- tests/steamLaunchOptions.test.ts | 73 +++++++++++++++--- tests/test_flatpak_overrides.py | 158 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+), 11 deletions(-) create mode 100644 tests/test_flatpak_overrides.py (limited to 'tests') diff --git a/tests/steamLaunchOptions.test.ts b/tests/steamLaunchOptions.test.ts index 0662fbb..c4dede3 100644 --- a/tests/steamLaunchOptions.test.ts +++ b/tests/steamLaunchOptions.test.ts @@ -4,6 +4,7 @@ import { applyWorkaroundChange, applyWorkaroundState, cleanupLegacyLaunchOptions, + cleanupPluginLaunchOptions, cleanupLegacyWrapper, getDefaultWorkaroundState, isLegacyWrapperToken, @@ -17,6 +18,7 @@ test("maps the supported workarounds to current launch variables", () => { const options = applyWorkaroundState('gamemoderun %command% --profile "high quality"', { dxvkFrameRate: 30, disableGamescopeWsi: true, + disableHdr: true, disableSteamdeckMode: true, disableVkbasalt: true, enableZink: true, @@ -24,12 +26,13 @@ test("maps the supported workarounds to current launch variables", () => { assert.equal( options, - 'ENABLE_GAMESCOPE_WSI=0 SteamDeck=0 DISABLE_VKBASALT=1 MESA_LOADER_DRIVER_OVERRIDE=zink DXVK_CONFIG="dxvk.maxFrameRate = 30" gamemoderun %command% --profile "high quality"', + 'ENABLE_GAMESCOPE_WSI=0 DXVK_HDR=0 SteamDeck=0 DISABLE_VKBASALT=1 MESA_LOADER_DRIVER_OVERRIDE=zink DXVK_CONFIG="dxvk.maxFrameRate = 30" gamemoderun %command% --profile "high quality"', ); assert.deepEqual(parseWorkaroundOptions(options), { state: { dxvkFrameRate: 30, disableGamescopeWsi: true, + disableHdr: true, disableSteamdeckMode: true, disableVkbasalt: true, enableZink: true, @@ -45,10 +48,24 @@ test("uses SteamDeck=0 before %command% without a wrapper", () => { ); }); -test("keeps WSI disable opt-in and does not add HDR assignments", () => { +test("defaults new profiles to disable Gamescope WSI and HDR", () => { const defaults = getDefaultWorkaroundState(); - assert.equal(applyWorkaroundState("%command%", defaults), ""); + assert.equal(defaults.disableGamescopeWsi, true); + assert.equal(defaults.disableHdr, true); + assert.equal( + applyWorkaroundState("%command%", defaults), + "ENABLE_GAMESCOPE_WSI=0 DXVK_HDR=0 %command%", + ); assert.equal(parseWorkaroundOptions("%command%").state.disableGamescopeWsi, false); + assert.equal(parseWorkaroundOptions("%command%").state.disableHdr, false); + assert.equal( + parseWorkaroundOptions(applyWorkaroundState("%command%", defaults)).state.disableGamescopeWsi, + true, + ); + assert.equal( + parseWorkaroundOptions(applyWorkaroundState("%command%", defaults)).state.disableHdr, + true, + ); assert.equal( applyWorkaroundChange("%command%", "disableGamescopeWsi", true), "ENABLE_GAMESCOPE_WSI=0 %command%", @@ -58,14 +75,6 @@ test("keeps WSI disable opt-in and does not add HDR assignments", () => { "", ); - const legacy = parseWorkaroundOptions("ENABLE_GAMESCOPE_WSI=0 DXVK_HDR=0 %command%"); - assert.equal(legacy.state.disableGamescopeWsi, true); - assert.deepEqual(legacy.issues, []); - assert.equal( - applyWorkaroundChange("ENABLE_GAMESCOPE_WSI=0 DXVK_HDR=0 %command%", "disableGamescopeWsi", false), - "", - ); - const invalid = parseWorkaroundOptions("ENABLE_GAMESCOPE_WSI=maybe %command%"); assert.equal(invalid.state.disableGamescopeWsi, false); assert.equal(invalid.issues.length, 1); @@ -74,6 +83,27 @@ test("keeps WSI disable opt-in and does not add HDR assignments", () => { assert.match(conflicting.issues.join(" "), /conflicting/); }); +test("manages DXVK HDR independently from Gamescope WSI", () => { + assert.equal( + applyWorkaroundChange("%command%", "disableHdr", true), + "DXVK_HDR=0 %command%", + ); + assert.equal(parseWorkaroundOptions("DXVK_HDR=0 %command%").state.disableHdr, true); + assert.equal(parseWorkaroundOptions("DXVK_HDR=1 %command%").state.disableHdr, false); + assert.equal( + applyWorkaroundChange("DXVK_HDR=0 %command%", "disableHdr", false), + "", + ); + assert.equal( + applyWorkaroundChange("DXVK_HDR=0 %command%", "disableGamescopeWsi", true), + "ENABLE_GAMESCOPE_WSI=0 DXVK_HDR=0 %command%", + ); + + const invalid = parseWorkaroundOptions("DXVK_HDR=maybe %command%"); + assert.equal(invalid.state.disableHdr, false); + assert.match(invalid.issues.join(" "), /Disable HDR has an unsupported value/); +}); + test("preserves unrelated prefixes, quoted tokens, suffix arguments, and dropped variables", () => { const options = applyWorkaroundChange( 'PROTON_USE_WOW64=1 MANGOHUD=1 MANGOHUD_CONFIG="alpha=0.01" ENABLE_VKBASALT=1 VK_INSTANCE_LAYERS="one:two" FOO="hello world" gamemoderun %command% --flag "two words"', @@ -203,6 +233,8 @@ test("cleans only the known legacy wrapper and preserves launch options", () => 'FOO=bar %command% --arg "~/lsfg"', ); assert.equal(cleanupLegacyWrapper("/home/deck/lsfg %command%"), ""); + assert.equal(cleanupLegacyWrapper("mako-run %command%"), ""); + assert.equal(cleanupLegacyWrapper("mako-launch %command%"), ""); assert.equal( cleanupLegacyWrapper("DXVK_FRAME_RATE=30 LSFG_PROCESS=decky-lsfg-vk %command%"), "DXVK_FRAME_RATE=30 LSFG_PROCESS=decky-lsfg-vk %command%", @@ -214,6 +246,25 @@ test("cleans only the known legacy wrapper and preserves launch options", () => assert.equal(isLegacyWrapperToken("/home/kurt/lsfg"), false); }); +test("removes plugin-managed launch options when a profile is removed", () => { + assert.equal( + cleanupPluginLaunchOptions( + 'FOO="keep this" ENABLE_GAMESCOPE_WSI=0 DXVK_HDR=0 SteamDeck=0 DISABLE_VKBASALT=1 MESA_LOADER_DRIVER_OVERRIDE=zink DXVK_CONFIG="dxgi.syncInterval = 0; dxvk.maxFrameRate = 30" ~/lsfg %command% --windowed', + ), + 'DXVK_CONFIG="dxgi.syncInterval = 0" FOO="keep this" %command% --windowed', + ); + assert.equal( + cleanupPluginLaunchOptions( + 'PROTON_USE_WOW64=1 MANGOHUD=1 MANGOHUD_CONFIG="alpha=0.01" LSFG_PROCESS=decky-lsfg-vk %command%', + ), + 'PROTON_USE_WOW64=1 MANGOHUD=1 MANGOHUD_CONFIG="alpha=0.01" LSFG_PROCESS=decky-lsfg-vk %command%', + ); + assert.equal( + cleanupPluginLaunchOptions('DXVK_CONFIG="dxvk.maxFrameRate = 30" %command%'), + "", + ); +}); + test("canonicalizes a bare command token without removing real arguments", () => { assert.equal(normalizeLaunchOptions("%command%"), ""); assert.equal(normalizeLaunchOptions("%COMMAND%"), ""); diff --git a/tests/test_flatpak_overrides.py b/tests/test_flatpak_overrides.py new file mode 100644 index 0000000..ed3ef6a --- /dev/null +++ b/tests/test_flatpak_overrides.py @@ -0,0 +1,158 @@ +import sys +import tempfile +import types +import unittest +from pathlib import Path +from unittest.mock import Mock + + +sys.modules.setdefault( + "decky", + types.SimpleNamespace(DECKY_USER_HOME="/home/deck", logger=Mock()), +) +sys.modules.setdefault("tomllib", types.SimpleNamespace(loads=Mock())) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "py_modules")) + +from lsfg_vk.flatpak_service import FlatpakService + + +class FlatpakOverrideTests(unittest.TestCase): + def setUp(self): + self.tempdir = tempfile.TemporaryDirectory() + home = Path(self.tempdir.name) / "home" / "deck" + home.mkdir(parents=True) + self.service = FlatpakService() + self.service.user_home = home + self.service.config_dir = home / ".config/lsfg-vk" + self.service.config_file_path = self.service.config_dir / "conf.toml" + self.service.legacy_script_path = home / "lsfg" + self.service.check_flatpak_available = Mock(return_value=True) + self.service._run_flatpak_command = Mock( + return_value=types.SimpleNamespace(returncode=0, stderr="", stdout="") + ) + self.app_id = "com.example.Game" + self.override_path = self.service._override_file_path(self.app_id) + + def tearDown(self): + self.tempdir.cleanup() + sys.modules.pop("lsfg_vk.plugin", None) + sys.modules.pop("lsfg_vk", None) + + def _paths(self): + return self.service._override_paths() + + def _write_override(self, content): + self.override_path.parent.mkdir(parents=True, exist_ok=True) + self.override_path.write_text(content, encoding="utf-8") + + def _show_response(self, content): + return types.SimpleNamespace(returncode=0, stderr="", stdout=content) + + def test_set_cleans_legacy_entries_and_verifies_readback(self): + paths = self._paths() + self._write_override( + "[Context]\n" + f"filesystems=/home/deck/keep;{paths['config_dir']}:rw;!{paths['legacy_home']};" + f"{paths['legacy_script']};{paths['legacy_dll']}:ro;{paths['dll_dir']}:ro;\n" + "unset-environment=KEEP_UNSET;LSFG_CONFIG;\n\n" + "[Environment]\n" + "KEEP_ENV=1\n" + "LSFG_CONFIG=\n" + "LSFGVK_CONFIG=old\n" + "ENABLE_GAMESCOPE_WSI=1\n" + "DXVK_HDR=1\n" + ) + expected = ( + "[Context]\n" + f"filesystems={paths['config_dir']}:rw;{paths['dll_dir']}:ro\n" + "[Environment]\n" + f"LSFGVK_CONFIG={paths['config_file']}\n" + "ENABLE_GAMESCOPE_WSI=0\n" + "DXVK_HDR=0\n" + ) + self.service._run_flatpak_command.side_effect = [ + self._show_response(""), + self._show_response(expected), + ] + + response = self.service.set_app_override(self.app_id) + command_args = self.service._run_flatpak_command.call_args_list[0].args[0] + cleaned = self.override_path.read_text(encoding="utf-8") + + self.assertTrue(response["success"]) + self.assertIn("--env=ENABLE_GAMESCOPE_WSI=0", command_args) + self.assertIn("--env=DXVK_HDR=0", command_args) + self.assertNotIn("--nofilesystem=/home/deck", command_args) + self.assertNotIn("--unset-env=LSFG_CONFIG", command_args) + self.assertIn("/home/deck/keep", cleaned) + self.assertIn("KEEP_ENV=1", cleaned) + self.assertNotIn("LSFG_CONFIG", cleaned) + self.assertNotIn(paths["legacy_home"], cleaned) + + def test_set_reports_failed_readback(self): + paths = self._paths() + self.service._run_flatpak_command.side_effect = [ + self._show_response(""), + self._show_response( + f"[Context]\nfilesystems={paths['config_dir']};{paths['dll_dir']}\n" + f"[Environment]\nLSFGVK_CONFIG={paths['config_file']}\n" + ), + ] + + response = self.service.set_app_override(self.app_id) + + self.assertFalse(response["success"]) + self.assertIn("verified", response["error"]) + + def test_remove_cleans_known_entries_preserves_unrelated_and_verifies(self): + paths = self._paths() + self._write_override( + "[Context]\n" + f"filesystems=/home/deck/keep;{paths['config_dir']};!{paths['legacy_home']};" + f"{paths['legacy_dll']};{paths['legacy_script']}\n" + "unset-environment=KEEP_UNSET;LSFG_CONFIG;ENABLE_GAMESCOPE_WSI\n\n" + "[Environment]\n" + "KEEP_ENV=1\n" + "LSFGVK_CONFIG=/old/path\n" + "DXVK_HDR=0\n" + ) + self.service._run_flatpak_command.side_effect = [ + self._show_response( + "[Context]\nfilesystems=/home/deck/keep\n" + "[Environment]\nKEEP_ENV=1\n" + ) + ] + + response = self.service.remove_app_override(self.app_id) + cleaned = self.override_path.read_text(encoding="utf-8") + + self.assertTrue(response["success"]) + self.assertEqual(self.service._run_flatpak_command.call_count, 1) + self.assertIn("/home/deck/keep", cleaned) + self.assertIn("KEEP_UNSET", cleaned) + self.assertIn("KEEP_ENV", cleaned) + for name in ("LSFGVK_CONFIG", "LSFG_CONFIG", "ENABLE_GAMESCOPE_WSI", "DXVK_HDR"): + self.assertNotIn(name, cleaned) + for path in paths.values(): + if path != paths["config_file"]: + self.assertNotIn(path, cleaned) + + def test_remove_reports_failed_readback(self): + self._write_override("[Context]\nfilesystems=/home/deck/keep\n") + paths = self._paths() + self.service._run_flatpak_command.side_effect = [ + self._show_response( + f"[Context]\nfilesystems={paths['config_dir']};{paths['dll_dir']}\n" + f"[Environment]\nLSFGVK_CONFIG={paths['config_file']}\n" + "ENABLE_GAMESCOPE_WSI=0\nDXVK_HDR=0\n" + ) + ] + + response = self.service.remove_app_override(self.app_id) + + self.assertFalse(response["success"]) + self.assertIn("verified", response["error"]) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From 1b932fd69c3dba925e0cbf027e05508b2daf5e8c Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Wed, 9 Sep 2026 01:01:42 -0400 Subject: add back launcher script and correct pathing --- tests/steamLaunchOptions.test.ts | 434 +++++++++++++-------------------------- tests/test_wrapper_service.py | 170 +++++++++++++++ 2 files changed, 318 insertions(+), 286 deletions(-) create mode 100644 tests/test_wrapper_service.py (limited to 'tests') diff --git a/tests/steamLaunchOptions.test.ts b/tests/steamLaunchOptions.test.ts index c4dede3..3170fe8 100644 --- a/tests/steamLaunchOptions.test.ts +++ b/tests/steamLaunchOptions.test.ts @@ -1,342 +1,138 @@ import assert from "node:assert/strict"; import test from "node:test"; import { - applyWorkaroundChange, - applyWorkaroundState, - cleanupLegacyLaunchOptions, + cleanupPluginAssignments, cleanupPluginLaunchOptions, cleanupLegacyWrapper, - getDefaultWorkaroundState, + hasWrapperLaunchIntegration, + installWrapperIntegration, + installWrapperLaunchOption, isLegacyWrapperToken, normalizeLaunchOptions, - parseWorkaroundOptions, readSteamLaunchOptions, - updateSteamLaunchOptions, + removeWrapperIntegration, + removeWrapperLaunchOption, } from "../src/utils/steamLaunchOptions.ts"; -test("maps the supported workarounds to current launch variables", () => { - const options = applyWorkaroundState('gamemoderun %command% --profile "high quality"', { - dxvkFrameRate: 30, - disableGamescopeWsi: true, - disableHdr: true, - disableSteamdeckMode: true, - disableVkbasalt: true, - enableZink: true, - }); +const wrapper = "~/.lsfg"; - assert.equal( - options, - 'ENABLE_GAMESCOPE_WSI=0 DXVK_HDR=0 SteamDeck=0 DISABLE_VKBASALT=1 MESA_LOADER_DRIVER_OVERRIDE=zink DXVK_CONFIG="dxvk.maxFrameRate = 30" gamemoderun %command% --profile "high quality"', - ); - assert.deepEqual(parseWorkaroundOptions(options), { - state: { - dxvkFrameRate: 30, - disableGamescopeWsi: true, - disableHdr: true, - disableSteamdeckMode: true, - disableVkbasalt: true, - enableZink: true, - }, - issues: [], +test("inserts one wrapper immediately before an existing command macro", () => { + assert.deepEqual(installWrapperLaunchOption('gamemoderun %command% --profile "high quality"', wrapper), { + options: 'gamemoderun ~/.lsfg %command% --profile "high quality"', + commandTokenAdded: false, + }); + assert.equal(hasWrapperLaunchIntegration(`gamemoderun ${wrapper} %command%`, wrapper), true); + assert.deepEqual(installWrapperLaunchOption(`gamemoderun ${wrapper} %command%`, wrapper), { + options: `gamemoderun ${wrapper} %command%`, + commandTokenAdded: false, }); }); -test("uses SteamDeck=0 before %command% without a wrapper", () => { - assert.equal( - applyWorkaroundChange("", "disableSteamdeckMode", true), - "SteamDeck=0 %command%", - ); -}); - -test("defaults new profiles to disable Gamescope WSI and HDR", () => { - const defaults = getDefaultWorkaroundState(); - assert.equal(defaults.disableGamescopeWsi, true); - assert.equal(defaults.disableHdr, true); - assert.equal( - applyWorkaroundState("%command%", defaults), - "ENABLE_GAMESCOPE_WSI=0 DXVK_HDR=0 %command%", - ); - assert.equal(parseWorkaroundOptions("%command%").state.disableGamescopeWsi, false); - assert.equal(parseWorkaroundOptions("%command%").state.disableHdr, false); - assert.equal( - parseWorkaroundOptions(applyWorkaroundState("%command%", defaults)).state.disableGamescopeWsi, - true, - ); - assert.equal( - parseWorkaroundOptions(applyWorkaroundState("%command%", defaults)).state.disableHdr, - true, - ); - assert.equal( - applyWorkaroundChange("%command%", "disableGamescopeWsi", true), - "ENABLE_GAMESCOPE_WSI=0 %command%", - ); - assert.equal( - applyWorkaroundChange("ENABLE_GAMESCOPE_WSI=0 %command%", "disableGamescopeWsi", false), - "", - ); - - const invalid = parseWorkaroundOptions("ENABLE_GAMESCOPE_WSI=maybe %command%"); - assert.equal(invalid.state.disableGamescopeWsi, false); - assert.equal(invalid.issues.length, 1); - const conflicting = parseWorkaroundOptions("DISABLE_GAMESCOPE_WSI=1 ENABLE_GAMESCOPE_WSI=1 %command%"); - assert.equal(conflicting.state.disableGamescopeWsi, true); - assert.match(conflicting.issues.join(" "), /conflicting/); -}); - -test("manages DXVK HDR independently from Gamescope WSI", () => { - assert.equal( - applyWorkaroundChange("%command%", "disableHdr", true), - "DXVK_HDR=0 %command%", - ); - assert.equal(parseWorkaroundOptions("DXVK_HDR=0 %command%").state.disableHdr, true); - assert.equal(parseWorkaroundOptions("DXVK_HDR=1 %command%").state.disableHdr, false); - assert.equal( - applyWorkaroundChange("DXVK_HDR=0 %command%", "disableHdr", false), - "", - ); - assert.equal( - applyWorkaroundChange("DXVK_HDR=0 %command%", "disableGamescopeWsi", true), - "ENABLE_GAMESCOPE_WSI=0 DXVK_HDR=0 %command%", - ); - - const invalid = parseWorkaroundOptions("DXVK_HDR=maybe %command%"); - assert.equal(invalid.state.disableHdr, false); - assert.match(invalid.issues.join(" "), /Disable HDR has an unsupported value/); -}); - -test("preserves unrelated prefixes, quoted tokens, suffix arguments, and dropped variables", () => { - const options = applyWorkaroundChange( - 'PROTON_USE_WOW64=1 MANGOHUD=1 MANGOHUD_CONFIG="alpha=0.01" ENABLE_VKBASALT=1 VK_INSTANCE_LAYERS="one:two" FOO="hello world" gamemoderun %command% --flag "two words"', - "disableSteamdeckMode", - true, - ); - assert.equal( - options, - 'SteamDeck=0 PROTON_USE_WOW64=1 MANGOHUD=1 MANGOHUD_CONFIG="alpha=0.01" ENABLE_VKBASALT=1 VK_INSTANCE_LAYERS="one:two" FOO="hello world" gamemoderun %command% --flag "two words"', - ); - assert.deepEqual(parseWorkaroundOptions(options).issues, []); - - assert.equal( - applyWorkaroundChange("FOO=bar --flag", "disableSteamdeckMode", true), - "SteamDeck=0 FOO=bar %command% --flag", - ); - assert.equal( - applyWorkaroundChange("FOO=1 %command% MANGOHUD=1", "disableSteamdeckMode", false), - "FOO=1 %command% MANGOHUD=1", - ); - assert.equal( - applyWorkaroundChange('FOO=bar --literal "%command%"', "disableSteamdeckMode", true), - 'SteamDeck=0 FOO=bar %command% --literal "%command%"', - ); - assert.equal( - applyWorkaroundChange("gamemoderun SteamDeck=1 %command%", "disableSteamdeckMode", true), - "SteamDeck=0 gamemoderun SteamDeck=1 %command%", - ); - assert.equal(parseWorkaroundOptions("gamemoderun SteamDeck=0 %command%").state.disableSteamdeckMode, false); -}); - -test("uses DXVK_CONFIG for the base cap and preserves other DXVK settings", () => { - assert.equal( - applyWorkaroundChange("%command%", "dxvkFrameRate", 60), - 'DXVK_CONFIG="dxvk.maxFrameRate = 60" %command%', - ); - assert.equal(parseWorkaroundOptions('DXVK_CONFIG="dxvk.maxFrameRate = 60" %command%').state.dxvkFrameRate, 60); - assert.equal( - applyWorkaroundChange( - 'DXVK_CONFIG="dxgi.syncInterval = 0; dxvk.maxFrameRate = 30" %command%', - "dxvkFrameRate", - 0, - ), - 'DXVK_CONFIG="dxgi.syncInterval = 0" %command%', - ); - assert.equal( - applyWorkaroundChange("DXVK_FRAME_RATE=30 %command%", "dxvkFrameRate", 45), - 'DXVK_CONFIG="dxvk.maxFrameRate = 45" %command%', - ); - assert.equal( - applyWorkaroundChange("DXVK_FRAME_RATE=30 %command%", "dxvkFrameRate", 0), - "", - ); - - const apiSpecific = parseWorkaroundOptions( - 'DXVK_CONFIG="dxgi.maxFrameRate = 30; d3d9.maxFrameRate = 30" %command%', - ); - assert.equal(apiSpecific.state.dxvkFrameRate, 30); - assert.deepEqual(apiSpecific.issues, []); - const partial = parseWorkaroundOptions('DXVK_CONFIG="dxgi.maxFrameRate = 30" %command%'); - assert.equal(partial.state.dxvkFrameRate, 30); - assert.match(partial.issues.join(" "), /only caps one DirectX API/); - const conflicting = parseWorkaroundOptions( - 'DXVK_CONFIG="dxgi.maxFrameRate = 30; d3d9.maxFrameRate = 60" %command%', - ); - assert.equal(conflicting.state.dxvkFrameRate, 0); - assert.match(conflicting.issues.join(" "), /conflicting/); -}); - -test("reports invalid and malformed FPS values instead of treating them as off", () => { - const invalid = parseWorkaroundOptions('DXVK_CONFIG="dxvk.maxFrameRate = 61" %command%'); - assert.equal(invalid.state.dxvkFrameRate, 0); - assert.match(invalid.issues.join(" "), /outside the supported 0-60 range/); - const malformed = parseWorkaroundOptions('DXVK_CONFIG="dxvk.maxFrameRate" %command%'); - assert.equal(malformed.state.dxvkFrameRate, 0); - assert.match(malformed.issues.join(" "), /malformed/); - const obsolete = parseWorkaroundOptions("DXVK_FRAME_RATE=wat %command%"); - assert.equal(obsolete.state.dxvkFrameRate, 0); - assert.match(obsolete.issues.join(" "), /obsolete/); - assert.throws(() => applyWorkaroundChange("%command%", "dxvkFrameRate", 61), /0 to 60/); - assert.throws(() => applyWorkaroundChange("%command%", "dxvkFrameRate", 1.5), /0 to 60/); -}); - -test("keeps vkBasalt disable mutually exclusive while preserving the dropped enable flag otherwise", () => { - assert.equal( - applyWorkaroundChange("ENABLE_VKBASALT=1 %command%", "disableSteamdeckMode", true), - "SteamDeck=0 ENABLE_VKBASALT=1 %command%", - ); - const disabled = applyWorkaroundChange("ENABLE_VKBASALT=1 %command%", "disableVkbasalt", true); - assert.equal(disabled, "DISABLE_VKBASALT=1 %command%"); - assert.equal( - applyWorkaroundChange(disabled, "disableVkbasalt", false), - "", - ); - const conflict = parseWorkaroundOptions("ENABLE_VKBASALT=1 DISABLE_VKBASALT=1 %command%"); - assert.equal(conflict.state.disableVkbasalt, true); - assert.match(conflict.issues.join(" "), /conflicting/); +test("normalizes blank and argument-only fields while refusing ambiguous launchers", () => { + assert.deepEqual(installWrapperLaunchOption("", wrapper), { + options: `${wrapper} %command%`, + commandTokenAdded: true, + }); + assert.deepEqual(installWrapperLaunchOption("FOO=bar --windowed", wrapper), { + options: `FOO=bar ${wrapper} %command% --windowed`, + commandTokenAdded: true, + }); + assert.throws(() => installWrapperLaunchOption("gamemoderun --windowed", wrapper), /refusing to guess/); + assert.throws(() => installWrapperLaunchOption('"%command%"', wrapper), /refusing to guess/); }); -test("handles current and legacy Zink forms and reports partial state", () => { - const enabled = applyWorkaroundChange("%command%", "enableZink", true); - assert.equal(enabled, "MESA_LOADER_DRIVER_OVERRIDE=zink %command%"); - assert.equal(parseWorkaroundOptions(enabled).state.enableZink, true); - - const legacy = parseWorkaroundOptions( - "__GLX_VENDOR_LIBRARY_NAME=mesa MESA_LOADER_DRIVER_OVERRIDE=zink GALLIUM_DRIVER=zink %command%", - ); - assert.equal(legacy.state.enableZink, true); - assert.deepEqual(legacy.issues, []); - - const partial = parseWorkaroundOptions("__GLX_VENDOR_LIBRARY_NAME=mesa MESA_LOADER_DRIVER_OVERRIDE=zink %command%"); - assert.equal(partial.state.enableZink, true); - assert.match(partial.issues.join(" "), /partially configured/); +test("preserves assignments, quoting, suffixes, and unrelated values", () => { + const options = 'FOO="hello world" VK_INSTANCE_LAYERS="one:two" gamemoderun %command% --flag "two words"'; assert.equal( - applyWorkaroundChange( - "__GLX_VENDOR_LIBRARY_NAME=mesa MESA_LOADER_DRIVER_OVERRIDE=zink GALLIUM_DRIVER=zink %command%", - "enableZink", - false, - ), - "", + installWrapperLaunchOption(options, wrapper).options, + 'FOO="hello world" VK_INSTANCE_LAYERS="one:two" gamemoderun ~/.lsfg %command% --flag "two words"', ); + assert.equal(removeWrapperLaunchOption(`${wrapper} %command% --arg "${wrapper}"`, wrapper, true), `--arg "${wrapper}"`); + assert.equal(normalizeLaunchOptions(" FOO=bar %COMMAND% --flag "), "FOO=bar %COMMAND% --flag"); }); -test("cleans only the known legacy wrapper and preserves launch options", () => { - assert.equal( - cleanupLegacyWrapper('FOO=bar ~/lsfg %command% --arg "~/lsfg"'), - 'FOO=bar %command% --arg "~/lsfg"', - ); - assert.equal(cleanupLegacyWrapper("/home/deck/lsfg %command%"), ""); - assert.equal(cleanupLegacyWrapper("mako-run %command%"), ""); - assert.equal(cleanupLegacyWrapper("mako-launch %command%"), ""); - assert.equal( - cleanupLegacyWrapper("DXVK_FRAME_RATE=30 LSFG_PROCESS=decky-lsfg-vk %command%"), - "DXVK_FRAME_RATE=30 LSFG_PROCESS=decky-lsfg-vk %command%", - ); - assert.equal( - cleanupLegacyWrapper("LSFG_PROCESS=decky-lsfg-vk %command%"), - "LSFG_PROCESS=decky-lsfg-vk %command%", - ); - assert.equal(isLegacyWrapperToken("/home/kurt/lsfg"), false); +test("cleans current, legacy, and bare Mako wrappers without touching suffix arguments", () => { + for (const token of ["~/lsfg", "/home/deck/lsfg", "mako-run", "mako-launch"]) { + assert.equal(cleanupLegacyWrapper(`FOO=bar ${token} %command% --arg "${token}"`), `FOO=bar %command% --arg "${token}"`); + } + assert.equal(cleanupLegacyWrapper(`FOO=bar ${wrapper} %command%`), "FOO=bar %command%"); + assert.equal(isLegacyWrapperToken("/home/kurt/lsfg"), true); + assert.equal(isLegacyWrapperToken("/opt/tools/lsfg"), false); + assert.equal(removeWrapperLaunchOption(`FOO=bar ${wrapper} %command% --arg`, wrapper), "FOO=bar %command% --arg"); }); -test("removes plugin-managed launch options when a profile is removed", () => { +test("removes only old plugin assignments and preserves DXVK settings", () => { assert.equal( - cleanupPluginLaunchOptions( - 'FOO="keep this" ENABLE_GAMESCOPE_WSI=0 DXVK_HDR=0 SteamDeck=0 DISABLE_VKBASALT=1 MESA_LOADER_DRIVER_OVERRIDE=zink DXVK_CONFIG="dxgi.syncInterval = 0; dxvk.maxFrameRate = 30" ~/lsfg %command% --windowed', + cleanupPluginAssignments( + 'FOO="keep this" ENABLE_GAMESCOPE_WSI=0 DXVK_HDR=0 SteamDeck=0 DISABLE_VKBASALT=1 MESA_LOADER_DRIVER_OVERRIDE=zink DXVK_CONFIG="dxgi.syncInterval = 0; dxvk.maxFrameRate = 30" %command%', ), - 'DXVK_CONFIG="dxgi.syncInterval = 0" FOO="keep this" %command% --windowed', + 'FOO="keep this" DXVK_CONFIG="dxgi.syncInterval = 0" %command%', ); assert.equal( - cleanupPluginLaunchOptions( - 'PROTON_USE_WOW64=1 MANGOHUD=1 MANGOHUD_CONFIG="alpha=0.01" LSFG_PROCESS=decky-lsfg-vk %command%', - ), - 'PROTON_USE_WOW64=1 MANGOHUD=1 MANGOHUD_CONFIG="alpha=0.01" LSFG_PROCESS=decky-lsfg-vk %command%', + cleanupPluginLaunchOptions(`DXVK_FRAME_RATE=30 ${wrapper} %command%`, wrapper), + "%command%", ); assert.equal( - cleanupPluginLaunchOptions('DXVK_CONFIG="dxvk.maxFrameRate = 30" %command%'), - "", + cleanupPluginAssignments("PROTON_USE_WOW64=1 MANGOHUD=1 MANGOHUD_CONFIG=alpha %command%"), + "PROTON_USE_WOW64=1 MANGOHUD=1 MANGOHUD_CONFIG=alpha %command%", ); }); -test("canonicalizes a bare command token without removing real arguments", () => { - assert.equal(normalizeLaunchOptions("%command%"), ""); - assert.equal(normalizeLaunchOptions("%COMMAND%"), ""); - assert.equal(normalizeLaunchOptions("FOO=bar %command%"), "FOO=bar %command%"); - assert.equal(normalizeLaunchOptions("%command% --windowed"), "%command% --windowed"); -}); - -test("is idempotent", () => { - const first = applyWorkaroundChange("gamemoderun %command%", "enableZink", true); - assert.equal(applyWorkaroundState(first, parseWorkaroundOptions(first).state), first); - assert.equal(applyWorkaroundChange(first, "enableZink", true), first); - const capped = applyWorkaroundChange(first, "dxvkFrameRate", 30); - assert.equal(applyWorkaroundChange(capped, "dxvkFrameRate", 30), capped); -}); - -test("reads and writes the matching Steam app-details launch-option field", async () => { +test("reads the matching app-details field and installs/removes Steam integration", async () => { const previousWindow = (globalThis as Record).window; const previousSteamClient = (globalThis as Record).SteamClient; - let normalOptions = "FOO=bar %command%"; + let appOptions = "FOO=bar %command%"; let shortcutOptions = "--windowed"; - const normalWrites: string[] = []; + let shortcutTarget = "/usr/bin/example-game"; + const appWrites: string[] = []; const shortcutWrites: string[] = []; + const targetWrites: string[] = []; const unregisters: number[] = []; - - const windowShim = { setTimeout, clearTimeout }; const apps = { RegisterForAppDetails(appId: number, callback: (details: SteamAppDetails) => void) { - if (appId === 42) { - callback({ strLaunchOptions: normalOptions, strShortcutLaunchOptions: "must-not-be-read" }); - } else { - callback({ - strShortcutExe: "/usr/bin/example-game", - strShortcutLaunchOptions: shortcutOptions, - strLaunchOptions: "must-not-be-read", - }); - } + callback(appId === 42 + ? { strLaunchOptions: appOptions, strShortcutLaunchOptions: "wrong-field" } + : { strShortcutExe: shortcutTarget, strShortcutLaunchOptions: shortcutOptions, strLaunchOptions: "wrong-field" }); return { unregister: () => unregisters.push(appId) }; }, SetAppLaunchOptions(appId: number, options: string) { assert.equal(appId, 42); - normalWrites.push(options); - normalOptions = options.replaceAll(" ", " "); + appWrites.push(options); + appOptions = options.replaceAll(" ", " "); }, SetShortcutLaunchOptions(appId: number, options: string) { assert.equal(appId, 43); shortcutWrites.push(options); shortcutOptions = options; }, + SetShortcutExe(appId: number, executable: string) { + assert.equal(appId, 43); + targetWrites.push(executable); + shortcutTarget = executable; + }, }; - - (globalThis as Record).window = windowShim; + (globalThis as Record).window = { setTimeout, clearTimeout }; (globalThis as Record).SteamClient = { Apps: apps }; try { - const normalBefore = await readSteamLaunchOptions(42, false); - assert.equal(normalBefore.options, "FOO=bar %command%"); - const normalAfter = await updateSteamLaunchOptions( - 42, - false, - (options) => applyWorkaroundChange(options, "disableSteamdeckMode", true), - ); - assert.equal(normalWrites.length, 1); + const normal = await readSteamLaunchOptions(42, false); + assert.equal(normal.options, "FOO=bar %command%"); + const installed = await installWrapperIntegration(42, false, wrapper); + assert.equal(installed.snapshot.options, `FOO=bar ${wrapper} %command%`.replaceAll(" ", " ")); + assert.equal(installed.commandTokenAdded, false); + assert.equal(appWrites.length, 1); assert.equal(shortcutWrites.length, 0); - assert.equal(normalAfter.options, "SteamDeck=0 FOO=bar %command%"); - const shortcutAfter = await updateSteamLaunchOptions( - 43, - true, - (options) => applyWorkaroundChange(options, "disableGamescopeWsi", true), - ); - assert.equal(shortcutWrites.length, 1); - assert.equal(shortcutWrites[0], "ENABLE_GAMESCOPE_WSI=0 %command% --windowed"); - assert.equal(shortcutAfter.options, shortcutWrites[0]); + const shortcut = await installWrapperIntegration(43, true, wrapper); + assert.equal(shortcut.originalExecutable, "/usr/bin/example-game"); + assert.equal(shortcut.snapshot.target, wrapper); + assert.deepEqual(targetWrites, [wrapper]); + const restored = await removeWrapperIntegration(43, true, wrapper, shortcut.originalExecutable); + assert.equal(restored.target, "/usr/bin/example-game"); + assert.deepEqual(targetWrites, [wrapper, "/usr/bin/example-game"]); + assert.equal(shortcutWrites.length, 0); + + const cleaned = await removeWrapperIntegration(42, false, wrapper, undefined, installed.commandTokenAdded); + assert.equal(cleaned.options, "FOO=bar %command%".replaceAll(" ", " ")); assert.ok(unregisters.includes(42)); assert.ok(unregisters.includes(43)); } finally { @@ -346,3 +142,69 @@ test("reads and writes the matching Steam app-details launch-option field", asyn else (globalThis as Record).SteamClient = previousSteamClient; } }); + +test("fails closed when shortcut Target ownership or setters are unavailable", async () => { + const previousWindow = (globalThis as Record).window; + const previousSteamClient = (globalThis as Record).SteamClient; + (globalThis as Record).window = { setTimeout, clearTimeout }; + (globalThis as Record).SteamClient = { + Apps: { + RegisterForAppDetails(_appId: number, callback: (details: SteamAppDetails) => void) { + callback({ strShortcutExe: "/usr/bin/other", strShortcutLaunchOptions: "" }); + return { unregister() {} }; + }, + }, + }; + try { + await assert.rejects(installWrapperIntegration(99, true, wrapper), /Target API is unavailable/); + await assert.rejects(removeWrapperIntegration(99, true, wrapper, "/usr/bin/original"), /Target changed externally/); + } finally { + if (previousWindow === undefined) delete (globalThis as Record).window; + else (globalThis as Record).window = previousWindow; + if (previousSteamClient === undefined) delete (globalThis as Record).SteamClient; + else (globalThis as Record).SteamClient = previousSteamClient; + } +}); + +test("restores launch options and shortcut Target when a setter fails after changing them", async () => { + const previousWindow = (globalThis as Record).window; + const previousSteamClient = (globalThis as Record).SteamClient; + let appOptions = "FOO=bar %command%"; + let shortcutTarget = "/usr/bin/original"; + const appWrites: string[] = []; + const targetWrites: string[] = []; + const apps = { + RegisterForAppDetails(appId: number, callback: (details: SteamAppDetails) => void) { + callback(appId === 42 + ? { strLaunchOptions: appOptions } + : { strShortcutExe: shortcutTarget, strShortcutLaunchOptions: "" }); + return { unregister() {} }; + }, + SetAppLaunchOptions(_appId: number, options: string) { + appWrites.push(options); + appOptions = options; + if (options.includes(wrapper)) throw new Error("simulated launch-option write failure"); + }, + SetShortcutExe(_appId: number, executable: string) { + targetWrites.push(executable); + shortcutTarget = executable; + if (executable === wrapper) throw new Error("simulated Target write failure"); + }, + }; + (globalThis as Record).window = { setTimeout, clearTimeout }; + (globalThis as Record).SteamClient = { Apps: apps }; + try { + await assert.rejects(installWrapperIntegration(42, false, wrapper), /simulated launch-option write failure/); + assert.equal(appOptions, "FOO=bar %command%"); + assert.deepEqual(appWrites, [`FOO=bar ${wrapper} %command%`, "FOO=bar %command%"]); + + await assert.rejects(installWrapperIntegration(43, true, wrapper), /simulated Target write failure/); + assert.equal(shortcutTarget, "/usr/bin/original"); + assert.deepEqual(targetWrites, [wrapper, "/usr/bin/original"]); + } finally { + if (previousWindow === undefined) delete (globalThis as Record).window; + else (globalThis as Record).window = previousWindow; + if (previousSteamClient === undefined) delete (globalThis as Record).SteamClient; + else (globalThis as Record).SteamClient = previousSteamClient; + } +}); diff --git a/tests/test_wrapper_service.py b/tests/test_wrapper_service.py new file mode 100644 index 0000000..f17a944 --- /dev/null +++ b/tests/test_wrapper_service.py @@ -0,0 +1,170 @@ +import os +import subprocess +import sys +import tempfile +import types +import unittest +from pathlib import Path +from unittest.mock import Mock + + +sys.modules.setdefault( + "decky", + types.SimpleNamespace(DECKY_USER_HOME="/home/deck", logger=Mock()), +) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "py_modules")) + +from lsfg_vk.wrapper_service import WrapperService + + +class WrapperServiceTests(unittest.TestCase): + def setUp(self): + self.tempdir = tempfile.TemporaryDirectory() + self.home = Path(self.tempdir.name) / "home" / "deck" + self.home.mkdir(parents=True) + self.service = WrapperService() + self.service.user_home = self.home + self.service.local_bin_dir = self.home / ".local/bin" + self.service.config_dir = self.home / ".config/lsfg-vk" + self.service.sidecar_path = self.service.config_dir / "workarounds.json" + self.service.wrapper_path = self.service.local_bin_dir / "lsfg" + + def tearDown(self): + self.tempdir.cleanup() + + def _state(self, **changes): + state = self.service.default_state() + state.update(changes) + return state + + def _run(self, appid, *args, env=None): + process_env = {"PATH": "/usr/bin:/bin", "SteamAppId": str(appid)} + if env: + process_env.update(env) + return subprocess.run( + [str(self.service.wrapper_path), *args], + env=process_env, + capture_output=True, + text=True, + check=True, + ) + + def test_writes_owned_dispatcher_and_validates_shell(self): + response = self.service.set("123", self._state(dxvkFrameRate=60, enableZink=True)) + self.assertTrue(response["success"]) + self.assertEqual(response["wrapper_path"], "~/.lsfg") + self.assertTrue(response["wrapper_owned"]) + self.assertEqual(response["state"]["dxvkFrameRate"], 60) + self.assertEqual(subprocess.run(["/bin/sh", "-n", str(self.service.wrapper_path)]).returncode, 0) + self.assertIn(self.service.MARKER, self.service.wrapper_path.read_text(encoding="utf-8")) + self.assertEqual(self.service.get("123")["state"], self._state(dxvkFrameRate=60, enableZink=True)) + + def test_dispatch_clears_managed_values_preserves_other_environment_and_appends_config(self): + self.service.set( + "123", + self._state(dxvkFrameRate=30, disableSteamdeckMode=True, disableVkbasalt=True, enableZink=True), + ) + result = self._run( + 123, + "/usr/bin/env", + env={ + "DXVK_CONFIG": "dxgi.syncInterval = 0", + "DXVK_FRAME_RATE": "5", + "ENABLE_GAMESCOPE_WSI": "1", + "DISABLE_VKBASALT": "0", + "MESA_LOADER_DRIVER_OVERRIDE": "llvmpipe", + "MANGOHUD": "1", + }, + ) + values = dict(line.split("=", 1) for line in result.stdout.splitlines() if "=" in line) + self.assertEqual(values["ENABLE_GAMESCOPE_WSI"], "0") + self.assertEqual(values["DXVK_HDR"], "0") + self.assertEqual(values["SteamDeck"], "0") + self.assertEqual(values["DISABLE_VKBASALT"], "1") + self.assertEqual(values["__GLX_VENDOR_LIBRARY_NAME"], "mesa") + self.assertEqual(values["MESA_LOADER_DRIVER_OVERRIDE"], "zink") + self.assertEqual(values["GALLIUM_DRIVER"], "zink") + self.assertEqual(values["DXVK_CONFIG"], "dxgi.syncInterval = 0; dxvk.maxFrameRate = 30") + self.assertEqual(values["MANGOHUD"], "1") + self.assertNotIn("DXVK_FRAME_RATE", values) + self.assertNotIn("ENABLE_VKBASALT", values) + + def test_appid_fallback_and_unmatched_passthrough(self): + self.service.set("123", self._state(disableGamescopeWsi=False, disableHdr=False)) + self.service.set("456", self._state(disableSteamdeckMode=True)) + fallback = subprocess.run( + [str(self.service.wrapper_path), "/usr/bin/env"], + env={"PATH": "/usr/bin:/bin", "SteamAppId": "bad", "SteamGameId": "456"}, + capture_output=True, + text=True, + check=True, + ) + fallback_values = dict(line.split("=", 1) for line in fallback.stdout.splitlines() if "=" in line) + self.assertEqual(fallback_values["SteamDeck"], "0") + self.assertEqual(fallback_values["SteamGameId"], "456") + + passthrough = subprocess.run( + [str(self.service.wrapper_path), "/usr/bin/env"], + env={"PATH": "/usr/bin:/bin", "SteamAppId": "999", "KEEP": "yes", "DXVK_HDR": "1"}, + capture_output=True, + text=True, + check=True, + ) + passthrough_values = dict(line.split("=", 1) for line in passthrough.stdout.splitlines() if "=" in line) + self.assertEqual(passthrough_values["KEEP"], "yes") + self.assertEqual(passthrough_values["DXVK_HDR"], "1") + + def test_flatpak_shortcut_receives_env_arguments_and_original_target(self): + fake_flatpak = self.home / ".local/bin/flatpak" + fake_flatpak.parent.mkdir(parents=True, exist_ok=True) + fake_flatpak.write_text( + "#!/bin/sh\n" + "printf 'ARG:%s\\n' \"$@\"\n", + encoding="utf-8", + ) + fake_flatpak.chmod(0o755) + self.service.set("123", self._state(dxvkFrameRate=20, enableZink=True), str(fake_flatpak)) + result = self._run(123, "run", "com.example.Game", "--windowed", env={"DXVK_CONFIG": "foo=1"}) + args = result.stdout.splitlines() + self.assertEqual(args[0], "ARG:run") + self.assertIn("ARG:--env=SteamAppId=123", args) + self.assertIn("ARG:--env=ENABLE_GAMESCOPE_WSI=0", args) + self.assertIn("ARG:--env=DXVK_HDR=0", args) + self.assertIn("ARG:--env=__GLX_VENDOR_LIBRARY_NAME=mesa", args) + self.assertIn("ARG:--env=MESA_LOADER_DRIVER_OVERRIDE=zink", args) + self.assertIn("ARG:--env=GALLIUM_DRIVER=zink", args) + self.assertIn("ARG:--env=DXVK_CONFIG=foo=1; dxvk.maxFrameRate = 20", args) + self.assertIn("ARG:com.example.Game", args) + self.assertIn("ARG:--windowed", args) + + def test_invalid_state_and_foreign_wrapper_fail_closed(self): + invalid = self.service.set("0", self.service.default_state()) + self.assertFalse(invalid["success"]) + invalid = self.service.set("123", {**self.service.default_state(), "dxvkFrameRate": 61}) + self.assertFalse(invalid["success"]) + + self.service.local_bin_dir.mkdir(parents=True, exist_ok=True) + self.service.wrapper_path.write_text("#!/bin/sh\necho foreign\n", encoding="utf-8") + response = self.service.set("123", self.service.default_state()) + self.assertFalse(response["success"]) + self.assertIn("unowned", response["error"]) + self.assertEqual(self.service.wrapper_path.read_text(encoding="utf-8"), "#!/bin/sh\necho foreign\n") + + def test_remove_keeps_a_safe_owned_passthrough_wrapper(self): + self.service.set("123", self.service.default_state()) + response = self.service.remove("123") + self.assertTrue(response["success"]) + self.assertIsNone(self.service.get("123")["state"]) + self.assertTrue(self.service.wrapper_path.exists()) + result = subprocess.run( + [str(self.service.wrapper_path), "/usr/bin/printf", "ok"], + env={"PATH": "/usr/bin:/bin", "SteamAppId": "123"}, + capture_output=True, + text=True, + check=True, + ) + self.assertEqual(result.stdout, "ok") + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From cc1e6f47dd9838b066822162a607d2859c043aff Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Wed, 9 Sep 2026 19:16:30 -0400 Subject: refactor: unify flatpak targets with steam profiles --- tests/test_flatpak_overrides.py | 158 --------------------------- tests/test_flatpak_service.py | 231 ++++++++++++++++++++++++++++++++++++++++ tests/test_steam_service.py | 77 ++++++++++++++ tests/test_wrapper_service.py | 79 +++++++++++++- 4 files changed, 386 insertions(+), 159 deletions(-) delete mode 100644 tests/test_flatpak_overrides.py create mode 100644 tests/test_flatpak_service.py create mode 100644 tests/test_steam_service.py (limited to 'tests') diff --git a/tests/test_flatpak_overrides.py b/tests/test_flatpak_overrides.py deleted file mode 100644 index ed3ef6a..0000000 --- a/tests/test_flatpak_overrides.py +++ /dev/null @@ -1,158 +0,0 @@ -import sys -import tempfile -import types -import unittest -from pathlib import Path -from unittest.mock import Mock - - -sys.modules.setdefault( - "decky", - types.SimpleNamespace(DECKY_USER_HOME="/home/deck", logger=Mock()), -) -sys.modules.setdefault("tomllib", types.SimpleNamespace(loads=Mock())) -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "py_modules")) - -from lsfg_vk.flatpak_service import FlatpakService - - -class FlatpakOverrideTests(unittest.TestCase): - def setUp(self): - self.tempdir = tempfile.TemporaryDirectory() - home = Path(self.tempdir.name) / "home" / "deck" - home.mkdir(parents=True) - self.service = FlatpakService() - self.service.user_home = home - self.service.config_dir = home / ".config/lsfg-vk" - self.service.config_file_path = self.service.config_dir / "conf.toml" - self.service.legacy_script_path = home / "lsfg" - self.service.check_flatpak_available = Mock(return_value=True) - self.service._run_flatpak_command = Mock( - return_value=types.SimpleNamespace(returncode=0, stderr="", stdout="") - ) - self.app_id = "com.example.Game" - self.override_path = self.service._override_file_path(self.app_id) - - def tearDown(self): - self.tempdir.cleanup() - sys.modules.pop("lsfg_vk.plugin", None) - sys.modules.pop("lsfg_vk", None) - - def _paths(self): - return self.service._override_paths() - - def _write_override(self, content): - self.override_path.parent.mkdir(parents=True, exist_ok=True) - self.override_path.write_text(content, encoding="utf-8") - - def _show_response(self, content): - return types.SimpleNamespace(returncode=0, stderr="", stdout=content) - - def test_set_cleans_legacy_entries_and_verifies_readback(self): - paths = self._paths() - self._write_override( - "[Context]\n" - f"filesystems=/home/deck/keep;{paths['config_dir']}:rw;!{paths['legacy_home']};" - f"{paths['legacy_script']};{paths['legacy_dll']}:ro;{paths['dll_dir']}:ro;\n" - "unset-environment=KEEP_UNSET;LSFG_CONFIG;\n\n" - "[Environment]\n" - "KEEP_ENV=1\n" - "LSFG_CONFIG=\n" - "LSFGVK_CONFIG=old\n" - "ENABLE_GAMESCOPE_WSI=1\n" - "DXVK_HDR=1\n" - ) - expected = ( - "[Context]\n" - f"filesystems={paths['config_dir']}:rw;{paths['dll_dir']}:ro\n" - "[Environment]\n" - f"LSFGVK_CONFIG={paths['config_file']}\n" - "ENABLE_GAMESCOPE_WSI=0\n" - "DXVK_HDR=0\n" - ) - self.service._run_flatpak_command.side_effect = [ - self._show_response(""), - self._show_response(expected), - ] - - response = self.service.set_app_override(self.app_id) - command_args = self.service._run_flatpak_command.call_args_list[0].args[0] - cleaned = self.override_path.read_text(encoding="utf-8") - - self.assertTrue(response["success"]) - self.assertIn("--env=ENABLE_GAMESCOPE_WSI=0", command_args) - self.assertIn("--env=DXVK_HDR=0", command_args) - self.assertNotIn("--nofilesystem=/home/deck", command_args) - self.assertNotIn("--unset-env=LSFG_CONFIG", command_args) - self.assertIn("/home/deck/keep", cleaned) - self.assertIn("KEEP_ENV=1", cleaned) - self.assertNotIn("LSFG_CONFIG", cleaned) - self.assertNotIn(paths["legacy_home"], cleaned) - - def test_set_reports_failed_readback(self): - paths = self._paths() - self.service._run_flatpak_command.side_effect = [ - self._show_response(""), - self._show_response( - f"[Context]\nfilesystems={paths['config_dir']};{paths['dll_dir']}\n" - f"[Environment]\nLSFGVK_CONFIG={paths['config_file']}\n" - ), - ] - - response = self.service.set_app_override(self.app_id) - - self.assertFalse(response["success"]) - self.assertIn("verified", response["error"]) - - def test_remove_cleans_known_entries_preserves_unrelated_and_verifies(self): - paths = self._paths() - self._write_override( - "[Context]\n" - f"filesystems=/home/deck/keep;{paths['config_dir']};!{paths['legacy_home']};" - f"{paths['legacy_dll']};{paths['legacy_script']}\n" - "unset-environment=KEEP_UNSET;LSFG_CONFIG;ENABLE_GAMESCOPE_WSI\n\n" - "[Environment]\n" - "KEEP_ENV=1\n" - "LSFGVK_CONFIG=/old/path\n" - "DXVK_HDR=0\n" - ) - self.service._run_flatpak_command.side_effect = [ - self._show_response( - "[Context]\nfilesystems=/home/deck/keep\n" - "[Environment]\nKEEP_ENV=1\n" - ) - ] - - response = self.service.remove_app_override(self.app_id) - cleaned = self.override_path.read_text(encoding="utf-8") - - self.assertTrue(response["success"]) - self.assertEqual(self.service._run_flatpak_command.call_count, 1) - self.assertIn("/home/deck/keep", cleaned) - self.assertIn("KEEP_UNSET", cleaned) - self.assertIn("KEEP_ENV", cleaned) - for name in ("LSFGVK_CONFIG", "LSFG_CONFIG", "ENABLE_GAMESCOPE_WSI", "DXVK_HDR"): - self.assertNotIn(name, cleaned) - for path in paths.values(): - if path != paths["config_file"]: - self.assertNotIn(path, cleaned) - - def test_remove_reports_failed_readback(self): - self._write_override("[Context]\nfilesystems=/home/deck/keep\n") - paths = self._paths() - self.service._run_flatpak_command.side_effect = [ - self._show_response( - f"[Context]\nfilesystems={paths['config_dir']};{paths['dll_dir']}\n" - f"[Environment]\nLSFGVK_CONFIG={paths['config_file']}\n" - "ENABLE_GAMESCOPE_WSI=0\nDXVK_HDR=0\n" - ) - ] - - response = self.service.remove_app_override(self.app_id) - - self.assertFalse(response["success"]) - self.assertIn("verified", response["error"]) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_flatpak_service.py b/tests/test_flatpak_service.py new file mode 100644 index 0000000..38dfcb5 --- /dev/null +++ b/tests/test_flatpak_service.py @@ -0,0 +1,231 @@ +import json +import sys +import tempfile +import types +import unittest +from pathlib import Path +from unittest.mock import Mock + + +sys.modules.setdefault( + "decky", + types.SimpleNamespace(DECKY_USER_HOME="/home/deck", logger=Mock()), +) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "py_modules")) + +from lsfg_vk.flatpak_service import FlatpakService + + +class FlatpakServiceTests(unittest.TestCase): + def setUp(self): + self.tempdir = tempfile.TemporaryDirectory() + self.home = Path(self.tempdir.name) / "home" / "deck" + self.home.mkdir(parents=True) + self.service = FlatpakService() + self.service.user_home = self.home + self.service.config_dir = self.home / ".config/lsfg-vk" + self.service.config_file_path = self.service.config_dir / "conf.toml" + self.service.check_flatpak_available = Mock(return_value=True) + self.service._run_flatpak_command = Mock() + self.bundle = self.home / "lsfg-vk-24.08.flatpak" + self.bundle.write_bytes(b"bundle") + self.service._bundled_extension_path = Mock(return_value=self.bundle) + + def tearDown(self): + self.tempdir.cleanup() + + @staticmethod + def _result(stdout="", returncode=0, stderr=""): + return types.SimpleNamespace(stdout=stdout, returncode=returncode, stderr=stderr) + + @staticmethod + def _extension_line(branch): + return f"org.freedesktop.Platform.VulkanLayer.lsfgvk\tx86_64\t{branch}\n" + + def test_runtime_branch_mapping_is_strict_and_branch_specific(self): + self.assertEqual( + FlatpakService.runtime_branch_from_ref( + "org.freedesktop.Platform/x86_64/24.08" + ), + "24.08", + ) + self.assertEqual( + FlatpakService.runtime_branch_from_ref( + "org.freedesktop.Platform//25.08" + ), + "25.08", + ) + with self.assertRaises(ValueError): + FlatpakService.runtime_branch_from_ref("org.gnome.Sdk/x86_64/46") + with self.assertRaises(ValueError): + FlatpakService.runtime_branch_from_ref( + "org.freedesktop.Platform/x86_64/26.08" + ) + + def test_resolve_reads_required_runtime_instead_of_any_installed_branch(self): + self.service._run_flatpak_command.side_effect = [ + self._result("org.freedesktop.Platform/x86_64/24.08\n"), + self._result(self._extension_line("23.08")), + ] + + response = self.service.resolve_app_support("com.example.Game") + + self.assertTrue(response["success"]) + self.assertEqual(response["runtime_branch"], "24.08") + self.assertEqual(response["support_status"], "needs-runtime") + self.assertFalse(response["extension_installed"]) + self.assertEqual( + self.service._run_flatpak_command.call_args_list[0].args[0], + ["info", "--show-runtime", "com.example.Game"], + ) + self.assertEqual( + self.service._run_flatpak_command.call_args_list[1].args[0], + ["list", "--runtime", "--columns=application,arch,branch"], + ) + + def test_install_records_only_a_new_user_owned_branch(self): + self.service._run_flatpak_command.side_effect = [ + self._result(""), + self._result(""), + self._result(self._extension_line("24.08")), + ] + + response = self.service.install_extension("24.08") + + self.assertTrue(response["success"]) + self.assertTrue(response["owned_by_plugin"]) + install_args = self.service._run_flatpak_command.call_args_list[1].args[0] + self.assertEqual(install_args[:4], ["install", "--user", "--noninteractive", "--or-update"]) + self.assertEqual( + json.loads(self.service.ownership_path.read_text(encoding="utf-8")), + {"version": 1, "plugin_owned_branches": ["24.08"]}, + ) + + def test_preexisting_branch_is_not_claimed_or_removed(self): + self.service._run_flatpak_command.return_value = self._result( + self._extension_line("24.08") + ) + + install_response = self.service.install_extension("24.08") + cleanup_response = self.service.remove_plugin_owned_extensions() + + self.assertTrue(install_response["success"]) + self.assertFalse(install_response["owned_by_plugin"]) + self.assertFalse(self.service.ownership_path.exists()) + self.assertTrue(cleanup_response["success"]) + self.assertEqual(self.service._run_flatpak_command.call_count, 1) + + def test_corrupt_ownership_metadata_fails_closed(self): + self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True) + self.service.ownership_path.write_text("{not-json", encoding="utf-8") + + response = self.service.remove_plugin_owned_extensions() + + self.assertFalse(response["success"]) + self.assertTrue(response["ownership_uncertain"]) + self.assertEqual(self.service._run_flatpak_command.call_count, 0) + + def test_dangling_ownership_symlink_fails_closed(self): + self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True) + self.service.ownership_path.symlink_to(self.home / "missing-metadata") + + response = self.service.remove_plugin_owned_extensions() + + self.assertFalse(response["success"]) + self.assertTrue(response["ownership_uncertain"]) + self.assertEqual(self.service._run_flatpak_command.call_count, 0) + + def test_ensure_app_support_installs_only_the_app_runtime_branch(self): + self.service._run_flatpak_command.side_effect = [ + self._result("org.freedesktop.Platform/x86_64/24.08\n"), + self._result(""), + self._result(""), + self._result(""), + self._result(""), + self._result(self._extension_line("24.08")), + self._result("org.freedesktop.Platform/x86_64/24.08\n"), + self._result(self._extension_line("24.08")), + ] + + response = self.service.ensure_app_support("com.example.Game") + + self.assertTrue(response["success"]) + self.assertEqual(response["support_status"], "ready") + self.assertEqual(response["runtime_branch"], "24.08") + install_args = self.service._run_flatpak_command.call_args_list[4].args[0] + self.assertEqual(install_args[0], "install") + self.assertIn("--user", install_args) + self.assertNotIn("23.08", install_args) + self.assertEqual( + json.loads(self.service.ownership_path.read_text(encoding="utf-8")), + {"version": 1, "plugin_owned_branches": ["24.08"]}, + ) + + def test_two_shortcuts_using_one_flatpak_share_one_extension_branch(self): + self.service._run_flatpak_command.side_effect = [ + self._result("org.freedesktop.Platform/x86_64/24.08\n"), + self._result(""), + self._result(""), + self._result(""), + self._result(""), + self._result(self._extension_line("24.08")), + self._result("org.freedesktop.Platform/x86_64/24.08\n"), + self._result(self._extension_line("24.08")), + self._result("org.freedesktop.Platform/x86_64/24.08\n"), + self._result(self._extension_line("24.08")), + ] + + first = self.service.ensure_app_support("net.pcsx2.PCSX2") + second = self.service.ensure_app_support("net.pcsx2.PCSX2.Dev") + + self.assertEqual(first["support_status"], "ready") + self.assertEqual(second["support_status"], "ready") + install_commands = [ + call.args[0] + for call in self.service._run_flatpak_command.call_args_list + if call.args[0][0] == "install" + ] + self.assertEqual(len(install_commands), 1) + self.assertEqual( + json.loads(self.service.ownership_path.read_text(encoding="utf-8")), + {"version": 1, "plugin_owned_branches": ["24.08"]}, + ) + + def test_cleanup_removes_all_owned_branches_without_reusing_stale_metadata(self): + self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True) + self.service.ownership_path.write_text( + json.dumps({"version": 1, "plugin_owned_branches": ["23.08", "24.08"]}), + encoding="utf-8", + ) + self.service._run_flatpak_command.side_effect = [ + self._result( + "\n".join( + [ + "\t".join([FlatpakService.EXTENSION_ID, "x86_64", "23.08"]), + "\t".join([FlatpakService.EXTENSION_ID, "x86_64", "24.08"]), + ] + ) + + "\n" + ), + self._result(""), + self._result("\t".join([FlatpakService.EXTENSION_ID, "x86_64", "24.08"]) + "\n"), + self._result("\t".join([FlatpakService.EXTENSION_ID, "x86_64", "24.08"]) + "\n"), + self._result(""), + self._result(""), + ] + + response = self.service.remove_plugin_owned_extensions() + + self.assertTrue(response["success"]) + self.assertEqual(response["removed_branches"], ["23.08", "24.08"]) + self.assertFalse(self.service.ownership_path.exists()) + uninstall_commands = [ + call.args[0] + for call in self.service._run_flatpak_command.call_args_list + if call.args[0][0] == "uninstall" + ] + self.assertEqual(len(uninstall_commands), 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_steam_service.py b/tests/test_steam_service.py new file mode 100644 index 0000000..849bb01 --- /dev/null +++ b/tests/test_steam_service.py @@ -0,0 +1,77 @@ +import sys +import types +import unittest +from pathlib import Path +from unittest.mock import Mock + + +sys.modules.setdefault( + "decky", + types.SimpleNamespace(DECKY_USER_HOME="/home/deck", logger=Mock()), +) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "py_modules")) + +from lsfg_vk.steam_service import SteamService, classify_shortcut_transport + + +class SteamTransportTests(unittest.TestCase): + def test_only_direct_canonical_flatpak_forms_are_classified(self): + self.assertEqual( + classify_shortcut_transport( + "/usr/bin/flatpak", + "run com.example.PCSX2 --fullscreen", + ), + {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"}, + ) + self.assertEqual( + classify_shortcut_transport( + "/usr/bin/flatpak run com.example.PCSX2", + "--fullscreen", + ), + {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"}, + ) + self.assertEqual( + classify_shortcut_transport( + "/usr/bin/bash", + "~/launch-game.sh --fullscreen", + ), + {"kind": "host"}, + ) + self.assertEqual( + classify_shortcut_transport( + "/usr/bin/flatpak", + "--user run com.example.PCSX2", + ), + {"kind": "host"}, + ) + self.assertEqual( + classify_shortcut_transport( + "/usr/bin/flatpak", + "run bash ~/launch-game.sh", + ), + {"kind": "host"}, + ) + + def test_shortcut_data_preserves_transport_inputs(self): + game = SteamService._shortcut_game( + { + "appid": 123456, + "AppName": "PCSX2 shortcut", + "Exe": "/usr/bin/flatpak", + "LaunchOptions": "run net.pcsx2.PCSX2 --fullscreen", + "StartDir": "/home/deck/Games", + } + ) + + self.assertEqual(game["appid"], "123456") + self.assertEqual(game["transport"], { + "kind": "flatpak", + "flatpakAppId": "net.pcsx2.PCSX2", + }) + self.assertEqual(game["executable"], "/usr/bin/flatpak") + self.assertEqual(game["arguments"], "run net.pcsx2.PCSX2 --fullscreen") + self.assertEqual(game["startDir"], "/home/deck/Games") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_wrapper_service.py b/tests/test_wrapper_service.py index f17a944..5010d23 100644 --- a/tests/test_wrapper_service.py +++ b/tests/test_wrapper_service.py @@ -123,10 +123,20 @@ class WrapperServiceTests(unittest.TestCase): encoding="utf-8", ) fake_flatpak.chmod(0o755) - self.service.set("123", self._state(dxvkFrameRate=20, enableZink=True), str(fake_flatpak)) + self.service.set( + "123", + self._state(dxvkFrameRate=20, enableZink=True), + str(fake_flatpak), + False, + {"kind": "flatpak", "flatpakAppId": "com.example.Game"}, + ) result = self._run(123, "run", "com.example.Game", "--windowed", env={"DXVK_CONFIG": "foo=1"}) args = result.stdout.splitlines() self.assertEqual(args[0], "ARG:run") + self.assertIn("ARG:--filesystem=" + str(self.service.config_dir) + ":rw", args) + self.assertIn("ARG:--filesystem=" + str(self.home / ".local/share/Steam/steamapps/common/Lossless Scaling") + ":ro", args) + self.assertIn("ARG:--env=LSFGVK_CONFIG=" + str(self.service.config_file_path), args) + self.assertIn("ARG:--env=LSFGVK_FLATPAK=1", args) self.assertIn("ARG:--env=SteamAppId=123", args) self.assertIn("ARG:--env=ENABLE_GAMESCOPE_WSI=0", args) self.assertIn("ARG:--env=DXVK_HDR=0", args) @@ -137,6 +147,73 @@ class WrapperServiceTests(unittest.TestCase): self.assertIn("ARG:com.example.Game", args) self.assertIn("ARG:--windowed", args) + def test_flatpak_full_executable_form_is_preserved(self): + fake_flatpak = self.home / ".local/bin/flatpak" + fake_flatpak.parent.mkdir(parents=True, exist_ok=True) + fake_flatpak.write_text( + "#!/bin/sh\n" + "printf 'ARG:%s\\n' \"$@\"\n", + encoding="utf-8", + ) + fake_flatpak.chmod(0o755) + response = self.service.set( + "123", + self._state(), + f"{fake_flatpak} run com.example.Game", + False, + {"kind": "flatpak", "flatpakAppId": "com.example.Game"}, + ) + self.assertTrue(response["success"]) + result = self._run(123, "--windowed") + args = result.stdout.splitlines() + self.assertEqual(args[0], "ARG:run") + self.assertIn("ARG:com.example.Game", args) + self.assertIn("ARG:--windowed", args) + + def test_flatpak_transport_rejects_non_run_invocation(self): + fake_flatpak = self.home / ".local/bin/flatpak" + fake_flatpak.parent.mkdir(parents=True, exist_ok=True) + fake_flatpak.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + fake_flatpak.chmod(0o755) + response = self.service.set( + "123", + self._state(), + str(fake_flatpak), + False, + {"kind": "flatpak", "flatpakAppId": "com.example.Game"}, + ) + self.assertTrue(response["success"]) + result = subprocess.run( + [str(self.service.wrapper_path), "bash", "launch-game.sh"], + env={"PATH": "/usr/bin:/bin", "SteamAppId": "123"}, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 64) + self.assertIn("direct flatpak run", result.stderr) + + def test_flatpak_transport_rejects_external_app_id_change(self): + fake_flatpak = self.home / ".local/bin/flatpak" + fake_flatpak.parent.mkdir(parents=True, exist_ok=True) + fake_flatpak.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + fake_flatpak.chmod(0o755) + response = self.service.set( + "123", + self._state(), + str(fake_flatpak), + False, + {"kind": "flatpak", "flatpakAppId": "com.example.Game"}, + ) + self.assertTrue(response["success"]) + result = subprocess.run( + [str(self.service.wrapper_path), "run", "com.other.Game"], + env={"PATH": "/usr/bin:/bin", "SteamAppId": "123"}, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 64) + self.assertIn("application ID changed externally", result.stderr) + def test_invalid_state_and_foreign_wrapper_fail_closed(self): invalid = self.service.set("0", self.service.default_state()) self.assertFalse(invalid["success"]) -- cgit v1.2.3 From fb4d053213bdbda271a54b517a11a89c4780f80a Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Wed, 9 Sep 2026 20:45:20 -0400 Subject: fix: restore profile and Flatpak controls --- tests/test_flatpak_service.py | 48 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) (limited to 'tests') diff --git a/tests/test_flatpak_service.py b/tests/test_flatpak_service.py index 38dfcb5..274c0c5 100644 --- a/tests/test_flatpak_service.py +++ b/tests/test_flatpak_service.py @@ -115,6 +115,54 @@ class FlatpakServiceTests(unittest.TestCase): self.assertTrue(cleanup_response["success"]) self.assertEqual(self.service._run_flatpak_command.call_count, 1) + def test_extension_toggle_is_idempotent_and_preserves_preexisting_branch(self): + self.service._run_flatpak_command.return_value = self._result( + self._extension_line("24.08") + ) + + enable_response = self.service.set_extension_enabled("24.08", True) + disable_response = self.service.set_extension_enabled("24.08", False) + + self.assertTrue(enable_response["success"]) + self.assertTrue(enable_response["enabled"]) + self.assertTrue(disable_response["success"]) + self.assertTrue(disable_response["enabled"]) + self.assertTrue(disable_response["preserved"]) + self.assertFalse(disable_response["owned_by_plugin"]) + self.assertEqual( + [call.args[0][0] for call in self.service._run_flatpak_command.call_args_list], + ["list", "list"], + ) + + def test_extension_toggle_removes_owned_branch_and_can_repeat_disable(self): + self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True) + self.service.ownership_path.write_text( + json.dumps({"version": 1, "plugin_owned_branches": ["24.08"]}), + encoding="utf-8", + ) + self.service._run_flatpak_command.side_effect = [ + self._result(self._extension_line("24.08")), + self._result(""), + self._result(""), + self._result(""), + ] + + disable_response = self.service.set_extension_enabled("24.08", False) + repeat_response = self.service.set_extension_enabled("24.08", False) + + self.assertTrue(disable_response["success"]) + self.assertFalse(disable_response["enabled"]) + self.assertTrue(disable_response["removed"]) + self.assertTrue(repeat_response["success"]) + self.assertFalse(repeat_response["enabled"]) + self.assertFalse(repeat_response["installed"]) + uninstall_commands = [ + call.args[0] + for call in self.service._run_flatpak_command.call_args_list + if call.args[0][0] == "uninstall" + ] + self.assertEqual(len(uninstall_commands), 1) + def test_corrupt_ownership_metadata_fails_closed(self): self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True) self.service.ownership_path.write_text("{not-json", encoding="utf-8") -- cgit v1.2.3 From bc75bb93d5aa9aa176262a138f47d3a1d53afbcb Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 10 Sep 2026 00:25:58 -0400 Subject: fix: simplify Flatpak runtime toggles --- tests/test_flatpak_service.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) (limited to 'tests') diff --git a/tests/test_flatpak_service.py b/tests/test_flatpak_service.py index 274c0c5..70ba228 100644 --- a/tests/test_flatpak_service.py +++ b/tests/test_flatpak_service.py @@ -93,7 +93,8 @@ class FlatpakServiceTests(unittest.TestCase): response = self.service.install_extension("24.08") self.assertTrue(response["success"]) - self.assertTrue(response["owned_by_plugin"]) + self.assertTrue(response["enabled"]) + self.assertTrue(response["installed"]) install_args = self.service._run_flatpak_command.call_args_list[1].args[0] self.assertEqual(install_args[:4], ["install", "--user", "--noninteractive", "--or-update"]) self.assertEqual( @@ -110,28 +111,33 @@ class FlatpakServiceTests(unittest.TestCase): cleanup_response = self.service.remove_plugin_owned_extensions() self.assertTrue(install_response["success"]) - self.assertFalse(install_response["owned_by_plugin"]) + self.assertTrue(install_response["enabled"]) + self.assertTrue(install_response["installed"]) self.assertFalse(self.service.ownership_path.exists()) self.assertTrue(cleanup_response["success"]) self.assertEqual(self.service._run_flatpak_command.call_count, 1) - def test_extension_toggle_is_idempotent_and_preserves_preexisting_branch(self): - self.service._run_flatpak_command.return_value = self._result( - self._extension_line("24.08") - ) + def test_extension_toggle_installs_and_uninstalls_preexisting_branch(self): + self.service._run_flatpak_command.side_effect = [ + self._result(self._extension_line("24.08")), + self._result(self._extension_line("24.08")), + self._result(""), + self._result(""), + ] enable_response = self.service.set_extension_enabled("24.08", True) disable_response = self.service.set_extension_enabled("24.08", False) self.assertTrue(enable_response["success"]) self.assertTrue(enable_response["enabled"]) + self.assertTrue(enable_response["installed"]) self.assertTrue(disable_response["success"]) - self.assertTrue(disable_response["enabled"]) - self.assertTrue(disable_response["preserved"]) - self.assertFalse(disable_response["owned_by_plugin"]) + self.assertFalse(disable_response["enabled"]) + self.assertFalse(disable_response["installed"]) + self.assertTrue(disable_response["removed"]) self.assertEqual( [call.args[0][0] for call in self.service._run_flatpak_command.call_args_list], - ["list", "list"], + ["list", "list", "uninstall", "list"], ) def test_extension_toggle_removes_owned_branch_and_can_repeat_disable(self): -- cgit v1.2.3 From 28ebc17785a8cca52f289b74261d1f310fd35904 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 10 Sep 2026 07:08:19 -0400 Subject: fix: detect wrapped Flatpak shortcuts --- tests/test_steam_service.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) (limited to 'tests') diff --git a/tests/test_steam_service.py b/tests/test_steam_service.py index 849bb01..9924186 100644 --- a/tests/test_steam_service.py +++ b/tests/test_steam_service.py @@ -51,6 +51,24 @@ class SteamTransportTests(unittest.TestCase): ), {"kind": "host"}, ) + self.assertEqual( + classify_shortcut_transport( + "~/.lsfg", + "run --branch=stable --arch=x86_64 com.example.PCSX2", + ), + {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"}, + ) + self.assertEqual( + classify_shortcut_transport( + "/home/deck/.lsfg", + "run com.example.PCSX2", + ), + {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"}, + ) + self.assertEqual( + classify_shortcut_transport("~/.lsfg", "--profile high"), + {"kind": "host"}, + ) def test_shortcut_data_preserves_transport_inputs(self): game = SteamService._shortcut_game( @@ -72,6 +90,21 @@ class SteamTransportTests(unittest.TestCase): self.assertEqual(game["arguments"], "run net.pcsx2.PCSX2 --fullscreen") self.assertEqual(game["startDir"], "/home/deck/Games") + def test_wrapped_flatpak_shortcut_remains_a_flatpak_target(self): + game = SteamService._shortcut_game( + { + "appid": 987654, + "AppName": "Wrapped Flatpak", + "Exe": "~/.lsfg", + "LaunchOptions": "run --branch=stable --arch=x86_64 com.example.Game", + } + ) + + self.assertEqual(game["transport"], { + "kind": "flatpak", + "flatpakAppId": "com.example.Game", + }) + if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From b197e25b45d53c6c7175a45dd0b14642f2aab198 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 10 Sep 2026 11:51:22 -0400 Subject: fixes for appimage and flatpak --- tests/steamLaunchOptions.test.ts | 62 ++++++++++++-- tests/test_flatpak_service.py | 174 +++++++++++++++++++++------------------ 2 files changed, 151 insertions(+), 85 deletions(-) (limited to 'tests') diff --git a/tests/steamLaunchOptions.test.ts b/tests/steamLaunchOptions.test.ts index 3170fe8..215e3f7 100644 --- a/tests/steamLaunchOptions.test.ts +++ b/tests/steamLaunchOptions.test.ts @@ -37,6 +37,10 @@ test("normalizes blank and argument-only fields while refusing ambiguous launche options: `FOO=bar ${wrapper} %command% --windowed`, commandTokenAdded: true, }); + assert.deepEqual(installWrapperLaunchOption('FOO=bar "/home/deck/game.AppImage"', wrapper, true), { + options: 'FOO=bar ~/.lsfg %command% "/home/deck/game.AppImage"', + commandTokenAdded: true, + }); assert.throws(() => installWrapperLaunchOption("gamemoderun --windowed", wrapper), /refusing to guess/); assert.throws(() => installWrapperLaunchOption('"%command%"', wrapper), /refusing to guess/); }); @@ -122,11 +126,11 @@ test("reads the matching app-details field and installs/removes Steam integratio assert.equal(appWrites.length, 1); assert.equal(shortcutWrites.length, 0); - const shortcut = await installWrapperIntegration(43, true, wrapper); + const shortcut = await installWrapperIntegration(43, true, wrapper, false, "flatpak"); assert.equal(shortcut.originalExecutable, "/usr/bin/example-game"); assert.equal(shortcut.snapshot.target, wrapper); assert.deepEqual(targetWrites, [wrapper]); - const restored = await removeWrapperIntegration(43, true, wrapper, shortcut.originalExecutable); + const restored = await removeWrapperIntegration(43, true, wrapper, shortcut.originalExecutable, false, "flatpak"); assert.equal(restored.target, "/usr/bin/example-game"); assert.deepEqual(targetWrites, [wrapper, "/usr/bin/example-game"]); assert.equal(shortcutWrites.length, 0); @@ -143,6 +147,54 @@ test("reads the matching app-details field and installs/removes Steam integratio } }); +test("uses shortcut launch options for a host shortcut without changing its Target", async () => { + const previousWindow = (globalThis as Record).window; + const previousSteamClient = (globalThis as Record).SteamClient; + const originalOptions = 'DESKTOPINTEGRATION=1 "/home/deck/AppImages/dusk.appimage"'; + let shortcutOptions = originalOptions; + let shortcutTarget = "env"; + const shortcutWrites: string[] = []; + const targetWrites: string[] = []; + const apps = { + RegisterForAppDetails(_appId: number, callback: (details: SteamAppDetails) => void) { + callback({ strShortcutExe: shortcutTarget, strShortcutLaunchOptions: shortcutOptions }); + return { unregister() {} }; + }, + SetShortcutLaunchOptions(_appId: number, options: string) { + shortcutWrites.push(options); + shortcutOptions = options; + }, + SetShortcutExe(_appId: number, executable: string) { + targetWrites.push(executable); + shortcutTarget = executable; + }, + }; + (globalThis as Record).window = { setTimeout, clearTimeout }; + (globalThis as Record).SteamClient = { Apps: apps }; + try { + const installed = await installWrapperIntegration(44, true, wrapper, false, "host"); + assert.equal(installed.originalExecutable, undefined); + assert.equal(installed.snapshot.target, "env"); + assert.equal(installed.snapshot.options, 'DESKTOPINTEGRATION=1 ~/.lsfg %command% "/home/deck/AppImages/dusk.appimage"'); + assert.deepEqual(targetWrites, []); + assert.deepEqual(shortcutWrites, [installed.snapshot.options]); + + const secondInstall = await installWrapperIntegration(44, true, wrapper, false, "host"); + assert.equal(secondInstall.snapshot.options, installed.snapshot.options); + assert.deepEqual(shortcutWrites, [installed.snapshot.options]); + + const restored = await removeWrapperIntegration(44, true, wrapper, undefined, installed.commandTokenAdded, "host"); + assert.equal(restored.target, "env"); + assert.equal(restored.options, originalOptions); + assert.deepEqual(targetWrites, []); + } finally { + if (previousWindow === undefined) delete (globalThis as Record).window; + else (globalThis as Record).window = previousWindow; + if (previousSteamClient === undefined) delete (globalThis as Record).SteamClient; + else (globalThis as Record).SteamClient = previousSteamClient; + } +}); + test("fails closed when shortcut Target ownership or setters are unavailable", async () => { const previousWindow = (globalThis as Record).window; const previousSteamClient = (globalThis as Record).SteamClient; @@ -156,8 +208,8 @@ test("fails closed when shortcut Target ownership or setters are unavailable", a }, }; try { - await assert.rejects(installWrapperIntegration(99, true, wrapper), /Target API is unavailable/); - await assert.rejects(removeWrapperIntegration(99, true, wrapper, "/usr/bin/original"), /Target changed externally/); + await assert.rejects(installWrapperIntegration(99, true, wrapper, false, "flatpak"), /Target API is unavailable/); + await assert.rejects(removeWrapperIntegration(99, true, wrapper, "/usr/bin/original", false, "flatpak"), /Target changed externally/); } finally { if (previousWindow === undefined) delete (globalThis as Record).window; else (globalThis as Record).window = previousWindow; @@ -198,7 +250,7 @@ test("restores launch options and shortcut Target when a setter fails after chan assert.equal(appOptions, "FOO=bar %command%"); assert.deepEqual(appWrites, [`FOO=bar ${wrapper} %command%`, "FOO=bar %command%"]); - await assert.rejects(installWrapperIntegration(43, true, wrapper), /simulated Target write failure/); + await assert.rejects(installWrapperIntegration(43, true, wrapper, false, "flatpak"), /simulated Target write failure/); assert.equal(shortcutTarget, "/usr/bin/original"); assert.deepEqual(targetWrites, [wrapper, "/usr/bin/original"]); } finally { diff --git a/tests/test_flatpak_service.py b/tests/test_flatpak_service.py index 70ba228..d5baf61 100644 --- a/tests/test_flatpak_service.py +++ b/tests/test_flatpak_service.py @@ -26,7 +26,12 @@ class FlatpakServiceTests(unittest.TestCase): self.service.config_dir = self.home / ".config/lsfg-vk" self.service.config_file_path = self.service.config_dir / "conf.toml" self.service.check_flatpak_available = Mock(return_value=True) - self.service._run_flatpak_command = Mock() + self.service._run_flatpak_command = Mock(side_effect=self._run_flatpak_command) + self.runtime_ref = "org.freedesktop.Platform/x86_64/24.08" + self.runtime_metadata = "" + self.user_branches = set() + self.system_branches = set() + self.install_branch = "24.08" self.bundle = self.home / "lsfg-vk-24.08.flatpak" self.bundle.write_bytes(b"bundle") self.service._bundled_extension_path = Mock(return_value=self.bundle) @@ -42,6 +47,22 @@ class FlatpakServiceTests(unittest.TestCase): def _extension_line(branch): return f"org.freedesktop.Platform.VulkanLayer.lsfgvk\tx86_64\t{branch}\n" + def _run_flatpak_command(self, args, **_kwargs): + if args[0] == "info" and args[1] == "--show-runtime": + return self._result(self.runtime_ref + "\n") + if args[0] == "info" and args[1] == "--show-metadata": + return self._result(self.runtime_metadata) + if args[0] == "list": + branches = self.user_branches if "--user" in args else self.system_branches + return self._result("".join(self._extension_line(branch) for branch in sorted(branches))) + if args[0] == "install": + self.user_branches.add(self.install_branch) + return self._result() + if args[0] == "uninstall": + self.user_branches.discard(args[-1].rsplit("/", 1)[-1]) + return self._result() + raise AssertionError(f"Unexpected Flatpak command: {args}") + def test_runtime_branch_mapping_is_strict_and_branch_specific(self): self.assertEqual( FlatpakService.runtime_branch_from_ref( @@ -62,11 +83,20 @@ class FlatpakServiceTests(unittest.TestCase): "org.freedesktop.Platform/x86_64/26.08" ) + def test_runtime_branch_mapping_reads_documented_gl_metadata(self): + metadata = """ +[Extension org.freedesktop.Platform.GL] +versions=25.08;25.08-extra;1.4 +version=1.4 +""" + self.assertEqual(FlatpakService.runtime_branch_from_metadata(metadata), "25.08") + with self.assertRaises(ValueError): + FlatpakService.runtime_branch_from_metadata( + "[Extension org.freedesktop.Platform.GL]\nversions=26.08;26.08-extra;1.4\n" + ) + def test_resolve_reads_required_runtime_instead_of_any_installed_branch(self): - self.service._run_flatpak_command.side_effect = [ - self._result("org.freedesktop.Platform/x86_64/24.08\n"), - self._result(self._extension_line("23.08")), - ] + self.user_branches = {"23.08"} response = self.service.resolve_app_support("com.example.Game") @@ -80,22 +110,48 @@ class FlatpakServiceTests(unittest.TestCase): ) self.assertEqual( self.service._run_flatpak_command.call_args_list[1].args[0], - ["list", "--runtime", "--columns=application,arch,branch"], + ["list", "--user", "--runtime", "--columns=application,arch,branch"], + ) + self.assertEqual( + self.service._run_flatpak_command.call_args_list[2].args[0], + ["list", "--system", "--runtime", "--columns=application,arch,branch"], ) - def test_install_records_only_a_new_user_owned_branch(self): - self.service._run_flatpak_command.side_effect = [ - self._result(""), - self._result(""), - self._result(self._extension_line("24.08")), - ] + def test_resolve_maps_kde_and_gnome_runtimes_from_gl_metadata(self): + metadata = "[Extension org.freedesktop.Platform.GL]\nversions=25.08;25.08-extra;1.4\n" + for runtime in ("org.kde.Platform/x86_64/6.10", "org.gnome.Platform/x86_64/49"): + with self.subTest(runtime=runtime): + self.service._run_flatpak_command.reset_mock() + self.runtime_ref = runtime + self.runtime_metadata = metadata + response = self.service.resolve_app_support("com.example.Game") + self.assertEqual(response["runtime_branch"], "25.08") + self.assertEqual(response["support_status"], "needs-runtime") + self.assertEqual( + self.service._run_flatpak_command.call_args_list[1].args[0], + ["info", "--show-metadata", runtime], + ) + + def test_system_extension_is_ready_without_installing_a_user_copy(self): + self.system_branches = {"24.08"} + + response = self.service.ensure_app_support("com.example.Game") + + self.assertTrue(response["success"]) + self.assertEqual(response["support_status"], "ready") + self.assertEqual( + [call.args[0][0] for call in self.service._run_flatpak_command.call_args_list], + ["info", "list", "list"], + ) + self.assertFalse(any(call.args[0][0] == "install" for call in self.service._run_flatpak_command.call_args_list)) + def test_install_records_only_a_new_user_owned_branch(self): response = self.service.install_extension("24.08") self.assertTrue(response["success"]) self.assertTrue(response["enabled"]) self.assertTrue(response["installed"]) - install_args = self.service._run_flatpak_command.call_args_list[1].args[0] + install_args = self.service._run_flatpak_command.call_args_list[2].args[0] self.assertEqual(install_args[:4], ["install", "--user", "--noninteractive", "--or-update"]) self.assertEqual( json.loads(self.service.ownership_path.read_text(encoding="utf-8")), @@ -103,9 +159,7 @@ class FlatpakServiceTests(unittest.TestCase): ) def test_preexisting_branch_is_not_claimed_or_removed(self): - self.service._run_flatpak_command.return_value = self._result( - self._extension_line("24.08") - ) + self.user_branches = {"24.08"} install_response = self.service.install_extension("24.08") cleanup_response = self.service.remove_plugin_owned_extensions() @@ -115,15 +169,10 @@ class FlatpakServiceTests(unittest.TestCase): self.assertTrue(install_response["installed"]) self.assertFalse(self.service.ownership_path.exists()) self.assertTrue(cleanup_response["success"]) - self.assertEqual(self.service._run_flatpak_command.call_count, 1) - - def test_extension_toggle_installs_and_uninstalls_preexisting_branch(self): - self.service._run_flatpak_command.side_effect = [ - self._result(self._extension_line("24.08")), - self._result(self._extension_line("24.08")), - self._result(""), - self._result(""), - ] + self.assertEqual(self.service._run_flatpak_command.call_count, 2) + + def test_extension_toggle_preserves_preexisting_branch(self): + self.user_branches = {"24.08"} enable_response = self.service.set_extension_enabled("24.08", True) disable_response = self.service.set_extension_enabled("24.08", False) @@ -132,36 +181,36 @@ class FlatpakServiceTests(unittest.TestCase): self.assertTrue(enable_response["enabled"]) self.assertTrue(enable_response["installed"]) self.assertTrue(disable_response["success"]) - self.assertFalse(disable_response["enabled"]) - self.assertFalse(disable_response["installed"]) - self.assertTrue(disable_response["removed"]) + self.assertTrue(disable_response["enabled"]) + self.assertTrue(disable_response["installed"]) + self.assertFalse(disable_response["removed"]) + self.assertEqual(self.user_branches, {"24.08"}) self.assertEqual( [call.args[0][0] for call in self.service._run_flatpak_command.call_args_list], - ["list", "list", "uninstall", "list"], + ["list", "list", "list", "list"], ) - def test_extension_toggle_removes_owned_branch_and_can_repeat_disable(self): + def test_extension_toggle_removes_owned_user_branch_but_preserves_system_branch(self): self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True) self.service.ownership_path.write_text( json.dumps({"version": 1, "plugin_owned_branches": ["24.08"]}), encoding="utf-8", ) - self.service._run_flatpak_command.side_effect = [ - self._result(self._extension_line("24.08")), - self._result(""), - self._result(""), - self._result(""), - ] + self.user_branches = {"24.08"} + self.system_branches = {"24.08"} disable_response = self.service.set_extension_enabled("24.08", False) repeat_response = self.service.set_extension_enabled("24.08", False) self.assertTrue(disable_response["success"]) - self.assertFalse(disable_response["enabled"]) + self.assertTrue(disable_response["enabled"]) self.assertTrue(disable_response["removed"]) self.assertTrue(repeat_response["success"]) - self.assertFalse(repeat_response["enabled"]) - self.assertFalse(repeat_response["installed"]) + self.assertTrue(repeat_response["enabled"]) + self.assertTrue(repeat_response["installed"]) + self.assertEqual(self.user_branches, set()) + self.assertEqual(self.system_branches, {"24.08"}) + self.assertFalse(self.service.ownership_path.exists()) uninstall_commands = [ call.args[0] for call in self.service._run_flatpak_command.call_args_list @@ -190,23 +239,16 @@ class FlatpakServiceTests(unittest.TestCase): self.assertEqual(self.service._run_flatpak_command.call_count, 0) def test_ensure_app_support_installs_only_the_app_runtime_branch(self): - self.service._run_flatpak_command.side_effect = [ - self._result("org.freedesktop.Platform/x86_64/24.08\n"), - self._result(""), - self._result(""), - self._result(""), - self._result(""), - self._result(self._extension_line("24.08")), - self._result("org.freedesktop.Platform/x86_64/24.08\n"), - self._result(self._extension_line("24.08")), - ] - response = self.service.ensure_app_support("com.example.Game") self.assertTrue(response["success"]) self.assertEqual(response["support_status"], "ready") self.assertEqual(response["runtime_branch"], "24.08") - install_args = self.service._run_flatpak_command.call_args_list[4].args[0] + install_args = next( + call.args[0] + for call in self.service._run_flatpak_command.call_args_list + if call.args[0][0] == "install" + ) self.assertEqual(install_args[0], "install") self.assertIn("--user", install_args) self.assertNotIn("23.08", install_args) @@ -216,19 +258,6 @@ class FlatpakServiceTests(unittest.TestCase): ) def test_two_shortcuts_using_one_flatpak_share_one_extension_branch(self): - self.service._run_flatpak_command.side_effect = [ - self._result("org.freedesktop.Platform/x86_64/24.08\n"), - self._result(""), - self._result(""), - self._result(""), - self._result(""), - self._result(self._extension_line("24.08")), - self._result("org.freedesktop.Platform/x86_64/24.08\n"), - self._result(self._extension_line("24.08")), - self._result("org.freedesktop.Platform/x86_64/24.08\n"), - self._result(self._extension_line("24.08")), - ] - first = self.service.ensure_app_support("net.pcsx2.PCSX2") second = self.service.ensure_app_support("net.pcsx2.PCSX2.Dev") @@ -251,22 +280,7 @@ class FlatpakServiceTests(unittest.TestCase): json.dumps({"version": 1, "plugin_owned_branches": ["23.08", "24.08"]}), encoding="utf-8", ) - self.service._run_flatpak_command.side_effect = [ - self._result( - "\n".join( - [ - "\t".join([FlatpakService.EXTENSION_ID, "x86_64", "23.08"]), - "\t".join([FlatpakService.EXTENSION_ID, "x86_64", "24.08"]), - ] - ) - + "\n" - ), - self._result(""), - self._result("\t".join([FlatpakService.EXTENSION_ID, "x86_64", "24.08"]) + "\n"), - self._result("\t".join([FlatpakService.EXTENSION_ID, "x86_64", "24.08"]) + "\n"), - self._result(""), - self._result(""), - ] + self.user_branches = {"23.08", "24.08"} response = self.service.remove_plugin_owned_extensions() -- cgit v1.2.3 From 670f36e8cc75da9c8b1b174c24e722657bbf2a56 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 10 Sep 2026 12:15:05 -0400 Subject: cleanup flatpak handles --- tests/steamLaunchOptions.test.ts | 20 ++++++++------------ tests/test_wrapper_service.py | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 12 deletions(-) (limited to 'tests') diff --git a/tests/steamLaunchOptions.test.ts b/tests/steamLaunchOptions.test.ts index 215e3f7..456d9d0 100644 --- a/tests/steamLaunchOptions.test.ts +++ b/tests/steamLaunchOptions.test.ts @@ -37,10 +37,6 @@ test("normalizes blank and argument-only fields while refusing ambiguous launche options: `FOO=bar ${wrapper} %command% --windowed`, commandTokenAdded: true, }); - assert.deepEqual(installWrapperLaunchOption('FOO=bar "/home/deck/game.AppImage"', wrapper, true), { - options: 'FOO=bar ~/.lsfg %command% "/home/deck/game.AppImage"', - commandTokenAdded: true, - }); assert.throws(() => installWrapperLaunchOption("gamemoderun --windowed", wrapper), /refusing to guess/); assert.throws(() => installWrapperLaunchOption('"%command%"', wrapper), /refusing to guess/); }); @@ -126,11 +122,11 @@ test("reads the matching app-details field and installs/removes Steam integratio assert.equal(appWrites.length, 1); assert.equal(shortcutWrites.length, 0); - const shortcut = await installWrapperIntegration(43, true, wrapper, false, "flatpak"); + const shortcut = await installWrapperIntegration(43, true, wrapper, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }); assert.equal(shortcut.originalExecutable, "/usr/bin/example-game"); assert.equal(shortcut.snapshot.target, wrapper); assert.deepEqual(targetWrites, [wrapper]); - const restored = await removeWrapperIntegration(43, true, wrapper, shortcut.originalExecutable, false, "flatpak"); + const restored = await removeWrapperIntegration(43, true, wrapper, shortcut.originalExecutable, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }); assert.equal(restored.target, "/usr/bin/example-game"); assert.deepEqual(targetWrites, [wrapper, "/usr/bin/example-game"]); assert.equal(shortcutWrites.length, 0); @@ -172,18 +168,18 @@ test("uses shortcut launch options for a host shortcut without changing its Targ (globalThis as Record).window = { setTimeout, clearTimeout }; (globalThis as Record).SteamClient = { Apps: apps }; try { - const installed = await installWrapperIntegration(44, true, wrapper, false, "host"); + const installed = await installWrapperIntegration(44, true, wrapper, false, { kind: "host" }); assert.equal(installed.originalExecutable, undefined); assert.equal(installed.snapshot.target, "env"); assert.equal(installed.snapshot.options, 'DESKTOPINTEGRATION=1 ~/.lsfg %command% "/home/deck/AppImages/dusk.appimage"'); assert.deepEqual(targetWrites, []); assert.deepEqual(shortcutWrites, [installed.snapshot.options]); - const secondInstall = await installWrapperIntegration(44, true, wrapper, false, "host"); + const secondInstall = await installWrapperIntegration(44, true, wrapper, false, { kind: "host" }); assert.equal(secondInstall.snapshot.options, installed.snapshot.options); assert.deepEqual(shortcutWrites, [installed.snapshot.options]); - const restored = await removeWrapperIntegration(44, true, wrapper, undefined, installed.commandTokenAdded, "host"); + const restored = await removeWrapperIntegration(44, true, wrapper, undefined, installed.commandTokenAdded, { kind: "host" }); assert.equal(restored.target, "env"); assert.equal(restored.options, originalOptions); assert.deepEqual(targetWrites, []); @@ -208,8 +204,8 @@ test("fails closed when shortcut Target ownership or setters are unavailable", a }, }; try { - await assert.rejects(installWrapperIntegration(99, true, wrapper, false, "flatpak"), /Target API is unavailable/); - await assert.rejects(removeWrapperIntegration(99, true, wrapper, "/usr/bin/original", false, "flatpak"), /Target changed externally/); + await assert.rejects(installWrapperIntegration(99, true, wrapper, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }), /Target API is unavailable/); + await assert.rejects(removeWrapperIntegration(99, true, wrapper, "/usr/bin/original", false, { kind: "flatpak", flatpakAppId: "com.example.Game" }), /Target changed externally/); } finally { if (previousWindow === undefined) delete (globalThis as Record).window; else (globalThis as Record).window = previousWindow; @@ -250,7 +246,7 @@ test("restores launch options and shortcut Target when a setter fails after chan assert.equal(appOptions, "FOO=bar %command%"); assert.deepEqual(appWrites, [`FOO=bar ${wrapper} %command%`, "FOO=bar %command%"]); - await assert.rejects(installWrapperIntegration(43, true, wrapper, false, "flatpak"), /simulated Target write failure/); + await assert.rejects(installWrapperIntegration(43, true, wrapper, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }), /simulated Target write failure/); assert.equal(shortcutTarget, "/usr/bin/original"); assert.deepEqual(targetWrites, [wrapper, "/usr/bin/original"]); } finally { diff --git a/tests/test_wrapper_service.py b/tests/test_wrapper_service.py index 5010d23..0632d93 100644 --- a/tests/test_wrapper_service.py +++ b/tests/test_wrapper_service.py @@ -170,6 +170,25 @@ class WrapperServiceTests(unittest.TestCase): self.assertIn("ARG:com.example.Game", args) self.assertIn("ARG:--windowed", args) + def test_host_transport_does_not_store_shortcut_target(self): + self.service.set( + "123", + self._state(), + "/usr/bin/flatpak", + False, + {"kind": "flatpak", "flatpakAppId": "com.example.Game"}, + ) + response = self.service.set( + "123", + self._state(), + "/usr/bin/ignored", + False, + {"kind": "host"}, + ) + self.assertTrue(response["success"]) + self.assertIsNone(response["shortcut_exe"]) + self.assertIsNone(self.service.get("123")["shortcut_exe"]) + def test_flatpak_transport_rejects_non_run_invocation(self): fake_flatpak = self.home / ".local/bin/flatpak" fake_flatpak.parent.mkdir(parents=True, exist_ok=True) -- cgit v1.2.3 From 9902135e53be129bd6096d51d5e510ab298c1ae2 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 10 Sep 2026 12:19:40 -0400 Subject: fix: handle bare Flatpak shortcut targets --- tests/steamLaunchOptions.test.ts | 17 +++++++++-------- tests/test_steam_service.py | 7 +++++++ 2 files changed, 16 insertions(+), 8 deletions(-) (limited to 'tests') diff --git a/tests/steamLaunchOptions.test.ts b/tests/steamLaunchOptions.test.ts index 456d9d0..c799907 100644 --- a/tests/steamLaunchOptions.test.ts +++ b/tests/steamLaunchOptions.test.ts @@ -83,7 +83,7 @@ test("reads the matching app-details field and installs/removes Steam integratio const previousSteamClient = (globalThis as Record).SteamClient; let appOptions = "FOO=bar %command%"; let shortcutOptions = "--windowed"; - let shortcutTarget = "/usr/bin/example-game"; + let shortcutTarget = '"flatpak"'; const appWrites: string[] = []; const shortcutWrites: string[] = []; const targetWrites: string[] = []; @@ -123,12 +123,12 @@ test("reads the matching app-details field and installs/removes Steam integratio assert.equal(shortcutWrites.length, 0); const shortcut = await installWrapperIntegration(43, true, wrapper, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }); - assert.equal(shortcut.originalExecutable, "/usr/bin/example-game"); + assert.equal(shortcut.originalExecutable, "/usr/bin/flatpak"); assert.equal(shortcut.snapshot.target, wrapper); assert.deepEqual(targetWrites, [wrapper]); const restored = await removeWrapperIntegration(43, true, wrapper, shortcut.originalExecutable, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }); - assert.equal(restored.target, "/usr/bin/example-game"); - assert.deepEqual(targetWrites, [wrapper, "/usr/bin/example-game"]); + assert.equal(restored.target, "/usr/bin/flatpak"); + assert.deepEqual(targetWrites, [wrapper, "/usr/bin/flatpak"]); assert.equal(shortcutWrites.length, 0); const cleaned = await removeWrapperIntegration(42, false, wrapper, undefined, installed.commandTokenAdded); @@ -198,12 +198,13 @@ test("fails closed when shortcut Target ownership or setters are unavailable", a (globalThis as Record).SteamClient = { Apps: { RegisterForAppDetails(_appId: number, callback: (details: SteamAppDetails) => void) { - callback({ strShortcutExe: "/usr/bin/other", strShortcutLaunchOptions: "" }); + callback({ strShortcutExe: _appId === 99 ? "/usr/bin/flatpak" : "garbage", strShortcutLaunchOptions: "" }); return { unregister() {} }; }, }, }; try { + await assert.rejects(installWrapperIntegration(98, true, wrapper, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }), /supported executable/); await assert.rejects(installWrapperIntegration(99, true, wrapper, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }), /Target API is unavailable/); await assert.rejects(removeWrapperIntegration(99, true, wrapper, "/usr/bin/original", false, { kind: "flatpak", flatpakAppId: "com.example.Game" }), /Target changed externally/); } finally { @@ -218,7 +219,7 @@ test("restores launch options and shortcut Target when a setter fails after chan const previousWindow = (globalThis as Record).window; const previousSteamClient = (globalThis as Record).SteamClient; let appOptions = "FOO=bar %command%"; - let shortcutTarget = "/usr/bin/original"; + let shortcutTarget = "/usr/bin/flatpak"; const appWrites: string[] = []; const targetWrites: string[] = []; const apps = { @@ -247,8 +248,8 @@ test("restores launch options and shortcut Target when a setter fails after chan assert.deepEqual(appWrites, [`FOO=bar ${wrapper} %command%`, "FOO=bar %command%"]); await assert.rejects(installWrapperIntegration(43, true, wrapper, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }), /simulated Target write failure/); - assert.equal(shortcutTarget, "/usr/bin/original"); - assert.deepEqual(targetWrites, [wrapper, "/usr/bin/original"]); + assert.equal(shortcutTarget, "/usr/bin/flatpak"); + assert.deepEqual(targetWrites, [wrapper, "/usr/bin/flatpak"]); } finally { if (previousWindow === undefined) delete (globalThis as Record).window; else (globalThis as Record).window = previousWindow; diff --git a/tests/test_steam_service.py b/tests/test_steam_service.py index 9924186..636c87f 100644 --- a/tests/test_steam_service.py +++ b/tests/test_steam_service.py @@ -23,6 +23,13 @@ class SteamTransportTests(unittest.TestCase): ), {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"}, ) + self.assertEqual( + classify_shortcut_transport( + "flatpak", + "run com.example.PCSX2 --fullscreen", + ), + {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"}, + ) self.assertEqual( classify_shortcut_transport( "/usr/bin/flatpak run com.example.PCSX2", -- cgit v1.2.3 From eb14f3b6fdb135e1c9fd608244aa67fc3eb6b662 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:47:27 -0400 Subject: test: cover explicit direct flatpak detection --- tests/test_steam_service.py | 99 +++++++++++---------------------------------- 1 file changed, 23 insertions(+), 76 deletions(-) (limited to 'tests') diff --git a/tests/test_steam_service.py b/tests/test_steam_service.py index 636c87f..719249d 100644 --- a/tests/test_steam_service.py +++ b/tests/test_steam_service.py @@ -11,73 +11,21 @@ sys.modules.setdefault( ) sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "py_modules")) -from lsfg_vk.steam_service import SteamService, classify_shortcut_transport +from lsfg_vk.steam_service import SteamService, is_direct_flatpak_shortcut -class SteamTransportTests(unittest.TestCase): - def test_only_direct_canonical_flatpak_forms_are_classified(self): - self.assertEqual( - classify_shortcut_transport( - "/usr/bin/flatpak", - "run com.example.PCSX2 --fullscreen", - ), - {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"}, - ) - self.assertEqual( - classify_shortcut_transport( - "flatpak", - "run com.example.PCSX2 --fullscreen", - ), - {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"}, - ) - self.assertEqual( - classify_shortcut_transport( - "/usr/bin/flatpak run com.example.PCSX2", - "--fullscreen", - ), - {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"}, - ) - self.assertEqual( - classify_shortcut_transport( - "/usr/bin/bash", - "~/launch-game.sh --fullscreen", - ), - {"kind": "host"}, - ) - self.assertEqual( - classify_shortcut_transport( - "/usr/bin/flatpak", - "--user run com.example.PCSX2", - ), - {"kind": "host"}, - ) - self.assertEqual( - classify_shortcut_transport( - "/usr/bin/flatpak", - "run bash ~/launch-game.sh", - ), - {"kind": "host"}, - ) - self.assertEqual( - classify_shortcut_transport( - "~/.lsfg", - "run --branch=stable --arch=x86_64 com.example.PCSX2", - ), - {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"}, - ) - self.assertEqual( - classify_shortcut_transport( - "/home/deck/.lsfg", - "run com.example.PCSX2", - ), - {"kind": "flatpak", "flatpakAppId": "com.example.PCSX2"}, - ) - self.assertEqual( - classify_shortcut_transport("~/.lsfg", "--profile high"), - {"kind": "host"}, - ) +class SteamShortcutTests(unittest.TestCase): + def test_only_direct_flatpak_targets_are_special(self): + self.assertTrue(is_direct_flatpak_shortcut("/usr/bin/flatpak")) + self.assertTrue(is_direct_flatpak_shortcut("flatpak")) + self.assertTrue(is_direct_flatpak_shortcut("/usr/bin/flatpak run com.example.Game")) + self.assertTrue(is_direct_flatpak_shortcut('~/.lsfg "/usr/bin/flatpak"')) + self.assertFalse(is_direct_flatpak_shortcut("/usr/bin/bash")) + self.assertFalse(is_direct_flatpak_shortcut("/home/deck/Emulation/tools/launchers/retroarch.sh")) + self.assertFalse(is_direct_flatpak_shortcut("/home/deck/Emulation/tools/launchers/ppsspp.sh")) + self.assertFalse(is_direct_flatpak_shortcut("/home/deck/AppImages/dusk.appimage")) - def test_shortcut_data_preserves_transport_inputs(self): + def test_shortcut_data_preserves_launch_shape_without_flatpak_identity(self): game = SteamService._shortcut_game( { "appid": 123456, @@ -89,28 +37,27 @@ class SteamTransportTests(unittest.TestCase): ) self.assertEqual(game["appid"], "123456") - self.assertEqual(game["transport"], { - "kind": "flatpak", - "flatpakAppId": "net.pcsx2.PCSX2", - }) + self.assertTrue(game["directFlatpak"]) + self.assertNotIn("transport", game) self.assertEqual(game["executable"], "/usr/bin/flatpak") self.assertEqual(game["arguments"], "run net.pcsx2.PCSX2 --fullscreen") self.assertEqual(game["startDir"], "/home/deck/Games") - def test_wrapped_flatpak_shortcut_remains_a_flatpak_target(self): + def test_emudeck_launcher_is_ordinary_non_steam(self): game = SteamService._shortcut_game( { "appid": 987654, - "AppName": "Wrapped Flatpak", - "Exe": "~/.lsfg", - "LaunchOptions": "run --branch=stable --arch=x86_64 com.example.Game", + "AppName": "1080 Snowboarding", + "Exe": '"/home/deck/Emulation/tools/launchers/retroarch.sh" -L core rom.z64', + "LaunchOptions": "", } ) - self.assertEqual(game["transport"], { - "kind": "flatpak", - "flatpakAppId": "com.example.Game", - }) + self.assertFalse(game["directFlatpak"]) + self.assertEqual( + game["executable"], + '"/home/deck/Emulation/tools/launchers/retroarch.sh" -L core rom.z64', + ) if __name__ == "__main__": -- cgit v1.2.3 From c6a2abf2f0cab99a8a41709a4bbf6e391a112b4f Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:48:00 -0400 Subject: test: simplify workaround wrapper coverage --- tests/test_wrapper_service.py | 149 +++++++----------------------------------- 1 file changed, 23 insertions(+), 126 deletions(-) (limited to 'tests') diff --git a/tests/test_wrapper_service.py b/tests/test_wrapper_service.py index 0632d93..5c291c7 100644 --- a/tests/test_wrapper_service.py +++ b/tests/test_wrapper_service.py @@ -1,4 +1,3 @@ -import os import subprocess import sys import tempfile @@ -24,10 +23,10 @@ class WrapperServiceTests(unittest.TestCase): self.home.mkdir(parents=True) self.service = WrapperService() self.service.user_home = self.home - self.service.local_bin_dir = self.home / ".local/bin" self.service.config_dir = self.home / ".config/lsfg-vk" + self.service.config_file_path = self.service.config_dir / "conf.toml" self.service.sidecar_path = self.service.config_dir / "workarounds.json" - self.service.wrapper_path = self.service.local_bin_dir / "lsfg" + self.service.wrapper_path = self.home / ".lsfg" def tearDown(self): self.tempdir.cleanup() @@ -59,7 +58,7 @@ class WrapperServiceTests(unittest.TestCase): self.assertIn(self.service.MARKER, self.service.wrapper_path.read_text(encoding="utf-8")) self.assertEqual(self.service.get("123")["state"], self._state(dxvkFrameRate=60, enableZink=True)) - def test_dispatch_clears_managed_values_preserves_other_environment_and_appends_config(self): + def test_dispatch_exports_appid_config_and_workarounds(self): self.service.set( "123", self._state(dxvkFrameRate=30, disableSteamdeckMode=True, disableVkbasalt=True, enableZink=True), @@ -71,12 +70,16 @@ class WrapperServiceTests(unittest.TestCase): "DXVK_CONFIG": "dxgi.syncInterval = 0", "DXVK_FRAME_RATE": "5", "ENABLE_GAMESCOPE_WSI": "1", + "DISABLE_LSFGVK": "1", + "DISABLE_LSFG": "1", "DISABLE_VKBASALT": "0", "MESA_LOADER_DRIVER_OVERRIDE": "llvmpipe", "MANGOHUD": "1", }, ) values = dict(line.split("=", 1) for line in result.stdout.splitlines() if "=" in line) + self.assertEqual(values["SteamAppId"], "123") + self.assertEqual(values["LSFGVK_CONFIG"], str(self.service.config_file_path)) self.assertEqual(values["ENABLE_GAMESCOPE_WSI"], "0") self.assertEqual(values["DXVK_HDR"], "0") self.assertEqual(values["SteamDeck"], "0") @@ -88,6 +91,20 @@ class WrapperServiceTests(unittest.TestCase): self.assertEqual(values["MANGOHUD"], "1") self.assertNotIn("DXVK_FRAME_RATE", values) self.assertNotIn("ENABLE_VKBASALT", values) + self.assertNotIn("DISABLE_LSFGVK", values) + self.assertNotIn("DISABLE_LSFG", values) + + def test_wrapper_is_transport_agnostic(self): + self.service.set("123", self._state()) + fake = self.home / "target" + fake.write_text("#!/bin/sh\nprintf '%s\\n' \"$@\"\n", encoding="utf-8") + fake.chmod(0o755) + result = self._run(123, str(fake), "run", "org.example.Game") + self.assertEqual(result.stdout.splitlines(), ["run", "org.example.Game"]) + content = self.service.wrapper_path.read_text(encoding="utf-8") + self.assertNotIn("flatpakAppId", content) + self.assertNotIn("shortcut_exe", content) + self.assertNotIn("--filesystem", content) def test_appid_fallback_and_unmatched_passthrough(self): self.service.set("123", self._state(disableGamescopeWsi=False, disableHdr=False)) @@ -101,7 +118,7 @@ class WrapperServiceTests(unittest.TestCase): ) fallback_values = dict(line.split("=", 1) for line in fallback.stdout.splitlines() if "=" in line) self.assertEqual(fallback_values["SteamDeck"], "0") - self.assertEqual(fallback_values["SteamGameId"], "456") + self.assertEqual(fallback_values["SteamAppId"], "456") passthrough = subprocess.run( [str(self.service.wrapper_path), "/usr/bin/env"], @@ -114,139 +131,19 @@ class WrapperServiceTests(unittest.TestCase): self.assertEqual(passthrough_values["KEEP"], "yes") self.assertEqual(passthrough_values["DXVK_HDR"], "1") - def test_flatpak_shortcut_receives_env_arguments_and_original_target(self): - fake_flatpak = self.home / ".local/bin/flatpak" - fake_flatpak.parent.mkdir(parents=True, exist_ok=True) - fake_flatpak.write_text( - "#!/bin/sh\n" - "printf 'ARG:%s\\n' \"$@\"\n", - encoding="utf-8", - ) - fake_flatpak.chmod(0o755) - self.service.set( - "123", - self._state(dxvkFrameRate=20, enableZink=True), - str(fake_flatpak), - False, - {"kind": "flatpak", "flatpakAppId": "com.example.Game"}, - ) - result = self._run(123, "run", "com.example.Game", "--windowed", env={"DXVK_CONFIG": "foo=1"}) - args = result.stdout.splitlines() - self.assertEqual(args[0], "ARG:run") - self.assertIn("ARG:--filesystem=" + str(self.service.config_dir) + ":rw", args) - self.assertIn("ARG:--filesystem=" + str(self.home / ".local/share/Steam/steamapps/common/Lossless Scaling") + ":ro", args) - self.assertIn("ARG:--env=LSFGVK_CONFIG=" + str(self.service.config_file_path), args) - self.assertIn("ARG:--env=LSFGVK_FLATPAK=1", args) - self.assertIn("ARG:--env=SteamAppId=123", args) - self.assertIn("ARG:--env=ENABLE_GAMESCOPE_WSI=0", args) - self.assertIn("ARG:--env=DXVK_HDR=0", args) - self.assertIn("ARG:--env=__GLX_VENDOR_LIBRARY_NAME=mesa", args) - self.assertIn("ARG:--env=MESA_LOADER_DRIVER_OVERRIDE=zink", args) - self.assertIn("ARG:--env=GALLIUM_DRIVER=zink", args) - self.assertIn("ARG:--env=DXVK_CONFIG=foo=1; dxvk.maxFrameRate = 20", args) - self.assertIn("ARG:com.example.Game", args) - self.assertIn("ARG:--windowed", args) - - def test_flatpak_full_executable_form_is_preserved(self): - fake_flatpak = self.home / ".local/bin/flatpak" - fake_flatpak.parent.mkdir(parents=True, exist_ok=True) - fake_flatpak.write_text( - "#!/bin/sh\n" - "printf 'ARG:%s\\n' \"$@\"\n", - encoding="utf-8", - ) - fake_flatpak.chmod(0o755) - response = self.service.set( - "123", - self._state(), - f"{fake_flatpak} run com.example.Game", - False, - {"kind": "flatpak", "flatpakAppId": "com.example.Game"}, - ) - self.assertTrue(response["success"]) - result = self._run(123, "--windowed") - args = result.stdout.splitlines() - self.assertEqual(args[0], "ARG:run") - self.assertIn("ARG:com.example.Game", args) - self.assertIn("ARG:--windowed", args) - - def test_host_transport_does_not_store_shortcut_target(self): - self.service.set( - "123", - self._state(), - "/usr/bin/flatpak", - False, - {"kind": "flatpak", "flatpakAppId": "com.example.Game"}, - ) - response = self.service.set( - "123", - self._state(), - "/usr/bin/ignored", - False, - {"kind": "host"}, - ) - self.assertTrue(response["success"]) - self.assertIsNone(response["shortcut_exe"]) - self.assertIsNone(self.service.get("123")["shortcut_exe"]) - - def test_flatpak_transport_rejects_non_run_invocation(self): - fake_flatpak = self.home / ".local/bin/flatpak" - fake_flatpak.parent.mkdir(parents=True, exist_ok=True) - fake_flatpak.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") - fake_flatpak.chmod(0o755) - response = self.service.set( - "123", - self._state(), - str(fake_flatpak), - False, - {"kind": "flatpak", "flatpakAppId": "com.example.Game"}, - ) - self.assertTrue(response["success"]) - result = subprocess.run( - [str(self.service.wrapper_path), "bash", "launch-game.sh"], - env={"PATH": "/usr/bin:/bin", "SteamAppId": "123"}, - capture_output=True, - text=True, - ) - self.assertEqual(result.returncode, 64) - self.assertIn("direct flatpak run", result.stderr) - - def test_flatpak_transport_rejects_external_app_id_change(self): - fake_flatpak = self.home / ".local/bin/flatpak" - fake_flatpak.parent.mkdir(parents=True, exist_ok=True) - fake_flatpak.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") - fake_flatpak.chmod(0o755) - response = self.service.set( - "123", - self._state(), - str(fake_flatpak), - False, - {"kind": "flatpak", "flatpakAppId": "com.example.Game"}, - ) - self.assertTrue(response["success"]) - result = subprocess.run( - [str(self.service.wrapper_path), "run", "com.other.Game"], - env={"PATH": "/usr/bin:/bin", "SteamAppId": "123"}, - capture_output=True, - text=True, - ) - self.assertEqual(result.returncode, 64) - self.assertIn("application ID changed externally", result.stderr) - def test_invalid_state_and_foreign_wrapper_fail_closed(self): invalid = self.service.set("0", self.service.default_state()) self.assertFalse(invalid["success"]) invalid = self.service.set("123", {**self.service.default_state(), "dxvkFrameRate": 61}) self.assertFalse(invalid["success"]) - self.service.local_bin_dir.mkdir(parents=True, exist_ok=True) self.service.wrapper_path.write_text("#!/bin/sh\necho foreign\n", encoding="utf-8") response = self.service.set("123", self.service.default_state()) self.assertFalse(response["success"]) self.assertIn("unowned", response["error"]) self.assertEqual(self.service.wrapper_path.read_text(encoding="utf-8"), "#!/bin/sh\necho foreign\n") - def test_remove_keeps_a_safe_owned_passthrough_wrapper(self): + def test_remove_keeps_safe_passthrough_wrapper(self): self.service.set("123", self.service.default_state()) response = self.service.remove("123") self.assertTrue(response["success"]) -- cgit v1.2.3 From 8b856c63bf626d0d86445a4715dce963aa1d5f16 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:48:47 -0400 Subject: test: cover explicit flatpak app preparation --- tests/test_flatpak_service.py | 390 ++++++++++++++++++++---------------------- 1 file changed, 183 insertions(+), 207 deletions(-) (limited to 'tests') diff --git a/tests/test_flatpak_service.py b/tests/test_flatpak_service.py index d5baf61..208e05b 100644 --- a/tests/test_flatpak_service.py +++ b/tests/test_flatpak_service.py @@ -25,16 +25,19 @@ class FlatpakServiceTests(unittest.TestCase): self.service.user_home = self.home self.service.config_dir = self.home / ".config/lsfg-vk" self.service.config_file_path = self.service.config_dir / "conf.toml" + self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True) self.service.check_flatpak_available = Mock(return_value=True) self.service._run_flatpak_command = Mock(side_effect=self._run_flatpak_command) self.runtime_ref = "org.freedesktop.Platform/x86_64/24.08" self.runtime_metadata = "" self.user_branches = set() self.system_branches = set() - self.install_branch = "24.08" - self.bundle = self.home / "lsfg-vk-24.08.flatpak" + self.apps = {"com.example.Game": "Example Game"} + self.bundle = self.home / "lsfg-vk.flatpak" self.bundle.write_bytes(b"bundle") self.service._bundled_extension_path = Mock(return_value=self.bundle) + self.dll_dir = self.home / ".local/share/Steam/steamapps/common/Lossless Scaling" + self.service._dll_directory = Mock(return_value=self.dll_dir) def tearDown(self): self.tempdir.cleanup() @@ -47,252 +50,225 @@ class FlatpakServiceTests(unittest.TestCase): def _extension_line(branch): return f"org.freedesktop.Platform.VulkanLayer.lsfgvk\tx86_64\t{branch}\n" + @staticmethod + def _parse_override(content): + section = None + filesystems = [] + unset_environment = [] + environment = {} + other = [] + for raw in content.splitlines(): + line = raw.strip() + if line.startswith("[") and line.endswith("]"): + section = line[1:-1] + continue + key, separator, value = line.partition("=") + if not separator: + continue + if section == "Context" and key == "filesystems": + filesystems.extend(item for item in value.split(";") if item) + elif section == "Context" and key == "unset-environment": + unset_environment.extend(item for item in value.split(";") if item) + elif section == "Environment": + environment[key] = value + else: + other.append((section, key, value)) + return filesystems, unset_environment, environment, other + + @staticmethod + def _serialize_override(filesystems, unset_environment, environment): + lines = ["[Context]"] + if filesystems: + lines.append("filesystems=" + ";".join(filesystems) + ";") + if unset_environment: + lines.append("unset-environment=" + ";".join(unset_environment) + ";") + if environment: + lines.append("") + lines.append("[Environment]") + lines.extend(f"{key}={value}" for key, value in environment.items()) + return "\n".join(lines) + "\n" + + def _apply_override(self, args): + app_id = args[-1] + path = self.service._override_path(app_id) + content = path.read_text(encoding="utf-8") if path.exists() else "" + filesystems, unset_environment, environment, _ = self._parse_override(content) + for arg in args[2:-1]: + if arg.startswith("--filesystem="): + value = arg.split("=", 1)[1] + if value not in filesystems: + filesystems.append(value) + elif arg.startswith("--env="): + key, value = arg.split("=", 1)[1].split("=", 1) + environment[key] = value + if key in unset_environment: + unset_environment.remove(key) + elif arg.startswith("--unset-env="): + key = arg.split("=", 1)[1] + environment.pop(key, None) + if key not in unset_environment: + unset_environment.append(key) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + self._serialize_override(filesystems, unset_environment, environment), + encoding="utf-8", + ) + return self._result() + def _run_flatpak_command(self, args, **_kwargs): - if args[0] == "info" and args[1] == "--show-runtime": + if args[:2] == ["info", "--show-runtime"]: return self._result(self.runtime_ref + "\n") - if args[0] == "info" and args[1] == "--show-metadata": + if args[:2] == ["info", "--show-metadata"]: return self._result(self.runtime_metadata) + if args[:2] == ["list", "--app"]: + return self._result("".join(f"{name}\t{app_id}\n" for app_id, name in self.apps.items())) if args[0] == "list": branches = self.user_branches if "--user" in args else self.system_branches return self._result("".join(self._extension_line(branch) for branch in sorted(branches))) if args[0] == "install": - self.user_branches.add(self.install_branch) + self.user_branches.add(self.runtime_ref.rsplit("/", 1)[-1]) return self._result() if args[0] == "uninstall": self.user_branches.discard(args[-1].rsplit("/", 1)[-1]) return self._result() + if args[:3] == ["override", "--user", "--show"]: + path = self.service._override_path(args[-1]) + return self._result(path.read_text(encoding="utf-8") if path.exists() else "") + if args[:2] == ["override", "--user"]: + return self._apply_override(args) raise AssertionError(f"Unexpected Flatpak command: {args}") - def test_runtime_branch_mapping_is_strict_and_branch_specific(self): - self.assertEqual( - FlatpakService.runtime_branch_from_ref( - "org.freedesktop.Platform/x86_64/24.08" - ), - "24.08", - ) - self.assertEqual( - FlatpakService.runtime_branch_from_ref( - "org.freedesktop.Platform//25.08" - ), - "25.08", - ) - with self.assertRaises(ValueError): - FlatpakService.runtime_branch_from_ref("org.gnome.Sdk/x86_64/46") - with self.assertRaises(ValueError): - FlatpakService.runtime_branch_from_ref( - "org.freedesktop.Platform/x86_64/26.08" - ) - - def test_runtime_branch_mapping_reads_documented_gl_metadata(self): - metadata = """ -[Extension org.freedesktop.Platform.GL] -versions=25.08;25.08-extra;1.4 -version=1.4 -""" - self.assertEqual(FlatpakService.runtime_branch_from_metadata(metadata), "25.08") - with self.assertRaises(ValueError): - FlatpakService.runtime_branch_from_metadata( - "[Extension org.freedesktop.Platform.GL]\nversions=26.08;26.08-extra;1.4\n" - ) - - def test_resolve_reads_required_runtime_instead_of_any_installed_branch(self): - self.user_branches = {"23.08"} - - response = self.service.resolve_app_support("com.example.Game") + def test_resolves_freedesktop_and_derived_runtimes(self): + runtime, branch = self.service._resolve_runtime("com.example.Game") + self.assertEqual(runtime, self.runtime_ref) + self.assertEqual(branch, "24.08") + + self.runtime_ref = "org.kde.Platform/x86_64/6.10" + self.runtime_metadata = "[Extension org.freedesktop.Platform.GL]\nversions=25.08;25.08-extra;1.4\n" + runtime, branch = self.service._resolve_runtime("com.example.Game") + self.assertEqual(runtime, self.runtime_ref) + self.assertEqual(branch, "25.08") + + def test_prepare_app_installs_runtime_and_persists_narrow_override(self): + response = self.service.prepare_app("com.example.Game") self.assertTrue(response["success"]) + self.assertTrue(response["prepared"]) + self.assertTrue(response["owned"]) self.assertEqual(response["runtime_branch"], "24.08") - self.assertEqual(response["support_status"], "needs-runtime") - self.assertFalse(response["extension_installed"]) - self.assertEqual( - self.service._run_flatpak_command.call_args_list[0].args[0], - ["info", "--show-runtime", "com.example.Game"], - ) - self.assertEqual( - self.service._run_flatpak_command.call_args_list[1].args[0], - ["list", "--user", "--runtime", "--columns=application,arch,branch"], - ) - self.assertEqual( - self.service._run_flatpak_command.call_args_list[2].args[0], - ["list", "--system", "--runtime", "--columns=application,arch,branch"], - ) - - def test_resolve_maps_kde_and_gnome_runtimes_from_gl_metadata(self): - metadata = "[Extension org.freedesktop.Platform.GL]\nversions=25.08;25.08-extra;1.4\n" - for runtime in ("org.kde.Platform/x86_64/6.10", "org.gnome.Platform/x86_64/49"): - with self.subTest(runtime=runtime): - self.service._run_flatpak_command.reset_mock() - self.runtime_ref = runtime - self.runtime_metadata = metadata - response = self.service.resolve_app_support("com.example.Game") - self.assertEqual(response["runtime_branch"], "25.08") - self.assertEqual(response["support_status"], "needs-runtime") - self.assertEqual( - self.service._run_flatpak_command.call_args_list[1].args[0], - ["info", "--show-metadata", runtime], - ) - - def test_system_extension_is_ready_without_installing_a_user_copy(self): + self.assertEqual(self.user_branches, {"24.08"}) + status = self.service._app_override_status("com.example.Game") + self.assertTrue(status["prepared"]) + content = self.service._override_path("com.example.Game").read_text(encoding="utf-8") + self.assertIn(str(self.service.config_dir) + ":ro", content) + self.assertIn(str(self.dll_dir) + ":ro", content) + self.assertIn("LSFGVK_CONFIG=" + str(self.service.config_file_path), content) + self.assertIn("LSFGVK_FLATPAK=1", content) + self.assertNotIn("ENABLE_GAMESCOPE_WSI", content) + state = json.loads(self.service.ownership_path.read_text(encoding="utf-8")) + self.assertEqual(state["plugin_owned_branches"], ["24.08"]) + self.assertIn("com.example.Game", state["prepared_apps"]) + + def test_prepare_is_idempotent(self): + first = self.service.prepare_app("com.example.Game") + first_content = self.service._override_path("com.example.Game").read_bytes() + second = self.service.prepare_app("com.example.Game") + + self.assertTrue(first["success"]) + self.assertTrue(second["success"]) + self.assertEqual(first_content, self.service._override_path("com.example.Game").read_bytes()) + install_calls = [call for call in self.service._run_flatpak_command.call_args_list if call.args[0][0] == "install"] + self.assertEqual(len(install_calls), 1) + + def test_preinstalled_runtime_is_not_owned(self): self.system_branches = {"24.08"} - - response = self.service.ensure_app_support("com.example.Game") + response = self.service.prepare_app("com.example.Game") self.assertTrue(response["success"]) - self.assertEqual(response["support_status"], "ready") - self.assertEqual( - [call.args[0][0] for call in self.service._run_flatpak_command.call_args_list], - ["info", "list", "list"], + state = json.loads(self.service.ownership_path.read_text(encoding="utf-8")) + self.assertEqual(state["plugin_owned_branches"], []) + self.assertIn("com.example.Game", state["prepared_apps"]) + + def test_external_preparation_is_preserved(self): + path = self.service._override_path("com.example.Game") + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + self._serialize_override( + [str(self.service.config_dir) + ":ro", str(self.dll_dir) + ":ro"], + ["DISABLE_LSFGVK", "DISABLE_LSFG"], + { + "LSFGVK_CONFIG": str(self.service.config_file_path), + "LSFGVK_FLATPAK": "1", + }, + ), + encoding="utf-8", ) - self.assertFalse(any(call.args[0][0] == "install" for call in self.service._run_flatpak_command.call_args_list)) + self.system_branches = {"24.08"} - def test_install_records_only_a_new_user_owned_branch(self): - response = self.service.install_extension("24.08") + response = self.service.prepare_app("com.example.Game") self.assertTrue(response["success"]) - self.assertTrue(response["enabled"]) - self.assertTrue(response["installed"]) - install_args = self.service._run_flatpak_command.call_args_list[2].args[0] - self.assertEqual(install_args[:4], ["install", "--user", "--noninteractive", "--or-update"]) - self.assertEqual( - json.loads(self.service.ownership_path.read_text(encoding="utf-8")), - {"version": 1, "plugin_owned_branches": ["24.08"]}, - ) - - def test_preexisting_branch_is_not_claimed_or_removed(self): - self.user_branches = {"24.08"} - - install_response = self.service.install_extension("24.08") - cleanup_response = self.service.remove_plugin_owned_extensions() - - self.assertTrue(install_response["success"]) - self.assertTrue(install_response["enabled"]) - self.assertTrue(install_response["installed"]) + self.assertTrue(response["prepared"]) + self.assertFalse(response["owned"]) self.assertFalse(self.service.ownership_path.exists()) - self.assertTrue(cleanup_response["success"]) - self.assertEqual(self.service._run_flatpak_command.call_count, 2) - - def test_extension_toggle_preserves_preexisting_branch(self): - self.user_branches = {"24.08"} - - enable_response = self.service.set_extension_enabled("24.08", True) - disable_response = self.service.set_extension_enabled("24.08", False) - - self.assertTrue(enable_response["success"]) - self.assertTrue(enable_response["enabled"]) - self.assertTrue(enable_response["installed"]) - self.assertTrue(disable_response["success"]) - self.assertTrue(disable_response["enabled"]) - self.assertTrue(disable_response["installed"]) - self.assertFalse(disable_response["removed"]) - self.assertEqual(self.user_branches, {"24.08"}) - self.assertEqual( - [call.args[0][0] for call in self.service._run_flatpak_command.call_args_list], - ["list", "list", "list", "list"], - ) - def test_extension_toggle_removes_owned_user_branch_but_preserves_system_branch(self): - self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True) - self.service.ownership_path.write_text( - json.dumps({"version": 1, "plugin_owned_branches": ["24.08"]}), - encoding="utf-8", - ) - self.user_branches = {"24.08"} - self.system_branches = {"24.08"} + def test_remove_restores_exact_previous_override(self): + original = "[Context]\nfilesystems=~/Documents;\n\n[Environment]\nFOO=bar\n" + path = self.service._override_path("com.example.Game") + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(original, encoding="utf-8") + self.assertTrue(self.service.prepare_app("com.example.Game")["success"]) - disable_response = self.service.set_extension_enabled("24.08", False) - repeat_response = self.service.set_extension_enabled("24.08", False) + response = self.service.remove_app_override("com.example.Game") - self.assertTrue(disable_response["success"]) - self.assertTrue(disable_response["enabled"]) - self.assertTrue(disable_response["removed"]) - self.assertTrue(repeat_response["success"]) - self.assertTrue(repeat_response["enabled"]) - self.assertTrue(repeat_response["installed"]) - self.assertEqual(self.user_branches, set()) - self.assertEqual(self.system_branches, {"24.08"}) + self.assertTrue(response["success"]) + self.assertEqual(path.read_text(encoding="utf-8"), original) self.assertFalse(self.service.ownership_path.exists()) - uninstall_commands = [ - call.args[0] - for call in self.service._run_flatpak_command.call_args_list - if call.args[0][0] == "uninstall" - ] - self.assertEqual(len(uninstall_commands), 1) - def test_corrupt_ownership_metadata_fails_closed(self): - self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True) - self.service.ownership_path.write_text("{not-json", encoding="utf-8") + def test_remove_deletes_override_created_by_plugin(self): + self.assertTrue(self.service.prepare_app("com.example.Game")["success"]) + path = self.service._override_path("com.example.Game") + self.assertTrue(path.exists()) - response = self.service.remove_plugin_owned_extensions() + response = self.service.remove_app_override("com.example.Game") - self.assertFalse(response["success"]) - self.assertTrue(response["ownership_uncertain"]) - self.assertEqual(self.service._run_flatpak_command.call_count, 0) + self.assertTrue(response["success"]) + self.assertFalse(path.exists()) - def test_dangling_ownership_symlink_fails_closed(self): - self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True) - self.service.ownership_path.symlink_to(self.home / "missing-metadata") + def test_remove_fails_closed_after_external_change(self): + self.assertTrue(self.service.prepare_app("com.example.Game")["success"]) + path = self.service._override_path("com.example.Game") + with path.open("a", encoding="utf-8") as handle: + handle.write("EXTERNAL=1\n") - response = self.service.remove_plugin_owned_extensions() + response = self.service.remove_app_override("com.example.Game") self.assertFalse(response["success"]) - self.assertTrue(response["ownership_uncertain"]) - self.assertEqual(self.service._run_flatpak_command.call_count, 0) + self.assertIn("changed after preparation", response["error"]) + self.assertTrue(path.exists()) + self.assertTrue(self.service.ownership_path.exists()) - def test_ensure_app_support_installs_only_the_app_runtime_branch(self): - response = self.service.ensure_app_support("com.example.Game") + def test_full_cleanup_removes_only_owned_state(self): + self.system_branches = {"23.08"} + self.assertTrue(self.service.prepare_app("com.example.Game")["success"]) + self.assertEqual(self.user_branches, {"24.08"}) - self.assertTrue(response["success"]) - self.assertEqual(response["support_status"], "ready") - self.assertEqual(response["runtime_branch"], "24.08") - install_args = next( - call.args[0] - for call in self.service._run_flatpak_command.call_args_list - if call.args[0][0] == "install" - ) - self.assertEqual(install_args[0], "install") - self.assertIn("--user", install_args) - self.assertNotIn("23.08", install_args) - self.assertEqual( - json.loads(self.service.ownership_path.read_text(encoding="utf-8")), - {"version": 1, "plugin_owned_branches": ["24.08"]}, - ) + response = self.service.remove_plugin_owned_environment() - def test_two_shortcuts_using_one_flatpak_share_one_extension_branch(self): - first = self.service.ensure_app_support("net.pcsx2.PCSX2") - second = self.service.ensure_app_support("net.pcsx2.PCSX2.Dev") - - self.assertEqual(first["support_status"], "ready") - self.assertEqual(second["support_status"], "ready") - install_commands = [ - call.args[0] - for call in self.service._run_flatpak_command.call_args_list - if call.args[0][0] == "install" - ] - self.assertEqual(len(install_commands), 1) - self.assertEqual( - json.loads(self.service.ownership_path.read_text(encoding="utf-8")), - {"version": 1, "plugin_owned_branches": ["24.08"]}, - ) + self.assertTrue(response["success"]) + self.assertEqual(response["removed_apps"], ["com.example.Game"]) + self.assertEqual(response["removed_branches"], ["24.08"]) + self.assertEqual(self.user_branches, set()) + self.assertEqual(self.system_branches, {"23.08"}) + self.assertFalse(self.service.ownership_path.exists()) - def test_cleanup_removes_all_owned_branches_without_reusing_stale_metadata(self): - self.service.ownership_path.parent.mkdir(parents=True, exist_ok=True) - self.service.ownership_path.write_text( - json.dumps({"version": 1, "plugin_owned_branches": ["23.08", "24.08"]}), - encoding="utf-8", - ) - self.user_branches = {"23.08", "24.08"} + def test_corrupt_ownership_metadata_fails_closed(self): + self.service.ownership_path.write_text("{not-json", encoding="utf-8") - response = self.service.remove_plugin_owned_extensions() + response = self.service.remove_plugin_owned_environment() - self.assertTrue(response["success"]) - self.assertEqual(response["removed_branches"], ["23.08", "24.08"]) - self.assertFalse(self.service.ownership_path.exists()) - uninstall_commands = [ - call.args[0] - for call in self.service._run_flatpak_command.call_args_list - if call.args[0][0] == "uninstall" - ] - self.assertEqual(len(uninstall_commands), 2) + self.assertFalse(response["success"]) + self.assertEqual(self.service._run_flatpak_command.call_count, 0) if __name__ == "__main__": -- cgit v1.2.3 From 92cffa96763448b83d5e762020631e3346d6e9da Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:49:29 -0400 Subject: test: cover explicit direct flatpak target integration --- tests/steamLaunchOptions.test.ts | 177 ++++++++++++++++----------------------- 1 file changed, 74 insertions(+), 103 deletions(-) (limited to 'tests') diff --git a/tests/steamLaunchOptions.test.ts b/tests/steamLaunchOptions.test.ts index c799907..93d37df 100644 --- a/tests/steamLaunchOptions.test.ts +++ b/tests/steamLaunchOptions.test.ts @@ -28,7 +28,7 @@ test("inserts one wrapper immediately before an existing command macro", () => { }); }); -test("normalizes blank and argument-only fields while refusing ambiguous launchers", () => { +test("normalizes blank and argument-only shortcut fields", () => { assert.deepEqual(installWrapperLaunchOption("", wrapper), { options: `${wrapper} %command%`, commandTokenAdded: true, @@ -41,7 +41,7 @@ test("normalizes blank and argument-only fields while refusing ambiguous launche assert.throws(() => installWrapperLaunchOption('"%command%"', wrapper), /refusing to guess/); }); -test("preserves assignments, quoting, suffixes, and unrelated values", () => { +test("preserves assignments quoting suffixes and released wrapper cleanup", () => { const options = 'FOO="hello world" VK_INSTANCE_LAYERS="one:two" gamemoderun %command% --flag "two words"'; assert.equal( installWrapperLaunchOption(options, wrapper).options, @@ -49,36 +49,28 @@ test("preserves assignments, quoting, suffixes, and unrelated values", () => { ); assert.equal(removeWrapperLaunchOption(`${wrapper} %command% --arg "${wrapper}"`, wrapper, true), `--arg "${wrapper}"`); assert.equal(normalizeLaunchOptions(" FOO=bar %COMMAND% --flag "), "FOO=bar %COMMAND% --flag"); -}); - -test("cleans current, legacy, and bare Mako wrappers without touching suffix arguments", () => { for (const token of ["~/lsfg", "/home/deck/lsfg", "mako-run", "mako-launch"]) { assert.equal(cleanupLegacyWrapper(`FOO=bar ${token} %command% --arg "${token}"`), `FOO=bar %command% --arg "${token}"`); } - assert.equal(cleanupLegacyWrapper(`FOO=bar ${wrapper} %command%`), "FOO=bar %command%"); assert.equal(isLegacyWrapperToken("/home/kurt/lsfg"), true); assert.equal(isLegacyWrapperToken("/opt/tools/lsfg"), false); - assert.equal(removeWrapperLaunchOption(`FOO=bar ${wrapper} %command% --arg`, wrapper), "FOO=bar %command% --arg"); }); -test("removes only old plugin assignments and preserves DXVK settings", () => { +test("removes only managed assignments and preserves unrelated values", () => { assert.equal( cleanupPluginAssignments( 'FOO="keep this" ENABLE_GAMESCOPE_WSI=0 DXVK_HDR=0 SteamDeck=0 DISABLE_VKBASALT=1 MESA_LOADER_DRIVER_OVERRIDE=zink DXVK_CONFIG="dxgi.syncInterval = 0; dxvk.maxFrameRate = 30" %command%', ), 'FOO="keep this" DXVK_CONFIG="dxgi.syncInterval = 0" %command%', ); - assert.equal( - cleanupPluginLaunchOptions(`DXVK_FRAME_RATE=30 ${wrapper} %command%`, wrapper), - "%command%", - ); + assert.equal(cleanupPluginLaunchOptions(`DXVK_FRAME_RATE=30 ${wrapper} %command%`, wrapper), "%command%"); assert.equal( cleanupPluginAssignments("PROTON_USE_WOW64=1 MANGOHUD=1 MANGOHUD_CONFIG=alpha %command%"), "PROTON_USE_WOW64=1 MANGOHUD=1 MANGOHUD_CONFIG=alpha %command%", ); }); -test("reads the matching app-details field and installs/removes Steam integration", async () => { +test("uses launch options for Steam and MAKO-style Target wrapping for direct Flatpak", async () => { const previousWindow = (globalThis as Record).window; const previousSteamClient = (globalThis as Record).SteamClient; let appOptions = "FOO=bar %command%"; @@ -120,18 +112,22 @@ test("reads the matching app-details field and installs/removes Steam integratio assert.equal(installed.snapshot.options, `FOO=bar ${wrapper} %command%`.replaceAll(" ", " ")); assert.equal(installed.commandTokenAdded, false); assert.equal(appWrites.length, 1); - assert.equal(shortcutWrites.length, 0); - const shortcut = await installWrapperIntegration(43, true, wrapper, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }); - assert.equal(shortcut.originalExecutable, "/usr/bin/flatpak"); - assert.equal(shortcut.snapshot.target, wrapper); - assert.deepEqual(targetWrites, [wrapper]); - const restored = await removeWrapperIntegration(43, true, wrapper, shortcut.originalExecutable, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }); + const shortcut = await installWrapperIntegration(43, true, wrapper, false, true); + assert.equal(shortcut.snapshot.target, '~/.lsfg "/usr/bin/flatpak"'); + assert.deepEqual(targetWrites, ['~/.lsfg "/usr/bin/flatpak"']); + assert.equal(shortcut.snapshot.options, "--windowed"); + assert.deepEqual(shortcutWrites, []); + + const second = await installWrapperIntegration(43, true, wrapper, false, true); + assert.equal(second.changed, false); + assert.deepEqual(targetWrites, ['~/.lsfg "/usr/bin/flatpak"']); + + const restored = await removeWrapperIntegration(43, true, wrapper, false, true); assert.equal(restored.target, "/usr/bin/flatpak"); - assert.deepEqual(targetWrites, [wrapper, "/usr/bin/flatpak"]); - assert.equal(shortcutWrites.length, 0); + assert.deepEqual(targetWrites, ['~/.lsfg "/usr/bin/flatpak"', "/usr/bin/flatpak"]); - const cleaned = await removeWrapperIntegration(42, false, wrapper, undefined, installed.commandTokenAdded); + const cleaned = await removeWrapperIntegration(42, false, wrapper, installed.commandTokenAdded); assert.equal(cleaned.options, "FOO=bar %command%".replaceAll(" ", " ")); assert.ok(unregisters.includes(42)); assert.ok(unregisters.includes(43)); @@ -143,46 +139,54 @@ test("reads the matching app-details field and installs/removes Steam integratio } }); -test("uses shortcut launch options for a host shortcut without changing its Target", async () => { +test("AppImage and EmuDeck script shortcuts stay launch-option based", async () => { const previousWindow = (globalThis as Record).window; const previousSteamClient = (globalThis as Record).SteamClient; - const originalOptions = 'DESKTOPINTEGRATION=1 "/home/deck/AppImages/dusk.appimage"'; - let shortcutOptions = originalOptions; - let shortcutTarget = "env"; - const shortcutWrites: string[] = []; - const targetWrites: string[] = []; - const apps = { - RegisterForAppDetails(_appId: number, callback: (details: SteamAppDetails) => void) { - callback({ strShortcutExe: shortcutTarget, strShortcutLaunchOptions: shortcutOptions }); - return { unregister() {} }; + const cases = [ + { + target: "env", + options: 'DESKTOPINTEGRATION=1 "/home/deck/AppImages/dusk.appimage"', + expected: 'DESKTOPINTEGRATION=1 ~/.lsfg %command% "/home/deck/AppImages/dusk.appimage"', }, - SetShortcutLaunchOptions(_appId: number, options: string) { - shortcutWrites.push(options); - shortcutOptions = options; + { + target: '"/home/deck/Emulation/tools/launchers/retroarch.sh" -L core rom.z64', + options: "", + expected: "~/.lsfg %command%", }, - SetShortcutExe(_appId: number, executable: string) { - targetWrites.push(executable); - shortcutTarget = executable; - }, - }; + ]; (globalThis as Record).window = { setTimeout, clearTimeout }; - (globalThis as Record).SteamClient = { Apps: apps }; try { - const installed = await installWrapperIntegration(44, true, wrapper, false, { kind: "host" }); - assert.equal(installed.originalExecutable, undefined); - assert.equal(installed.snapshot.target, "env"); - assert.equal(installed.snapshot.options, 'DESKTOPINTEGRATION=1 ~/.lsfg %command% "/home/deck/AppImages/dusk.appimage"'); - assert.deepEqual(targetWrites, []); - assert.deepEqual(shortcutWrites, [installed.snapshot.options]); - - const secondInstall = await installWrapperIntegration(44, true, wrapper, false, { kind: "host" }); - assert.equal(secondInstall.snapshot.options, installed.snapshot.options); - assert.deepEqual(shortcutWrites, [installed.snapshot.options]); - - const restored = await removeWrapperIntegration(44, true, wrapper, undefined, installed.commandTokenAdded, { kind: "host" }); - assert.equal(restored.target, "env"); - assert.equal(restored.options, originalOptions); - assert.deepEqual(targetWrites, []); + for (const [index, item] of cases.entries()) { + let shortcutTarget = item.target; + let shortcutOptions = item.options; + const targetWrites: string[] = []; + const shortcutWrites: string[] = []; + (globalThis as Record).SteamClient = { + Apps: { + RegisterForAppDetails(_appId: number, callback: (details: SteamAppDetails) => void) { + callback({ strShortcutExe: shortcutTarget, strShortcutLaunchOptions: shortcutOptions }); + return { unregister() {} }; + }, + SetShortcutLaunchOptions(_appId: number, options: string) { + shortcutWrites.push(options); + shortcutOptions = options; + }, + SetShortcutExe(_appId: number, executable: string) { + targetWrites.push(executable); + shortcutTarget = executable; + }, + }, + }; + const installed = await installWrapperIntegration(100 + index, true, wrapper, false, false); + assert.equal(installed.snapshot.target, item.target); + assert.equal(installed.snapshot.options, item.expected); + assert.deepEqual(targetWrites, []); + assert.deepEqual(shortcutWrites, [item.expected]); + const restored = await removeWrapperIntegration(100 + index, true, wrapper, installed.commandTokenAdded, false); + assert.equal(restored.target, item.target); + assert.equal(restored.options, item.options); + assert.deepEqual(targetWrites, []); + } } finally { if (previousWindow === undefined) delete (globalThis as Record).window; else (globalThis as Record).window = previousWindow; @@ -191,65 +195,32 @@ test("uses shortcut launch options for a host shortcut without changing its Targ } }); -test("fails closed when shortcut Target ownership or setters are unavailable", async () => { +test("direct Flatpak fails closed and rolls Target writes back", async () => { const previousWindow = (globalThis as Record).window; const previousSteamClient = (globalThis as Record).SteamClient; + let shortcutTarget = "/usr/bin/flatpak"; + const targetWrites: string[] = []; (globalThis as Record).window = { setTimeout, clearTimeout }; (globalThis as Record).SteamClient = { Apps: { RegisterForAppDetails(_appId: number, callback: (details: SteamAppDetails) => void) { - callback({ strShortcutExe: _appId === 99 ? "/usr/bin/flatpak" : "garbage", strShortcutLaunchOptions: "" }); + callback({ strShortcutExe: shortcutTarget, strShortcutLaunchOptions: "" }); return { unregister() {} }; }, + SetShortcutExe(_appId: number, executable: string) { + targetWrites.push(executable); + shortcutTarget = executable; + if (executable.startsWith(wrapper)) throw new Error("simulated Target write failure"); + }, }, }; try { - await assert.rejects(installWrapperIntegration(98, true, wrapper, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }), /supported executable/); - await assert.rejects(installWrapperIntegration(99, true, wrapper, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }), /Target API is unavailable/); - await assert.rejects(removeWrapperIntegration(99, true, wrapper, "/usr/bin/original", false, { kind: "flatpak", flatpakAppId: "com.example.Game" }), /Target changed externally/); - } finally { - if (previousWindow === undefined) delete (globalThis as Record).window; - else (globalThis as Record).window = previousWindow; - if (previousSteamClient === undefined) delete (globalThis as Record).SteamClient; - else (globalThis as Record).SteamClient = previousSteamClient; - } -}); - -test("restores launch options and shortcut Target when a setter fails after changing them", async () => { - const previousWindow = (globalThis as Record).window; - const previousSteamClient = (globalThis as Record).SteamClient; - let appOptions = "FOO=bar %command%"; - let shortcutTarget = "/usr/bin/flatpak"; - const appWrites: string[] = []; - const targetWrites: string[] = []; - const apps = { - RegisterForAppDetails(appId: number, callback: (details: SteamAppDetails) => void) { - callback(appId === 42 - ? { strLaunchOptions: appOptions } - : { strShortcutExe: shortcutTarget, strShortcutLaunchOptions: "" }); - return { unregister() {} }; - }, - SetAppLaunchOptions(_appId: number, options: string) { - appWrites.push(options); - appOptions = options; - if (options.includes(wrapper)) throw new Error("simulated launch-option write failure"); - }, - SetShortcutExe(_appId: number, executable: string) { - targetWrites.push(executable); - shortcutTarget = executable; - if (executable === wrapper) throw new Error("simulated Target write failure"); - }, - }; - (globalThis as Record).window = { setTimeout, clearTimeout }; - (globalThis as Record).SteamClient = { Apps: apps }; - try { - await assert.rejects(installWrapperIntegration(42, false, wrapper), /simulated launch-option write failure/); - assert.equal(appOptions, "FOO=bar %command%"); - assert.deepEqual(appWrites, [`FOO=bar ${wrapper} %command%`, "FOO=bar %command%"]); - - await assert.rejects(installWrapperIntegration(43, true, wrapper, false, { kind: "flatpak", flatpakAppId: "com.example.Game" }), /simulated Target write failure/); + await assert.rejects(installWrapperIntegration(43, true, wrapper, false, true), /simulated Target write failure/); assert.equal(shortcutTarget, "/usr/bin/flatpak"); - assert.deepEqual(targetWrites, [wrapper, "/usr/bin/flatpak"]); + assert.deepEqual(targetWrites, ['~/.lsfg "/usr/bin/flatpak"', "/usr/bin/flatpak"]); + + shortcutTarget = "garbage"; + await assert.rejects(installWrapperIntegration(43, true, wrapper, false, true), /supported direct Flatpak/); } finally { if (previousWindow === undefined) delete (globalThis as Record).window; else (globalThis as Record).window = previousWindow; -- cgit v1.2.3 From 9e5afad9226475bffe6f2cceb9e5405d16d17a65 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:54:01 -0400 Subject: test: retain released flatpak target recognition --- tests/test_steam_service.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'tests') diff --git a/tests/test_steam_service.py b/tests/test_steam_service.py index 719249d..3f0283b 100644 --- a/tests/test_steam_service.py +++ b/tests/test_steam_service.py @@ -20,6 +20,8 @@ class SteamShortcutTests(unittest.TestCase): self.assertTrue(is_direct_flatpak_shortcut("flatpak")) self.assertTrue(is_direct_flatpak_shortcut("/usr/bin/flatpak run com.example.Game")) self.assertTrue(is_direct_flatpak_shortcut('~/.lsfg "/usr/bin/flatpak"')) + self.assertTrue(is_direct_flatpak_shortcut('~/lsfg "usr/bin/flatpak"')) + self.assertTrue(is_direct_flatpak_shortcut('~/.local/bin/mako-run "/usr/bin/flatpak"')) self.assertFalse(is_direct_flatpak_shortcut("/usr/bin/bash")) self.assertFalse(is_direct_flatpak_shortcut("/home/deck/Emulation/tools/launchers/retroarch.sh")) self.assertFalse(is_direct_flatpak_shortcut("/home/deck/Emulation/tools/launchers/ppsspp.sh")) -- cgit v1.2.3 From 8a74b326b65d56808cf3ddb464e55ab0d1a45074 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:55:03 -0400 Subject: test: cover flatpak cleanup on plugin uninstall --- tests/test_plugin_migration.py | 49 ++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 14 deletions(-) (limited to 'tests') diff --git a/tests/test_plugin_migration.py b/tests/test_plugin_migration.py index a392f85..ab036b0 100644 --- a/tests/test_plugin_migration.py +++ b/tests/test_plugin_migration.py @@ -6,7 +6,7 @@ from unittest.mock import Mock class PluginMigrationTests(unittest.TestCase): - def test_migration_only_runs_decky_path_migrations(self): + def _load_plugin(self): decky = types.SimpleNamespace( DECKY_HOME="/decky", DECKY_USER_HOME="/home/deck", @@ -19,10 +19,24 @@ class PluginMigrationTests(unittest.TestCase): previous_tomllib = sys.modules.get("tomllib") sys.modules["decky"] = decky sys.modules["tomllib"] = types.SimpleNamespace(loads=Mock()) - try: - sys.path.insert(0, "py_modules") - from lsfg_vk.plugin import Plugin + sys.path.insert(0, "py_modules") + from lsfg_vk.plugin import Plugin + return Plugin, decky, previous_decky, previous_tomllib + def _restore(self, previous_decky, previous_tomllib): + sys.path.remove("py_modules") + if previous_decky is None: + sys.modules.pop("decky", None) + else: + sys.modules["decky"] = previous_decky + if previous_tomllib is None: + sys.modules.pop("tomllib", None) + else: + sys.modules["tomllib"] = previous_tomllib + + def test_migration_only_runs_decky_path_migrations(self): + Plugin, decky, previous_decky, previous_tomllib = self._load_plugin() + try: plugin = Plugin.__new__(Plugin) plugin.installation_service = Mock() plugin.flatpak_service = Mock() @@ -33,17 +47,24 @@ class PluginMigrationTests(unittest.TestCase): decky.migrate_settings.assert_called_once() decky.migrate_runtime.assert_called_once() plugin.installation_service.install.assert_not_called() - plugin.flatpak_service.migrate_v2.assert_not_called() + plugin.flatpak_service.prepare_app.assert_not_called() + finally: + self._restore(previous_decky, previous_tomllib) + + def test_uninstall_cleans_owned_flatpak_state(self): + Plugin, _decky, previous_decky, previous_tomllib = self._load_plugin() + try: + plugin = Plugin.__new__(Plugin) + plugin.installation_service = Mock() + plugin.flatpak_service = Mock() + plugin.flatpak_service.remove_plugin_owned_environment.return_value = {"success": True} + + asyncio.run(plugin._uninstall()) + + plugin.flatpak_service.remove_plugin_owned_environment.assert_called_once_with() + plugin.installation_service.cleanup_on_uninstall.assert_called_once_with() finally: - sys.path.remove("py_modules") - if previous_decky is None: - sys.modules.pop("decky", None) - else: - sys.modules["decky"] = previous_decky - if previous_tomllib is None: - sys.modules.pop("tomllib", None) - else: - sys.modules["tomllib"] = previous_tomllib + self._restore(previous_decky, previous_tomllib) if __name__ == "__main__": -- cgit v1.2.3 From 3681bbb57c8f66bb789b493fe8bcd920245136a3 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:57:46 -0400 Subject: test: isolate app override restoration from runtime ownership --- tests/test_flatpak_service.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'tests') diff --git a/tests/test_flatpak_service.py b/tests/test_flatpak_service.py index 208e05b..065871b 100644 --- a/tests/test_flatpak_service.py +++ b/tests/test_flatpak_service.py @@ -213,6 +213,7 @@ class FlatpakServiceTests(unittest.TestCase): self.assertFalse(self.service.ownership_path.exists()) def test_remove_restores_exact_previous_override(self): + self.system_branches = {"24.08"} original = "[Context]\nfilesystems=~/Documents;\n\n[Environment]\nFOO=bar\n" path = self.service._override_path("com.example.Game") path.parent.mkdir(parents=True, exist_ok=True) @@ -226,6 +227,7 @@ class FlatpakServiceTests(unittest.TestCase): self.assertFalse(self.service.ownership_path.exists()) def test_remove_deletes_override_created_by_plugin(self): + self.system_branches = {"24.08"} self.assertTrue(self.service.prepare_app("com.example.Game")["success"]) path = self.service._override_path("com.example.Game") self.assertTrue(path.exists()) @@ -234,8 +236,10 @@ class FlatpakServiceTests(unittest.TestCase): self.assertTrue(response["success"]) self.assertFalse(path.exists()) + self.assertFalse(self.service.ownership_path.exists()) def test_remove_fails_closed_after_external_change(self): + self.system_branches = {"24.08"} self.assertTrue(self.service.prepare_app("com.example.Game")["success"]) path = self.service._override_path("com.example.Game") with path.open("a", encoding="utf-8") as handle: -- cgit v1.2.3 From af7202363bbe4ec5c3d40197cf88c306fa06ee46 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:40:50 -0400 Subject: test: cover launch-options-only steam integration --- tests/steamLaunchOptions.test.ts | 77 ++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 51 deletions(-) (limited to 'tests') diff --git a/tests/steamLaunchOptions.test.ts b/tests/steamLaunchOptions.test.ts index 93d37df..40aeb3c 100644 --- a/tests/steamLaunchOptions.test.ts +++ b/tests/steamLaunchOptions.test.ts @@ -70,21 +70,19 @@ test("removes only managed assignments and preserves unrelated values", () => { ); }); -test("uses launch options for Steam and MAKO-style Target wrapping for direct Flatpak", async () => { +test("uses launch options for Steam and non-Steam shortcuts without a Target API", async () => { const previousWindow = (globalThis as Record).window; const previousSteamClient = (globalThis as Record).SteamClient; let appOptions = "FOO=bar %command%"; let shortcutOptions = "--windowed"; - let shortcutTarget = '"flatpak"'; const appWrites: string[] = []; const shortcutWrites: string[] = []; - const targetWrites: string[] = []; const unregisters: number[] = []; const apps = { RegisterForAppDetails(appId: number, callback: (details: SteamAppDetails) => void) { callback(appId === 42 ? { strLaunchOptions: appOptions, strShortcutLaunchOptions: "wrong-field" } - : { strShortcutExe: shortcutTarget, strShortcutLaunchOptions: shortcutOptions, strLaunchOptions: "wrong-field" }); + : { strShortcutLaunchOptions: shortcutOptions, strLaunchOptions: "wrong-field" }); return { unregister: () => unregisters.push(appId) }; }, SetAppLaunchOptions(appId: number, options: string) { @@ -97,11 +95,6 @@ test("uses launch options for Steam and MAKO-style Target wrapping for direct Fl shortcutWrites.push(options); shortcutOptions = options; }, - SetShortcutExe(appId: number, executable: string) { - assert.equal(appId, 43); - targetWrites.push(executable); - shortcutTarget = executable; - }, }; (globalThis as Record).window = { setTimeout, clearTimeout }; (globalThis as Record).SteamClient = { Apps: apps }; @@ -113,19 +106,12 @@ test("uses launch options for Steam and MAKO-style Target wrapping for direct Fl assert.equal(installed.commandTokenAdded, false); assert.equal(appWrites.length, 1); - const shortcut = await installWrapperIntegration(43, true, wrapper, false, true); - assert.equal(shortcut.snapshot.target, '~/.lsfg "/usr/bin/flatpak"'); - assert.deepEqual(targetWrites, ['~/.lsfg "/usr/bin/flatpak"']); - assert.equal(shortcut.snapshot.options, "--windowed"); - assert.deepEqual(shortcutWrites, []); + const shortcut = await installWrapperIntegration(43, true, wrapper); + assert.equal(shortcut.snapshot.options, `~/.lsfg %command% --windowed`); + assert.deepEqual(shortcutWrites, [`~/.lsfg %command% --windowed`]); - const second = await installWrapperIntegration(43, true, wrapper, false, true); - assert.equal(second.changed, false); - assert.deepEqual(targetWrites, ['~/.lsfg "/usr/bin/flatpak"']); - - const restored = await removeWrapperIntegration(43, true, wrapper, false, true); - assert.equal(restored.target, "/usr/bin/flatpak"); - assert.deepEqual(targetWrites, ['~/.lsfg "/usr/bin/flatpak"', "/usr/bin/flatpak"]); + const restored = await removeWrapperIntegration(43, true, wrapper, shortcut.commandTokenAdded); + assert.equal(restored.options, "--windowed"); const cleaned = await removeWrapperIntegration(42, false, wrapper, installed.commandTokenAdded); assert.equal(cleaned.options, "FOO=bar %command%".replaceAll(" ", " ")); @@ -139,53 +125,45 @@ test("uses launch options for Steam and MAKO-style Target wrapping for direct Fl } }); -test("AppImage and EmuDeck script shortcuts stay launch-option based", async () => { +test("AppImage EmuDeck and direct Flatpak shortcuts all stay launch-option based", async () => { const previousWindow = (globalThis as Record).window; const previousSteamClient = (globalThis as Record).SteamClient; const cases = [ { - target: "env", options: 'DESKTOPINTEGRATION=1 "/home/deck/AppImages/dusk.appimage"', expected: 'DESKTOPINTEGRATION=1 ~/.lsfg %command% "/home/deck/AppImages/dusk.appimage"', }, { - target: '"/home/deck/Emulation/tools/launchers/retroarch.sh" -L core rom.z64', options: "", expected: "~/.lsfg %command%", }, + { + options: "run org.example.Game", + expected: "~/.lsfg %command% run org.example.Game", + }, ]; (globalThis as Record).window = { setTimeout, clearTimeout }; try { for (const [index, item] of cases.entries()) { - let shortcutTarget = item.target; let shortcutOptions = item.options; - const targetWrites: string[] = []; const shortcutWrites: string[] = []; (globalThis as Record).SteamClient = { Apps: { RegisterForAppDetails(_appId: number, callback: (details: SteamAppDetails) => void) { - callback({ strShortcutExe: shortcutTarget, strShortcutLaunchOptions: shortcutOptions }); + callback({ strShortcutLaunchOptions: shortcutOptions }); return { unregister() {} }; }, SetShortcutLaunchOptions(_appId: number, options: string) { shortcutWrites.push(options); shortcutOptions = options; }, - SetShortcutExe(_appId: number, executable: string) { - targetWrites.push(executable); - shortcutTarget = executable; - }, }, }; - const installed = await installWrapperIntegration(100 + index, true, wrapper, false, false); - assert.equal(installed.snapshot.target, item.target); + const installed = await installWrapperIntegration(100 + index, true, wrapper); assert.equal(installed.snapshot.options, item.expected); - assert.deepEqual(targetWrites, []); assert.deepEqual(shortcutWrites, [item.expected]); - const restored = await removeWrapperIntegration(100 + index, true, wrapper, installed.commandTokenAdded, false); - assert.equal(restored.target, item.target); + const restored = await removeWrapperIntegration(100 + index, true, wrapper, installed.commandTokenAdded); assert.equal(restored.options, item.options); - assert.deepEqual(targetWrites, []); } } finally { if (previousWindow === undefined) delete (globalThis as Record).window; @@ -195,32 +173,29 @@ test("AppImage and EmuDeck script shortcuts stay launch-option based", async () } }); -test("direct Flatpak fails closed and rolls Target writes back", async () => { +test("launch option write failure rolls back the original value", async () => { const previousWindow = (globalThis as Record).window; const previousSteamClient = (globalThis as Record).SteamClient; - let shortcutTarget = "/usr/bin/flatpak"; - const targetWrites: string[] = []; + let appOptions = "FOO=bar %command%"; + const writes: string[] = []; (globalThis as Record).window = { setTimeout, clearTimeout }; (globalThis as Record).SteamClient = { Apps: { RegisterForAppDetails(_appId: number, callback: (details: SteamAppDetails) => void) { - callback({ strShortcutExe: shortcutTarget, strShortcutLaunchOptions: "" }); + callback({ strLaunchOptions: appOptions }); return { unregister() {} }; }, - SetShortcutExe(_appId: number, executable: string) { - targetWrites.push(executable); - shortcutTarget = executable; - if (executable.startsWith(wrapper)) throw new Error("simulated Target write failure"); + SetAppLaunchOptions(_appId: number, options: string) { + writes.push(options); + appOptions = options; + if (options.includes(wrapper)) throw new Error("simulated launch option failure"); }, }, }; try { - await assert.rejects(installWrapperIntegration(43, true, wrapper, false, true), /simulated Target write failure/); - assert.equal(shortcutTarget, "/usr/bin/flatpak"); - assert.deepEqual(targetWrites, ['~/.lsfg "/usr/bin/flatpak"', "/usr/bin/flatpak"]); - - shortcutTarget = "garbage"; - await assert.rejects(installWrapperIntegration(43, true, wrapper, false, true), /supported direct Flatpak/); + await assert.rejects(installWrapperIntegration(42, false, wrapper), /simulated launch option failure/); + assert.equal(appOptions, "FOO=bar %command%"); + assert.deepEqual(writes, [`FOO=bar ${wrapper} %command%`, "FOO=bar %command%"]); } finally { if (previousWindow === undefined) delete (globalThis as Record).window; else (globalThis as Record).window = previousWindow; -- cgit v1.2.3 From 7b9d1c604099dd21d6c710ca00dfd9b001afc172 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:41:00 -0400 Subject: test: keep steam shortcut discovery flatpak agnostic --- tests/test_steam_service.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) (limited to 'tests') diff --git a/tests/test_steam_service.py b/tests/test_steam_service.py index 3f0283b..76cdf47 100644 --- a/tests/test_steam_service.py +++ b/tests/test_steam_service.py @@ -11,23 +11,11 @@ sys.modules.setdefault( ) sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "py_modules")) -from lsfg_vk.steam_service import SteamService, is_direct_flatpak_shortcut +from lsfg_vk.steam_service import SteamService class SteamShortcutTests(unittest.TestCase): - def test_only_direct_flatpak_targets_are_special(self): - self.assertTrue(is_direct_flatpak_shortcut("/usr/bin/flatpak")) - self.assertTrue(is_direct_flatpak_shortcut("flatpak")) - self.assertTrue(is_direct_flatpak_shortcut("/usr/bin/flatpak run com.example.Game")) - self.assertTrue(is_direct_flatpak_shortcut('~/.lsfg "/usr/bin/flatpak"')) - self.assertTrue(is_direct_flatpak_shortcut('~/lsfg "usr/bin/flatpak"')) - self.assertTrue(is_direct_flatpak_shortcut('~/.local/bin/mako-run "/usr/bin/flatpak"')) - self.assertFalse(is_direct_flatpak_shortcut("/usr/bin/bash")) - self.assertFalse(is_direct_flatpak_shortcut("/home/deck/Emulation/tools/launchers/retroarch.sh")) - self.assertFalse(is_direct_flatpak_shortcut("/home/deck/Emulation/tools/launchers/ppsspp.sh")) - self.assertFalse(is_direct_flatpak_shortcut("/home/deck/AppImages/dusk.appimage")) - - def test_shortcut_data_preserves_launch_shape_without_flatpak_identity(self): + def test_direct_flatpak_shortcut_is_ordinary_non_steam_metadata(self): game = SteamService._shortcut_game( { "appid": 123456, @@ -39,13 +27,15 @@ class SteamShortcutTests(unittest.TestCase): ) self.assertEqual(game["appid"], "123456") - self.assertTrue(game["directFlatpak"]) + self.assertEqual(game["name"], "PCSX2 shortcut") + self.assertTrue(game["nonSteam"]) + self.assertNotIn("directFlatpak", game) self.assertNotIn("transport", game) self.assertEqual(game["executable"], "/usr/bin/flatpak") self.assertEqual(game["arguments"], "run net.pcsx2.PCSX2 --fullscreen") self.assertEqual(game["startDir"], "/home/deck/Games") - def test_emudeck_launcher_is_ordinary_non_steam(self): + def test_emudeck_launcher_is_ordinary_non_steam_metadata(self): game = SteamService._shortcut_game( { "appid": 987654, @@ -55,11 +45,19 @@ class SteamShortcutTests(unittest.TestCase): } ) - self.assertFalse(game["directFlatpak"]) + self.assertEqual(game["appid"], "987654") + self.assertTrue(game["nonSteam"]) + self.assertNotIn("directFlatpak", game) self.assertEqual( game["executable"], '"/home/deck/Emulation/tools/launchers/retroarch.sh" -L core rom.z64', ) + self.assertEqual(game["arguments"], "") + + def test_shortcut_rejects_invalid_identity(self): + self.assertIsNone(SteamService._shortcut_game({"appid": 0, "AppName": "Bad"})) + self.assertIsNone(SteamService._shortcut_game({"appid": 1, "AppName": ""})) + self.assertIsNone(SteamService._shortcut_game("bad")) if __name__ == "__main__": -- cgit v1.2.3 From 00451fc89cd6254c656f72a9a8181cfd93ea8936 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:41:21 -0400 Subject: test: cover flatpak selector profiles --- tests/test_configuration_profiles.py | 80 ++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 tests/test_configuration_profiles.py (limited to 'tests') diff --git a/tests/test_configuration_profiles.py b/tests/test_configuration_profiles.py new file mode 100644 index 0000000..789842e --- /dev/null +++ b/tests/test_configuration_profiles.py @@ -0,0 +1,80 @@ +import sys +import tempfile +import types +import unittest +from pathlib import Path +from unittest.mock import Mock + + +sys.modules.setdefault( + "decky", + types.SimpleNamespace(DECKY_USER_HOME="/home/deck", logger=Mock()), +) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "py_modules")) + +from lsfg_vk.config_schema import ConfigurationManager +from lsfg_vk.configuration import ConfigurationService + + +class ConfigurationProfileTests(unittest.TestCase): + def setUp(self): + self.tempdir = tempfile.TemporaryDirectory() + self.home = Path(self.tempdir.name) / "home" / "deck" + self.home.mkdir(parents=True) + self.runtime = Mock() + self.service = ConfigurationService(runtime_service=self.runtime) + self.service.user_home = self.home + self.service.config_dir = self.home / ".config/lsfg-vk" + self.service.config_file_path = self.service.config_dir / "conf.toml" + + def tearDown(self): + self.tempdir.cleanup() + + def test_selector_only_profile_survives_round_trip(self): + content = """version = 2 + +[global] +allow_fp16 = true + +[[profile]] +name = "flatpak:org.example.Game" +pacing_mode = "vsync" +multiplier = 3 +flow_scale = 0.8 +performance_mode = false +override_present_mode = true +preserve_swapchain_image_count = false +""" + parsed = ConfigurationManager.parse_toml_content_multi_profile(content) + self.assertIn("flatpak:org.example.Game", parsed["profiles"]) + self.assertEqual(parsed["profiles"]["flatpak:org.example.Game"]["active_in"], []) + rendered = ConfigurationManager.generate_toml_content_multi_profile(parsed) + reparsed = ConfigurationManager.parse_toml_content_multi_profile(rendered) + self.assertEqual(reparsed["profiles"]["flatpak:org.example.Game"]["multiplier"], 3) + + def test_game_reset_all_preserves_flatpak_profiles(self): + self.service.update_game_config("123", "Steam Game", {"multiplier": 2}) + self.service.update_flatpak_config("org.example.Game", {"multiplier": 3}) + + result = self.service.reset_all_game_configs() + data = self.service._get_profile_data() + + self.assertTrue(result["success"]) + self.assertNotIn("Steam Game", data["profiles"]) + self.assertIn("flatpak:org.example.Game", data["profiles"]) + self.assertEqual(data["profiles"]["flatpak:org.example.Game"]["multiplier"], 3) + + def test_flatpak_reset_all_preserves_steam_profiles(self): + self.service.update_game_config("123", "Steam Game", {"multiplier": 2}) + self.service.update_flatpak_config("org.example.Game", {"multiplier": 3}) + + result = self.service.reset_all_flatpak_configs() + data = self.service._get_profile_data() + + self.assertTrue(result["success"]) + self.assertIn("Steam Game", data["profiles"]) + self.assertNotIn("flatpak:org.example.Game", data["profiles"]) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From 4e5d7eb600a094d6f52bd915814de85ea6735836 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:41:59 -0400 Subject: test: cover flatpak profile environment integration --- tests/test_flatpak_profile_service.py | 210 ++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 tests/test_flatpak_profile_service.py (limited to 'tests') diff --git a/tests/test_flatpak_profile_service.py b/tests/test_flatpak_profile_service.py new file mode 100644 index 0000000..8d58413 --- /dev/null +++ b/tests/test_flatpak_profile_service.py @@ -0,0 +1,210 @@ +import hashlib +import sys +import tempfile +import types +import unittest +from pathlib import Path +from unittest.mock import Mock + + +sys.modules.setdefault( + "decky", + types.SimpleNamespace(DECKY_USER_HOME="/home/deck", logger=Mock()), +) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "py_modules")) + +from lsfg_vk.configuration import ConfigurationService +from lsfg_vk.flatpak_profile_service import FlatpakProfileService + + +class FakeFlatpakService: + def __init__(self, home: Path): + self.user_home = home + self.config_dir = home / ".config/lsfg-vk" + self.config_file_path = self.config_dir / "conf.toml" + self.backup_dir = self.config_dir / "flatpak-overrides" + self.state = {"version": 2, "plugin_owned_branches": [], "prepared_apps": {}} + self.commands = [] + self.running = "" + + def _read_state(self): + return self.state + + def _write_state(self, state): + self.state = state + + def _override_path(self, app_id): + return self.user_home / ".local/share/flatpak/overrides" / app_id + + def _backup_path(self, app_id): + return self.backup_dir / f"{app_id}.ini" + + @staticmethod + def _sha256(content): + return hashlib.sha256(content).hexdigest() + + def _snapshot_override(self, app_id): + path = self._override_path(app_id) + if not path.exists(): + return False, b"" + return True, path.read_bytes() + + @staticmethod + def _write_file(path, content, mode=0o644): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + path.chmod(mode) + + def prepare_app(self, app_id): + apps = self.state["prepared_apps"] + if app_id not in apps: + existed, original = self._snapshot_override(app_id) + if existed: + self._write_file(self._backup_path(app_id), original.decode("utf-8")) + apps[app_id] = {"override_existed": existed, "managed_sha256": ""} + entry = apps[app_id] + baseline = "" + if entry["override_existed"]: + baseline = self._backup_path(app_id).read_text(encoding="utf-8") + managed = baseline + "\n[Context]\nfilesystems=/config:ro;/dll:ro;\n[Environment]\nLSFGVK_CONFIG=/config/conf.toml\nLSFGVK_FLATPAK=1\n" + self._write_file(self._override_path(app_id), managed) + entry["managed_sha256"] = self._sha256(managed.encode()) + return {"success": True, "owned": True, "prepared": True, "runtime": "org.freedesktop.Platform/x86_64/24.08", "runtime_branch": "24.08"} + + def remove_app_override(self, app_id): + entry = self.state["prepared_apps"].get(app_id) + if entry is None: + return {"success": True, "prepared": False, "owned": False} + existed, current = self._snapshot_override(app_id) + current_hash = self._sha256(current) if existed else self._sha256(b"") + if current_hash != entry["managed_sha256"]: + return {"success": False, "error": "Flatpak override changed after preparation"} + path = self._override_path(app_id) + backup = self._backup_path(app_id) + if entry["override_existed"]: + self._write_file(path, backup.read_text(encoding="utf-8")) + else: + path.unlink(missing_ok=True) + backup.unlink(missing_ok=True) + self.state["prepared_apps"].pop(app_id) + return {"success": True, "prepared": False, "owned": False} + + def get_flatpak_apps(self): + app_id = "org.example.Game" + return { + "success": True, + "apps": [{ + "app_id": app_id, + "app_name": "Example Game", + "runtime": "org.freedesktop.Platform/x86_64/24.08", + "runtime_branch": "24.08", + "runtime_ready": True, + "prepared": app_id in self.state["prepared_apps"], + "owned": app_id in self.state["prepared_apps"], + "error": None, + }], + } + + def _run_flatpak_command(self, args, **_kwargs): + self.commands.append(args) + if args[:3] == ["override", "--user", "--show"]: + path = self._override_path(args[3]) + return types.SimpleNamespace(returncode=0, stdout=path.read_text(encoding="utf-8") if path.exists() else "", stderr="") + if args[0] == "override": + app_id = args[-1] + path = self._override_path(app_id) + content = path.read_text(encoding="utf-8") if path.exists() else "" + env = [item.removeprefix("--env=") for item in args if item.startswith("--env=")] + unset = [item.removeprefix("--unset-env=") for item in args if item.startswith("--unset-env=")] + if unset: + content += "\n[Context]\nunset-environment=" + ";".join(unset) + ";\n" + if env: + content += "\n[Environment]\n" + "\n".join(env) + "\n" + self._write_file(path, content) + return types.SimpleNamespace(returncode=0, stdout="", stderr="") + if args[0] == "ps": + return types.SimpleNamespace(returncode=0, stdout=self.running, stderr="") + raise AssertionError(args) + + +class FlatpakProfileServiceTests(unittest.TestCase): + def setUp(self): + self.tempdir = tempfile.TemporaryDirectory() + self.home = Path(self.tempdir.name) / "home" / "deck" + self.home.mkdir(parents=True) + self.flatpak = FakeFlatpakService(self.home) + self.runtime = Mock() + self.configuration = ConfigurationService(runtime_service=self.runtime) + self.configuration.user_home = self.home + self.configuration.config_dir = self.flatpak.config_dir + self.configuration.config_file_path = self.flatpak.config_file_path + self.service = FlatpakProfileService(self.flatpak, self.configuration) + self.app_id = "org.example.Game" + + def tearDown(self): + self.tempdir.cleanup() + + def test_enable_creates_selector_profile_and_default_workarounds(self): + result = self.service.enable_app(self.app_id) + config = self.configuration.get_flatpak_config(self.app_id) + content = self.flatpak._override_path(self.app_id).read_text(encoding="utf-8") + + self.assertTrue(result["success"]) + self.assertTrue(config["exists"]) + self.assertEqual(config["profile"], "flatpak:org.example.Game") + self.assertEqual(config["config"]["active_in"], []) + self.assertIn("LSFGVK_PROFILE=flatpak:org.example.Game", content) + self.assertIn("ENABLE_GAMESCOPE_WSI=0", content) + self.assertIn("DXVK_HDR=0", content) + + def test_workaround_update_rebuilds_from_original_override(self): + baseline = "[Environment]\nDXVK_CONFIG=dxgi.syncInterval = 0\nKEEP=yes\n" + self.flatpak._write_file(self.flatpak._override_path(self.app_id), baseline) + self.assertTrue(self.service.enable_app(self.app_id)["success"]) + + state = self.service.default_state() + state.update({"dxvkFrameRate": 30, "disableHdr": False, "enableZink": True}) + result = self.service.set_workaround_state(self.app_id, state) + command = next( + args for args in reversed(self.flatpak.commands) + if args[0] == "override" and any(item.startswith("--env=LSFGVK_PROFILE=") for item in args) + ) + + self.assertTrue(result["success"]) + self.assertIn("--env=DXVK_CONFIG=dxgi.syncInterval = 0; dxvk.maxFrameRate = 30", command) + self.assertNotIn("--env=DXVK_HDR=0", command) + self.assertIn("--env=MESA_LOADER_DRIVER_OVERRIDE=zink", command) + + def test_remove_restores_exact_original_override_and_profile(self): + baseline = "[Environment]\nKEEP=yes\n" + self.flatpak._write_file(self.flatpak._override_path(self.app_id), baseline) + self.assertTrue(self.service.enable_app(self.app_id)["success"]) + + removed = self.service.remove_app(self.app_id) + + self.assertTrue(removed["success"]) + self.assertEqual(self.flatpak._override_path(self.app_id).read_text(encoding="utf-8"), baseline) + self.assertFalse(self.configuration.get_flatpak_config(self.app_id)["exists"]) + + def test_external_override_change_fails_closed(self): + self.assertTrue(self.service.enable_app(self.app_id)["success"]) + path = self.flatpak._override_path(self.app_id) + path.write_text(path.read_text(encoding="utf-8") + "EXTERNAL=yes\n", encoding="utf-8") + + result = self.service.set_workaround_state(self.app_id, self.service.default_state()) + + self.assertFalse(result["success"]) + self.assertIn("changed after preparation", result["error"]) + + def test_running_detection_uses_owned_selector_state(self): + self.assertTrue(self.service.enable_app(self.app_id)["success"]) + self.flatpak.running = "org.example.Game\ttrue\t1234\norg.other.App\ttrue\t9999\n" + + result = self.service.get_running_apps() + + self.assertTrue(result["success"]) + self.assertEqual(result["apps"], [{"app_id": self.app_id, "active": True, "pid": "1234"}]) + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From b529bc1f0d6a5418cb60ebc6c127796aba08068e Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:42:30 -0400 Subject: test: cover flatpak profile uninstall cleanup --- tests/test_plugin_migration.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/test_plugin_migration.py b/tests/test_plugin_migration.py index ab036b0..824cbf6 100644 --- a/tests/test_plugin_migration.py +++ b/tests/test_plugin_migration.py @@ -51,17 +51,35 @@ class PluginMigrationTests(unittest.TestCase): finally: self._restore(previous_decky, previous_tomllib) - def test_uninstall_cleans_owned_flatpak_state(self): + def test_uninstall_cleans_owned_flatpak_state_and_profiles(self): Plugin, _decky, previous_decky, previous_tomllib = self._load_plugin() try: plugin = Plugin.__new__(Plugin) plugin.installation_service = Mock() plugin.flatpak_service = Mock() + plugin.configuration_service = Mock() plugin.flatpak_service.remove_plugin_owned_environment.return_value = {"success": True} asyncio.run(plugin._uninstall()) plugin.flatpak_service.remove_plugin_owned_environment.assert_called_once_with() + plugin.configuration_service.reset_all_flatpak_configs.assert_called_once_with() + plugin.installation_service.cleanup_on_uninstall.assert_called_once_with() + finally: + self._restore(previous_decky, previous_tomllib) + + def test_uninstall_preserves_profiles_when_flatpak_cleanup_fails(self): + Plugin, _decky, previous_decky, previous_tomllib = self._load_plugin() + try: + plugin = Plugin.__new__(Plugin) + plugin.installation_service = Mock() + plugin.flatpak_service = Mock() + plugin.configuration_service = Mock() + plugin.flatpak_service.remove_plugin_owned_environment.return_value = {"success": False, "error": "changed"} + + asyncio.run(plugin._uninstall()) + + plugin.configuration_service.reset_all_flatpak_configs.assert_not_called() plugin.installation_service.cleanup_on_uninstall.assert_called_once_with() finally: self._restore(previous_decky, previous_tomllib) -- cgit v1.2.3 From 252079e1df34329db1e3dc11c66c8c9d40a3a616 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:45:05 -0400 Subject: test: cover minimal steam shortcut metadata --- tests/test_steam_service.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) (limited to 'tests') diff --git a/tests/test_steam_service.py b/tests/test_steam_service.py index 76cdf47..004ffb6 100644 --- a/tests/test_steam_service.py +++ b/tests/test_steam_service.py @@ -26,14 +26,11 @@ class SteamShortcutTests(unittest.TestCase): } ) - self.assertEqual(game["appid"], "123456") - self.assertEqual(game["name"], "PCSX2 shortcut") - self.assertTrue(game["nonSteam"]) - self.assertNotIn("directFlatpak", game) - self.assertNotIn("transport", game) - self.assertEqual(game["executable"], "/usr/bin/flatpak") - self.assertEqual(game["arguments"], "run net.pcsx2.PCSX2 --fullscreen") - self.assertEqual(game["startDir"], "/home/deck/Games") + self.assertEqual(game, { + "appid": "123456", + "name": "PCSX2 shortcut", + "nonSteam": True, + }) def test_emudeck_launcher_is_ordinary_non_steam_metadata(self): game = SteamService._shortcut_game( @@ -45,14 +42,11 @@ class SteamShortcutTests(unittest.TestCase): } ) - self.assertEqual(game["appid"], "987654") - self.assertTrue(game["nonSteam"]) - self.assertNotIn("directFlatpak", game) - self.assertEqual( - game["executable"], - '"/home/deck/Emulation/tools/launchers/retroarch.sh" -L core rom.z64', - ) - self.assertEqual(game["arguments"], "") + self.assertEqual(game, { + "appid": "987654", + "name": "1080 Snowboarding", + "nonSteam": True, + }) def test_shortcut_rejects_invalid_identity(self): self.assertIsNone(SteamService._shortcut_game({"appid": 0, "AppName": "Bad"})) -- cgit v1.2.3 From 834fa5c585ec94e85cae0ba96a39d72111c9cf4d Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 10 Sep 2026 19:47:00 -0400 Subject: tests --- tests/test_plugin_migration.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) (limited to 'tests') diff --git a/tests/test_plugin_migration.py b/tests/test_plugin_migration.py index 824cbf6..7ab4621 100644 --- a/tests/test_plugin_migration.py +++ b/tests/test_plugin_migration.py @@ -17,13 +17,14 @@ class PluginMigrationTests(unittest.TestCase): ) previous_decky = sys.modules.get("decky") previous_tomllib = sys.modules.get("tomllib") + previous_plugin = sys.modules.pop("lsfg_vk.plugin", None) sys.modules["decky"] = decky sys.modules["tomllib"] = types.SimpleNamespace(loads=Mock()) sys.path.insert(0, "py_modules") from lsfg_vk.plugin import Plugin - return Plugin, decky, previous_decky, previous_tomllib + return Plugin, decky, previous_decky, previous_tomllib, previous_plugin - def _restore(self, previous_decky, previous_tomllib): + def _restore(self, previous_decky, previous_tomllib, previous_plugin): sys.path.remove("py_modules") if previous_decky is None: sys.modules.pop("decky", None) @@ -33,9 +34,13 @@ class PluginMigrationTests(unittest.TestCase): sys.modules.pop("tomllib", None) else: sys.modules["tomllib"] = previous_tomllib + if previous_plugin is None: + sys.modules.pop("lsfg_vk.plugin", None) + else: + sys.modules["lsfg_vk.plugin"] = previous_plugin def test_migration_only_runs_decky_path_migrations(self): - Plugin, decky, previous_decky, previous_tomllib = self._load_plugin() + Plugin, decky, previous_decky, previous_tomllib, previous_plugin = self._load_plugin() try: plugin = Plugin.__new__(Plugin) plugin.installation_service = Mock() @@ -49,10 +54,10 @@ class PluginMigrationTests(unittest.TestCase): plugin.installation_service.install.assert_not_called() plugin.flatpak_service.prepare_app.assert_not_called() finally: - self._restore(previous_decky, previous_tomllib) + self._restore(previous_decky, previous_tomllib, previous_plugin) def test_uninstall_cleans_owned_flatpak_state_and_profiles(self): - Plugin, _decky, previous_decky, previous_tomllib = self._load_plugin() + Plugin, _decky, previous_decky, previous_tomllib, previous_plugin = self._load_plugin() try: plugin = Plugin.__new__(Plugin) plugin.installation_service = Mock() @@ -66,10 +71,10 @@ class PluginMigrationTests(unittest.TestCase): plugin.configuration_service.reset_all_flatpak_configs.assert_called_once_with() plugin.installation_service.cleanup_on_uninstall.assert_called_once_with() finally: - self._restore(previous_decky, previous_tomllib) + self._restore(previous_decky, previous_tomllib, previous_plugin) def test_uninstall_preserves_profiles_when_flatpak_cleanup_fails(self): - Plugin, _decky, previous_decky, previous_tomllib = self._load_plugin() + Plugin, _decky, previous_decky, previous_tomllib, previous_plugin = self._load_plugin() try: plugin = Plugin.__new__(Plugin) plugin.installation_service = Mock() @@ -82,7 +87,7 @@ class PluginMigrationTests(unittest.TestCase): plugin.configuration_service.reset_all_flatpak_configs.assert_not_called() plugin.installation_service.cleanup_on_uninstall.assert_called_once_with() finally: - self._restore(previous_decky, previous_tomllib) + self._restore(previous_decky, previous_tomllib, previous_plugin) if __name__ == "__main__": -- cgit v1.2.3 From 904e2e6131071c3b132d3148947b613c2830b1bb Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Thu, 10 Sep 2026 20:49:05 -0400 Subject: fix; correct flatpak extension sources --- tests/test_flatpak_service.py | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) (limited to 'tests') diff --git a/tests/test_flatpak_service.py b/tests/test_flatpak_service.py index 065871b..4d804c7 100644 --- a/tests/test_flatpak_service.py +++ b/tests/test_flatpak_service.py @@ -32,10 +32,8 @@ class FlatpakServiceTests(unittest.TestCase): self.runtime_metadata = "" self.user_branches = set() self.system_branches = set() + self.user_extension_origin = "flathub" self.apps = {"com.example.Game": "Example Game"} - self.bundle = self.home / "lsfg-vk.flatpak" - self.bundle.write_bytes(b"bundle") - self.service._bundled_extension_path = Mock(return_value=self.bundle) self.dll_dir = self.home / ".local/share/Steam/steamapps/common/Lossless Scaling" self.service._dll_directory = Mock(return_value=self.dll_dir) @@ -120,6 +118,8 @@ class FlatpakServiceTests(unittest.TestCase): return self._result(self.runtime_ref + "\n") if args[:2] == ["info", "--show-metadata"]: return self._result(self.runtime_metadata) + if args[:3] == ["info", "--user", "--show-origin"]: + return self._result(self.user_extension_origin) if args[:2] == ["list", "--app"]: return self._result("".join(f"{name}\t{app_id}\n" for app_id, name in self.apps.items())) if args[0] == "list": @@ -157,6 +157,22 @@ class FlatpakServiceTests(unittest.TestCase): self.assertTrue(response["owned"]) self.assertEqual(response["runtime_branch"], "24.08") self.assertEqual(self.user_branches, {"24.08"}) + install_calls = [ + call.args[0] + for call in self.service._run_flatpak_command.call_args_list + if call.args[0][0] == "install" + ] + self.assertEqual( + install_calls, + [[ + "install", + "--user", + "--noninteractive", + "--or-update", + "flathub", + "org.freedesktop.Platform.VulkanLayer.lsfgvk//24.08", + ]], + ) status = self.service._app_override_status("com.example.Game") self.assertTrue(status["prepared"]) content = self.service._override_path("com.example.Game").read_text(encoding="utf-8") @@ -180,6 +196,25 @@ class FlatpakServiceTests(unittest.TestCase): install_calls = [call for call in self.service._run_flatpak_command.call_args_list if call.args[0][0] == "install"] self.assertEqual(len(install_calls), 1) + def test_replaces_extension_from_another_remote(self): + self.user_branches = {"24.08"} + self.user_extension_origin = "lsfgvk-origin" + + response = self.service.install_extension("24.08") + + self.assertTrue(response["success"]) + commands = [call.args[0] for call in self.service._run_flatpak_command.call_args_list] + self.assertIn( + [ + "uninstall", + "--user", + "--noninteractive", + "org.freedesktop.Platform.VulkanLayer.lsfgvk/x86_64/24.08", + ], + commands, + ) + self.assertEqual(self.user_branches, {"24.08"}) + def test_preinstalled_runtime_is_not_owned(self): self.system_branches = {"24.08"} response = self.service.prepare_app("com.example.Game") -- cgit v1.2.3 From d2bfdafa92f3d31cc5392bf8a5a1f1df5c2358ce Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 11 Sep 2026 09:05:14 -0400 Subject: flatpak correctness, ui alignment, tests --- tests/nowPlaying.test.ts | 76 +++++++++++++++++++++++++++++++++++ tests/test_flatpak_profile_service.py | 25 +++++++++++- tests/test_flatpak_service.py | 6 +++ 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 tests/nowPlaying.test.ts (limited to 'tests') diff --git a/tests/nowPlaying.test.ts b/tests/nowPlaying.test.ts new file mode 100644 index 0000000..3f0d891 --- /dev/null +++ b/tests/nowPlaying.test.ts @@ -0,0 +1,76 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { resolveNowPlayingTarget, selectMostRecentRunningFlatpak } from "../src/utils/nowPlaying.ts"; + +const flatpak = (app_id: string, app_name = app_id) => ({ + app_id, + app_name, + runtime_ready: true, + prepared: true, + owned: true, + enabled: true, + profile: `flatpak:${app_id}`, + workarounds: { + dxvkFrameRate: 0, + disableGamescopeWsi: true, + disableHdr: true, + disableSteamdeckMode: false, + disableVkbasalt: false, + enableZink: false, + }, +}); + +const game = (nonSteam = true, configured = true) => ({ + appid: "123456", + name: nonSteam ? "1080 Snowboarding" : "Native Game", + nonSteam, + configured, +}); + +test("selects the newest active managed Flatpak", () => { + const apps = [flatpak("org.example.old"), flatpak("org.example.new")]; + const running = [ + { app_id: "org.example.old", active: true, pid: "100", start_time: 500 }, + { app_id: "org.example.new", active: true, pid: "200", start_time: 600 }, + ]; + + assert.equal(selectMostRecentRunningFlatpak(apps, running)?.app_id, "org.example.new"); +}); + +test("prefers active Flatpak status before process age", () => { + const apps = [flatpak("org.example.running"), flatpak("org.example.active")]; + const running = [ + { app_id: "org.example.running", active: false, pid: "900", start_time: 900 }, + { app_id: "org.example.active", active: true, pid: "100", start_time: 100 }, + ]; + + assert.equal(selectMostRecentRunningFlatpak(apps, running)?.app_id, "org.example.active"); +}); + +test("Flatpak runtime wins while a Steam shortcut is running", () => { + const target = resolveNowPlayingTarget(game(true), flatpak("org.libretro.RetroArch", "RetroArch")); + + assert.equal(target?.kind, "flatpak"); + assert.equal(target?.kind === "flatpak" ? target.launcher?.name : null, "1080 Snowboarding"); +}); + +test("Flatpak runtime wins over a native Steam game", () => { + assert.equal(resolveNowPlayingTarget(game(false), flatpak("org.example.Game"))?.kind, "flatpak"); +}); + +test("direct Flatpak launch creates a Flatpak Now Playing target", () => { + const target = resolveNowPlayingTarget(null, flatpak("org.example.Game")); + + assert.equal(target?.kind, "flatpak"); + assert.equal(target?.kind === "flatpak" ? target.launcher : null, null); +}); + +test("configured Steam target remains the fallback", () => { + const target = resolveNowPlayingTarget(game(false), null); + + assert.equal(target?.kind, "steam"); +}); + +test("unconfigured Steam target has no Now Playing controls", () => { + assert.equal(resolveNowPlayingTarget(game(false, false), null), null); +}); diff --git a/tests/test_flatpak_profile_service.py b/tests/test_flatpak_profile_service.py index 8d58413..2b9933b 100644 --- a/tests/test_flatpak_profile_service.py +++ b/tests/test_flatpak_profile_service.py @@ -14,6 +14,7 @@ sys.modules.setdefault( sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "py_modules")) from lsfg_vk.configuration import ConfigurationService +from lsfg_vk.flatpak_service import FlatpakService from lsfg_vk.flatpak_profile_service import FlatpakProfileService @@ -26,6 +27,7 @@ class FakeFlatpakService: self.state = {"version": 2, "plugin_owned_branches": [], "prepared_apps": {}} self.commands = [] self.running = "" + self.start_times = {} def _read_state(self): return self.state @@ -49,6 +51,9 @@ class FakeFlatpakService: return False, b"" return True, path.read_bytes() + def _process_start_time(self, pid): + return self.start_times.get(pid) + @staticmethod def _write_file(path, content, mode=0o644): path.parent.mkdir(parents=True, exist_ok=True) @@ -175,6 +180,17 @@ class FlatpakProfileServiceTests(unittest.TestCase): self.assertNotIn("--env=DXVK_HDR=0", command) self.assertIn("--env=MESA_LOADER_DRIVER_OVERRIDE=zink", command) + def test_config_update_returns_without_relisting_flatpaks(self): + self.assertTrue(self.service.enable_app(self.app_id)["success"]) + self.service.get_app = Mock(side_effect=AssertionError("config updates must not relist Flatpaks")) + + result = self.service.update_config(self.app_id, {"multiplier": 4}) + + self.assertTrue(result["success"]) + self.assertEqual(result["app_id"], self.app_id) + self.assertEqual(result["config"]["multiplier"], 4) + self.service.get_app.assert_not_called() + def test_remove_restores_exact_original_override_and_profile(self): baseline = "[Environment]\nKEEP=yes\n" self.flatpak._write_file(self.flatpak._override_path(self.app_id), baseline) @@ -199,11 +215,18 @@ class FlatpakProfileServiceTests(unittest.TestCase): def test_running_detection_uses_owned_selector_state(self): self.assertTrue(self.service.enable_app(self.app_id)["success"]) self.flatpak.running = "org.example.Game\ttrue\t1234\norg.other.App\ttrue\t9999\n" + self.flatpak.start_times["1234"] = 200 result = self.service.get_running_apps() self.assertTrue(result["success"]) - self.assertEqual(result["apps"], [{"app_id": self.app_id, "active": True, "pid": "1234"}]) + self.assertEqual(result["apps"], [{"app_id": self.app_id, "active": True, "pid": "1234", "start_time": 200}]) + + def test_process_start_time_parser_handles_parentheses_in_command_name(self): + fields = ["S"] + ["0"] * 18 + ["4242"] + stat = "1234 (retro)arch) " + " ".join(fields) + + self.assertEqual(FlatpakService._parse_process_start_time(stat), 4242) if __name__ == "__main__": diff --git a/tests/test_flatpak_service.py b/tests/test_flatpak_service.py index 4d804c7..17d0016 100644 --- a/tests/test_flatpak_service.py +++ b/tests/test_flatpak_service.py @@ -149,6 +149,12 @@ class FlatpakServiceTests(unittest.TestCase): self.assertEqual(runtime, self.runtime_ref) self.assertEqual(branch, "25.08") + def test_clean_env_targets_deck_user_session_bus(self): + env = self.service._clean_env() + user_id = self.home.stat().st_uid + self.assertEqual(env["XDG_RUNTIME_DIR"], f"/run/user/{user_id}") + self.assertEqual(env["DBUS_SESSION_BUS_ADDRESS"], f"unix:path=/run/user/{user_id}/bus") + def test_prepare_app_installs_runtime_and_persists_narrow_override(self): response = self.service.prepare_app("com.example.Game") -- cgit v1.2.3 From a8388009b2ba141caafe7f2e35f7029d93e7811f Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 11 Sep 2026 09:38:35 -0400 Subject: handle edge cases on flatpaks and ordering --- tests/nowPlaying.test.ts | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/nowPlaying.test.ts b/tests/nowPlaying.test.ts index 3f0d891..8a52b67 100644 --- a/tests/nowPlaying.test.ts +++ b/tests/nowPlaying.test.ts @@ -54,8 +54,12 @@ test("Flatpak runtime wins while a Steam shortcut is running", () => { assert.equal(target?.kind === "flatpak" ? target.launcher?.name : null, "1080 Snowboarding"); }); -test("Flatpak runtime wins over a native Steam game", () => { - assert.equal(resolveNowPlayingTarget(game(false), flatpak("org.example.Game"))?.kind, "flatpak"); +test("native Steam game wins over an unrelated Flatpak", () => { + assert.equal(resolveNowPlayingTarget(game(false), flatpak("org.example.Game"))?.kind, "steam"); +}); + +test("unconfigured native Steam game blocks unrelated Flatpak Now Playing", () => { + assert.equal(resolveNowPlayingTarget(game(false, false), flatpak("org.example.Game")), null); }); test("direct Flatpak launch creates a Flatpak Now Playing target", () => { @@ -65,6 +69,23 @@ test("direct Flatpak launch creates a Flatpak Now Playing target", () => { assert.equal(target?.kind === "flatpak" ? target.launcher : null, null); }); +test("multiple inactive Flatpaks do not create an arbitrary Now Playing target", () => { + const apps = [flatpak("org.example.one"), flatpak("org.example.two")]; + const running = [ + { app_id: "org.example.one", active: false, pid: "100", start_time: 500 }, + { app_id: "org.example.two", active: false, pid: "200", start_time: 600 }, + ]; + + assert.equal(selectMostRecentRunningFlatpak(apps, running), null); +}); + +test("one inactive Flatpak remains a usable fallback", () => { + const apps = [flatpak("org.example.one")]; + const running = [{ app_id: "org.example.one", active: false, pid: "100", start_time: 500 }]; + + assert.equal(selectMostRecentRunningFlatpak(apps, running)?.app_id, "org.example.one"); +}); + test("configured Steam target remains the fallback", () => { const target = resolveNowPlayingTarget(game(false), null); -- cgit v1.2.3 From 954b2abc47d8772211cb8ecee523900f823184e8 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 11 Sep 2026 11:08:12 -0400 Subject: better uninstall cleanup --- tests/test_configuration_profiles.py | 19 +++++++++++ tests/test_installation_cleanup.py | 63 ++++++++++++++++++++++++++++++++++++ tests/test_plugin_migration.py | 8 ++++- tests/test_wrapper_service.py | 29 +++++++++++++++++ 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 tests/test_installation_cleanup.py (limited to 'tests') diff --git a/tests/test_configuration_profiles.py b/tests/test_configuration_profiles.py index 789842e..64db0f1 100644 --- a/tests/test_configuration_profiles.py +++ b/tests/test_configuration_profiles.py @@ -75,6 +75,25 @@ preserve_swapchain_image_count = false self.assertIn("Steam Game", data["profiles"]) self.assertNotIn("flatpak:org.example.Game", data["profiles"]) + def test_global_config_update_does_not_change_profile_values(self): + self.service.update_game_config("123", "Steam Game", {"multiplier": 2}) + + result = self.service.update_global_config({"no_fp16": True}) + data = self.service._get_profile_data() + + self.assertTrue(result["success"]) + self.assertTrue(result["global_config"]["no_fp16"]) + self.assertTrue(data["global_config"]["no_fp16"]) + self.assertEqual(data["profiles"]["Steam Game"]["multiplier"], 2) + + def test_profile_update_cannot_overwrite_global_fp16_setting(self): + self.service.update_global_config({"no_fp16": True}) + + self.service.update_game_config("123", "Steam Game", {"multiplier": 3, "no_fp16": False}) + + data = self.service._get_profile_data() + self.assertTrue(data["global_config"]["no_fp16"]) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_installation_cleanup.py b/tests/test_installation_cleanup.py new file mode 100644 index 0000000..3336505 --- /dev/null +++ b/tests/test_installation_cleanup.py @@ -0,0 +1,63 @@ +import sys +import tempfile +import types +import unittest +from pathlib import Path +from unittest.mock import Mock + + +sys.modules.setdefault( + "decky", + types.SimpleNamespace(DECKY_USER_HOME="/home/deck", logger=Mock()), +) +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "py_modules")) + +from lsfg_vk.base_service import BaseService +from lsfg_vk.installation import InstallationService + + +class InstallationCleanupTests(unittest.TestCase): + def test_uninstall_removes_legacy_files_and_prunes_only_empty_directories(self): + with tempfile.TemporaryDirectory() as temporary: + home = Path(temporary) / "home" / "deck" + service = InstallationService.__new__(InstallationService) + BaseService.__init__(service) + service.log = Mock() + service.user_home = home + service.local_bin_dir = home / ".local/bin" + service.local_lib_dir = home / ".local/lib" + service.local_share_dir = home / ".local/share/vulkan/implicit_layer.d" + service.config_dir = home / ".config/lsfg-vk" + service.config_file_path = service.config_dir / "conf.toml" + service.legacy_script_path = home / "lsfg" + service.lib_file = service.local_lib_dir / "liblsfg-vk-layer.so" + service.lib_x86_file = service.local_lib_dir / "liblsfg-vk-layer.x86.so" + service.json_file = service.local_share_dir / "VkLayer_LSFGVK_frame_generation.json" + service.json_x86_file = service.local_share_dir / "VkLayer_LSFGVK_frame_generation.x86.json" + service.cli_file = service.local_bin_dir / "lsfg-vk-cli" + service.legacy_lib_file = service.local_lib_dir / "liblsfg-vk.so" + service.legacy_json_file = service.local_share_dir / "VkLayer_LS_frame_generation.json" + + for path in ( + service.lib_file, + service.config_file_path, + service.legacy_script_path, + ): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("owned", encoding="utf-8") + unrelated = home / ".local/bin/keep-me" + unrelated.parent.mkdir(parents=True, exist_ok=True) + unrelated.write_text("user file", encoding="utf-8") + + result = service.uninstall() + + self.assertTrue(result["success"]) + self.assertFalse(service.lib_file.exists()) + self.assertFalse(service.config_file_path.exists()) + self.assertFalse(service.legacy_script_path.exists()) + self.assertTrue(unrelated.exists()) + self.assertTrue(service.local_bin_dir.exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_plugin_migration.py b/tests/test_plugin_migration.py index 7ab4621..1a7cb29 100644 --- a/tests/test_plugin_migration.py +++ b/tests/test_plugin_migration.py @@ -63,12 +63,16 @@ class PluginMigrationTests(unittest.TestCase): plugin.installation_service = Mock() plugin.flatpak_service = Mock() plugin.configuration_service = Mock() + plugin.wrapper_service = Mock() plugin.flatpak_service.remove_plugin_owned_environment.return_value = {"success": True} + plugin.configuration_service.reset_all_flatpak_configs.return_value = {"success": True} + plugin.wrapper_service.purge.return_value = {"success": True} asyncio.run(plugin._uninstall()) plugin.flatpak_service.remove_plugin_owned_environment.assert_called_once_with() plugin.configuration_service.reset_all_flatpak_configs.assert_called_once_with() + plugin.wrapper_service.purge.assert_called_once_with() plugin.installation_service.cleanup_on_uninstall.assert_called_once_with() finally: self._restore(previous_decky, previous_tomllib, previous_plugin) @@ -80,12 +84,14 @@ class PluginMigrationTests(unittest.TestCase): plugin.installation_service = Mock() plugin.flatpak_service = Mock() plugin.configuration_service = Mock() + plugin.wrapper_service = Mock() plugin.flatpak_service.remove_plugin_owned_environment.return_value = {"success": False, "error": "changed"} asyncio.run(plugin._uninstall()) plugin.configuration_service.reset_all_flatpak_configs.assert_not_called() - plugin.installation_service.cleanup_on_uninstall.assert_called_once_with() + plugin.wrapper_service.purge.assert_not_called() + plugin.installation_service.cleanup_on_uninstall.assert_not_called() finally: self._restore(previous_decky, previous_tomllib, previous_plugin) diff --git a/tests/test_wrapper_service.py b/tests/test_wrapper_service.py index 5c291c7..20a08ff 100644 --- a/tests/test_wrapper_service.py +++ b/tests/test_wrapper_service.py @@ -158,6 +158,35 @@ class WrapperServiceTests(unittest.TestCase): ) self.assertEqual(result.stdout, "ok") + def test_purge_removes_owned_wrapper_and_state(self): + self.service.set("123", self._state(), non_steam=True) + self.assertTrue(self.service.get("123")["non_steam"]) + self.assertEqual(self.service.list_apps()["apps"][0]["non_steam"], True) + response = self.service.purge() + self.assertTrue(response["success"]) + self.assertEqual(response["removed_files"], [str(self.service.wrapper_path), str(self.service.sidecar_path)]) + self.assertFalse(self.service.wrapper_path.exists()) + self.assertFalse(self.service.sidecar_path.exists()) + + def test_purge_refuses_foreign_wrapper(self): + self.service.wrapper_path.write_text("#!/bin/sh\necho foreign\n", encoding="utf-8") + response = self.service.purge() + self.assertFalse(response["success"]) + self.assertIn("unowned", response["error"]) + self.assertTrue(self.service.wrapper_path.exists()) + + def test_purge_refuses_invalid_state(self): + self.service.config_dir.mkdir(parents=True, exist_ok=True) + self.service.sidecar_path.write_text("not json", encoding="utf-8") + self.service.wrapper_path.write_text( + f"#!/bin/sh\n{self.service.MARKER}\nexec \"$@\"\n", + encoding="utf-8", + ) + response = self.service.purge() + self.assertFalse(response["success"]) + self.assertTrue(self.service.wrapper_path.exists()) + self.assertTrue(self.service.sidecar_path.exists()) + if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From 4b7852b153106a487ad8fac654ea59f00aa8a8e4 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 11 Sep 2026 11:40:53 -0400 Subject: handle multiple flatpak actives --- tests/nowPlaying.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'tests') diff --git a/tests/nowPlaying.test.ts b/tests/nowPlaying.test.ts index 8a52b67..c99ffc4 100644 --- a/tests/nowPlaying.test.ts +++ b/tests/nowPlaying.test.ts @@ -47,6 +47,16 @@ test("prefers active Flatpak status before process age", () => { assert.equal(selectMostRecentRunningFlatpak(apps, running)?.app_id, "org.example.active"); }); +test("deduplicates multiple process rows for one managed Flatpak", () => { + const apps = [flatpak("com.heroicgameslauncher.hgl")]; + const running = [ + { app_id: "com.heroicgameslauncher.hgl", active: false, pid: "228081", start_time: null }, + { app_id: "com.heroicgameslauncher.hgl", active: false, pid: "228116", start_time: null }, + ]; + + assert.equal(selectMostRecentRunningFlatpak(apps, running)?.app_id, "com.heroicgameslauncher.hgl"); +}); + test("Flatpak runtime wins while a Steam shortcut is running", () => { const target = resolveNowPlayingTarget(game(true), flatpak("org.libretro.RetroArch", "RetroArch")); -- cgit v1.2.3 From c7ca9c7ec2660d279f5e6957ef882390a8783667 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 11 Sep 2026 11:47:16 -0400 Subject: handle decky uninstall to avoid unlaunchable game wrappers --- tests/test_plugin_migration.py | 5 +++-- tests/test_wrapper_service.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) (limited to 'tests') diff --git a/tests/test_plugin_migration.py b/tests/test_plugin_migration.py index 1a7cb29..fc51470 100644 --- a/tests/test_plugin_migration.py +++ b/tests/test_plugin_migration.py @@ -66,13 +66,14 @@ class PluginMigrationTests(unittest.TestCase): plugin.wrapper_service = Mock() plugin.flatpak_service.remove_plugin_owned_environment.return_value = {"success": True} plugin.configuration_service.reset_all_flatpak_configs.return_value = {"success": True} - plugin.wrapper_service.purge.return_value = {"success": True} + plugin.wrapper_service.neutralize.return_value = {"success": True} asyncio.run(plugin._uninstall()) plugin.flatpak_service.remove_plugin_owned_environment.assert_called_once_with() plugin.configuration_service.reset_all_flatpak_configs.assert_called_once_with() - plugin.wrapper_service.purge.assert_called_once_with() + plugin.wrapper_service.neutralize.assert_called_once_with() + plugin.wrapper_service.purge.assert_not_called() plugin.installation_service.cleanup_on_uninstall.assert_called_once_with() finally: self._restore(previous_decky, previous_tomllib, previous_plugin) diff --git a/tests/test_wrapper_service.py b/tests/test_wrapper_service.py index 20a08ff..46e029b 100644 --- a/tests/test_wrapper_service.py +++ b/tests/test_wrapper_service.py @@ -187,6 +187,24 @@ class WrapperServiceTests(unittest.TestCase): self.assertTrue(self.service.wrapper_path.exists()) self.assertTrue(self.service.sidecar_path.exists()) + def test_neutralize_leaves_dependency_free_passthrough_wrapper(self): + self.service.set("123", self._state()) + response = self.service.neutralize() + self.assertTrue(response["success"]) + self.assertFalse(self.service.sidecar_path.exists()) + self.assertTrue(self.service.wrapper_path.exists()) + content = self.service.wrapper_path.read_text(encoding="utf-8") + self.assertIn(self.service.MARKER, content) + self.assertNotIn("LSFGVK_CONFIG", content) + self.assertEqual(self._run(123, "/usr/bin/printf", "ok").stdout, "ok") + + def test_neutralize_refuses_foreign_wrapper(self): + self.service.wrapper_path.write_text("#!/bin/sh\necho foreign\n", encoding="utf-8") + response = self.service.neutralize() + self.assertFalse(response["success"]) + self.assertIn("unowned", response["error"]) + self.assertEqual(self.service.wrapper_path.read_text(encoding="utf-8"), "#!/bin/sh\necho foreign\n") + if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From f3074fbe1427411dc3b5597d2e87918383299e3f Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 11 Sep 2026 13:47:52 -0400 Subject: feat: non steam tab, faster bulk actions --- tests/gameTargets.test.ts | 41 ++++++++++++++++++++++++++++++++++++ tests/nowPlaying.test.ts | 7 ++++++ tests/test_configuration_profiles.py | 17 ++++++++++++++- 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 tests/gameTargets.test.ts (limited to 'tests') diff --git a/tests/gameTargets.test.ts b/tests/gameTargets.test.ts new file mode 100644 index 0000000..9896d76 --- /dev/null +++ b/tests/gameTargets.test.ts @@ -0,0 +1,41 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { getTargetSource, mergeGameTargets, targetsForSource } from "../src/utils/gameTargets.ts"; + +const config = (appid: string, profile: string) => ({ appid, profile, config: {} }); + +test("workaround sidecar source wins over current discovery metadata", () => { + const installed = [{ appid: "123", name: "Shortcut", nonSteam: false }]; + const workarounds = [{ appid: "123", non_steam: true, command_token_added: true }]; + + assert.equal(getTargetSource("123", installed, workarounds), "nonSteam"); + assert.equal(mergeGameTargets([config("123", "Shortcut")], installed, workarounds)[0].source, "nonSteam"); +}); + +test("configured profiles without reliable source are unknown", () => { + const targets = mergeGameTargets([config("456", "Missing Game")], [], []); + + assert.deepEqual(targets[0], { + appid: "456", + name: "Missing Game", + nonSteam: false, + source: "unknown", + configured: true, + }); +}); + +test("unknown configured profiles are visible in both source tabs", () => { + const targets = mergeGameTargets([ + config("123", "Steam Game"), + config("456", "Missing Game"), + ], [ + { appid: "123", name: "Steam Game", nonSteam: false }, + { appid: "789", name: "Shortcut", nonSteam: true }, + ], []); + + const steamTargets = targetsForSource(targets, "steam"); + const nonSteamTargets = targetsForSource(targets, "nonSteam"); + + assert.deepEqual(steamTargets.map((target) => target.appid).sort(), ["123", "456"]); + assert.deepEqual(nonSteamTargets.map((target) => target.appid).sort(), ["456", "789"]); +}); diff --git a/tests/nowPlaying.test.ts b/tests/nowPlaying.test.ts index c99ffc4..a6b116a 100644 --- a/tests/nowPlaying.test.ts +++ b/tests/nowPlaying.test.ts @@ -24,6 +24,7 @@ const game = (nonSteam = true, configured = true) => ({ appid: "123456", name: nonSteam ? "1080 Snowboarding" : "Native Game", nonSteam, + source: nonSteam ? "nonSteam" : "steam", configured, }); @@ -102,6 +103,12 @@ test("configured Steam target remains the fallback", () => { assert.equal(target?.kind, "steam"); }); +test("configured non-Steam target remains the fallback", () => { + const target = resolveNowPlayingTarget(game(true), null); + + assert.equal(target?.kind, "nonSteam"); +}); + test("unconfigured Steam target has no Now Playing controls", () => { assert.equal(resolveNowPlayingTarget(game(false, false), null), null); }); diff --git a/tests/test_configuration_profiles.py b/tests/test_configuration_profiles.py index 64db0f1..898a9e7 100644 --- a/tests/test_configuration_profiles.py +++ b/tests/test_configuration_profiles.py @@ -3,7 +3,7 @@ import tempfile import types import unittest from pathlib import Path -from unittest.mock import Mock +from unittest.mock import Mock, patch sys.modules.setdefault( @@ -64,6 +64,21 @@ preserve_swapchain_image_count = false self.assertIn("flatpak:org.example.Game", data["profiles"]) self.assertEqual(data["profiles"]["flatpak:org.example.Game"]["multiplier"], 3) + def test_scoped_game_reset_is_one_write_and_preserves_other_profiles(self): + self.service.update_game_config("123", "Steam Game", {"multiplier": 2}) + self.service.update_game_config("456", "Non-Steam Game", {"multiplier": 3}) + self.service.update_flatpak_config("org.example.Game", {"multiplier": 4}) + + with patch.object(self.service, "_save_profile_data", wraps=self.service._save_profile_data) as save: + result = self.service.reset_game_configs(["123"]) + + data = self.service._get_profile_data() + self.assertTrue(result["success"]) + self.assertEqual(save.call_count, 1) + self.assertNotIn("Steam Game", data["profiles"]) + self.assertIn("Non-Steam Game", data["profiles"]) + self.assertIn("flatpak:org.example.Game", data["profiles"]) + def test_flatpak_reset_all_preserves_steam_profiles(self): self.service.update_game_config("123", "Steam Game", {"multiplier": 2}) self.service.update_flatpak_config("org.example.Game", {"multiplier": 3}) -- cgit v1.2.3 From 81feb03288166755545401b9df2ff2c9bc3ae7d7 Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Fri, 11 Sep 2026 16:48:35 -0400 Subject: handle flatpak grey, id proton and exclusions --- tests/gameTargets.test.ts | 9 +++++++++ tests/steamLaunchOptions.test.ts | 20 +++++++++++++++++--- tests/test_steam_service.py | 20 +++++++++++++++++++- 3 files changed, 45 insertions(+), 4 deletions(-) (limited to 'tests') diff --git a/tests/gameTargets.test.ts b/tests/gameTargets.test.ts index 9896d76..2eb8b7e 100644 --- a/tests/gameTargets.test.ts +++ b/tests/gameTargets.test.ts @@ -39,3 +39,12 @@ test("unknown configured profiles are visible in both source tabs", () => { assert.deepEqual(steamTargets.map((target) => target.appid).sort(), ["123", "456"]); assert.deepEqual(nonSteamTargets.map((target) => target.appid).sort(), ["456", "789"]); }); + +test("direct Flatpak shortcuts remain non-Steam targets", () => { + const targets = mergeGameTargets([], [ + { appid: "123", name: "Flatpak shortcut", nonSteam: true, isFlatpakShortcut: true }, + ], []); + + assert.equal(targets[0].source, "nonSteam"); + assert.equal(targets[0].isFlatpakShortcut, true); +}); diff --git a/tests/steamLaunchOptions.test.ts b/tests/steamLaunchOptions.test.ts index 40aeb3c..1d2f762 100644 --- a/tests/steamLaunchOptions.test.ts +++ b/tests/steamLaunchOptions.test.ts @@ -28,7 +28,7 @@ test("inserts one wrapper immediately before an existing command macro", () => { }); }); -test("normalizes blank and argument-only shortcut fields", () => { +test("normalizes blank, malformed, and argument-only launch fields", () => { assert.deepEqual(installWrapperLaunchOption("", wrapper), { options: `${wrapper} %command%`, commandTokenAdded: true, @@ -37,8 +37,22 @@ test("normalizes blank and argument-only shortcut fields", () => { options: `FOO=bar ${wrapper} %command% --windowed`, commandTokenAdded: true, }); - assert.throws(() => installWrapperLaunchOption("gamemoderun --windowed", wrapper), /refusing to guess/); - assert.throws(() => installWrapperLaunchOption('"%command%"', wrapper), /refusing to guess/); + assert.deepEqual(installWrapperLaunchOption("gamemoderun --windowed", wrapper), { + options: `${wrapper} %command% gamemoderun --windowed`, + commandTokenAdded: true, + }); + assert.deepEqual(installWrapperLaunchOption('"%command%"', wrapper), { + options: `${wrapper} %command%`, + commandTokenAdded: false, + }); + assert.deepEqual(installWrapperLaunchOption(`${wrapper} %command`, wrapper), { + options: `${wrapper} %command%`, + commandTokenAdded: false, + }); + assert.deepEqual(installWrapperLaunchOption(`${wrapper} --windowed`, wrapper), { + options: `${wrapper} %command% --windowed`, + commandTokenAdded: true, + }); }); test("preserves assignments quoting suffixes and released wrapper cleanup", () => { diff --git a/tests/test_steam_service.py b/tests/test_steam_service.py index 004ffb6..75cdabf 100644 --- a/tests/test_steam_service.py +++ b/tests/test_steam_service.py @@ -15,7 +15,7 @@ from lsfg_vk.steam_service import SteamService class SteamShortcutTests(unittest.TestCase): - def test_direct_flatpak_shortcut_is_ordinary_non_steam_metadata(self): + def test_direct_flatpak_shortcut_is_marked(self): game = SteamService._shortcut_game( { "appid": 123456, @@ -30,6 +30,24 @@ class SteamShortcutTests(unittest.TestCase): "appid": "123456", "name": "PCSX2 shortcut", "nonSteam": True, + "isFlatpakShortcut": True, + }) + + def test_bare_flatpak_shortcut_is_marked(self): + game = SteamService._shortcut_game( + { + "appid": 654321, + "AppName": "Faugus shortcut", + "Exe": '"flatpak"', + "LaunchOptions": "run io.github.Faugus.faugus-launcher --game elliot", + } + ) + + self.assertEqual(game, { + "appid": "654321", + "name": "Faugus shortcut", + "nonSteam": True, + "isFlatpakShortcut": True, }) def test_emudeck_launcher_is_ordinary_non_steam_metadata(self): -- cgit v1.2.3