Dotfiles_ArchAP/tests/test_session_restore.py

343 lines
17 KiB
Python

"""Contrat Restore Plan V1 : intentions déterministes et aucune exécution."""
import copy
import importlib.util
import io
import json
from pathlib import Path
import unittest
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
RESTORE_SOURCE = ROOT / "quickshell/.config/quickshell/labfy-sway/session/session_restore.py"
SNAPSHOT_SOURCE = ROOT / "quickshell/.config/quickshell/labfy-sway/session/session_snapshot.py"
def load_module(name, path):
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
restore = load_module("session_restore_test", RESTORE_SOURCE)
snapshot_backend = load_module("session_snapshot_restore_test", SNAPSHOT_SOURCE)
def window(
window_id,
app_id,
*,
desktop_entry=None,
identity_confidence="exact",
title=None,
workspace="1",
output="eDP-1",
category="user-application",
managed_by=None,
floating=False,
fullscreen=0,
focused=False,
con_id=100,
pid=1000,
parent=None,
index=0,
xclass=None,
instance=None,
executable=None,
scratchpad=None,
):
return {
"window_id": window_id,
"snapshot_identity": f"{app_id or xclass or executable or 'unknown'}#{window_id[1:]}",
"runtime": {"con_id": con_id, "pid": pid},
"workspace": workspace,
"output": output,
"app_id": app_id,
"xwayland": {"class": xclass, "instance": instance},
"executable_basename": executable or app_id,
"title_hint": title,
"restore_identity": {
"app_id": app_id,
"class": xclass,
"instance": instance,
"desktop_entry": desktop_entry,
"confidence": identity_confidence,
"matched_by": "desktop-id" if desktop_entry else None,
},
"restore_adapter": "generic-desktop" if desktop_entry else "unknown",
"classification": {"category": category, "managed_by": managed_by},
"focused": focused,
"urgent": False,
"floating": floating,
"fullscreen_mode": fullscreen,
"rect": {"x": 0, "y": 0, "width": 800, "height": 600},
"percent": 1.0,
"marks": [],
"scratchpad": scratchpad or {"member": False, "visibility": "not-applicable", "state": "none"},
"tree_position": {"parent_container_id": parent, "branch": "tiling", "index": index},
}
def session(
windows,
*,
persistent=False,
name="test",
outputs=("eDP-1",),
workspace_outputs=None,
containers=None,
focused=None,
captured_at="2026-10-05T00:00:00.000Z",
):
workspace_outputs = workspace_outputs or {}
workspace_names = []
for item in windows:
workspace = item.get("workspace")
if workspace is not None and workspace not in workspace_names:
workspace_names.append(workspace)
metadata = {
"captured_at": captured_at,
"desktop": "sway:wlroots:swayfx",
"session_type": "wayland",
}
if persistent:
metadata.update({
"session_name": name,
"created_at": captured_at,
"updated_at": captured_at,
})
focused_id = focused
if focused_id is None:
focused_id = next((item["window_id"] for item in windows if item.get("focused")), None)
return {
"schema": "labfy.sway.session-snapshot",
"version": 1,
"metadata": metadata,
"compositor": {
"variant": "sway",
"version": "0.6",
"sway_version": "1.12.0",
"layout_engine": "sway-native",
},
"outputs": [
{"name": item, "active": True, "focused": index == 0}
for index, item in enumerate(outputs)
],
"workspaces": [
{
"name": item,
"output": workspace_outputs.get(item, outputs[0] if outputs else None),
"layout": "splith",
"orientation": "horizontal",
}
for item in workspace_names
],
"containers": containers or [],
"windows": copy.deepcopy(windows),
"focus": {
"output": outputs[0] if outputs else None,
"workspace": next((item.get("workspace") for item in windows if item["window_id"] == focused_id), None),
"container_id": None,
"window_id": focused_id,
},
}
def action_types(plan):
return [item["action"] for item in plan["actions"]]
class RestorePlannerTest(unittest.TestCase):
def test_exact_reuse_identical_session_has_no_mutation(self):
saved = window("w1", "firefox", desktop_entry="firefox.desktop", focused=True)
live = copy.deepcopy(saved)
live["runtime"] = {"con_id": 999, "pid": 8888}
plan = restore.build_restore_plan(session([saved], persistent=True), session([live]))
self.assertEqual(action_types(plan), ["reuse-window"])
self.assertEqual(plan["matches"][0]["confidence"], "exact")
self.assertEqual(plan["diagnostics"]["mutating_commands_executed"], 0)
def test_missing_exact_user_application_uses_future_uwsm_backend(self):
saved = window("w1", "firefox", desktop_entry="firefox.desktop", focused=True)
plan = restore.build_restore_plan(session([saved], persistent=True), session([]))
launch = next(item for item in plan["actions"] if item["action"] == "launch-application")
self.assertEqual(launch["launch"]["argv"], ["uwsm", "app", "--", "firefox.desktop"])
self.assertFalse(launch["launch"]["execute_in_step19a"])
def test_synthetic_firefox_kitty_limusic_with_only_limusic_live(self):
saved = [
window("w1", "firefox", desktop_entry="firefox.desktop"),
window("w2", "kitty", desktop_entry="kitty.desktop", executable="kitty"),
window("w3", "limusic-app", desktop_entry="limusic.desktop", workspace="10", category="autostart-managed-application", managed_by="sway-autostart"),
]
live = [copy.deepcopy(saved[2])]
plan = restore.build_restore_plan(session(saved, persistent=True), session(live))
launches = [item["desktop_entry"] for item in plan["actions"] if item["action"] == "launch-application"]
self.assertEqual(launches, ["firefox.desktop", "kitty.desktop"])
self.assertEqual(sum(item["action"] == "reuse-window" for item in plan["actions"]), 1)
def test_existing_application_on_wrong_workspace_is_reused_then_moved(self):
saved = window("w1", "firefox", desktop_entry="firefox.desktop", workspace="1", focused=True)
live = window("w1", "firefox", desktop_entry="firefox.desktop", workspace="2", focused=True)
plan = restore.build_restore_plan(session([saved], persistent=True), session([live]))
self.assertIn("reuse-window", action_types(plan))
self.assertIn("move-to-workspace", action_types(plan))
self.assertNotIn("launch-application", action_types(plan))
move = next(item for item in plan["actions"] if item["action"] == "move-to-workspace")
self.assertEqual(move["desired_workspace"], "1")
def test_multi_window_matches_only_best_title_and_never_reuses_twice(self):
saved_a = window("w1", "firefox", desktop_entry="firefox.desktop", title="Firefox — A")
saved_b = window("w2", "firefox", desktop_entry="firefox.desktop", title="Firefox — B")
live_b = window("w1", "firefox", desktop_entry="firefox.desktop", title="Firefox — B", pid=5000)
plan = restore.build_restore_plan(session([saved_a, saved_b], persistent=True), session([live_b]))
self.assertEqual(len(plan["matches"]), 1)
self.assertEqual(plan["matches"][0]["source_window"], "w2")
launch = next(item for item in plan["actions"] if item["action"] == "launch-application")
self.assertEqual(launch["source_window"], "w1")
def test_title_hint_is_not_required_identity(self):
saved = window("w1", "firefox", desktop_entry="firefox.desktop", title="Ancien titre")
live = window("w9", "firefox", desktop_entry="firefox.desktop", title="Nouveau titre")
plan = restore.build_restore_plan(session([saved], persistent=True), session([live]))
self.assertEqual(plan["matches"][0]["source_window"], "w1")
def test_app_id_without_desktop_entry_is_high_confidence(self):
saved = window("w1", "custom-app", identity_confidence="unresolved")
live = window("w7", "custom-app", identity_confidence="unresolved")
plan = restore.build_restore_plan(session([saved], persistent=True), session([live]))
self.assertEqual(plan["matches"][0]["confidence"], "high")
self.assertIn("app-id", plan["matches"][0]["matched_by"])
def test_xwayland_class_without_app_id_is_high_confidence(self):
saved = window("w1", None, xclass="LegacyApp", executable=None, identity_confidence="unresolved")
live = window("w8", None, xclass="LegacyApp", executable=None, identity_confidence="unresolved")
saved["executable_basename"] = None
live["executable_basename"] = None
plan = restore.build_restore_plan(session([saved], persistent=True), session([live]))
self.assertEqual(plan["matches"][0]["confidence"], "high")
self.assertEqual(plan["matches"][0]["matched_by"], ["class"])
def test_indistinguishable_live_candidates_are_ambiguous_but_one_to_one(self):
saved = window("w1", "firefox", desktop_entry="firefox.desktop")
first = window("w1", "firefox", desktop_entry="firefox.desktop", pid=2000)
second = window("w2", "firefox", desktop_entry="firefox.desktop", pid=3000)
plan = restore.build_restore_plan(session([saved], persistent=True), session([first, second]))
self.assertEqual(len(plan["matches"]), 1)
self.assertEqual(plan["matches"][0]["confidence"], "ambiguous")
def test_absent_autostart_application_is_not_launched(self):
saved = window("w1", "limusic-app", desktop_entry="limusic.desktop", category="autostart-managed-application", managed_by="sway-autostart")
plan = restore.build_restore_plan(session([saved], persistent=True), session([]))
self.assertEqual(action_types(plan), ["skip-autostart-managed"])
self.assertNotIn("launch-application", action_types(plan))
def test_unresolved_window_requires_manual_action(self):
saved = window("w1", None, identity_confidence="unresolved", executable=None)
saved["executable_basename"] = None
plan = restore.build_restore_plan(session([saved], persistent=True), session([]))
self.assertEqual(action_types(plan), ["manual-required"])
self.assertEqual(plan["actions"][0]["reason"], "no-exact-launch-identity")
def test_missing_output_is_explicit_and_has_no_fallback_launch(self):
saved = window("w1", "firefox", desktop_entry="firefox.desktop", output="HDMI-A-1")
plan = restore.build_restore_plan(session([saved], persistent=True, outputs=("HDMI-A-1",)), session([], outputs=("eDP-1",)))
self.assertEqual(action_types(plan), ["manual-required"])
self.assertEqual(plan["actions"][0]["policy"], "deferred-no-fallback")
def test_floating_difference_produces_intention_only(self):
saved = window("w1", "kitty", desktop_entry="kitty.desktop", floating=True)
live = window("w1", "kitty", desktop_entry="kitty.desktop", floating=False)
plan = restore.build_restore_plan(session([saved], persistent=True), session([live]))
action = next(item for item in plan["actions"] if item["action"] == "restore-floating")
self.assertTrue(action["desired_floating"])
def test_fullscreen_difference_produces_intention_only(self):
saved = window("w1", "firefox", desktop_entry="firefox.desktop", fullscreen=1)
live = window("w1", "firefox", desktop_entry="firefox.desktop", fullscreen=0)
plan = restore.build_restore_plan(session([saved], persistent=True), session([live]))
action = next(item for item in plan["actions"] if item["action"] == "restore-fullscreen")
self.assertEqual(action["desired_fullscreen_mode"], 1)
def test_layout_difference_is_partially_supported(self):
saved = window("w1", "firefox", desktop_entry="firefox.desktop", parent="c1")
live = window("w1", "firefox", desktop_entry="firefox.desktop")
container = {
"container_id": "c1", "parent_container_id": None,
"tree_position": {"branch": "tiling", "index": 0},
"layout": "splitv", "orientation": "vertical", "percent": 1.0,
}
plan = restore.build_restore_plan(session([saved], persistent=True, containers=[container]), session([live]))
action = next(item for item in plan["actions"] if item["action"] == "restore-tree-position")
self.assertEqual(action["capability"], "partially-supported")
def test_scratchpad_hidden_intention(self):
scratch = {"member": True, "visibility": "hidden", "state": "fresh"}
saved = window("w1", "kitty", desktop_entry="kitty.desktop", workspace=None, output=None, scratchpad=scratch)
live = window("w1", "kitty", desktop_entry="kitty.desktop")
plan = restore.build_restore_plan(session([saved], persistent=True), session([live]))
self.assertIn("restore-scratchpad-hidden", action_types(plan))
def test_focus_is_last_after_mutating_intentions(self):
saved = window("w1", "firefox", desktop_entry="firefox.desktop", workspace="1", focused=True)
live = window("w1", "firefox", desktop_entry="firefox.desktop", workspace="2", focused=True)
plan = restore.build_restore_plan(session([saved], persistent=True, focused="w1"), session([live], focused="w1"))
self.assertEqual(plan["actions"][-1]["action"], "restore-focus")
self.assertTrue(plan["actions"][-1]["depends_on"])
def test_infrastructure_is_never_launched(self):
saved = window("w1", "quickshell", desktop_entry="org.quickshell.desktop", category="session-infrastructure")
plan = restore.build_restore_plan(session([saved], persistent=True), session([]))
self.assertEqual(action_types(plan), ["manual-required"])
self.assertNotIn("launch-application", action_types(plan))
def test_runtime_ids_are_observations_not_matching_identity(self):
saved = window("w1", "firefox", desktop_entry="firefox.desktop", con_id=7, pid=10)
live = window("w9", "firefox", desktop_entry="firefox.desktop", con_id=900, pid=9999)
plan = restore.build_restore_plan(session([saved], persistent=True), session([live]))
self.assertEqual(plan["matches"][0]["source_window"], "w1")
self.assertEqual(plan["policy"]["runtime_identity_fields"], [])
self.assertEqual(plan["live"]["windows"][0]["runtime"], {"con_id": 900, "pid": 9999})
def test_same_inputs_produce_byte_equivalent_plan(self):
saved = [
window("w1", "firefox", desktop_entry="firefox.desktop", title="A"),
window("w2", "firefox", desktop_entry="firefox.desktop", title="B"),
]
live = [window("w7", "firefox", desktop_entry="firefox.desktop", title="B")]
source = session(saved, persistent=True)
current = session(live)
first = restore.build_restore_plan(source, current)
second = restore.build_restore_plan(copy.deepcopy(source), copy.deepcopy(current))
self.assertEqual(json.dumps(first, sort_keys=True), json.dumps(second, sort_keys=True))
def test_cli_plan_uses_only_loader_collector_and_pure_planner(self):
saved = window("w1", "firefox", desktop_entry="firefox.desktop")
source = session([saved], persistent=True, name="cli")
current = session([copy.deepcopy(saved)])
before = copy.deepcopy(source)
output = io.StringIO()
with mock.patch.object(snapshot_backend, "show_session", return_value=source) as loader, mock.patch.object(
snapshot_backend, "collect", return_value=current
) as collector, mock.patch.object(snapshot_backend, "validate_snapshot") as validator, mock.patch("sys.stdout", output):
self.assertEqual(snapshot_backend.main(("plan", "cli")), 0)
result = json.loads(output.getvalue())
self.assertEqual(result["schema"], "labfy.sway.restore-plan")
loader.assert_called_once_with("cli")
collector.assert_called_once()
validator.assert_called_once_with(current)
self.assertEqual(source, before)
self.assertEqual(result["diagnostics"]["applications_launched"], 0)
def test_planner_module_has_no_process_or_filesystem_execution_api(self):
source_text = RESTORE_SOURCE.read_text(encoding="utf-8")
self.assertNotIn("import subprocess", source_text)
self.assertNotIn("subprocess.", source_text)
self.assertNotIn("swaymsg", source_text)
self.assertNotIn("os.system", source_text)
if __name__ == "__main__":
unittest.main()