import Quickshell import Quickshell.Io import QtQuick import qs.services // Does a change made through the service actually stick, and does the service // SEE that it stuck? // // The first version of this harness set every role to the handler it already // had and asserted no error appeared. It passed while the feature was broken: // the write landed, the refresh afterwards silently no-oped, and the settings // page kept showing the old value with no error anywhere. "No error" is not // evidence of anything -- the value has to move. // // So this changes one role to a genuinely different application, waits for the // service to report the new value, and changes it back. The contract restores // the original from the outside regardless of how this ends. ShellRoot { id: root readonly property string role: "browser" property string original: "" property string target: "" property int phase: 0 property var log: [] property bool done: false function desktopId(entry) { const id = String(entry?.id ?? ""); return id.endsWith(".desktop") ? id : id + ".desktop"; } function isBrowser(entry) { // DesktopEntries returns a QML list, for which Array.isArray is false; // joining first and splitting on both separators handles either form. const raw = entry.categories; const joined = Array.isArray(raw) ? raw.join(";") : String(raw ?? ""); for (const value of joined.split(/[;,]/)) { if (value.trim().toLowerCase() === "webbrowser") return true; } return false; } function finish(outcome) { if (root.done) return; root.done = true; Quickshell.execDetached(["sh", "-c", "printf '%s' " + JSON.stringify(JSON.stringify({ outcome: outcome, original: root.original, target: root.target, log: root.log, error: String(DefaultApps.lastError) })) + " > " + Quickshell.env("PANAMA_HARNESS_OUT")]); Qt.callLater(() => Qt.quit()); } Component.onCompleted: DefaultApps.refresh() Timer { interval: 300 repeat: true running: !root.done onTriggered: { const apps = DesktopEntries.applications.values; const handlers = DefaultApps.handlers ?? ({}); // DesktopEntries populates asynchronously; the service checks a // desktop id against it, so starting early fails for the wrong // reason. if (apps.length === 0 || Object.keys(handlers).length === 0) return; if (DefaultApps.busy) return; const current = String(handlers[root.role] ?? ""); if (root.phase === 0) { root.original = current; const other = apps.filter(entry => root.isBrowser(entry) && root.desktopId(entry) !== current); if (other.length === 0) { root.finish("skipped-no-second-browser"); return; } root.target = root.desktopId(other[0]); root.log = root.log.concat(["start at " + current]); root.phase = 1; DefaultApps.setDefault(root.role, root.target); return; } if (root.phase === 1) { if (current !== root.target) { root.log = root.log.concat(["after change the service still reports " + current]); root.phase = 4; // Put it back before reporting the failure. DefaultApps.setDefault(root.role, root.original); return; } root.log = root.log.concat(["service observed " + current]); root.phase = 2; DefaultApps.setDefault(root.role, root.original); return; } if (root.phase === 2) { if (current !== root.original) { root.log = root.log.concat(["restore not observed, service reports " + current]); root.finish("restore-not-observed"); return; } root.log = root.log.concat(["restored to " + current]); root.finish("ok"); return; } if (root.phase === 4) { root.finish("change-not-observed"); return; } } } // A stuck harness fails rather than hangs. Timer { interval: 40000 running: true onTriggered: root.finish("timeout") } }