Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
contents: write
strategy:
matrix:
go-version: ['1.23', '1.24']
go-version: ['1.25']

steps:
- name: Check out code
Expand All @@ -40,7 +40,7 @@ jobs:

- name: Upload coverage reports
uses: codecov/codecov-action@v5
if: matrix.go-version == '1.24'
if: matrix.go-version == '1.25'
with:
file: ./coverage.out
fail_ci_if_error: false
Expand All @@ -58,7 +58,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
go-version: '1.25'
cache: true

- name: Run golangci-lint
Expand All @@ -80,7 +80,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
go-version: '1.25'
cache: true

- name: Build main package
Expand All @@ -105,7 +105,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
go-version: '1.25'
cache: true

- name: Build examples
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Compiled server binary
/server

# Dependency directories (remove the comment below to include it)
# vendor/

Expand Down
87 changes: 70 additions & 17 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ A production-grade, AI-powered workflow orchestration engine with a visual build
---

## Phase 2: Expanded Capabilities (Complete)
*Uncommitted - pending push*
*Complete*

### Observability Foundation (WS1)
- [x] MetricsCollector - Prometheus metrics with 6 pre-registered vectors
Expand Down Expand Up @@ -84,11 +84,11 @@ A production-grade, AI-powered workflow orchestration engine with a visual build
## Phase 3: Quality, Testing & Stability (In Progress)

### Copilot SDK Testing
- [ ] Mock-based unit tests for all Copilot client methods
- [ ] Tool handler invocation tests with realistic payloads
- [ ] Session lifecycle tests (create, send, destroy)
- [ ] Error path coverage (CLI not found, session failure, empty response, malformed JSON)
- [ ] Integration verification with mock Copilot server
- [x] Mock-based unit tests for all Copilot client methods
- [x] Tool handler invocation tests with realistic payloads
- [x] Session lifecycle tests (create, send, destroy)
- [x] Error path coverage (CLI not found, session failure, empty response, malformed JSON)
- [x] Integration verification with mock Copilot server

### E2E Test Expansion
- [ ] Update moduleTypeMap in all e2e specs with 6 new module types
Expand Down Expand Up @@ -117,7 +117,60 @@ A production-grade, AI-powered workflow orchestration engine with a visual build

---

## Phase 4: Production Readiness (Planned)
## Phase 4: EventBus Integration (Complete)
*PR #18 merged*

### EventBus Bridge
- [x] EventBusBridge adapter (MessageBroker → EventBus)
- [x] WorkflowEventEmitter with lifecycle events (workflow.started, workflow.completed, workflow.failed, step.started, step.completed, step.failed)

### EventBus Trigger
- [x] EventBusTrigger for native EventBus subscriptions
- [x] Configure with topics, event filtering, async mode
- [x] Start/Stop with subscription lifecycle management

### Engine Integration
- [x] Engine integration with workflow/step event emission
- [x] canHandleTrigger support for "eventbus" trigger type
- [x] TriggerWorkflow emits start/complete/fail events

### UI Updates
- [x] messaging.broker.eventbus module type in NodePalette (30 total)

---

## Phase 5: AI Server Bootstrap, Test Coverage & E2E Testing (In Progress)

### AI Server Bootstrap (WS1)
- [ ] cmd/server/main.go with HTTP mux and AI handler registration
- [ ] CLI flags for config, address, AI provider configuration
- [ ] Graceful shutdown with signal handling
- [ ] initAIService with conditional Anthropic/Copilot provider registration
- [ ] cmd/server/main_test.go with route verification tests

### Go Test Coverage (WS2)
- [ ] Root package (engine_test.go): 68.6% → ≥80%
- [ ] Module package: 77.1% → ≥80%
- [ ] Dynamic package: 75.4% → ≥80%
- [ ] AI packages: maintain ≥85%

### Playwright E2E Tests (WS3)
- [ ] Shared helpers (helpers.ts) with complete module type map
- [ ] deep-module-coverage.spec.ts: All 30 module types verified
- [ ] deep-complex-workflows.spec.ts: Multi-node workflow tests
- [ ] deep-property-editing.spec.ts: All field types tested
- [ ] deep-keyboard-shortcuts.spec.ts: Shortcut verification
- [ ] deep-ai-panel.spec.ts: AI Copilot panel tests
- [ ] deep-component-browser.spec.ts: Component Browser tests
- [ ] deep-import-export.spec.ts: Complex round-trip tests
- [ ] deep-edge-cases.spec.ts: Edge case coverage
- [ ] deep-accessibility.spec.ts: A11y testing
- [ ] deep-toast-notifications.spec.ts: Toast behavior tests
- [ ] deep-visual-regression.spec.ts: Visual regression baselines

---

## Phase 6: Production Readiness (Planned)

### Workflow Execution Runtime
- [ ] End-to-end workflow execution from YAML config
Expand Down Expand Up @@ -146,13 +199,13 @@ A production-grade, AI-powered workflow orchestration engine with a visual build

## Coverage Targets

| Package | Current | Target |
|---------|---------|--------|
| workflow (root) | 70.4% | 80% |
| ai | 84.8% | 85% |
| ai/copilot | 6.2% | 70% |
| ai/llm | 84.5% | 85% |
| config | 100% | 100% |
| dynamic | 75.4% | 80% |
| handlers | 50.9% | 70% |
| module | 76.1% | 80% |
| Package | Current | Target | Status |
|---------|---------|--------|--------|
| workflow (root) | 68.6% | 80% | Below target |
| ai | 84.8% | 85% | Near target |
| ai/copilot | 90.3% | 70% | ✓ Exceeded |
| ai/llm | 84.5% | 85% | Near target |
| config | 100% | 100% | ✓ Met |
| dynamic | 75.4% | 80% | Below target |
| handlers | 70.8% | 70% | ✓ Met |
| module | 77.1% | 80% | Below target |
2 changes: 1 addition & 1 deletion ai/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func (h *Handler) HandleProviders(w http.ResponseWriter, r *http.Request) {
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
_ = json.NewEncoder(w).Encode(v)
}

func writeError(w http.ResponseWriter, status int, msg string) {
Expand Down
75 changes: 75 additions & 0 deletions ai/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,78 @@ func TestHandleProviders(t *testing.T) {
t.Error("response missing providers field")
}
}

func TestHandler_RegisterRoutes(t *testing.T) {
h := setupTestHandler()
mux := http.NewServeMux()
h.RegisterRoutes(mux)

// Each registered route should respond (not 404).
routes := []struct {
method string
path string
}{
{http.MethodPost, "/api/ai/generate"},
{http.MethodPost, "/api/ai/component"},
{http.MethodPost, "/api/ai/suggest"},
{http.MethodGet, "/api/ai/providers"},
}

for _, rt := range routes {
t.Run(rt.method+" "+rt.path, func(t *testing.T) {
var body *bytes.Reader
if rt.method == http.MethodPost {
body = bytes.NewReader([]byte("{}"))
} else {
body = bytes.NewReader(nil)
}
req := httptest.NewRequest(rt.method, rt.path, body)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code == http.StatusNotFound {
t.Errorf("route %s %s returned 404, expected it to be registered", rt.method, rt.path)
}
})
}
}

func TestHandleGenerate_MethodNotAllowed(t *testing.T) {
h := setupTestHandler()
mux := http.NewServeMux()
h.RegisterRoutes(mux)

// GET to a POST-only route should return 405.
req := httptest.NewRequest(http.MethodGet, "/api/ai/generate", nil)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)

if w.Code != http.StatusMethodNotAllowed {
t.Errorf("expected 405 for GET /api/ai/generate, got %d", w.Code)
}
}

func TestHandleComponent_InvalidBody(t *testing.T) {
h := setupTestHandler()

req := httptest.NewRequest(http.MethodPost, "/api/ai/component", bytes.NewReader([]byte("not json")))
w := httptest.NewRecorder()

h.HandleComponent(w, req)

if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", w.Code)
}
}

func TestHandleSuggest_InvalidBody(t *testing.T) {
h := setupTestHandler()

req := httptest.NewRequest(http.MethodPost, "/api/ai/suggest", bytes.NewReader([]byte("not json")))
w := httptest.NewRecorder()

h.HandleSuggest(w, req)

if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", w.Code)
}
}
16 changes: 8 additions & 8 deletions ai/copilot/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,13 @@ func (c *Client) GenerateWorkflow(ctx context.Context, req ai.GenerateRequest) (
if err != nil {
return nil, err
}
defer session.Destroy()
defer func() { _ = session.Destroy() }()

prompt := ai.GeneratePrompt(req)

resp, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt})
if err != nil {
return nil, fmt.Errorf("Copilot request failed: %w", err)
return nil, fmt.Errorf("copilot request failed: %w", err)
}

if resp == nil || resp.Data.Content == nil {
Expand All @@ -185,13 +185,13 @@ func (c *Client) GenerateComponent(ctx context.Context, spec ai.ComponentSpec) (
if err != nil {
return "", err
}
defer session.Destroy()
defer func() { _ = session.Destroy() }()

prompt := ai.ComponentPrompt(spec)

resp, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt})
if err != nil {
return "", fmt.Errorf("Copilot request failed: %w", err)
return "", fmt.Errorf("copilot request failed: %w", err)
}

if resp == nil || resp.Data.Content == nil {
Expand All @@ -207,13 +207,13 @@ func (c *Client) SuggestWorkflow(ctx context.Context, useCase string) ([]ai.Work
if err != nil {
return nil, err
}
defer session.Destroy()
defer func() { _ = session.Destroy() }()

prompt := ai.SuggestPrompt(useCase)

resp, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt})
if err != nil {
return nil, fmt.Errorf("Copilot request failed: %w", err)
return nil, fmt.Errorf("copilot request failed: %w", err)
}

if resp == nil || resp.Data.Content == nil {
Expand Down Expand Up @@ -244,13 +244,13 @@ func (c *Client) IdentifyMissingComponents(ctx context.Context, cfg *config.Work
if err != nil {
return nil, err
}
defer session.Destroy()
defer func() { _ = session.Destroy() }()

prompt := ai.MissingComponentsPrompt(types)

resp, err := session.SendAndWait(ctx, copilot.MessageOptions{Prompt: prompt})
if err != nil {
return nil, fmt.Errorf("Copilot request failed: %w", err)
return nil, fmt.Errorf("copilot request failed: %w", err)
}

if resp == nil || resp.Data.Content == nil {
Expand Down
8 changes: 4 additions & 4 deletions ai/copilot/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func TestGenerateWorkflow_SendError(t *testing.T) {
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), "Copilot request failed") {
if !strings.Contains(err.Error(), "copilot request failed") {
t.Errorf("unexpected error message: %v", err)
}
}
Expand Down Expand Up @@ -327,7 +327,7 @@ func TestGenerateComponent_SendError(t *testing.T) {
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), "Copilot request failed") {
if !strings.Contains(err.Error(), "copilot request failed") {
t.Errorf("unexpected error message: %v", err)
}
}
Expand Down Expand Up @@ -440,7 +440,7 @@ func TestSuggestWorkflow_SendError(t *testing.T) {
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), "Copilot request failed") {
if !strings.Contains(err.Error(), "copilot request failed") {
t.Errorf("unexpected error message: %v", err)
}
}
Expand Down Expand Up @@ -546,7 +546,7 @@ func TestIdentifyMissingComponents_SendError(t *testing.T) {
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), "Copilot request failed") {
if !strings.Contains(err.Error(), "copilot request failed") {
t.Errorf("unexpected error message: %v", err)
}
}
Expand Down
8 changes: 2 additions & 6 deletions ai/llm/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func (c *Client) call(ctx context.Context, system string, messages []message, to
if err != nil {
return nil, fmt.Errorf("API request failed: %w", err)
}
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()

respBody, err := io.ReadAll(resp.Body)
if err != nil {
Expand All @@ -150,11 +150,7 @@ func (c *Client) callWithToolLoop(ctx context.Context, system string, userConten
tools := Tools()
apiTools := make([]toolDef, len(tools))
for i, t := range tools {
apiTools[i] = toolDef{
Name: t.Name,
Description: t.Description,
InputSchema: t.InputSchema,
}
apiTools[i] = toolDef(t)
}

userMsg, _ := json.Marshal(userContent)
Expand Down
Loading
Loading