commits
tags
from pathlib import Path
import pytest
from rpa import migrate, schema
ROOT = Path(__file__).resolve().parent.parent
def g(steps, settings=None):
"""Bygg en graf ur en steglista (via migreringen) + valfria settings."""
graph = migrate.steps_to_graph(steps)
graph["settings"] = settings or {}
return graph
GOOD = g([
{"type": "goto", "url": "https://example.com/"},
{"type": "wait_for_login", "timeout": 120},
{"type": "list", "value": "a", "var": "links"},
{"type": "loop", "over": "links", "steps": [
{"type": "extract_text", "by": "css", "value": ":scope", "scope": "item", "var": "lnk"},
{"type": "list_append", "var": "collected", "text": "{lnk}"},
]},
{"type": "if", "branches": [
{"condition": {"kind": "var_contains", "var": "collected", "text": "x"},
"steps": [{"type": "screenshot", "name": "hit"}]},
], "else": [{"type": "screenshot", "name": "miss"}]},
{"type": "save_var", "var": "collected", "filename": "out.csv"},
], {"headless": True})
DESKTOP = g([
{"type": "desktop_launch", "path": "notepad.exe"},
{"type": "desktop_type", "by": "control_type", "value": "Edit", "text": "hej {namn}"},
{"type": "desktop_press_key", "keys": "^s"},
{"type": "desktop_click_xy", "x": 10, "y": 20},
])
def test_good_graph_validates():
assert schema.validate_graph(GOOD) == []
def test_desktop_graph_validates():
assert schema.validate_graph(DESKTOP) == []
def test_desktop_steps_have_category():
desktop = [s for s in schema.STEP_TYPES if s["type"].startswith("desktop_")]
assert len(desktop) == 12
assert all(s["category"] == "desktop" for s in desktop)
def test_jump_to_label_removed():
assert "jump_to_label" not in schema.STEP_TYPE_NAMES
def test_unknown_node_type_rejected():
errors = schema.validate_graph({"nodes": [{"id": "n1", "type": "teleport"}], "edges": []})
assert any("okänd nodtyp" in e for e in errors)
def test_missing_required_field_strict_only():
flow = {"nodes": [{"id": "n1", "type": "goto"}], "edges": []}
assert any("url" in e for e in schema.validate_graph(flow, strict=True))
assert schema.validate_graph(flow, strict=False) == []
def test_bad_enum_rejected_even_non_strict():
flow = {"nodes": [{"id": "n1", "type": "click", "value": "Knapp", "by": "telepathy"}], "edges": []}
assert any(".by" in e for e in schema.validate_graph(flow, strict=False))
def test_edge_to_unknown_node_rejected():
flow = {"nodes": [{"id": "n1", "type": "wait", "seconds": 1}],
"edges": [{"source": "n1", "target": "ghost"}]}
assert any("okänd nod" in e for e in schema.validate_graph(flow))
def test_bad_source_handle_rejected():
flow = {"nodes": [{"id": "a", "type": "wait", "seconds": 1}, {"id": "b", "type": "wait", "seconds": 1}],
"edges": [{"source": "a", "target": "b", "sourceHandle": "case9"}]}
assert any("utgångshandtag" in e for e in schema.validate_graph(flow))
def test_if_node_handles_and_branch_validation():
ifnode = {"id": "f", "type": "if", "branches": [{"condition": {"kind": "var_equals", "var": "x", "text": "1"}}]}
h = schema.node_handles(ifnode)
assert h["outputs"] == ["case0", "else"]
assert schema.validate_graph({"nodes": [ifnode], "edges": []}) == []
# inga grenar -> fel
assert any("kräver minst en gren" in e for e in schema.validate_graph(
{"nodes": [{"id": "f", "type": "if", "branches": []}], "edges": []}))
def test_loop_node_handles():
assert schema.node_handles("loop")["outputs"] == ["each", "done"]
def test_call_flow_in_step_types_with_default_handles():
assert "call_flow" in schema.STEP_TYPE_NAMES
assert schema.node_handles("call_flow")["outputs"] == ["out"]
def test_call_flow_target_required_strict_only():
flow = {"nodes": [{"id": "n1", "type": "call_flow"}], "edges": []}
assert any("target" in e for e in schema.validate_graph(flow, strict=True))
assert schema.validate_graph(flow, strict=False) == []
def test_call_flow_bad_io_entry_rejected():
flow = {"nodes": [{"id": "n1", "type": "call_flow", "target": "x",
"out": [{"from": "a"}]}], "edges": []}
assert any(".out[0]" in e for e in schema.validate_graph(flow, strict=False))
@pytest.mark.parametrize("name", ["ocr_read", "prompt", "lookup", "merge_json", "read_url"])
def test_new_steps_registered_with_default_handles(name):
assert name in schema.STEP_TYPE_NAMES
assert schema.node_handles(name)["outputs"] == ["out"]
def test_prompt_input_needs_var_strict_only():
node = {"id": "n1", "type": "prompt", "mode": "input", "message": "?"}
assert any(".var" in e for e in schema.validate_graph({"nodes": [node], "edges": []}, strict=True))
assert schema.validate_graph({"nodes": [node], "edges": []}, strict=False) == []
def test_lookup_needs_a_source_strict_only():
node = {"id": "n1", "type": "lookup", "key": "k", "var": "v"}
assert any("from_var" in e for e in schema.validate_graph({"nodes": [node], "edges": []}, strict=True))
def test_ocr_read_source_enum_checked():
node = {"id": "n1", "type": "ocr_read", "var": "v", "source": "telepathy"}
assert any(".source" in e for e in schema.validate_graph({"nodes": [node], "edges": []}, strict=False))
def test_stop_step_registered():
assert "stop" in schema.STEP_TYPE_NAMES
def test_var_equals_empty_string_is_valid():
"""Att jämföra en variabel mot tom sträng ('är den tom?') ska gå igenom."""
node = {"id": "f", "type": "if",
"branches": [{"condition": {"kind": "var_equals", "var": "x", "text": ""}}]}
assert schema.validate_graph({"nodes": [node], "edges": []}, strict=True) == []
def test_subflow_graph_is_validated():
flow = {
"nodes": [], "edges": [],
"subflows": [{"id": "s", "name": "S",
"nodes": [{"id": "n", "type": "goto"}], "edges": []}],
}
assert any("subflow" in e and "url" in e for e in schema.validate_graph(flow, strict=True))
def test_unknown_setting_rejected():
errors = schema.validate_graph({"nodes": [], "edges": [], "settings": {"turbo": True}})
assert any("turbo" in e for e in errors)
def test_engine_and_schema_node_types_match():
"""Drift-vakt: varje nodtyp motorn hanterar ska finnas i STEP_TYPES och tvärtom."""
fe = (ROOT / "rpa" / "flow_engine.py").read_text(encoding="utf-8")
ge = (ROOT / "rpa" / "graph_engine.py").read_text(encoding="utf-8")
engine_types = schema.engine_step_types_from_source(fe, ge)
schema_types = set(schema.STEP_TYPE_NAMES)
assert engine_types == schema_types, (
f"bara i motorn: {engine_types - schema_types}; bara i schema: {schema_types - engine_types}"
)
def test_json_schema_is_serializable_graph():
import json
js = schema.flow_json_schema()
json.dumps(js)
assert "node" in js["$defs"] and "edge" in js["$defs"]
assert js["required"] == ["nodes", "edges"]
def test_reference_markdown_mentions_every_step():
md = schema.step_reference_markdown()
for name in schema.STEP_TYPE_NAMES:
assert f"`{name}`" in md
assert "sourceHandle" in md and "each" in md