mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-04 18:13:03 +00:00
407 lines
14 KiB
Python
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"}
|