feat(T4.1-T4.4): implement advanced operations & analytics (part 1)
T4.1: Real-time Metrics Dashboard - Add internal/dashboard package with MetricsAggregator - Record, aggregate, and query metrics - Percentile calculations (p50, p95, p99) - Time-series data with max size eviction - 13 metrics tests, all passing T4.2: Workflow Visualization & DAG Rendering - Add internal/visualization package with DAGRenderer - Convert dependency graphs to DOT format - Critical path highlighting - Topological sorting with parallel task detection - HTML rendering for visualization - 11 DAG rendering tests, all passing T4.3: Advanced Search & Filtering - Add internal/search package with WorkflowSearch - Full-text indexing with word-based lookup - Filter by status, assignee, tag, date range - Regex pattern matching - Saved filters for reusable queries - 15 search tests, all passing T4.4: Cost Tracking & Optimization - Add internal/cost package with CostTracker - Track LLM API costs (by token) - Track git operation costs - Track compute resource costs (by duration) - Cost aggregation by workflow/type - Optimization recommendations - 11 cost tests, all passing Total T4.1-T4.4: 50 tests passing Next: T4.5-T4.8 (alerting, profiling, multi-cluster, self-deployment)
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
package visualization
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAddNode(t *testing.T) {
|
||||
dag := NewDAGRenderer()
|
||||
dag.AddNode("T1", "completed", 100)
|
||||
|
||||
assert.NotNil(t, dag.nodes["T1"])
|
||||
assert.Equal(t, "completed", dag.nodes["T1"].Status)
|
||||
}
|
||||
|
||||
func TestAddEdge(t *testing.T) {
|
||||
dag := NewDAGRenderer()
|
||||
dag.AddNode("T1", "completed", 100)
|
||||
dag.AddNode("T2", "completed", 200)
|
||||
|
||||
err := dag.AddEdge("T1", "T2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(dag.edges["T1"]))
|
||||
}
|
||||
|
||||
func TestAddEdgeNotFound(t *testing.T) {
|
||||
dag := NewDAGRenderer()
|
||||
dag.AddNode("T1", "completed", 100)
|
||||
|
||||
err := dag.AddEdge("T1", "T2")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMarkCriticalPath(t *testing.T) {
|
||||
dag := NewDAGRenderer()
|
||||
dag.AddNode("T1", "completed", 100)
|
||||
dag.AddNode("T2", "completed", 200)
|
||||
|
||||
err := dag.MarkCriticalPath([]string{"T1", "T2"})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, dag.nodes["T1"].Critical)
|
||||
assert.True(t, dag.nodes["T2"].Critical)
|
||||
}
|
||||
|
||||
func TestRenderDOT(t *testing.T) {
|
||||
dag := NewDAGRenderer()
|
||||
dag.AddNode("T1", "completed", 100)
|
||||
dag.AddNode("T2", "running", 200)
|
||||
dag.AddEdge("T1", "T2")
|
||||
|
||||
dot := dag.RenderDOT()
|
||||
assert.True(t, strings.Contains(dot, "digraph WorkflowDAG"))
|
||||
assert.True(t, strings.Contains(dot, "T1"))
|
||||
assert.True(t, strings.Contains(dot, "T2"))
|
||||
assert.True(t, strings.Contains(dot, "->"))
|
||||
}
|
||||
|
||||
func TestRenderHTML(t *testing.T) {
|
||||
dag := NewDAGRenderer()
|
||||
dag.AddNode("T1", "completed", 100)
|
||||
|
||||
html := dag.RenderHTML()
|
||||
assert.True(t, strings.Contains(html, "<html>"))
|
||||
assert.True(t, strings.Contains(html, "Workflow DAG"))
|
||||
assert.True(t, strings.Contains(html, "T1"))
|
||||
}
|
||||
|
||||
func TestGetTopologicalSort(t *testing.T) {
|
||||
dag := NewDAGRenderer()
|
||||
dag.AddNode("T1", "completed", 100)
|
||||
dag.AddNode("T2", "completed", 200)
|
||||
dag.AddNode("T3", "completed", 150)
|
||||
|
||||
dag.AddEdge("T1", "T2")
|
||||
dag.AddEdge("T2", "T3")
|
||||
|
||||
sorted, err := dag.GetTopologicalSort()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, len(sorted))
|
||||
}
|
||||
|
||||
func TestGetParallel(t *testing.T) {
|
||||
dag := NewDAGRenderer()
|
||||
dag.AddNode("T1", "completed", 100)
|
||||
dag.AddNode("T2", "completed", 200)
|
||||
dag.AddNode("T3", "completed", 150)
|
||||
|
||||
dag.AddEdge("T1", "T3")
|
||||
|
||||
parallel := dag.GetParallel()
|
||||
assert.True(t, len(parallel) > 0)
|
||||
}
|
||||
|
||||
func TestGetStats(t *testing.T) {
|
||||
dag := NewDAGRenderer()
|
||||
dag.AddNode("T1", "completed", 100)
|
||||
dag.AddNode("T2", "completed", 200)
|
||||
dag.AddEdge("T1", "T2")
|
||||
|
||||
stats := dag.GetStats()
|
||||
assert.Equal(t, 2, stats["node_count"])
|
||||
assert.Equal(t, 1, stats["edge_count"])
|
||||
assert.Equal(t, 300.0, stats["total_duration"])
|
||||
}
|
||||
|
||||
func TestCriticalPathHighlighting(t *testing.T) {
|
||||
dag := NewDAGRenderer()
|
||||
dag.AddNode("T1", "completed", 100)
|
||||
dag.AddNode("T2", "completed", 200)
|
||||
dag.MarkCriticalPath([]string{"T1", "T2"})
|
||||
|
||||
dot := dag.RenderDOT()
|
||||
assert.True(t, strings.Contains(dot, "fillcolor=red"))
|
||||
}
|
||||
|
||||
func TestComplexDAG(t *testing.T) {
|
||||
dag := NewDAGRenderer()
|
||||
|
||||
// Create a diamond-shaped DAG
|
||||
dag.AddNode("Start", "completed", 50)
|
||||
dag.AddNode("A", "completed", 100)
|
||||
dag.AddNode("B", "completed", 150)
|
||||
dag.AddNode("End", "completed", 50)
|
||||
|
||||
dag.AddEdge("Start", "A")
|
||||
dag.AddEdge("Start", "B")
|
||||
dag.AddEdge("A", "End")
|
||||
dag.AddEdge("B", "End")
|
||||
|
||||
stats := dag.GetStats()
|
||||
assert.Equal(t, 4, stats["node_count"])
|
||||
assert.Equal(t, 4, stats["edge_count"])
|
||||
}
|
||||
Reference in New Issue
Block a user