Files
skill-template/tools/ui_inspector/tests/test_models_fingerprint_diff.py
chendelian 633ecff4e2 chore: archive tools/ui_inspector snapshot before migration removal.
Keep the last in-template copy on main so history can restore it; product continues in jiangchang-ui-recorder.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 11:19:14 +08:00

296 lines
11 KiB
Python

"""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()