Bläddra i källkod

test: a harness that runs one node against the live services

fszontagh 1 månad sedan
förälder
incheckning
86e1fcd1dc
2 ändrade filer med 151 tillägg och 0 borttagningar
  1. 126 0
      scripts/verify-node.py
  2. 25 0
      tests/nodes/baseline-if-condition.json

+ 126 - 0
scripts/verify-node.py

@@ -0,0 +1,126 @@
+#!/usr/bin/env python3
+"""Run one node verification case against the live SmartBotic services.
+
+Usage: scripts/verify-node.py tests/nodes/<case>.json
+Exit 0 if every assertion holds, 1 otherwise.
+"""
+import json
+import sys
+import time
+import urllib.error
+import urllib.request
+
+BASE = "http://localhost:8090/api/v1"
+
+
+def call(method, path, token=None, body=None):
+    data = json.dumps(body).encode() if body is not None else None
+    req = urllib.request.Request(BASE + path, data=data, method=method)
+    req.add_header("Content-Type", "application/json")
+    if token:
+        req.add_header("Authorization", "Bearer " + token)
+    try:
+        with urllib.request.urlopen(req, timeout=30) as res:
+            raw = res.read()
+            return json.loads(raw) if raw else {}
+    except urllib.error.HTTPError as e:
+        raise SystemExit(f"{method} {path} failed: {e.code} {e.read().decode()[:400]}")
+
+
+def login():
+    return call("POST", "/auth/login", body={"username": "admin", "password": "admin"})["accessToken"]
+
+
+def subset_matches(expected, actual, path):
+    """Every key in expected must be present and equal in actual. Returns list of failures."""
+    fails = []
+    if isinstance(expected, dict):
+        if not isinstance(actual, dict):
+            return [f"{path}: expected an object, got {type(actual).__name__}"]
+        for key, want in expected.items():
+            if key not in actual:
+                fails.append(f"{path}.{key}: missing")
+            else:
+                fails += subset_matches(want, actual[key], f"{path}.{key}")
+    elif isinstance(expected, list):
+        if not isinstance(actual, list):
+            return [f"{path}: expected a list, got {type(actual).__name__}"]
+        if len(expected) != len(actual):
+            fails.append(f"{path}: expected {len(expected)} items, got {len(actual)}")
+        else:
+            for i, want in enumerate(expected):
+                fails += subset_matches(want, actual[i], f"{path}[{i}]")
+    elif expected != actual:
+        fails.append(f"{path}: expected {expected!r}, got {actual!r}")
+    return fails
+
+
+def main():
+    if len(sys.argv) != 2:
+        raise SystemExit("usage: verify-node.py <case.json>")
+    case = json.load(open(sys.argv[1]))
+    token = login()
+
+    call("POST", "/nodes/migrate", token, {"nodesPath": "./nodes"})
+
+    created = call("POST", "/workflows", token, {
+        "name": case["name"],
+        "nodes": case["nodes"],
+        "connections": case["connections"],
+        "settings": {},
+    })
+    workflow_id = created.get("id") or created.get("_id")
+    if not workflow_id:
+        raise SystemExit(f"no workflow id in create response: {json.dumps(created)[:300]}")
+
+    try:
+        started = call("POST", f"/workflows/{workflow_id}/execute", token, {})
+        execution_id = started["executionId"]
+
+        execution = None
+        for _ in range(60):
+            time.sleep(0.5)
+            execution = call("GET", f"/executions/{execution_id}", token)
+            if execution.get("status") in ("completed", "failed", "cancelled"):
+                break
+        else:
+            raise SystemExit(f"execution {execution_id} did not finish in 30s")
+
+        by_id = {n["nodeId"]: n for n in execution.get("nodeExecutions", [])}
+        failures = []
+
+        for node_id, want in case.get("expect", {}).items():
+            got = by_id.get(node_id)
+            if got is None:
+                failures.append(f"{node_id}: did not run")
+                continue
+            if "status" in want and got.get("status") != want["status"]:
+                failures.append(
+                    f"{node_id}: status {got.get('status')!r}, expected {want['status']!r}"
+                    + (f" (error: {got.get('error')})" if got.get("error") else "")
+                )
+            if "output" in want:
+                failures += subset_matches(want["output"], got.get("output"), node_id)
+
+        for node_id in case.get("expectMissing", []):
+            got = by_id.get(node_id)
+            if got is not None and got.get("status") == "completed":
+                failures.append(f"{node_id}: ran, but should not have")
+
+        print(f"case: {case['name']}  execution: {execution_id}  status: {execution.get('status')}")
+        for node_id, node in sorted(by_id.items()):
+            print(f"  {node_id:24} {node.get('status'):10} {json.dumps(node.get('output'))[:120]}")
+
+        if failures:
+            print("\nFAIL")
+            for f in failures:
+                print("  " + f)
+            return 1
+        print("\nPASS")
+        return 0
+    finally:
+        call("DELETE", f"/workflows/{workflow_id}", token)
+
+
+if __name__ == "__main__":
+    sys.exit(main())

+ 25 - 0
tests/nodes/baseline-if-condition.json

@@ -0,0 +1,25 @@
+{
+  "name": "verify-baseline-if-condition",
+  "nodes": [
+    {"id": "n1", "name": "Trigger", "type": "click-trigger", "position": {"x": 0, "y": 0}, "config": {}},
+    {"id": "n2", "name": "Fixture", "type": "code", "position": {"x": 0, "y": 100},
+     "config": {"code": "return { status: 'paid', amount: 42 };"}},
+    {"id": "n3", "name": "If", "type": "if-condition", "position": {"x": 0, "y": 200},
+     "config": {"conditions": [{"field": "data.result.status", "operator": "equals", "value": "paid"}], "combineWith": "and"}},
+    {"id": "n4", "name": "OnTrue", "type": "code", "position": {"x": -100, "y": 300},
+     "config": {"code": "return { marker: 'true-branch' };"}},
+    {"id": "n5", "name": "OnFalse", "type": "code", "position": {"x": 100, "y": 300},
+     "config": {"code": "return { marker: 'false-branch' };"}}
+  ],
+  "connections": [
+    {"sourceNodeId": "n1", "sourceOutput": "main", "targetNodeId": "n2", "targetInput": "data"},
+    {"sourceNodeId": "n2", "sourceOutput": "main", "targetNodeId": "n3", "targetInput": "data"},
+    {"sourceNodeId": "n3", "sourceOutput": "true", "targetNodeId": "n4", "targetInput": "data"},
+    {"sourceNodeId": "n3", "sourceOutput": "false", "targetNodeId": "n5", "targetInput": "data"}
+  ],
+  "expect": {
+    "n3": {"status": "completed", "output": {"result": true, "_activeBranch": "true"}},
+    "n4": {"status": "completed", "output": {"result": {"marker": "true-branch"}}}
+  },
+  "expectMissing": ["n5"]
+}