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>
This commit is contained in:
295
tools/ui_inspector/tests/test_models_fingerprint_diff.py
Normal file
295
tools/ui_inspector/tests/test_models_fingerprint_diff.py
Normal file
@@ -0,0 +1,295 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user