fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
552 lines
16 KiB
Python
552 lines
16 KiB
Python
import uuid
|
|
from datetime import datetime, timezone
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from application.agents.workflows.schemas import (
|
|
AgentNodeConfig,
|
|
AgentType,
|
|
ConditionCase,
|
|
ConditionNodeConfig,
|
|
ExecutionStatus,
|
|
NodeExecutionLog,
|
|
NodeType,
|
|
Position,
|
|
StateOperation,
|
|
Workflow,
|
|
WorkflowCreate,
|
|
WorkflowEdge,
|
|
WorkflowEdgeCreate,
|
|
WorkflowGraph,
|
|
WorkflowNode,
|
|
WorkflowNodeCreate,
|
|
WorkflowRun,
|
|
WorkflowRunCreate,
|
|
)
|
|
|
|
|
|
# ── Enum tests ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestNodeType:
|
|
@pytest.mark.unit
|
|
def test_values(self):
|
|
assert NodeType.START == "start"
|
|
assert NodeType.END == "end"
|
|
assert NodeType.AGENT == "agent"
|
|
assert NodeType.NOTE == "note"
|
|
assert NodeType.STATE == "state"
|
|
assert NodeType.CONDITION == "condition"
|
|
assert NodeType.CODE == "code"
|
|
|
|
@pytest.mark.unit
|
|
def test_all_members(self):
|
|
assert set(NodeType) == {
|
|
NodeType.START,
|
|
NodeType.END,
|
|
NodeType.AGENT,
|
|
NodeType.NOTE,
|
|
NodeType.STATE,
|
|
NodeType.CONDITION,
|
|
NodeType.CODE,
|
|
}
|
|
|
|
|
|
class TestAgentType:
|
|
@pytest.mark.unit
|
|
def test_values(self):
|
|
assert AgentType.CLASSIC == "classic"
|
|
assert AgentType.REACT == "react"
|
|
assert AgentType.AGENTIC == "agentic"
|
|
assert AgentType.RESEARCH == "research"
|
|
|
|
|
|
class TestExecutionStatus:
|
|
@pytest.mark.unit
|
|
def test_values(self):
|
|
assert ExecutionStatus.PENDING == "pending"
|
|
assert ExecutionStatus.RUNNING == "running"
|
|
assert ExecutionStatus.COMPLETED == "completed"
|
|
assert ExecutionStatus.FAILED == "failed"
|
|
|
|
|
|
# ── Position ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestPosition:
|
|
@pytest.mark.unit
|
|
def test_defaults(self):
|
|
p = Position()
|
|
assert p.x == 0.0
|
|
assert p.y == 0.0
|
|
|
|
@pytest.mark.unit
|
|
def test_custom_values(self):
|
|
p = Position(x=10.5, y=-3.2)
|
|
assert p.x == 10.5
|
|
assert p.y == -3.2
|
|
|
|
@pytest.mark.unit
|
|
def test_extra_fields_forbidden(self):
|
|
with pytest.raises(ValidationError):
|
|
Position(x=0, y=0, z=1)
|
|
|
|
|
|
# ── AgentNodeConfig ──────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestAgentNodeConfig:
|
|
@pytest.mark.unit
|
|
def test_defaults(self):
|
|
c = AgentNodeConfig()
|
|
assert c.agent_type == AgentType.CLASSIC
|
|
assert c.llm_name is None
|
|
assert c.system_prompt == "You are a helpful assistant."
|
|
assert c.prompt_template == ""
|
|
assert c.output_variable is None
|
|
assert c.stream_to_user is True
|
|
assert c.tools == []
|
|
assert c.sources == []
|
|
assert c.chunks == "2"
|
|
assert c.retriever == ""
|
|
assert c.model_id is None
|
|
assert c.json_schema is None
|
|
|
|
@pytest.mark.unit
|
|
def test_custom_values(self):
|
|
c = AgentNodeConfig(
|
|
agent_type=AgentType.REACT,
|
|
llm_name="gpt-4",
|
|
tools=["search"],
|
|
sources=["src1"],
|
|
chunks="5",
|
|
model_id="m1",
|
|
json_schema={"type": "object"},
|
|
)
|
|
assert c.agent_type == AgentType.REACT
|
|
assert c.llm_name == "gpt-4"
|
|
assert c.tools == ["search"]
|
|
assert c.sources == ["src1"]
|
|
assert c.chunks == "5"
|
|
assert c.model_id == "m1"
|
|
assert c.json_schema == {"type": "object"}
|
|
|
|
@pytest.mark.unit
|
|
def test_extra_fields_allowed(self):
|
|
c = AgentNodeConfig(custom_field="value")
|
|
assert c.custom_field == "value"
|
|
|
|
|
|
# ── ConditionCase / ConditionNodeConfig ──────────────────────────────────────
|
|
|
|
|
|
class TestConditionCase:
|
|
@pytest.mark.unit
|
|
def test_alias(self):
|
|
c = ConditionCase(expression="x > 1", sourceHandle="handle-1")
|
|
assert c.source_handle == "handle-1"
|
|
|
|
@pytest.mark.unit
|
|
def test_defaults(self):
|
|
c = ConditionCase(sourceHandle="h")
|
|
assert c.name is None
|
|
assert c.expression == ""
|
|
|
|
@pytest.mark.unit
|
|
def test_extra_forbidden(self):
|
|
with pytest.raises(ValidationError):
|
|
ConditionCase(sourceHandle="h", extra="nope")
|
|
|
|
|
|
class TestConditionNodeConfig:
|
|
@pytest.mark.unit
|
|
def test_defaults(self):
|
|
c = ConditionNodeConfig()
|
|
assert c.mode == "simple"
|
|
assert c.cases == []
|
|
|
|
@pytest.mark.unit
|
|
def test_with_cases(self):
|
|
c = ConditionNodeConfig(
|
|
mode="advanced",
|
|
cases=[{"expression": "x > 1", "sourceHandle": "h1"}],
|
|
)
|
|
assert c.mode == "advanced"
|
|
assert len(c.cases) == 1
|
|
assert c.cases[0].source_handle == "h1"
|
|
|
|
|
|
# ── StateOperation ───────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestStateOperation:
|
|
@pytest.mark.unit
|
|
def test_defaults(self):
|
|
s = StateOperation()
|
|
assert s.expression == ""
|
|
assert s.target_variable == ""
|
|
|
|
@pytest.mark.unit
|
|
def test_extra_forbidden(self):
|
|
with pytest.raises(ValidationError):
|
|
StateOperation(expression="a", target_variable="b", extra="no")
|
|
|
|
|
|
# ── WorkflowEdgeCreate / WorkflowEdge ───────────────────────────────────────
|
|
|
|
|
|
class TestWorkflowEdgeCreate:
|
|
@pytest.mark.unit
|
|
def test_aliases(self):
|
|
e = WorkflowEdgeCreate(
|
|
id="e1",
|
|
workflow_id="w1",
|
|
source="n1",
|
|
target="n2",
|
|
sourceHandle="sh",
|
|
targetHandle="th",
|
|
)
|
|
assert e.source_id == "n1"
|
|
assert e.target_id == "n2"
|
|
assert e.source_handle == "sh"
|
|
assert e.target_handle == "th"
|
|
|
|
@pytest.mark.unit
|
|
def test_optional_handles_default_none(self):
|
|
e = WorkflowEdgeCreate(id="e1", workflow_id="w1", source="n1", target="n2")
|
|
assert e.source_handle is None
|
|
assert e.target_handle is None
|
|
|
|
|
|
class TestWorkflowEdge:
|
|
@pytest.mark.unit
|
|
def test_uuid_id(self):
|
|
oid = str(uuid.uuid4())
|
|
e = WorkflowEdge(
|
|
id=oid,
|
|
workflow_id="w1",
|
|
source="n1",
|
|
target="n2",
|
|
)
|
|
assert e.id == oid
|
|
|
|
@pytest.mark.unit
|
|
def test_string_id_passthrough(self):
|
|
e = WorkflowEdge(
|
|
id="string-id",
|
|
workflow_id="w1",
|
|
source="n1",
|
|
target="n2",
|
|
)
|
|
assert e.id == "string-id"
|
|
|
|
@pytest.mark.unit
|
|
def test_model_dump(self):
|
|
e = WorkflowEdge(
|
|
id="e1",
|
|
workflow_id="w1",
|
|
source="n1",
|
|
target="n2",
|
|
sourceHandle="sh",
|
|
targetHandle="th",
|
|
)
|
|
doc = e.model_dump()
|
|
assert doc == {
|
|
"id": "e1",
|
|
"workflow_id": "w1",
|
|
"source_id": "n1",
|
|
"target_id": "n2",
|
|
"source_handle": "sh",
|
|
"target_handle": "th",
|
|
}
|
|
|
|
|
|
# ── WorkflowNodeCreate / WorkflowNode ───────────────────────────────────────
|
|
|
|
|
|
class TestWorkflowNodeCreate:
|
|
@pytest.mark.unit
|
|
def test_defaults(self):
|
|
n = WorkflowNodeCreate(id="n1", workflow_id="w1", type=NodeType.AGENT)
|
|
assert n.title == "Node"
|
|
assert n.description is None
|
|
assert n.position.x == 0.0
|
|
assert n.position.y == 0.0
|
|
assert n.config == {}
|
|
|
|
@pytest.mark.unit
|
|
def test_position_from_dict(self):
|
|
n = WorkflowNodeCreate(
|
|
id="n1",
|
|
workflow_id="w1",
|
|
type=NodeType.START,
|
|
position={"x": 100, "y": 200},
|
|
)
|
|
assert isinstance(n.position, Position)
|
|
assert n.position.x == 100
|
|
assert n.position.y == 200
|
|
|
|
@pytest.mark.unit
|
|
def test_position_from_position_object(self):
|
|
pos = Position(x=5, y=10)
|
|
n = WorkflowNodeCreate(
|
|
id="n1", workflow_id="w1", type=NodeType.END, position=pos
|
|
)
|
|
assert n.position is pos
|
|
|
|
|
|
class TestWorkflowNode:
|
|
@pytest.mark.unit
|
|
def test_uuid_id(self):
|
|
oid = str(uuid.uuid4())
|
|
n = WorkflowNode(
|
|
id=oid, workflow_id="w1", type=NodeType.AGENT
|
|
)
|
|
assert n.id == oid
|
|
|
|
@pytest.mark.unit
|
|
def test_model_dump(self):
|
|
n = WorkflowNode(
|
|
id="n1",
|
|
workflow_id="w1",
|
|
type=NodeType.AGENT,
|
|
title="My Node",
|
|
description="desc",
|
|
position={"x": 10, "y": 20},
|
|
config={"key": "val"},
|
|
)
|
|
doc = n.model_dump()
|
|
assert doc == {
|
|
"id": "n1",
|
|
"workflow_id": "w1",
|
|
"type": NodeType.AGENT,
|
|
"title": "My Node",
|
|
"description": "desc",
|
|
"position": {"x": 10.0, "y": 20.0},
|
|
"config": {"key": "val"},
|
|
}
|
|
|
|
|
|
# ── WorkflowCreate / Workflow ───────────────────────────────────────────────
|
|
|
|
|
|
class TestWorkflowCreate:
|
|
@pytest.mark.unit
|
|
def test_defaults(self):
|
|
w = WorkflowCreate()
|
|
assert w.name == "New Workflow"
|
|
assert w.description is None
|
|
assert w.user is None
|
|
|
|
@pytest.mark.unit
|
|
def test_custom_values(self):
|
|
w = WorkflowCreate(name="Test", description="d", user="u1")
|
|
assert w.name == "Test"
|
|
assert w.description == "d"
|
|
assert w.user == "u1"
|
|
|
|
|
|
class TestWorkflow:
|
|
@pytest.mark.unit
|
|
def test_uuid_id(self):
|
|
oid = str(uuid.uuid4())
|
|
w = Workflow(id=oid)
|
|
assert w.id == oid
|
|
|
|
@pytest.mark.unit
|
|
def test_string_id(self):
|
|
w = Workflow(id="abc")
|
|
assert w.id == "abc"
|
|
|
|
@pytest.mark.unit
|
|
def test_none_id(self):
|
|
w = Workflow()
|
|
assert w.id is None
|
|
|
|
@pytest.mark.unit
|
|
def test_datetime_defaults(self):
|
|
before = datetime.now(timezone.utc)
|
|
w = Workflow()
|
|
after = datetime.now(timezone.utc)
|
|
assert before <= w.created_at <= after
|
|
assert before <= w.updated_at <= after
|
|
|
|
@pytest.mark.unit
|
|
def test_model_dump(self):
|
|
w = Workflow(name="W", description="d", user="u1")
|
|
doc = w.model_dump()
|
|
assert doc["name"] == "W"
|
|
assert doc["description"] == "d"
|
|
assert doc["user"] == "u1"
|
|
assert "created_at" in doc
|
|
assert "updated_at" in doc
|
|
|
|
|
|
# ── WorkflowGraph ───────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestWorkflowGraph:
|
|
@pytest.fixture
|
|
def graph(self):
|
|
workflow = Workflow(name="test")
|
|
nodes = [
|
|
WorkflowNode(id="start", workflow_id="w1", type=NodeType.START),
|
|
WorkflowNode(id="agent1", workflow_id="w1", type=NodeType.AGENT),
|
|
WorkflowNode(id="end", workflow_id="w1", type=NodeType.END),
|
|
]
|
|
edges = [
|
|
WorkflowEdge(
|
|
id="e1", workflow_id="w1", source="start", target="agent1"
|
|
),
|
|
WorkflowEdge(
|
|
id="e2", workflow_id="w1", source="agent1", target="end"
|
|
),
|
|
]
|
|
return WorkflowGraph(workflow=workflow, nodes=nodes, edges=edges)
|
|
|
|
@pytest.mark.unit
|
|
def test_get_node_by_id_found(self, graph):
|
|
node = graph.get_node_by_id("agent1")
|
|
assert node is not None
|
|
assert node.id == "agent1"
|
|
assert node.type == NodeType.AGENT
|
|
|
|
@pytest.mark.unit
|
|
def test_get_node_by_id_not_found(self, graph):
|
|
assert graph.get_node_by_id("nonexistent") is None
|
|
|
|
@pytest.mark.unit
|
|
def test_get_start_node(self, graph):
|
|
start = graph.get_start_node()
|
|
assert start is not None
|
|
assert start.id == "start"
|
|
assert start.type == NodeType.START
|
|
|
|
@pytest.mark.unit
|
|
def test_get_start_node_missing(self):
|
|
g = WorkflowGraph(
|
|
workflow=Workflow(),
|
|
nodes=[
|
|
WorkflowNode(id="a", workflow_id="w", type=NodeType.AGENT),
|
|
],
|
|
)
|
|
assert g.get_start_node() is None
|
|
|
|
@pytest.mark.unit
|
|
def test_get_outgoing_edges(self, graph):
|
|
edges = graph.get_outgoing_edges("start")
|
|
assert len(edges) == 1
|
|
assert edges[0].target_id == "agent1"
|
|
|
|
@pytest.mark.unit
|
|
def test_get_outgoing_edges_none(self, graph):
|
|
edges = graph.get_outgoing_edges("end")
|
|
assert edges == []
|
|
|
|
@pytest.mark.unit
|
|
def test_empty_graph(self):
|
|
g = WorkflowGraph(workflow=Workflow())
|
|
assert g.nodes == []
|
|
assert g.edges == []
|
|
assert g.get_start_node() is None
|
|
|
|
|
|
# ── NodeExecutionLog ─────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestNodeExecutionLog:
|
|
@pytest.mark.unit
|
|
def test_required_fields(self):
|
|
now = datetime.now(timezone.utc)
|
|
log = NodeExecutionLog(
|
|
node_id="n1",
|
|
node_type="agent",
|
|
status=ExecutionStatus.RUNNING,
|
|
started_at=now,
|
|
)
|
|
assert log.node_id == "n1"
|
|
assert log.completed_at is None
|
|
assert log.error is None
|
|
assert log.state_delta == {}
|
|
|
|
@pytest.mark.unit
|
|
def test_full_log(self):
|
|
started = datetime.now(timezone.utc)
|
|
completed = datetime.now(timezone.utc)
|
|
log = NodeExecutionLog(
|
|
node_id="n1",
|
|
node_type="agent",
|
|
status=ExecutionStatus.COMPLETED,
|
|
started_at=started,
|
|
completed_at=completed,
|
|
error=None,
|
|
state_delta={"key": "value"},
|
|
)
|
|
assert log.completed_at == completed
|
|
assert log.state_delta == {"key": "value"}
|
|
|
|
@pytest.mark.unit
|
|
def test_extra_forbidden(self):
|
|
with pytest.raises(ValidationError):
|
|
NodeExecutionLog(
|
|
node_id="n",
|
|
node_type="agent",
|
|
status=ExecutionStatus.PENDING,
|
|
started_at=datetime.now(timezone.utc),
|
|
extra="no",
|
|
)
|
|
|
|
|
|
# ── WorkflowRunCreate / WorkflowRun ─────────────────────────────────────────
|
|
|
|
|
|
class TestWorkflowRunCreate:
|
|
@pytest.mark.unit
|
|
def test_defaults(self):
|
|
r = WorkflowRunCreate(workflow_id="w1")
|
|
assert r.workflow_id == "w1"
|
|
assert r.inputs == {}
|
|
|
|
|
|
class TestWorkflowRun:
|
|
@pytest.mark.unit
|
|
def test_defaults(self):
|
|
r = WorkflowRun(workflow_id="w1")
|
|
assert r.status == ExecutionStatus.PENDING
|
|
assert r.inputs == {}
|
|
assert r.outputs == {}
|
|
assert r.steps == []
|
|
assert r.completed_at is None
|
|
|
|
@pytest.mark.unit
|
|
def test_uuid_id(self):
|
|
oid = str(uuid.uuid4())
|
|
r = WorkflowRun(id=oid, workflow_id="w1")
|
|
assert r.id == oid
|
|
|
|
@pytest.mark.unit
|
|
def test_model_dump(self):
|
|
now = datetime.now(timezone.utc)
|
|
log = NodeExecutionLog(
|
|
node_id="n1",
|
|
node_type="agent",
|
|
status=ExecutionStatus.COMPLETED,
|
|
started_at=now,
|
|
)
|
|
r = WorkflowRun(
|
|
workflow_id="w1",
|
|
status=ExecutionStatus.RUNNING,
|
|
inputs={"q": "hello"},
|
|
outputs={"a": "world"},
|
|
steps=[log],
|
|
)
|
|
doc = r.model_dump()
|
|
assert doc["workflow_id"] == "w1"
|
|
assert doc["status"] == ExecutionStatus.RUNNING
|
|
assert doc["inputs"] == {"q": "hello"}
|
|
assert doc["outputs"] == {"a": "world"}
|
|
assert len(doc["steps"]) == 1
|
|
assert doc["steps"][0]["node_id"] == "n1"
|
|
assert doc["completed_at"] is None
|