166 lines
6.6 KiB
Python
166 lines
6.6 KiB
Python
# Copyright (c) 2026 Patrick Motsch
|
|
# All rights reserved.
|
|
"""Unit tests for the workflow-file (versioned envelope) schema."""
|
|
|
|
import pytest
|
|
|
|
from modules.features.graphicalEditor._workflowFileSchema import (
|
|
WORKFLOW_FILE_KIND,
|
|
WORKFLOW_FILE_SCHEMA_VERSION,
|
|
WorkflowFileSchemaError,
|
|
buildFileFromWorkflow,
|
|
buildFileName,
|
|
envelopeToWorkflowData,
|
|
isWorkflowFileEnvelope,
|
|
normalizeGraph,
|
|
validateFileEnvelope,
|
|
)
|
|
|
|
|
|
def _sampleWorkflowRow() -> dict:
|
|
return {
|
|
"id": "wf-123",
|
|
"mandateId": "mand-1",
|
|
"featureInstanceId": "inst-1",
|
|
"currentVersionId": "ver-1",
|
|
"eventId": "evt-1",
|
|
"active": True,
|
|
"label": "Test Workflow",
|
|
"description": "Round-trip sample",
|
|
"tags": ["test"],
|
|
"templateScope": None,
|
|
"sharedReadOnly": False,
|
|
"notifyOnFailure": True,
|
|
"isTemplate": False,
|
|
"graph": {
|
|
"nodes": [
|
|
{"id": "n1", "type": "trigger.manual", "x": 50, "y": 200, "parameters": {}},
|
|
{"id": "n2", "type": "ai.prompt", "position": {"x": 300, "y": 200}, "parameters": {"aiPrompt": "Hi"}},
|
|
],
|
|
"connections": [
|
|
{"source": "n1", "target": "n2", "sourceOutput": 0, "targetInput": 0},
|
|
],
|
|
},
|
|
"invocations": [{"id": "inv-1", "kind": "manual", "enabled": True, "title": {"de": "Start"}}],
|
|
"sysCreatedAt": 1700000000.0,
|
|
"sysModifiedAt": 1700000100.0,
|
|
}
|
|
|
|
|
|
def _sampleNodeTypes() -> list:
|
|
return ["trigger.manual", "ai.prompt"]
|
|
|
|
|
|
class TestBuildFile:
|
|
def test_envelopeContainsKindAndSchemaVersion(self):
|
|
envelope = buildFileFromWorkflow(_sampleWorkflowRow())
|
|
assert envelope["$kind"] == WORKFLOW_FILE_KIND
|
|
assert envelope["$schemaVersion"] == WORKFLOW_FILE_SCHEMA_VERSION
|
|
assert "$exportedAt" in envelope
|
|
|
|
def test_persistenceFieldsAreStripped(self):
|
|
envelope = buildFileFromWorkflow(_sampleWorkflowRow())
|
|
for forbidden in ("id", "mandateId", "featureInstanceId", "currentVersionId", "eventId", "active", "sysCreatedAt", "sysModifiedAt"):
|
|
assert forbidden not in envelope, f"{forbidden} must not appear in exported file"
|
|
|
|
def test_portableFieldsAreCopied(self):
|
|
envelope = buildFileFromWorkflow(_sampleWorkflowRow())
|
|
assert envelope["label"] == "Test Workflow"
|
|
assert envelope["description"] == "Round-trip sample"
|
|
assert envelope["tags"] == ["test"]
|
|
assert envelope["notifyOnFailure"] is True
|
|
|
|
def test_graphPositionsAreNormalized(self):
|
|
envelope = buildFileFromWorkflow(_sampleWorkflowRow())
|
|
nodes = envelope["graph"]["nodes"]
|
|
assert nodes[0]["x"] == 50
|
|
assert nodes[0]["y"] == 200
|
|
assert nodes[1]["x"] == 300
|
|
assert nodes[1]["y"] == 200
|
|
assert "position" not in nodes[1]
|
|
|
|
|
|
class TestValidate:
|
|
def test_validEnvelopeReturnsNoErrors(self):
|
|
envelope = buildFileFromWorkflow(_sampleWorkflowRow())
|
|
normalized, warnings = validateFileEnvelope(envelope, knownNodeTypes=_sampleNodeTypes())
|
|
assert normalized["label"] == "Test Workflow"
|
|
assert warnings == []
|
|
|
|
def test_missingSchemaVersionRaises(self):
|
|
with pytest.raises(WorkflowFileSchemaError):
|
|
validateFileEnvelope({"label": "x", "graph": {}})
|
|
|
|
def test_unsupportedSchemaVersionRaises(self):
|
|
with pytest.raises(WorkflowFileSchemaError):
|
|
validateFileEnvelope({"$schemaVersion": "99.0", "label": "x", "graph": {}})
|
|
|
|
def test_missingLabelRaises(self):
|
|
with pytest.raises(WorkflowFileSchemaError):
|
|
validateFileEnvelope({"$schemaVersion": WORKFLOW_FILE_SCHEMA_VERSION, "graph": {}})
|
|
|
|
def test_missingGraphRaises(self):
|
|
with pytest.raises(WorkflowFileSchemaError):
|
|
validateFileEnvelope({"$schemaVersion": WORKFLOW_FILE_SCHEMA_VERSION, "label": "x"})
|
|
|
|
def test_unknownNodeTypeRaises(self):
|
|
envelope = buildFileFromWorkflow(_sampleWorkflowRow())
|
|
with pytest.raises(WorkflowFileSchemaError):
|
|
validateFileEnvelope(envelope, knownNodeTypes=["trigger.manual"])
|
|
|
|
def test_emptyNodesProducesWarning(self):
|
|
envelope = {
|
|
"$schemaVersion": WORKFLOW_FILE_SCHEMA_VERSION,
|
|
"label": "Empty",
|
|
"graph": {"nodes": [], "connections": []},
|
|
}
|
|
_, warnings = validateFileEnvelope(envelope)
|
|
assert any("no nodes" in w.lower() for w in warnings)
|
|
|
|
def test_danglingConnectionProducesWarning(self):
|
|
envelope = {
|
|
"$schemaVersion": WORKFLOW_FILE_SCHEMA_VERSION,
|
|
"label": "Bad",
|
|
"graph": {
|
|
"nodes": [{"id": "a", "type": "trigger.manual"}],
|
|
"connections": [{"source": "a", "target": "ghost"}],
|
|
},
|
|
}
|
|
_, warnings = validateFileEnvelope(envelope, knownNodeTypes=["trigger.manual"])
|
|
assert any("ghost" in w for w in warnings)
|
|
|
|
|
|
class TestRoundTrip:
|
|
def test_exportThenImportPreservesGraphStructure(self):
|
|
original = _sampleWorkflowRow()
|
|
envelope = buildFileFromWorkflow(original)
|
|
normalized, _ = validateFileEnvelope(envelope, knownNodeTypes=_sampleNodeTypes())
|
|
data = envelopeToWorkflowData(normalized, mandateId="mand-2", featureInstanceId="inst-2")
|
|
|
|
assert data["mandateId"] == "mand-2"
|
|
assert data["featureInstanceId"] == "inst-2"
|
|
assert data["active"] is False, "imports must be inactive by default"
|
|
assert data["label"] == original["label"]
|
|
assert data["description"] == original["description"]
|
|
assert len(data["graph"]["nodes"]) == len(original["graph"]["nodes"])
|
|
assert len(data["graph"]["connections"]) == len(original["graph"]["connections"])
|
|
for forbidden in ("id", "currentVersionId", "eventId"):
|
|
assert forbidden not in data
|
|
|
|
|
|
class TestHelpers:
|
|
def test_isWorkflowFileEnvelopeAcceptsValid(self):
|
|
assert isWorkflowFileEnvelope(buildFileFromWorkflow(_sampleWorkflowRow())) is True
|
|
|
|
def test_isWorkflowFileEnvelopeRejectsRandom(self):
|
|
assert isWorkflowFileEnvelope({"foo": "bar"}) is False
|
|
assert isWorkflowFileEnvelope("not-a-dict") is False
|
|
assert isWorkflowFileEnvelope(None) is False
|
|
|
|
def test_buildFileNameProducesSafeSlug(self):
|
|
assert buildFileName("PWG: Pilot Workflow!") == "pwg-pilot-workflow.workflow.json"
|
|
assert buildFileName("") == "workflow.workflow.json"
|
|
|
|
def test_normalizeGraphHandlesMissingFields(self):
|
|
assert normalizeGraph(None) == {"nodes": [], "connections": []}
|
|
assert normalizeGraph({}) == {"nodes": [], "connections": []}
|