mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-30 10:52:15 +00:00
- Rewrite .github/workflows/desktop.yml: 2-job pipeline (validate + build-and-release) with rolling desktop-latest pre-release on push to main and stable desktop-v* releases - Add UpdateChecker component: checks for updates on startup + every 30 min, background download with progress bar, one-click relaunch - Configure Tauri updater: endpoints pointing to desktop-latest release, pubkey placeholder - Add tauri-plugin-process for relaunch support (Cargo.toml, lib.rs, package.json) - Add macOS Entitlements.plist for notarization (network + file access, no sandbox) - Add scripts/bump-desktop-version.sh for atomic version bumps across 3 config files - Add desktop/README.md with dev setup, auto-update architecture, signing docs - Update .gitignore for desktop/node_modules, dist, target - Configure macOS minimumSystemVersion, Windows timestampUrl - Include all Phase 14-21 work: agent hardening, RBAC, taint tracking, workflows, skills, knowledge graph, sessions, A2A, MCP templates, WASM sandbox, TUI dashboard, production tools, CLI expansion, API expansion, learning productionization, Tauri desktop app, and 10 new channels Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
161 lines
5.8 KiB
Python
161 lines
5.8 KiB
Python
"""Tests for workflow engine (Phase 15.1)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from openjarvis.core.events import EventBus, EventType
|
|
from openjarvis.workflow.builder import WorkflowBuilder
|
|
from openjarvis.workflow.engine import WorkflowEngine
|
|
from openjarvis.workflow.graph import WorkflowGraph
|
|
from openjarvis.workflow.types import NodeType, WorkflowEdge, WorkflowNode
|
|
|
|
|
|
class TestWorkflowGraph:
|
|
def test_add_node(self):
|
|
g = WorkflowGraph("test")
|
|
g.add_node(WorkflowNode(id="a", node_type=NodeType.AGENT))
|
|
assert g.get_node("a") is not None
|
|
assert len(g.nodes) == 1
|
|
|
|
def test_add_duplicate_node_raises(self):
|
|
g = WorkflowGraph("test")
|
|
g.add_node(WorkflowNode(id="a", node_type=NodeType.AGENT))
|
|
with pytest.raises(ValueError, match="Duplicate"):
|
|
g.add_node(WorkflowNode(id="a", node_type=NodeType.AGENT))
|
|
|
|
def test_add_edge(self):
|
|
g = WorkflowGraph("test")
|
|
g.add_node(WorkflowNode(id="a", node_type=NodeType.AGENT))
|
|
g.add_node(WorkflowNode(id="b", node_type=NodeType.AGENT))
|
|
g.add_edge(WorkflowEdge(source="a", target="b"))
|
|
assert len(g.edges) == 1
|
|
|
|
def test_add_edge_missing_source_raises(self):
|
|
g = WorkflowGraph("test")
|
|
g.add_node(WorkflowNode(id="b", node_type=NodeType.AGENT))
|
|
with pytest.raises(ValueError, match="Source"):
|
|
g.add_edge(WorkflowEdge(source="a", target="b"))
|
|
|
|
def test_validate_acyclic(self):
|
|
g = WorkflowGraph("test")
|
|
g.add_node(WorkflowNode(id="a", node_type=NodeType.AGENT))
|
|
g.add_node(WorkflowNode(id="b", node_type=NodeType.AGENT))
|
|
g.add_edge(WorkflowEdge(source="a", target="b"))
|
|
valid, msg = g.validate()
|
|
assert valid
|
|
|
|
def test_validate_cyclic(self):
|
|
g = WorkflowGraph("test")
|
|
g.add_node(WorkflowNode(id="a", node_type=NodeType.AGENT))
|
|
g.add_node(WorkflowNode(id="b", node_type=NodeType.AGENT))
|
|
g.add_edge(WorkflowEdge(source="a", target="b"))
|
|
g.add_edge(WorkflowEdge(source="b", target="a"))
|
|
valid, msg = g.validate()
|
|
assert not valid
|
|
assert "Cycle" in msg
|
|
|
|
def test_topological_sort(self):
|
|
g = WorkflowGraph("test")
|
|
g.add_node(WorkflowNode(id="a", node_type=NodeType.AGENT))
|
|
g.add_node(WorkflowNode(id="b", node_type=NodeType.AGENT))
|
|
g.add_node(WorkflowNode(id="c", node_type=NodeType.AGENT))
|
|
g.add_edge(WorkflowEdge(source="a", target="b"))
|
|
g.add_edge(WorkflowEdge(source="b", target="c"))
|
|
order = g.topological_sort()
|
|
assert order.index("a") < order.index("b")
|
|
assert order.index("b") < order.index("c")
|
|
|
|
def test_execution_stages(self):
|
|
g = WorkflowGraph("test")
|
|
g.add_node(WorkflowNode(id="a", node_type=NodeType.AGENT))
|
|
g.add_node(WorkflowNode(id="b", node_type=NodeType.AGENT))
|
|
g.add_node(WorkflowNode(id="c", node_type=NodeType.AGENT))
|
|
g.add_edge(WorkflowEdge(source="a", target="c"))
|
|
g.add_edge(WorkflowEdge(source="b", target="c"))
|
|
stages = g.execution_stages()
|
|
# a and b can run in parallel (stage 1), c after (stage 2)
|
|
assert len(stages) == 2
|
|
assert set(stages[0]) == {"a", "b"}
|
|
assert stages[1] == ["c"]
|
|
|
|
def test_predecessors_successors(self):
|
|
g = WorkflowGraph("test")
|
|
g.add_node(WorkflowNode(id="a", node_type=NodeType.AGENT))
|
|
g.add_node(WorkflowNode(id="b", node_type=NodeType.AGENT))
|
|
g.add_edge(WorkflowEdge(source="a", target="b"))
|
|
assert g.predecessors("b") == ["a"]
|
|
assert g.successors("a") == ["b"]
|
|
|
|
|
|
class TestWorkflowBuilder:
|
|
def test_build_simple(self):
|
|
wf = (
|
|
WorkflowBuilder("test")
|
|
.add_agent("a", agent="simple")
|
|
.add_agent("b", agent="simple")
|
|
.connect("a", "b")
|
|
.build()
|
|
)
|
|
assert wf.name == "test"
|
|
assert len(wf.nodes) == 2
|
|
assert len(wf.edges) == 1
|
|
|
|
def test_sequential(self):
|
|
wf = (
|
|
WorkflowBuilder("seq")
|
|
.add_agent("a", agent="simple")
|
|
.add_agent("b", agent="simple")
|
|
.add_agent("c", agent="simple")
|
|
.sequential("a", "b", "c")
|
|
.build()
|
|
)
|
|
assert len(wf.edges) == 2
|
|
|
|
def test_build_cyclic_raises(self):
|
|
with pytest.raises(ValueError, match="Invalid"):
|
|
(
|
|
WorkflowBuilder("bad")
|
|
.add_agent("a", agent="simple")
|
|
.add_agent("b", agent="simple")
|
|
.connect("a", "b")
|
|
.connect("b", "a")
|
|
.build()
|
|
)
|
|
|
|
|
|
class TestWorkflowEngine:
|
|
def test_run_invalid_graph(self):
|
|
engine = WorkflowEngine()
|
|
g = WorkflowGraph("bad")
|
|
g.add_node(WorkflowNode(id="a", node_type=NodeType.AGENT))
|
|
g.add_node(WorkflowNode(id="b", node_type=NodeType.AGENT))
|
|
g.add_edge(WorkflowEdge(source="a", target="b"))
|
|
g.add_edge(WorkflowEdge(source="b", target="a"))
|
|
result = engine.run(g)
|
|
assert not result.success
|
|
|
|
def test_run_transform_node(self):
|
|
bus = EventBus(record_history=True)
|
|
engine = WorkflowEngine(bus=bus)
|
|
wf = (
|
|
WorkflowBuilder("test")
|
|
.add_transform("t", transform="concatenate")
|
|
.build()
|
|
)
|
|
result = engine.run(wf, initial_input="hello")
|
|
assert result.success
|
|
|
|
def test_events_emitted(self):
|
|
bus = EventBus(record_history=True)
|
|
engine = WorkflowEngine(bus=bus)
|
|
wf = (
|
|
WorkflowBuilder("test")
|
|
.add_transform("t", transform="concatenate")
|
|
.build()
|
|
)
|
|
engine.run(wf, initial_input="hello")
|
|
event_types = {e.event_type for e in bus.history}
|
|
assert EventType.WORKFLOW_START in event_types
|
|
assert EventType.WORKFLOW_END in event_types
|