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>
208 lines
8.5 KiB
Python
208 lines
8.5 KiB
Python
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"
|