"""Phase 2 unit tests: models, fingerprint, dedupe, state diff, redaction.""" from __future__ import annotations import sys import tempfile import unittest from pathlib import Path _HERE = Path(__file__).resolve().parents[1] if str(_HERE) not in sys.path: sys.path.insert(0, str(_HERE)) from models.action import ActionFlow, ActionRecord from models.element import ElementRef, element_fingerprint from models.locator import LocatorCandidate from models.serialize import dumps_json, to_dict from models.state import UIState, compute_state_fingerprint from core.policies.element_identity import element_identity_key from redaction import redact_input_payload, redact_text from snapshot_service import SnapshotService from state_diff import diff_states, propose_postconditions class TestSerialization(unittest.TestCase): def test_state_serialize(self) -> None: st = UIState( surface="browser", driver="playwright", url="https://example.com", page_title="Example", locatable=[{"control_type": "button", "name": "Go", "value": ""}], ) st.ensure_fingerprint() raw = dumps_json(st) self.assertIn("openclaw.ui_inspector.state.v1", raw) self.assertIn("https://example.com", raw) self.assertNotIn("\\u", raw) # ensure_ascii=False for CJK; ASCII ok either way d = to_dict(st) self.assertEqual(d["schema"], "openclaw.ui_inspector.state.v1") self.assertTrue(d["fingerprint"]) def test_action_serialize(self) -> None: act = ActionRecord( action_type="click", surface="windows", before_state_id="st_a", after_state_id="st_b", sequence=1, ) d = to_dict(act) self.assertEqual(d["schema"], "openclaw.ui_inspector.action.v1") self.assertEqual(d["capability"], "implemented") flow = ActionFlow(flow_id="f1", flow_name="demo", actions=[act.id]) self.assertEqual(to_dict(flow)["schema"], "openclaw.ui_inspector.action_flow.v1") def test_element_locator_serialize(self) -> None: loc = LocatorCandidate( strategy="automation_id", properties={"automation_id": "ok", "control_type": "Button"}, expression="AutomationId=ok", stability_score=0.9, ) loc.mark_counts(1) el = ElementRef( logical_name="ok_button", surface="windows", automation_id="ok", control_type="ButtonControl", locator_candidates=[loc], primary_locator=loc, ) el.ensure_fingerprint() d = to_dict(el) self.assertEqual(d["schema"], "openclaw.ui_inspector.element.v1") self.assertTrue(d["primary_locator"]["production_eligible"]) class TestFingerprint(unittest.TestCase): def test_stable_fingerprint(self) -> None: kwargs = dict( surface="browser", url="https://a.test/x", page_title="T", window={"title": "T", "class_name": "Chrome", "process_name": "chrome.exe"}, locatable=[ {"control_type": "textbox", "name": "q", "value": "hello", "automation_id": "q"} ], ) a = compute_state_fingerprint(**kwargs) b = compute_state_fingerprint(**kwargs) self.assertEqual(a, b) def test_session_fields_ignored(self) -> None: base_loc = [{"control_type": "button", "name": "A", "value": "", "automation_id": "a"}] fp1 = compute_state_fingerprint( surface="windows", window={"title": "App", "class_name": "X", "pid": 111, "hwnd": 222}, locatable=base_loc, ) fp2 = compute_state_fingerprint( surface="windows", window={"title": "App", "class_name": "X", "pid": 999, "hwnd": 888}, locatable=base_loc, ) self.assertEqual(fp1, fp2) def test_url_changes_fingerprint(self) -> None: loc = [{"control_type": "button", "name": "A", "value": "", "automation_id": "a"}] a = compute_state_fingerprint(surface="browser", url="https://a.test/1", locatable=loc) b = compute_state_fingerprint(surface="browser", url="https://a.test/2", locatable=loc) self.assertNotEqual(a, b) def test_anonymous_dom_elements_do_not_merge_across_paths(self) -> None: common = dict(surface="browser", role="span", name="", value="") a = element_fingerprint(**common, dom_fingerprint="span|#video-tab") b = element_fingerprint(**common, dom_fingerprint="span|#comment-input") self.assertNotEqual(a, b) def test_contenteditable_value_is_not_identity(self) -> None: common = dict( surface="browser", role="combobox", contenteditable=True, dom_fingerprint="div|#comment-input [contenteditable]", ) self.assertEqual( element_fingerprint(**common, name="first", value="first"), element_fingerprint(**common, name="second", value="second"), ) def test_link_href_is_identity_when_visible_text_changes(self) -> None: common = { "role": "link", "tag": "a", "href": "/detail/42", "css": "#results > li.card > a", } self.assertEqual( element_identity_key( {**common, "name": "Result with 10 likes"}, surface="browser", page_url="https://example.test/search?page=1", ), element_identity_key( {**common, "name": "Result with 11 likes"}, surface="browser", page_url="https://example.test/search?page=2", ), ) def test_value_change_detected(self) -> None: a = compute_state_fingerprint( surface="windows", locatable=[{"control_type": "Edit", "name": "box", "value": "1", "automation_id": "e"}], ) b = compute_state_fingerprint( surface="windows", locatable=[{"control_type": "Edit", "name": "box", "value": "2", "automation_id": "e"}], ) self.assertNotEqual(a, b) class TestDedupe(unittest.TestCase): def test_same_state_deduped(self) -> None: with tempfile.TemporaryDirectory() as tmp: svc = SnapshotService(Path(tmp), persist_evidence=False) class Dump: surface = "browser" driver = "playwright" window = {"title": "T", "class_name": "P"} tree = {} index = [] locatable = [ {"control_type": "button", "name": "Go", "value": "", "automation_id": "go"} ] stats = {"locatable_count": 1, "nodes_kept": 1} selection = {} url = "https://example.com" page_title = "T" screenshot_bytes = None use_hwnd_screenshot = False s1 = svc.capture_from_dump(Dump(), change_reason="entry") s2 = svc.capture_from_dump(Dump(), change_reason="heartbeat") self.assertEqual(s1.id, s2.id) self.assertEqual(s2.observation_count, 2) self.assertEqual(len(svc.state_ids()), 1) class TestStateDiff(unittest.TestCase): def test_diff_and_postconditions(self) -> None: before = { "url": "https://a.test/1", "page_title": "One", "window": {"class_name": "WeWorkWindow", "title": "企业微信", "hwnd": 1}, "locatable": [ { "control_type": "Edit", "name": "searchedit", "value": "q", "automation_id": "searchedit", } ], "stats": {"locatable_count": 1}, } after = { "url": "https://a.test/1", "page_title": "Chat", "window": {"class_name": "WeWorkWindow", "title": "企业微信", "hwnd": 1}, "locatable": [ { "control_type": "Edit", "name": "send_rich_edit", "value": "", "automation_id": "send_rich_edit", } ], "stats": {"locatable_count": 1}, } changes = diff_states(before, after) kinds = {c.kind for c in changes} self.assertIn("title_changed", kinds) self.assertIn("element_appeared", kinds) self.assertIn("element_disappeared", kinds) props = propose_postconditions(changes) self.assertTrue(all(p.status == "proposed" for p in props)) self.assertTrue(any("appeared" in p.description.lower() or "出现" in p.description or "appeared" in p.description for p in props) or props) class TestRedaction(unittest.TestCase): def test_password_redacted(self) -> None: out, sens = redact_input_payload( {"text": "secret123", "name": "password"}, field_name="password", input_type="password", ) self.assertTrue(sens) self.assertEqual(out["text"], "[REDACTED]") def test_phone_redacted(self) -> None: self.assertIn("[REDACTED]", redact_text("call 13800138000 please")) def test_raw_tree_and_index_are_sanitized_before_write(self) -> None: secret = "Bearer_abcdefghijklmnopqrstuvwxyz123456" class SensitiveDump: surface = "browser" driver = "playwright" window = {"title": "Login"} tree = { "role": "textbox", "name": "password", "input_type": "password", "value": secret, } index = [dict(tree)] locatable = [dict(tree)] stats = {} selection = {} url = "https://example.com/login" page_title = "Login" screenshot_bytes = None use_hwnd_screenshot = False with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) svc = SnapshotService(root) svc.capture_from_dump( SensitiveDump(), change_reason="sensitive", force_persist=True ) persisted = "\n".join( p.read_text(encoding="utf-8", errors="ignore") for p in root.rglob("*") if p.is_file() ) self.assertNotIn(secret, persisted) class TestActionBeforeAfter(unittest.TestCase): def test_click_links_states(self) -> None: act = ActionRecord( action_type="click", before_state_id="st_before", after_state_id="st_after", target_snapshot={"name": "btn"}, ) self.assertEqual(act.before_state_id, "st_before") self.assertEqual(act.after_state_id, "st_after") self.assertEqual(act.capability, "implemented") if __name__ == "__main__": unittest.main()