"""Opt-in multi-window preferences and real service fault recovery. FERESE_TEST_RIME=1 python3 scripts/tests/test_rime_resilience_isolated.py Uses disposable native GTK windows only; never writes user chat/profile data. """ import importlib.util import json import os from pathlib import Path import subprocess import time import unittest ROOT = Path(__file__).resolve().parents[2] spec = importlib.util.spec_from_file_location("preview", ROOT / "scripts/rime-preview.py") p = importlib.util.module_from_spec(spec) spec.loader.exec_module(p) ALT = "ferese-rime-second-20261028.service" @unittest.skipUnless(os.environ.get("FERESE_TEST_RIME") == "1", "opt-in Rime resilience validation") class Resilience(unittest.TestCase): def test_preferences_and_recovery(self): p.start() env = {**os.environ, **p.child_environment()} results = [] def ctl(*args): return json.loads(p.run([str(ROOT / "target/debug/ferese-rime"), *args], env=env))["state"] def keys(*args): p.probe(list(args)); time.sleep(.15) def text(name="entry-state.json"): return json.loads((p.DATA / name).read_text())["normal"] def expect(value, name="entry-state.json"): p.wait_for(lambda: text(name) == value, f"Expected {value!r} in {name}", timeout=5) def ready_after(pid, timeout=20): def ready(): try: state = ctl("--status") return state["pid"] != int(pid) and state["active"] and state["context"] except (RuntimeError, ValueError): return False p.wait_for(ready, "Input service did not automatically recover", timeout=timeout) p.focus_entry(); p.action("clear") keys("--key", "Escape") ctl("--select-schema", "luna_pinyin_simp"); ctl("--mode", "rime") keys("--keys", "nihao "); expect("你好") try: p.start_unit(ALT, ["/usr/bin/gjs", str(ROOT / "scripts/tests/fixtures/rime-entry.js"), str(p.DATA / "second-state.json"), "dev.ferese.RimeSecond"], p.child_environment(), "512M") p.wait_for(lambda: any(w["focused"] and w["app_id"] == "dev.ferese.RimeSecond" for w in p.windows()), "Second window did not focus") p.wait_for(lambda: ctl("--status")["context"]["app_id"] == "dev.ferese.RimeSecond", "Second input context missing") ctl("--mode", "ascii"); keys("--keys", "abc"); expect("abc", "second-state.json") p.focus_entry(); p.action("normal"); time.sleep(.25) self.assertFalse(ctl("--status")["profile"]["ascii"]) p.action("clear"); keys("--keys", "nihao "); expect("你好") results.append("two windows retain independent ASCII/Rime modes on focus changes") ctl("--select-schema", "luna_pinyin") p.action("clear"); keys("--keys", "hanzi "); expect("漢字") # Reopening the same application must use its persisted preference. ctl("--mode", "ascii") subprocess.run(["systemctl", "--user", "stop", p.APP], check=True) p.wait_for(lambda: not any(w["app_id"] == "dev.ferese.RimeValidation" for w in p.windows()), "First window did not close") p.start_unit(p.APP, ["/usr/bin/gjs", str(ROOT / "scripts/tests/fixtures/rime-entry.js"), str(p.DATA / "entry-state.json")], p.child_environment(), "512M") p.focus_entry() p.wait_for(lambda: (ctl("--status")["context"] or {}).get("app_id") == "dev.ferese.RimeValidation", "Reopened context missing") self.assertEqual(ctl("--status")["profile"], {"schema":"luna_pinyin", "ascii":True}) keys("--keys", "abc"); expect("abc") results.append("closed/reopened application restores mode and Rime schema") ctl("--mode", "rime"); p.action("clear"); keys("--keys", "hanzi") desktop = p.prop(p.MAIN, "MainPID"); old = p.prop(p.IME, "MainPID") started = time.monotonic() subprocess.run(["systemctl", "--user", "kill", "--signal=KILL", p.IME], check=True) ready_after(old) self.assertEqual(p.prop(p.MAIN, "MainPID"), desktop) self.assertEqual(ctl("--status")["profile"], {"schema":"luna_pinyin", "ascii":False}) keys("--keys", "hanzi "); expect("漢字") results.append(f"SIGKILL recovery restores current-window schema in {time.monotonic()-started:.2f}s without stale composition") p.action("clear"); old = p.prop(p.IME, "MainPID"); started=time.monotonic() subprocess.run(["systemctl", "--user", "kill", "--signal=STOP", p.IME], check=True) ready_after(old, timeout=20) self.assertEqual(p.prop(p.MAIN, "MainPID"), desktop) keys("--keys", "hanzi "); expect("漢字") results.append(f"stalled input loop is killed by watchdog and restored in {time.monotonic()-started:.2f}s") finally: subprocess.run(["systemctl", "--user", "stop", ALT], capture_output=True) p.focus_entry() ctl("--select-schema", "luna_pinyin_simp"); ctl("--mode", "rime") (p.DATA / "resilience-validation.json").write_text(json.dumps({"status":"passed","checks":results}, ensure_ascii=False,indent=2)+"\n") print(json.dumps(results, ensure_ascii=False)) if __name__ == "__main__": unittest.main()