feat: RoutingWorkflow + LLM Router + Memory Activity
ci / test (push) Successful in 2m12s

- Add RoutingWorkflow: generic state machine executor for WorkflowSpec
- Add LLM Router: natural language → WorkflowSpec generation
- Add RetrieveMemoryActivity: query poimen-memory for context
- Add activities: AnalyzeCode, SecurityScan, GenerateReport, Notify, etc.
- Add agent-prompts/router: LLM prompt documentation
- Extend starter with --route flag for routing workflows
- Remove orchestrator job (trigger via API/message instead)
- Clean up: move docs to Desktop, add .gitignore for *.md
This commit is contained in:
Test
2026-09-02 19:21:53 -07:00
parent 5a465b145c
commit a0e64224a7
74 changed files with 3950 additions and 12421 deletions
+145
View File
@@ -0,0 +1,145 @@
// +build integration
package tests
import (
"context"
"encoding/json"
"os"
"testing"
"time"
"github.com/rockliang/poimen/workflows/internal/routing"
"github.com/stretchr/testify/require"
)
// TestRoutingE2E_GenerateAndValidate tests full flow against real api.riotpiao.com
// Run with: go test -tags=integration -v -run TestRoutingE2E ./tests/...
func TestRoutingE2E_GenerateAndValidate(t *testing.T) {
if os.Getenv("RUN_INTEGRATION_TESTS") != "1" {
t.Skip("Skipping integration test. Set RUN_INTEGRATION_TESTS=1 to run.")
}
// Load knowledge base
kb, err := routing.LoadKnowledgeBaseFromDefaultPath()
require.NoError(t, err, "failed to load knowledge base")
// Create router
router, err := routing.NewLLMRouter(kb)
require.NoError(t, err, "failed to create router")
// Create validator
validator := routing.NewValidator(kb)
tests := []struct {
name string
message string
context map[string]interface{}
isCron bool
}{
{
name: "one-time repo analysis",
message: "Analyze https://github.com/rockliang/poimen for code quality and security issues",
context: map[string]interface{}{"branch": "main"},
isCron: false,
},
{
name: "scheduled security scan",
message: "Run daily security scan at 3 AM on https://github.com/rockliang/poimen",
isCron: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
// Generate workflow spec
input := routing.LLMRouterInput{
Message: tt.message,
Context: tt.context,
}
output, err := router.Route(ctx, input)
require.NoError(t, err, "LLM router failed")
// Log generated spec
specJSON, _ := json.MarshalIndent(output, "", " ")
t.Logf("Generated spec:\n%s", string(specJSON))
// Validate based on type
if tt.isCron {
require.True(t, output.IsCron, "expected cron workflow")
require.NotNil(t, output.CronSpec, "cron spec is nil")
require.NotEmpty(t, output.CronSpec.Schedule, "cron schedule is empty")
result := validator.ValidateCronWorkflowSpec(output.CronSpec)
require.True(t, result.Valid, "validation failed: %v", result.Errors)
t.Logf("Cron workflow validated: %s (schedule: %s)",
output.CronSpec.Name, output.CronSpec.Schedule)
} else {
require.False(t, output.IsCron, "expected one-time workflow")
require.NotNil(t, output.Spec, "spec is nil")
result := validator.ValidateWorkflowSpec(output.Spec)
require.True(t, result.Valid, "validation failed: %v", result.Errors)
t.Logf("One-time workflow validated: %s (%d states)",
output.Spec.Name, len(output.Spec.States))
}
})
}
}
// TestRoutingE2E_FullPipeline tests LLM router -> validation -> (simulated) execution
func TestRoutingE2E_FullPipeline(t *testing.T) {
if os.Getenv("RUN_INTEGRATION_TESTS") != "1" {
t.Skip("Skipping integration test. Set RUN_INTEGRATION_TESTS=1 to run.")
}
kb, err := routing.LoadKnowledgeBaseFromDefaultPath()
require.NoError(t, err)
router, err := routing.NewLLMRouter(kb)
require.NoError(t, err)
validator := routing.NewValidator(kb)
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
// Generate workflow
output, err := router.Route(ctx, routing.LLMRouterInput{
Message: "Clone and analyze https://github.com/rockliang/poimen for security vulnerabilities",
})
require.NoError(t, err)
require.False(t, output.IsCron)
require.NotNil(t, output.Spec)
// Validate
result := validator.ValidateWorkflowSpec(output.Spec)
require.True(t, result.Valid, "validation failed: %v", result.Errors)
// Verify structure
require.NotEmpty(t, output.Spec.Name)
require.NotEmpty(t, output.Spec.States)
// First state should be CloneRepoActivity
require.Equal(t, "CloneRepoActivity", output.Spec.States[0].Resource,
"expected first activity to be CloneRepoActivity")
// Check that flaky activities have retry policies
for _, state := range output.Spec.States {
if state.Type == routing.StateTypeTask {
if kb.IsFlaky(state.Resource) {
require.NotNil(t, state.Retry, "flaky activity %s should have retry policy", state.Resource)
require.GreaterOrEqual(t, state.Retry.MaxAttempts, int32(2),
"flaky activity %s should have at least 2 retries", state.Resource)
}
}
}
t.Logf("Full pipeline test passed: %s with %d states", output.Spec.Name, len(output.Spec.States))
}
+272
View File
@@ -0,0 +1,272 @@
package tests
import (
"context"
"fmt"
"testing"
"github.com/rockliang/poimen/workflows/internal/routing"
"github.com/rockliang/poimen/workflows/statemachine"
"github.com/stretchr/testify/require"
"go.temporal.io/sdk/testsuite"
)
func TestRoutingWorkflow_SimpleWorkflow(t *testing.T) {
testSuite := &testsuite.WorkflowTestSuite{}
env := testSuite.NewTestWorkflowEnvironment()
// Register mock activity
env.RegisterActivity(mockCloneRepoActivity)
// Create simple workflow spec
spec := &routing.WorkflowSpec{
Name: "test-workflow",
Input: map[string]interface{}{
"repo": "https://github.com/test/repo",
"branch": "main",
},
States: []routing.State{
{
Name: "Clone",
Type: routing.StateTypeTask,
Resource: "mockCloneRepoActivity",
Parameters: map[string]interface{}{
"repo": "${input.repo}",
"branch": "${input.branch}",
},
Timeout: "5m",
Retry: &routing.RetryPolicy{
MaxAttempts: 2,
BackoffRate: 1.5,
InitialInterval: "1s",
},
End: true,
},
},
}
input := statemachine.RoutingWorkflowInput{Spec: spec}
env.ExecuteWorkflow(statemachine.RoutingWorkflow, input)
require.True(t, env.IsWorkflowCompleted())
require.NoError(t, env.GetWorkflowError())
var output statemachine.RoutingWorkflowOutput
require.NoError(t, env.GetWorkflowResult(&output))
require.Equal(t, "COMPLETED", output.Status)
require.NotNil(t, output.FinalOutput)
}
func TestRoutingWorkflow_MultiStepWorkflow(t *testing.T) {
testSuite := &testsuite.WorkflowTestSuite{}
env := testSuite.NewTestWorkflowEnvironment()
// Register mock activities
env.RegisterActivity(mockCloneRepoActivity)
env.RegisterActivity(mockAnalyzeActivity)
// Create multi-step workflow spec
spec := &routing.WorkflowSpec{
Name: "multi-step-workflow",
Input: map[string]interface{}{
"repo": "https://github.com/test/repo",
},
States: []routing.State{
{
Name: "Clone",
Type: routing.StateTypeTask,
Resource: "mockCloneRepoActivity",
Parameters: map[string]interface{}{
"repo": "${input.repo}",
},
Timeout: "5m",
Next: "Analyze",
},
{
Name: "Analyze",
Type: routing.StateTypeTask,
Resource: "mockAnalyzeActivity",
Parameters: map[string]interface{}{
"path": "${Clone.output.path}",
},
Timeout: "10m",
End: true,
},
},
}
input := statemachine.RoutingWorkflowInput{Spec: spec}
env.ExecuteWorkflow(statemachine.RoutingWorkflow, input)
require.True(t, env.IsWorkflowCompleted())
require.NoError(t, env.GetWorkflowError())
var output statemachine.RoutingWorkflowOutput
require.NoError(t, env.GetWorkflowResult(&output))
t.Logf("Output: %+v", output)
t.Logf("Error: %s", output.Error)
require.Equal(t, "COMPLETED", output.Status)
require.Contains(t, output.StepResults, "Clone")
require.Contains(t, output.StepResults, "Analyze")
}
func TestRoutingWorkflow_PassState(t *testing.T) {
testSuite := &testsuite.WorkflowTestSuite{}
env := testSuite.NewTestWorkflowEnvironment()
// Create workflow with Pass state
spec := &routing.WorkflowSpec{
Name: "pass-state-workflow",
Input: map[string]interface{}{},
States: []routing.State{
{
Name: "StaticResult",
Type: routing.StateTypePass,
Result: map[string]interface{}{"status": "ok", "message": "static result"},
End: true,
},
},
}
input := statemachine.RoutingWorkflowInput{Spec: spec}
env.ExecuteWorkflow(statemachine.RoutingWorkflow, input)
require.True(t, env.IsWorkflowCompleted())
require.NoError(t, env.GetWorkflowError())
var output statemachine.RoutingWorkflowOutput
require.NoError(t, env.GetWorkflowResult(&output))
require.Equal(t, "COMPLETED", output.Status)
}
func TestRoutingWorkflow_FailState(t *testing.T) {
testSuite := &testsuite.WorkflowTestSuite{}
env := testSuite.NewTestWorkflowEnvironment()
// Create workflow with Fail state
spec := &routing.WorkflowSpec{
Name: "fail-state-workflow",
Input: map[string]interface{}{},
States: []routing.State{
{
Name: "HandleError",
Type: routing.StateTypeFail,
Error: "WorkflowError",
Cause: "Something went wrong",
},
},
}
input := statemachine.RoutingWorkflowInput{Spec: spec}
env.ExecuteWorkflow(statemachine.RoutingWorkflow, input)
require.True(t, env.IsWorkflowCompleted())
require.NoError(t, env.GetWorkflowError())
var output statemachine.RoutingWorkflowOutput
require.NoError(t, env.GetWorkflowResult(&output))
require.Equal(t, "FAILED", output.Status)
require.Contains(t, output.Error, "WorkflowError")
}
func TestRoutingWorkflow_ErrorCatch(t *testing.T) {
testSuite := &testsuite.WorkflowTestSuite{}
env := testSuite.NewTestWorkflowEnvironment()
// Register mock activities
env.RegisterActivity(mockFailingActivity)
// Create workflow with error handling
spec := &routing.WorkflowSpec{
Name: "error-catch-workflow",
Input: map[string]interface{}{},
States: []routing.State{
{
Name: "FlakyStep",
Type: routing.StateTypeTask,
Resource: "mockFailingActivity",
Parameters: map[string]interface{}{},
Timeout: "1m",
Retry: &routing.RetryPolicy{
MaxAttempts: 1,
BackoffRate: 1.0,
InitialInterval: "1s",
},
Catch: []routing.CatchClause{
{
ErrorEquals: []string{"ActivityError"},
Next: "HandleError",
},
},
Next: "Success",
},
{
Name: "Success",
Type: routing.StateTypePass,
Result: "success",
End: true,
},
{
Name: "HandleError",
Type: routing.StateTypeFail,
Error: "CaughtError",
Cause: "Activity failed and was caught",
},
},
}
input := statemachine.RoutingWorkflowInput{Spec: spec}
env.ExecuteWorkflow(statemachine.RoutingWorkflow, input)
require.True(t, env.IsWorkflowCompleted())
require.NoError(t, env.GetWorkflowError())
var output statemachine.RoutingWorkflowOutput
require.NoError(t, env.GetWorkflowResult(&output))
require.Equal(t, "FAILED", output.Status)
require.Contains(t, output.Error, "CaughtError")
}
func TestRoutingWorkflow_EmptySpec(t *testing.T) {
testSuite := &testsuite.WorkflowTestSuite{}
env := testSuite.NewTestWorkflowEnvironment()
// Empty spec
input := statemachine.RoutingWorkflowInput{Spec: nil}
env.ExecuteWorkflow(statemachine.RoutingWorkflow, input)
require.True(t, env.IsWorkflowCompleted())
require.NoError(t, env.GetWorkflowError())
var output statemachine.RoutingWorkflowOutput
require.NoError(t, env.GetWorkflowResult(&output))
require.Equal(t, "FAILED", output.Status)
require.Contains(t, output.Error, "empty")
}
// Mock activities
func mockCloneRepoActivity(ctx context.Context, params map[string]interface{}) (map[string]interface{}, error) {
return map[string]interface{}{
"path": "/tmp/cloned-repo",
"commit": "abc123",
"branch": "main",
}, nil
}
func mockAnalyzeActivity(ctx context.Context, params map[string]interface{}) (map[string]interface{}, error) {
return map[string]interface{}{
"quality": 0.85,
"issues": []string{},
"summary": "Code analysis complete",
}, nil
}
func mockFailingActivity(ctx context.Context, params map[string]interface{}) (map[string]interface{}, error) {
return nil, fmt.Errorf("mock activity failure")
}
+141
View File
@@ -0,0 +1,141 @@
// +build integration
package tests
import (
"context"
"os"
"testing"
"time"
"github.com/rockliang/poimen/workflows/internal/routing"
"github.com/rockliang/poimen/workflows/statemachine"
"github.com/stretchr/testify/require"
"go.temporal.io/sdk/client"
)
// TestTemporalRoutingWorkflow tests full flow against real Temporal cluster
// Run with: TEMPORAL_HOSTPORT=temporal.riotpiao.com:7233 RUN_INTEGRATION_TESTS=1 go test -tags=integration -v -run TestTemporalRoutingWorkflow ./tests/...
func TestTemporalRoutingWorkflow(t *testing.T) {
if os.Getenv("RUN_INTEGRATION_TESTS") != "1" {
t.Skip("Skipping integration test. Set RUN_INTEGRATION_TESTS=1 to run.")
}
hostPort := os.Getenv("TEMPORAL_HOSTPORT")
if hostPort == "" {
hostPort = "temporal.riotpiao.com:7233"
}
namespace := os.Getenv("TEMPORAL_NAMESPACE")
if namespace == "" {
namespace = "poimen-harness"
}
t.Logf("Connecting to Temporal at %s (namespace: %s)", hostPort, namespace)
// Connect to Temporal
c, err := client.Dial(client.Options{
HostPort: hostPort,
Namespace: namespace,
})
if err != nil {
t.Skipf("Skipping - cannot connect to Temporal: %v", err)
}
defer c.Close()
t.Log("Connected to Temporal successfully")
// Test 1: Generate spec via LLM and submit
t.Run("LLM_Route_And_Submit", func(t *testing.T) {
// Load KB and create router
kb, err := routing.LoadKnowledgeBaseFromDefaultPath()
require.NoError(t, err)
router, err := routing.NewLLMRouter(kb)
require.NoError(t, err)
// Generate workflow spec
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
output, err := router.Route(ctx, routing.LLMRouterInput{
Message: "Clone and analyze https://github.com/rockliang/poimen",
})
require.NoError(t, err)
require.False(t, output.IsCron)
require.NotNil(t, output.Spec)
t.Logf("Generated spec: %s with %d states", output.Spec.Name, len(output.Spec.States))
// Submit to Temporal
workflowID := "test-routing-" + time.Now().Format("20060102-150405")
input := statemachine.RoutingWorkflowInput{Spec: output.Spec}
run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
ID: workflowID,
TaskQueue: "poimen-taskqueue",
}, statemachine.RoutingWorkflow, input)
require.NoError(t, err)
t.Logf("Workflow submitted: ID=%s, RunID=%s", run.GetID(), run.GetRunID())
// Check workflow started (don't wait for completion - activities may not be registered)
desc, err := c.DescribeWorkflowExecution(ctx, workflowID, "")
require.NoError(t, err)
t.Logf("Workflow status: %s", desc.WorkflowExecutionInfo.Status.String())
// Cancel the workflow (since activities may not be running)
err = c.CancelWorkflow(ctx, workflowID, "")
if err != nil {
t.Logf("Cancel failed (may already be done): %v", err)
} else {
t.Log("Workflow cancelled")
}
})
// Test 2: Submit simple Pass-only workflow (no activities needed)
t.Run("PassOnly_Workflow", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
spec := &routing.WorkflowSpec{
Name: "pass-only-test",
Input: map[string]interface{}{"test": true},
States: []routing.State{
{
Name: "Step1",
Type: routing.StateTypePass,
Result: map[string]interface{}{"status": "step1-done"},
Next: "Step2",
},
{
Name: "Step2",
Type: routing.StateTypePass,
Result: map[string]interface{}{"status": "step2-done", "final": true},
End: true,
},
},
}
workflowID := "test-pass-only-" + time.Now().Format("20060102-150405")
input := statemachine.RoutingWorkflowInput{Spec: spec}
run, err := c.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
ID: workflowID,
TaskQueue: "poimen-taskqueue",
}, statemachine.RoutingWorkflow, input)
require.NoError(t, err)
t.Logf("Pass-only workflow submitted: ID=%s", run.GetID())
// Wait for result (Pass states don't need workers)
var result statemachine.RoutingWorkflowOutput
err = run.Get(ctx, &result)
require.NoError(t, err)
t.Logf("Workflow result: status=%s", result.Status)
require.Equal(t, "COMPLETED", result.Status)
require.Contains(t, result.StepResults, "Step1")
require.Contains(t, result.StepResults, "Step2")
})
}