Files
DocsGPT/tests/api/user/test_workflows.py
T
2026-03-30 16:13:08 +01:00

407 lines
14 KiB
Python

from datetime import datetime, timezone
from unittest.mock import Mock, patch
import pytest
from bson import ObjectId
@pytest.mark.unit
class TestSerializeWorkflow:
def test_serializes_full_workflow(self):
from application.api.user.workflows.routes import serialize_workflow
now = datetime(2024, 6, 15, 10, 30, 0, tzinfo=timezone.utc)
doc = {
"_id": ObjectId(),
"name": "My Workflow",
"description": "A test workflow",
"created_at": now,
"updated_at": now,
}
result = serialize_workflow(doc)
assert result["id"] == str(doc["_id"])
assert result["name"] == "My Workflow"
assert result["description"] == "A test workflow"
assert result["created_at"] == now.isoformat()
def test_handles_missing_optional_fields(self):
from application.api.user.workflows.routes import serialize_workflow
doc = {"_id": ObjectId()}
result = serialize_workflow(doc)
assert result["name"] is None
assert result["created_at"] is None
@pytest.mark.unit
class TestSerializeNode:
def test_serializes_node(self):
from application.api.user.workflows.routes import serialize_node
node = {
"id": "node-1",
"type": "agent",
"title": "Agent Node",
"description": "Does things",
"position": {"x": 100, "y": 200},
"config": {"model": "gpt-4"},
}
result = serialize_node(node)
assert result["id"] == "node-1"
assert result["type"] == "agent"
assert result["title"] == "Agent Node"
assert result["data"] == {"model": "gpt-4"}
assert result["position"] == {"x": 100, "y": 200}
def test_defaults_for_missing_fields(self):
from application.api.user.workflows.routes import serialize_node
node = {"id": "n1", "type": "start"}
result = serialize_node(node)
assert result["data"] == {}
assert result["title"] is None
@pytest.mark.unit
class TestSerializeEdge:
def test_serializes_edge(self):
from application.api.user.workflows.routes import serialize_edge
edge = {
"id": "edge-1",
"source_id": "node-1",
"target_id": "node-2",
"source_handle": "output",
"target_handle": "input",
}
result = serialize_edge(edge)
assert result["id"] == "edge-1"
assert result["source"] == "node-1"
assert result["target"] == "node-2"
assert result["sourceHandle"] == "output"
assert result["targetHandle"] == "input"
@pytest.mark.unit
class TestGetWorkflowGraphVersion:
def test_returns_version(self):
from application.api.user.workflows.routes import get_workflow_graph_version
assert get_workflow_graph_version({"current_graph_version": 3}) == 3
def test_defaults_to_1(self):
from application.api.user.workflows.routes import get_workflow_graph_version
assert get_workflow_graph_version({}) == 1
def test_handles_invalid_version(self):
from application.api.user.workflows.routes import get_workflow_graph_version
assert get_workflow_graph_version({"current_graph_version": "bad"}) == 1
def test_handles_zero_version(self):
from application.api.user.workflows.routes import get_workflow_graph_version
assert get_workflow_graph_version({"current_graph_version": 0}) == 1
def test_handles_negative_version(self):
from application.api.user.workflows.routes import get_workflow_graph_version
assert get_workflow_graph_version({"current_graph_version": -1}) == 1
@pytest.mark.unit
class TestFetchGraphDocuments:
def test_returns_versioned_docs(self):
from application.api.user.workflows.routes import fetch_graph_documents
collection = Mock()
docs = [{"id": "n1", "graph_version": 2}]
collection.find.return_value = docs
result = fetch_graph_documents(collection, "wf1", 2)
assert result == docs
collection.find.assert_called_once_with(
{"workflow_id": "wf1", "graph_version": 2}
)
def test_falls_back_to_unversioned_for_v1(self):
from application.api.user.workflows.routes import fetch_graph_documents
collection = Mock()
unversioned_docs = [{"id": "n1"}]
collection.find.side_effect = [[], unversioned_docs]
result = fetch_graph_documents(collection, "wf1", 1)
assert result == unversioned_docs
assert collection.find.call_count == 2
def test_no_fallback_for_higher_versions(self):
from application.api.user.workflows.routes import fetch_graph_documents
collection = Mock()
collection.find.return_value = []
result = fetch_graph_documents(collection, "wf1", 3)
assert result == []
assert collection.find.call_count == 1
@pytest.mark.unit
class TestValidateWorkflowStructure:
def _make_minimal_workflow(self):
nodes = [
{"id": "start", "type": "start"},
{"id": "end", "type": "end"},
]
edges = [{"id": "e1", "source": "start", "target": "end"}]
return nodes, edges
def test_valid_minimal_workflow(self):
from application.api.user.workflows.routes import validate_workflow_structure
nodes, edges = self._make_minimal_workflow()
errors = validate_workflow_structure(nodes, edges)
assert errors == []
def test_empty_nodes(self):
from application.api.user.workflows.routes import validate_workflow_structure
errors = validate_workflow_structure([], [])
assert any("at least one node" in e for e in errors)
def test_missing_start_node(self):
from application.api.user.workflows.routes import validate_workflow_structure
nodes = [{"id": "end", "type": "end"}]
edges = []
errors = validate_workflow_structure(nodes, edges)
assert any("start node" in e for e in errors)
def test_missing_end_node(self):
from application.api.user.workflows.routes import validate_workflow_structure
nodes = [{"id": "start", "type": "start"}]
edges = [{"id": "e1", "source": "start", "target": "somewhere"}]
errors = validate_workflow_structure(nodes, edges)
assert any("end node" in e for e in errors)
def test_start_node_without_outgoing_edge(self):
from application.api.user.workflows.routes import validate_workflow_structure
nodes = [
{"id": "start", "type": "start"},
{"id": "end", "type": "end"},
]
edges = []
errors = validate_workflow_structure(nodes, edges)
assert any("outgoing edge" in e for e in errors)
def test_edge_references_nonexistent_node(self):
from application.api.user.workflows.routes import validate_workflow_structure
nodes = [
{"id": "start", "type": "start"},
{"id": "end", "type": "end"},
]
edges = [{"id": "e1", "source": "start", "target": "ghost"}]
errors = validate_workflow_structure(nodes, edges)
assert any("non-existent target" in e for e in errors)
def test_node_without_id(self):
from application.api.user.workflows.routes import validate_workflow_structure
nodes = [
{"id": "start", "type": "start"},
{"type": "end"},
]
edges = [{"id": "e1", "source": "start", "target": None}]
errors = validate_workflow_structure(nodes, edges)
assert any("must have an id" in e for e in errors)
def test_node_without_type(self):
from application.api.user.workflows.routes import validate_workflow_structure
nodes = [
{"id": "start", "type": "start"},
{"id": "end"},
]
edges = [{"id": "e1", "source": "start", "target": "end"}]
errors = validate_workflow_structure(nodes, edges)
assert any("must have a type" in e for e in errors)
def test_condition_node_needs_two_outgoing_edges(self):
from application.api.user.workflows.routes import validate_workflow_structure
nodes = [
{"id": "start", "type": "start"},
{
"id": "cond",
"type": "condition",
"title": "Check",
"data": {
"cases": [
{"expression": "x > 1", "sourceHandle": "case1"},
]
},
},
{"id": "end", "type": "end"},
]
edges = [
{"id": "e1", "source": "start", "target": "cond"},
{"id": "e2", "source": "cond", "target": "end", "sourceHandle": "else"},
]
errors = validate_workflow_structure(nodes, edges)
assert any("at least 2 outgoing edges" in e for e in errors)
def test_condition_node_needs_else_branch(self):
from application.api.user.workflows.routes import validate_workflow_structure
nodes = [
{"id": "start", "type": "start"},
{
"id": "cond",
"type": "condition",
"title": "Check",
"data": {
"cases": [
{"expression": "x > 1", "sourceHandle": "case1"},
]
},
},
{"id": "end1", "type": "end"},
{"id": "end2", "type": "end"},
]
edges = [
{"id": "e1", "source": "start", "target": "cond"},
{"id": "e2", "source": "cond", "target": "end1", "sourceHandle": "case1"},
{"id": "e3", "source": "cond", "target": "end2", "sourceHandle": "case1"},
]
errors = validate_workflow_structure(nodes, edges)
assert any("else" in e for e in errors)
@pytest.mark.unit
class TestCanReachEnd:
def test_direct_end_node(self):
from application.api.user.workflows.routes import _can_reach_end
node_map = {"end": {"id": "end", "type": "end"}}
assert _can_reach_end("end", [], node_map, {"end"}) is True
def test_reachable_through_chain(self):
from application.api.user.workflows.routes import _can_reach_end
node_map = {
"a": {"id": "a"},
"b": {"id": "b"},
"end": {"id": "end", "type": "end"},
}
edges = [
{"source": "a", "target": "b"},
{"source": "b", "target": "end"},
]
assert _can_reach_end("a", edges, node_map, {"end"}) is True
def test_unreachable(self):
from application.api.user.workflows.routes import _can_reach_end
node_map = {
"a": {"id": "a"},
"b": {"id": "b"},
}
edges = [{"source": "a", "target": "b"}]
assert _can_reach_end("a", edges, node_map, {"end"}) is False
def test_handles_cycles(self):
from application.api.user.workflows.routes import _can_reach_end
node_map = {"a": {"id": "a"}, "b": {"id": "b"}}
edges = [
{"source": "a", "target": "b"},
{"source": "b", "target": "a"},
]
assert _can_reach_end("a", edges, node_map, {"end"}) is False
@pytest.mark.unit
class TestValidateJsonSchemaPayload:
def test_none_input(self):
from application.api.user.workflows.routes import validate_json_schema_payload
result, error = validate_json_schema_payload(None)
assert result is None
assert error is None
@patch("application.api.user.workflows.routes.normalize_json_schema_payload")
def test_valid_schema(self, mock_normalize):
from application.api.user.workflows.routes import validate_json_schema_payload
mock_normalize.return_value = {"type": "object"}
result, error = validate_json_schema_payload({"type": "object"})
assert result == {"type": "object"}
assert error is None
@patch("application.api.user.workflows.routes.normalize_json_schema_payload")
def test_invalid_schema(self, mock_normalize):
from application.api.user.workflows.routes import validate_json_schema_payload
from application.core.json_schema_utils import JsonSchemaValidationError
mock_normalize.side_effect = JsonSchemaValidationError("bad schema")
result, error = validate_json_schema_payload({"bad": True})
assert result is None
assert "bad schema" in error
@pytest.mark.unit
class TestNormalizeAgentNodeJsonSchemas:
def test_non_agent_nodes_pass_through(self):
from application.api.user.workflows.routes import (
normalize_agent_node_json_schemas,
)
nodes = [
{"id": "n1", "type": "start"},
{"id": "n2", "type": "end"},
]
result = normalize_agent_node_json_schemas(nodes)
assert result == nodes
@patch("application.api.user.workflows.routes.normalize_json_schema_payload")
def test_normalizes_agent_node_schema(self, mock_normalize):
from application.api.user.workflows.routes import (
normalize_agent_node_json_schemas,
)
mock_normalize.return_value = {"type": "object", "properties": {}}
nodes = [
{
"id": "a1",
"type": "agent",
"data": {"json_schema": {"type": "object"}},
}
]
result = normalize_agent_node_json_schemas(nodes)
assert result[0]["data"]["json_schema"] == {
"type": "object",
"properties": {},
}
def test_agent_node_without_schema(self):
from application.api.user.workflows.routes import (
normalize_agent_node_json_schemas,
)
nodes = [{"id": "a1", "type": "agent", "data": {"model": "gpt-4"}}]
result = normalize_agent_node_json_schemas(nodes)
assert result[0]["data"] == {"model": "gpt-4"}