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:
2026-08-07 11:19:14 +08:00
parent 46bc573777
commit 633ecff4e2
92 changed files with 13704 additions and 288 deletions

View File

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,207 @@
from pathlib import Path
from flow_session import FlowSession
from recorder.base import RawUserEvent
from diagnostics.capture_validator import validate_capture
from diagnostics.replay_preflight import build_replay_preflight
def test_session_always_creates_raw_event_journal(tmp_path: Path) -> None:
session = FlowSession(tmp_path, flow_name="contract", surface="browser", dump_fn=None)
session.manager.start()
session.manager.status = "recording"
event = RawUserEvent(kind="click", surface="browser", target={"role": "button", "name": "Go"})
session.journal.append(event, channel="semantic")
assert (tmp_path / "journal" / "raw_events.jsonl").exists()
def test_validator_flags_oversized_click_target(tmp_path: Path) -> None:
import json
actions = tmp_path / "actions"
actions.mkdir()
(tmp_path / "elements").mkdir()
(tmp_path / "observations").mkdir()
(tmp_path / "journal").mkdir()
(tmp_path / "journal" / "raw_events.jsonl").write_text("{}\n", encoding="utf-8")
(actions / "a.json").write_text(
json.dumps({
"id": "a", "sequence": 1, "action_type": "click",
"target_snapshot": {"rect": {"width": 1200, "height": 5000}},
"evidence": {"eventual_observation_id": "obs"},
}),
encoding="utf-8",
)
result = validate_capture(tmp_path)
assert any(issue["code"] == "CLICK_TARGET_OVERSIZED" for issue in result["issues"])
def test_validator_flags_missing_desktop_input_and_generic_targets(tmp_path: Path) -> None:
import json
(tmp_path / "actions").mkdir()
(tmp_path / "elements").mkdir()
(tmp_path / "observations").mkdir()
(tmp_path / "journal").mkdir()
journal = {
"event": {
"kind": "typing_activity", "surface": "windows",
"target": {"name": "searchedit", "control_type": "TreeControl"},
}
}
(tmp_path / "journal" / "raw_events.jsonl").write_text(
json.dumps(journal) + "\n", encoding="utf-8"
)
rows = [
{
"id": "a1", "sequence": 1, "surface": "windows", "action_type": "click",
"target_snapshot": {
"name": "search_result", "control_type": "TreeControl",
"bounds": {"width": 600, "height": 500},
},
"evidence": {"eventual_observation_id": "obs"},
},
{
"id": "a2", "sequence": 2, "surface": "windows", "action_type": "press_key",
"target_snapshot": {
"name": "WeCom", "control_type": "WindowControl",
"bounds": {"width": 1440, "height": 900},
},
"input": {"key": "Enter"},
"evidence": {"eventual_observation_id": "obs"},
},
]
for row in rows:
(tmp_path / "actions" / f"{row['id']}.json").write_text(
json.dumps(row), encoding="utf-8"
)
codes = {issue["code"] for issue in validate_capture(tmp_path)["issues"]}
assert "CLICK_TARGET_GENERIC_CONTAINER" in codes
assert "KEY_TARGET_ROOT_WINDOW" in codes
assert "TEXT_INPUT_TRANSACTION_MISSING" in codes
replay = build_replay_preflight(tmp_path)
assert replay["ready"] is False
assert replay["quality_gate"]["passed"] is False
assert replay["summary"]["quality_blockers"] >= 1
def test_validator_flags_missing_production_locator_and_raw_link(tmp_path: Path) -> None:
import json
actions = tmp_path / "actions"
elements = tmp_path / "elements"
actions.mkdir()
elements.mkdir()
(tmp_path / "observations").mkdir()
(tmp_path / "journal").mkdir()
(tmp_path / "journal" / "raw_events.jsonl").write_text("{}\n", encoding="utf-8")
(elements / "el.json").write_text(
json.dumps({"id": "el", "primary_locator": None}), encoding="utf-8"
)
(actions / "a.json").write_text(
json.dumps({
"id": "a", "sequence": 1, "surface": "browser",
"action_type": "click", "target_element_id": "el",
"target_snapshot": {"role": "link", "rect": {"width": 20, "height": 20}},
"raw_event": {}, "evidence": {"eventual_observation_id": "obs"},
}),
encoding="utf-8",
)
result = validate_capture(tmp_path)
codes = {issue["code"] for issue in result["issues"]}
assert "ACTION_PRODUCTION_LOCATOR_MISSING" in codes
assert "ACTION_RAW_EVENT_LINK_MISSING" in codes
def test_validator_flags_graphic_leaf_low_stability_and_split_fill(tmp_path: Path) -> None:
import json
actions = tmp_path / "actions"
elements = tmp_path / "elements"
actions.mkdir()
elements.mkdir()
(tmp_path / "observations").mkdir()
(tmp_path / "journal").mkdir()
(tmp_path / "journal" / "raw_events.jsonl").write_text("{}\n", encoding="utf-8")
locator = {
"strategy": "css", "expression": "css:.hash", "actual_count": 1,
"unique": True, "production_eligible": True, "stability_score": 0.32,
"validation": "live",
}
(elements / "el.json").write_text(
json.dumps({"id": "el", "primary_locator": locator}), encoding="utf-8"
)
rows = [
{
"id": "a1", "sequence": 1, "surface": "browser", "action_type": "fill",
"target_element_id": "el", "target_snapshot": {"tag": "input"},
"input": {"text": "a"},
"raw_event": {"meta": {"event_id": "doc:1", "source_ts_ms": 1000}},
"evidence": {"eventual_observation_id": "obs", "locator_unique": True},
},
{
"id": "a2", "sequence": 2, "surface": "browser", "action_type": "fill",
"target_element_id": "el", "target_snapshot": {"tag": "input"},
"input": {"text": "ai"},
"raw_event": {"meta": {"event_id": "doc:2", "source_ts_ms": 1100}},
"evidence": {"eventual_observation_id": "obs", "locator_unique": True},
},
{
"id": "a3", "sequence": 3, "surface": "browser", "action_type": "click",
"target_element_id": "el", "target_snapshot": {"tag": "path"},
"raw_event": {"meta": {"event_id": "doc:3"}},
"evidence": {"eventual_observation_id": "obs", "locator_unique": True},
},
]
for row in rows:
(actions / f"{row['id']}.json").write_text(json.dumps(row), encoding="utf-8")
codes = {issue["code"] for issue in validate_capture(tmp_path)["issues"]}
assert "DUPLICATE_FILL_TRANSACTION" in codes
assert "GRAPHIC_LEAF_TARGET" in codes
assert "LOW_STABILITY_LOCATOR" in codes
def test_replay_preflight_is_honest_about_event_time_validation(tmp_path: Path) -> None:
import json
(tmp_path / "actions").mkdir()
(tmp_path / "elements").mkdir()
(tmp_path / "actions" / "a.json").write_text(json.dumps({
"id": "a", "sequence": 1, "action_type": "click",
"target_element_id": "el", "raw_event_ids": ["doc:1"],
"raw_event": {"meta": {"page_id": "page", "document_id": "doc"}},
}), encoding="utf-8")
(tmp_path / "elements" / "el.json").write_text(json.dumps({
"id": "el", "logical_name": "go", "primary_locator": {
"strategy": "role", "expression": "role:button,Go",
"actual_count": 1, "unique": True, "production_eligible": True,
"stability_score": 0.95, "validation": "live",
},
}), encoding="utf-8")
report = build_replay_preflight(tmp_path)
assert report["mode"] == "static_non_mutating"
assert report["summary"]["pass"] == 1
assert report["actions"][0]["verification"] == "event_time_unique"
def test_replay_preflight_rejects_unique_root_window_click(tmp_path: Path) -> None:
import json
(tmp_path / "actions").mkdir()
(tmp_path / "elements").mkdir()
(tmp_path / "actions" / "a.json").write_text(json.dumps({
"id": "a", "sequence": 1, "surface": "windows", "action_type": "click",
"target_element_id": "root", "target_snapshot": {
"control_type": "WindowControl", "name": "WeCom",
"bounds": {"width": 1440, "height": 900},
},
}), encoding="utf-8")
(tmp_path / "elements" / "root.json").write_text(json.dumps({
"id": "root", "logical_name": "WeCom", "primary_locator": {
"strategy": "window_name_type", "expression": "window:WeCom",
"actual_count": 1, "unique": True, "production_eligible": True,
"stability_score": 0.9, "validation": "snapshot_only",
},
}), encoding="utf-8")
report = build_replay_preflight(tmp_path)
assert report["summary"]["fail"] == 1
assert report["actions"][0]["reason"] == "oversized_or_root_window_click_target"

View File

@@ -0,0 +1,215 @@
"""Locator compiler / catalog / uniqueness tests."""
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 locators.browser import compile_browser_locators
from locators.catalog import ElementCatalogService
from locators.validator import choose_production_primary, validate_locator_against_rows
from locators.windows import compile_windows_locators
from models.locator import LocatorCandidate
class TestBrowserLocators(unittest.TestCase):
def test_ordering_prefers_role_and_testid(self) -> None:
el = {
"role": "button",
"name": "Submit",
"test_id": "submit-btn",
"text": "Submit",
"id": "x1",
}
rows = [
{"role": "button", "name": "Submit", "test_id": "submit-btn", "text": "Submit", "id": "x1"},
{"role": "button", "name": "Cancel", "test_id": "cancel-btn", "text": "Cancel"},
]
cands = compile_browser_locators(el, rows=rows)
strategies = [c.strategy for c in cands]
self.assertIn("role", strategies)
self.assertIn("test_id", strategies)
primary = choose_production_primary(cands)
self.assertIsNotNone(primary)
assert primary is not None
self.assertFalse(primary.uses_coordinates)
def test_contenteditable_ignores_dynamic_value_and_uses_role_css(self) -> None:
cands = compile_browser_locators(
{
"role": "combobox",
"name": "user typed comment",
"text": "user typed comment",
"value": "user typed comment",
"contenteditable": True,
"css": "#comment-input [contenteditable=true]",
}
)
role = next(c for c in cands if c.strategy == "role")
css = next(c for c in cands if c.strategy == "css")
self.assertEqual(role.properties, {"role": "combobox"})
self.assertNotIn("user typed comment", role.expression)
self.assertFalse(css.production_eligible)
def test_live_unique_css_becomes_executable_fallback(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
service = ElementCatalogService(Path(tmp), flow_id="f")
el = service.upsert_from_snapshot(
{
"role": "path",
"tag": "path",
"css": "#comment-input .submit svg path",
"fingerprint": "path|submit",
},
surface="browser",
frame={"url": "https://example.test/video"},
live_counts={"css": 1},
)
self.assertIsNotNone(el.primary_locator)
assert el.primary_locator is not None
self.assertEqual(el.primary_locator.strategy, "css")
self.assertTrue(el.primary_locator.unique)
def test_link_href_beats_ambiguous_card_css(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
service = ElementCatalogService(Path(tmp), flow_id="f")
el = service.upsert_from_snapshot(
{
"role": "link",
"tag": "a",
"name": "A result card with dynamic text",
"href": "/detail/42",
"href_count": 1,
"css": "#results > li.card > a.card-link",
"css_count": 24,
"fingerprint": "a|card",
},
surface="browser",
frame={"url": "https://example.test/search"},
live_counts={"href": 1, "css": 24},
)
self.assertIsNotNone(el.primary_locator)
assert el.primary_locator is not None
self.assertEqual(el.primary_locator.strategy, "href")
self.assertEqual(el.primary_locator.actual_count, 1)
self.assertTrue(el.primary_locator.production_eligible)
def test_scoped_contenteditable_stable_css_can_be_primary(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
service = ElementCatalogService(Path(tmp), flow_id="f")
el = service.upsert_from_snapshot(
{
"role": "combobox",
"tag": "div",
"contenteditable": True,
"stable_css": "#comment-form div[contenteditable]",
"stable_css_count": 1,
"css": "#comment-form > div.hash-a > div.hash-b",
"css_count": 1,
"fingerprint": "div|editor",
},
surface="browser",
frame={"url": "https://example.test/detail"},
live_counts={"stable_css": 1, "css": 1},
)
self.assertIsNotNone(el.primary_locator)
assert el.primary_locator is not None
self.assertEqual(el.primary_locator.strategy, "stable_css")
self.assertGreater(el.primary_locator.stability_score, 0.7)
class TestWindowsLocators(unittest.TestCase):
def test_ordering_and_name_value_split(self) -> None:
el = {
"automation_id": "searchedit",
"name": "searchedit",
"value": "张三",
"control_type": "EditControl",
"parent_chain": [{"name": "search_layout"}],
}
rows = [
{
"automation_id": "searchedit",
"name": "searchedit",
"value": "张三",
"control_type": "EditControl",
}
]
cands = compile_windows_locators(
el, rows=rows, window_scope={"class_name": "WeWorkWindow", "title": "企业微信"}
)
strategies = [c.strategy for c in cands]
self.assertTrue(strategies[0].startswith("window_automation_id"))
self.assertIn("window_name_value_type", strategies)
primary = choose_production_primary(cands)
self.assertIsNotNone(primary)
assert primary is not None
self.assertEqual(primary.actual_count, 1)
def test_coordinates_never_primary(self) -> None:
el = {
"name": "x",
"control_type": "ButtonControl",
"bounds": {"left": 1, "top": 2, "right": 3, "bottom": 4},
}
# Two matches → ambiguous for name+type; coords present
rows = [
{"name": "x", "control_type": "ButtonControl"},
{"name": "x", "control_type": "ButtonControl"},
]
cands = compile_windows_locators(el, rows=rows)
primary = choose_production_primary(cands)
self.assertIsNone(primary)
self.assertTrue(any(c.uses_coordinates for c in cands))
for c in cands:
if c.uses_coordinates:
self.assertFalse(c.production_eligible)
class TestUniqueness(unittest.TestCase):
def test_ambiguous_not_primary(self) -> None:
c = LocatorCandidate(
strategy="name",
properties={"name": "item", "control_type": "ButtonControl"},
stability_score=0.8,
)
rows = [
{"name": "item", "control_type": "ButtonControl"},
{"name": "item", "control_type": "ButtonControl"},
]
validate_locator_against_rows(c, rows)
self.assertTrue(c.ambiguous)
self.assertFalse(c.production_eligible)
self.assertIsNone(choose_production_primary([c]))
class TestCatalogMerge(unittest.TestCase):
def test_merge_observations(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
svc = ElementCatalogService(Path(tmp), flow_id="f1")
el = {
"automation_id": "ok",
"name": "ok",
"control_type": "ButtonControl",
"value": "",
}
rows = [el]
a = svc.upsert_from_snapshot(
el, surface="windows", window_scope={"class_name": "X"}, rows=rows
)
b = svc.upsert_from_snapshot(
el, surface="windows", window_scope={"class_name": "X"}, rows=rows
)
self.assertEqual(a.id, b.id)
self.assertEqual(b.observation_count, 2)
self.assertTrue(svc.catalog_path.exists())
if __name__ == "__main__":
unittest.main()

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,30 @@
import json
from pathlib import Path
from core.pipeline.event_journal import EventJournal
from core.policies.element_identity import element_identity_key
def test_journal_is_append_only(tmp_path: Path) -> None:
journal = EventJournal(tmp_path)
assert journal.append({"kind": "pointerdown"}) == 1
assert journal.append({"kind": "click"}) == 2
rows = [json.loads(line) for line in journal.path.read_text(encoding="utf-8").splitlines()]
assert [row["journal_sequence"] for row in rows] == [1, 2]
assert [row["event"]["kind"] for row in rows] == ["pointerdown", "click"]
def test_focus_classes_and_query_strings_do_not_split_identity() -> None:
first = {
"role": "textbox", "tag": "input", "placeholder": "Search",
"css": "#header input.search",
}
focused = {
**first,
"class_name": "search focused hash-123",
"css": "#header input.search.focused.hash-123",
}
one = element_identity_key(first, surface="browser", page_url="https://example.com/home?a=1")
two = element_identity_key(focused, surface="browser", page_url="https://example.com/results?q=x")
assert one == two

View File

@@ -0,0 +1,70 @@
from core.policies.target_scoring import resolve_browser_target
def test_huge_control_named_ancestor_never_beats_comment_hit() -> None:
hit = {
"tag": "path", "role": "path", "cursor": "pointer",
"rect": {"left": 1047, "top": 545, "right": 1070, "bottom": 568, "width": 23, "height": 23},
"css": "#comment-input-container .send > svg > path",
}
huge = {
"tag": "div", "role": "div", "class_name": "playerControlHeight",
"rect": {"left": 160, "top": -244, "right": 1440, "bottom": 6698, "width": 1280, "height": 6942},
"css": "#root .playerControlHeight",
}
target, scored = resolve_browser_target(hit, [hit, huge], x=1056, y=558)
assert target["tag"] == "path"
assert any("oversized_ancestor" in row["reasons"] for row in scored)
def test_nearest_semantic_wrapper_beats_svg_path() -> None:
hit = {
"tag": "path", "role": "path", "cursor": "pointer",
"rect": {"left": 10, "top": 10, "right": 30, "bottom": 30, "width": 20, "height": 20},
}
wrapper = {
"tag": "span", "role": "span", "cursor": "pointer", "class_name": "sendControl",
"rect": {"left": 5, "top": 5, "right": 35, "bottom": 35, "width": 30, "height": 30},
}
target, _ = resolve_browser_target(hit, [hit, wrapper], x=20, y=20)
assert target["class_name"] == "sendControl"
def test_contenteditable_root_beats_transient_inner_block() -> None:
inner = {
"tag": "div", "role": "textbox", "contenteditable": True,
"contenteditable_attr": False,
"rect": {"left": 10, "top": 10, "right": 300, "bottom": 35, "width": 290, "height": 25},
}
root = {
"tag": "div", "role": "combobox", "explicit_role": "combobox",
"contenteditable": True, "contenteditable_attr": True,
"rect": {"left": 10, "top": 10, "right": 300, "bottom": 35, "width": 290, "height": 25},
}
target, _ = resolve_browser_target(inner, [inner, root], x=20, y=20)
assert target["contenteditable_attr"] is True
def test_unique_clickable_wrapper_beats_ambiguous_graphic_leaf() -> None:
rect_path = {"left": 1047, "top": 345, "right": 1070, "bottom": 368, "width": 23, "height": 23}
rect_wrapper = {"left": 1041, "top": 338, "right": 1077, "bottom": 374, "width": 36, "height": 36}
path = {
"tag": "path", "role": "path", "cursor": "pointer",
"rect": rect_path, "css_count": 2,
}
svg = {
"tag": "svg", "role": "svg", "cursor": "pointer",
"rect": rect_wrapper, "css_count": 1,
}
wrapper = {
"tag": "span", "role": "span", "cursor": "pointer",
"rect": rect_wrapper, "css_count": 1,
"stable_css": "#comment-form > div.controls > span",
"stable_css_count": 1,
}
target, scored = resolve_browser_target(
path, [path, svg, wrapper], x=1055, y=352
)
assert target["tag"] == "span"
assert any("graphic_leaf" in row["reasons"] for row in scored)
assert any("event_time_unique_locator" in row["reasons"] for row in scored)

View File

@@ -0,0 +1,120 @@
import json
import sys
from pathlib import Path
_HERE = Path(__file__).resolve().parents[2]
if str(_HERE) not in sys.path:
sys.path.insert(0, str(_HERE))
from app import (
_DEFAULT_FLOW_NAME_SLUGS,
_default_window_geometry,
_flow_name_for_mode,
_format_geometry,
_format_live_action,
_merge_current_live_action,
)
import artifacts as art
import target
import user_prefs
def test_default_geometry_is_narrow_tall_and_leaves_title_bar_clear() -> None:
width, height, x, y = _default_window_geometry((0, 0, 1920, 1040))
assert (width, height) == (576, 951)
assert (x, y) == (1328, 73)
assert y >= 64
assert x + width == 1920 - 16
def test_default_geometry_supports_small_and_left_side_monitors() -> None:
width, height, x, y = _default_window_geometry(
(-1366, 0, 0, 728), side="left"
)
assert (width, height) == (560, 648)
assert (x, y) == (-1350, 64)
assert _format_geometry(width, height, x, y) == "560x648-1350+64"
def test_flow_name_defaults_follow_pick_mode() -> None:
assert _flow_name_for_mode(target.SURFACE_BROWSER) == "browser"
assert _flow_name_for_mode(target.SURFACE_WINDOWS) == "desktop"
assert _flow_name_for_mode(target.SURFACE_ANDROID) == "mobile"
assert _DEFAULT_FLOW_NAME_SLUGS == frozenset({"browser", "desktop", "mobile"})
def test_local_ts_is_compact_yyyymmddhhmmss() -> None:
stamp = art._local_ts()
assert len(stamp) == 14
assert stamp.isdigit()
assert "T" not in stamp
assert "+" not in stamp
def test_create_flow_dir_uses_compact_timestamp(tmp_path: Path) -> None:
flow_dir = art.create_flow(tmp_path, flow_name="browser")
name = flow_dir.name
assert name.endswith("_browser")
stamp = name[: -len("_browser")]
assert len(stamp) == 14
assert stamp.isdigit()
def test_hidden_legacy_recording_preference_migrates_to_auto(
tmp_path: Path, monkeypatch
) -> None:
prefs_path = tmp_path / "user_prefs.json"
prefs_path.write_text(
json.dumps(
{
"mode": "browser",
"start_url": "https://example.com",
"recording_mode": "legacy_interval",
}
),
encoding="utf-8",
)
monkeypatch.setattr(user_prefs, "_PREFS_PATH", prefs_path)
assert user_prefs.load_prefs()["recording_mode"] == "auto"
def test_live_action_formatter_is_surface_neutral_and_two_phase() -> None:
text = _format_live_action(
{
"sequence": 3,
"action_type": "click",
"phase": "captured",
"surface": "browser",
"target_name": "提交",
"target_type": "button",
}
)
assert "#3 点击" in text
assert "browser" in text
assert "证据采集中" in text
assert "等待异步证据" in text
def test_current_live_action_overwrites_without_allowing_late_evidence_to_regress() -> None:
first = {"id": "action-1", "sequence": 1, "phase": "captured"}
first_evidence = {"id": "action-1", "sequence": 1, "phase": "evidence"}
second = {"id": "action-2", "sequence": 2, "phase": "captured"}
current = _merge_current_live_action(None, first)
current = _merge_current_live_action(current, first_evidence)
assert current == first_evidence
current = _merge_current_live_action(current, second)
assert current == second
current = _merge_current_live_action(current, first_evidence)
assert current == second
def test_current_live_action_ignores_updates_without_an_action_id() -> None:
current = {"id": "action-2", "sequence": 2, "phase": "captured"}
assert _merge_current_live_action(current, {"sequence": 3}) is current