foxygit / RPA-Remisser Log in
commits tags

/tests/test_schema.py · 7.04 KB

raw
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