Skip to content

Commit 25defdf

Browse files
Copilotintel352
andauthored
Break handler↔module concrete-type dependency via shared interfaces package (#78)
* Initial plan * feat: break handler↔module concrete-type dependency via interfaces package - Create interfaces/pipeline.go with EventRecorder, PipelineRunner, StepRegistryProvider - Add SetLogger, SetEventRecorder, Run methods to module.Pipeline - Alias module.EventRecorder to interfaces.EventRecorder - Update handlers/pipeline.go to use interfaces (removes module import) - Update engine.go PipelineAdder to use interfaces.PipelineRunner - Add mockPipelineRunner test to demonstrate mock-based handler testing Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> * refactor: inject logger/recorder at config time, not per-execution - SetLogger/SetEventRecorder now propagate to all already-registered pipelines - AddPipeline now injects logger/recorder immediately if already set on the handler - ExecuteWorkflow only calls Run() — no mutation on the hot path - Add mockEventRecorder and TestPipelineHandler_InjectionAtConfigTime test - Fix test comment: "without depending on concrete module types" Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> Co-authored-by: Jonathan Langevin <codingsloth@pm.me>
1 parent 32f4c4d commit 25defdf

5 files changed

Lines changed: 193 additions & 27 deletions

File tree

engine.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/GoCodeAlone/workflow/capability"
1111
"github.com/GoCodeAlone/workflow/config"
1212
"github.com/GoCodeAlone/workflow/dynamic"
13+
"github.com/GoCodeAlone/workflow/interfaces"
1314
"github.com/GoCodeAlone/workflow/module"
1415
"github.com/GoCodeAlone/workflow/plugin"
1516
"github.com/GoCodeAlone/workflow/schema"
@@ -32,7 +33,7 @@ type WorkflowHandler interface {
3233
// PipelineAdder is implemented by workflow handlers that can receive named pipelines.
3334
// This allows the engine to add pipelines without importing the handlers package.
3435
type PipelineAdder interface {
35-
AddPipeline(name string, p *module.Pipeline)
36+
AddPipeline(name string, p interfaces.PipelineRunner)
3637
}
3738

3839
// ModuleFactory is a function that creates a module from a name and configuration

handlers/pipeline.go

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,42 +7,59 @@ import (
77
"strings"
88

99
"github.com/CrisisTextLine/modular"
10-
"github.com/GoCodeAlone/workflow/module"
10+
"github.com/GoCodeAlone/workflow/interfaces"
1111
)
1212

1313
// PipelineWorkflowHandler manages and executes pipeline-based workflows.
1414
type PipelineWorkflowHandler struct {
15-
pipelines map[string]*module.Pipeline
16-
stepRegistry *module.StepRegistry
15+
pipelines map[string]interfaces.PipelineRunner
16+
stepRegistry interfaces.StepRegistryProvider
1717
logger *slog.Logger
18-
eventRecorder module.EventRecorder
18+
eventRecorder interfaces.EventRecorder
1919
}
2020

2121
// NewPipelineWorkflowHandler creates a new PipelineWorkflowHandler.
2222
func NewPipelineWorkflowHandler() *PipelineWorkflowHandler {
2323
return &PipelineWorkflowHandler{
24-
pipelines: make(map[string]*module.Pipeline),
24+
pipelines: make(map[string]interfaces.PipelineRunner),
2525
}
2626
}
2727

2828
// SetStepRegistry sets the step registry used to create pipeline steps.
29-
func (h *PipelineWorkflowHandler) SetStepRegistry(registry *module.StepRegistry) {
29+
func (h *PipelineWorkflowHandler) SetStepRegistry(registry interfaces.StepRegistryProvider) {
3030
h.stepRegistry = registry
3131
}
3232

33-
// SetLogger sets the logger for pipeline execution.
33+
// SetLogger sets the logger for pipeline execution and propagates it to
34+
// all already-registered pipelines. Pipelines added after this call will
35+
// also have the logger injected in AddPipeline.
3436
func (h *PipelineWorkflowHandler) SetLogger(logger *slog.Logger) {
3537
h.logger = logger
38+
for _, p := range h.pipelines {
39+
p.SetLogger(logger)
40+
}
3641
}
3742

38-
// SetEventRecorder sets the event recorder for pipeline execution events.
39-
// When set, each pipeline execution will record events to this recorder.
40-
func (h *PipelineWorkflowHandler) SetEventRecorder(recorder module.EventRecorder) {
43+
// SetEventRecorder sets the event recorder for pipeline execution events and
44+
// propagates it to all already-registered pipelines. Pipelines added after
45+
// this call will also have the recorder injected in AddPipeline.
46+
func (h *PipelineWorkflowHandler) SetEventRecorder(recorder interfaces.EventRecorder) {
4147
h.eventRecorder = recorder
48+
for _, p := range h.pipelines {
49+
p.SetEventRecorder(recorder)
50+
}
4251
}
4352

4453
// AddPipeline registers a named pipeline with the handler.
45-
func (h *PipelineWorkflowHandler) AddPipeline(name string, p *module.Pipeline) {
54+
// If a logger or event recorder has already been set on the handler,
55+
// they are injected into the pipeline immediately at configuration time.
56+
func (h *PipelineWorkflowHandler) AddPipeline(name string, p interfaces.PipelineRunner) {
57+
if h.logger != nil {
58+
p.SetLogger(h.logger)
59+
}
60+
if h.eventRecorder != nil {
61+
p.SetEventRecorder(h.eventRecorder)
62+
}
4663
h.pipelines[name] = p
4764
}
4865

@@ -87,20 +104,10 @@ func (h *PipelineWorkflowHandler) ExecuteWorkflow(ctx context.Context, workflowT
87104
return nil, fmt.Errorf("pipeline %q not found", name)
88105
}
89106

90-
// Set logger on pipeline if available
91-
if h.logger != nil && pipeline.Logger == nil {
92-
pipeline.Logger = h.logger
93-
}
94-
95-
// Set event recorder on pipeline if available
96-
if h.eventRecorder != nil && pipeline.EventRecorder == nil {
97-
pipeline.EventRecorder = h.eventRecorder
98-
}
99-
100-
pc, err := pipeline.Execute(ctx, data)
107+
result, err := pipeline.Run(ctx, data)
101108
if err != nil {
102109
return nil, fmt.Errorf("pipeline %q execution failed: %w", name, err)
103110
}
104111

105-
return pc.Current, nil
112+
return result, nil
106113
}

handlers/pipeline_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,36 @@ package handlers
22

33
import (
44
"context"
5+
"log/slog"
56
"strings"
67
"testing"
78

9+
"github.com/GoCodeAlone/workflow/interfaces"
810
"github.com/GoCodeAlone/workflow/module"
911
)
1012

13+
// mockPipelineRunner is a minimal PipelineRunner mock used to verify that
14+
// PipelineWorkflowHandler tests do not require concrete module types.
15+
type mockPipelineRunner struct {
16+
runResult map[string]any
17+
runErr error
18+
loggerSet bool
19+
recSet bool
20+
}
21+
22+
func (m *mockPipelineRunner) Run(_ context.Context, _ map[string]any) (map[string]any, error) {
23+
return m.runResult, m.runErr
24+
}
25+
func (m *mockPipelineRunner) SetLogger(_ *slog.Logger) { m.loggerSet = true }
26+
func (m *mockPipelineRunner) SetEventRecorder(_ interfaces.EventRecorder) { m.recSet = true }
27+
28+
// mockEventRecorder is a no-op EventRecorder for tests.
29+
type mockEventRecorder struct{}
30+
31+
func (mockEventRecorder) RecordEvent(_ context.Context, _, _ string, _ map[string]any) error {
32+
return nil
33+
}
34+
1135
func TestPipelineHandler_CanHandle_PrefixFormat(t *testing.T) {
1236
h := NewPipelineWorkflowHandler()
1337

@@ -233,3 +257,65 @@ func TestPipelineHandler_MultiplePipelines(t *testing.T) {
233257
t.Error("expected CanHandle true for pipeline-b")
234258
}
235259
}
260+
261+
// TestPipelineHandler_MockRunner verifies that PipelineWorkflowHandler works
262+
// with a mock PipelineRunner without depending on concrete module types.
263+
func TestPipelineHandler_MockRunner(t *testing.T) {
264+
h := NewPipelineWorkflowHandler()
265+
h.SetLogger(slog.Default())
266+
267+
mock := &mockPipelineRunner{
268+
runResult: map[string]any{"mocked": true},
269+
}
270+
h.AddPipeline("mock-pipeline", mock)
271+
272+
if !h.CanHandle("pipeline:mock-pipeline") {
273+
t.Fatal("expected CanHandle true for mock-pipeline")
274+
}
275+
276+
result, err := h.ExecuteWorkflow(context.Background(), "pipeline:mock-pipeline", "", map[string]any{})
277+
if err != nil {
278+
t.Fatalf("unexpected error: %v", err)
279+
}
280+
if result["mocked"] != true {
281+
t.Errorf("expected mocked=true, got %v", result["mocked"])
282+
}
283+
if !mock.loggerSet {
284+
t.Error("expected SetLogger to be called on mock runner at configuration time")
285+
}
286+
}
287+
288+
// TestPipelineHandler_InjectionAtConfigTime verifies that logger and recorder
289+
// are injected into pipelines at configuration time, not on each execution.
290+
func TestPipelineHandler_InjectionAtConfigTime(t *testing.T) {
291+
h := NewPipelineWorkflowHandler()
292+
293+
// Add pipeline before logger/recorder are set.
294+
m1 := &mockPipelineRunner{runResult: map[string]any{}}
295+
h.AddPipeline("p1", m1)
296+
if m1.loggerSet || m1.recSet {
297+
t.Error("expected no injection before logger/recorder are set")
298+
}
299+
300+
// SetLogger should propagate to the already-registered pipeline.
301+
h.SetLogger(slog.Default())
302+
if !m1.loggerSet {
303+
t.Error("expected SetLogger to propagate to existing pipeline p1")
304+
}
305+
306+
// SetEventRecorder should propagate to the already-registered pipeline.
307+
h.SetEventRecorder(mockEventRecorder{})
308+
if !m1.recSet {
309+
t.Error("expected SetEventRecorder to propagate to existing pipeline p1")
310+
}
311+
312+
// Pipeline added after both are set should receive them in AddPipeline.
313+
m2 := &mockPipelineRunner{runResult: map[string]any{}}
314+
h.AddPipeline("p2", m2)
315+
if !m2.loggerSet {
316+
t.Error("expected logger injected into p2 via AddPipeline")
317+
}
318+
if !m2.recSet {
319+
t.Error("expected recorder injected into p2 via AddPipeline")
320+
}
321+
}

interfaces/pipeline.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Package interfaces defines shared interface types used across the workflow
2+
// engine, handlers, and module packages. Placing these interfaces here breaks
3+
// the direct handler→module concrete-type dependency and enables each package
4+
// to be tested in isolation with mocks.
5+
package interfaces
6+
7+
import (
8+
"context"
9+
"log/slog"
10+
)
11+
12+
// EventRecorder records pipeline execution events for observability.
13+
// *store.EventRecorderAdapter and any compatible recorder satisfy this interface.
14+
type EventRecorder interface {
15+
RecordEvent(ctx context.Context, executionID string, eventType string, data map[string]any) error
16+
}
17+
18+
// PipelineRunner is the interface satisfied by *module.Pipeline.
19+
// It allows workflow handlers to execute pipelines without importing
20+
// the concrete module types, enabling handler unit tests with mocks.
21+
type PipelineRunner interface {
22+
// Run executes the pipeline with the given trigger data and returns the
23+
// merged result map (equivalent to PipelineContext.Current).
24+
Run(ctx context.Context, data map[string]any) (map[string]any, error)
25+
26+
// SetLogger sets the logger used for pipeline execution.
27+
// Implementations should be idempotent: if a logger is already set,
28+
// a subsequent call should be a no-op.
29+
SetLogger(logger *slog.Logger)
30+
31+
// SetEventRecorder sets the recorder used for pipeline execution events.
32+
// Implementations should be idempotent: if a recorder is already set,
33+
// a subsequent call should be a no-op.
34+
SetEventRecorder(recorder EventRecorder)
35+
}
36+
37+
// StepRegistryProvider exposes the step types registered in a step registry.
38+
// *module.StepRegistry satisfies this interface.
39+
type StepRegistryProvider interface {
40+
// Types returns all registered step type names.
41+
Types() []string
42+
}

module/pipeline_executor.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"log/slog"
77
"time"
8+
9+
"github.com/GoCodeAlone/workflow/interfaces"
810
)
911

1012
// ErrorStrategy defines how a pipeline handles step errors.
@@ -19,9 +21,9 @@ const (
1921
// EventRecorder is an optional interface for recording execution events.
2022
// When set on Pipeline, execution events are appended for observability.
2123
// The store.EventStore can satisfy this via an adapter at the wiring layer.
22-
type EventRecorder interface {
23-
RecordEvent(ctx context.Context, executionID string, eventType string, data map[string]any) error
24-
}
24+
// This is a type alias for interfaces.EventRecorder so callers using
25+
// module.EventRecorder or interfaces.EventRecorder interchangeably are unaffected.
26+
type EventRecorder = interfaces.EventRecorder
2527

2628
// Pipeline is an ordered sequence of steps with error handling.
2729
type Pipeline struct {
@@ -295,3 +297,31 @@ func (p *Pipeline) runCompensation(ctx context.Context, pc *PipelineContext, log
295297

296298
return firstErr
297299
}
300+
301+
// SetLogger sets the logger for pipeline execution if one is not already set.
302+
// This implements part of interfaces.PipelineRunner and allows the handler
303+
// to inject a logger without directly accessing the Logger field.
304+
func (p *Pipeline) SetLogger(logger *slog.Logger) {
305+
if p.Logger == nil {
306+
p.Logger = logger
307+
}
308+
}
309+
310+
// SetEventRecorder sets the event recorder for pipeline execution if one is
311+
// not already set. This implements part of interfaces.PipelineRunner.
312+
func (p *Pipeline) SetEventRecorder(recorder interfaces.EventRecorder) {
313+
if p.EventRecorder == nil {
314+
p.EventRecorder = recorder
315+
}
316+
}
317+
318+
// Run executes the pipeline and returns the merged result data map.
319+
// It implements interfaces.PipelineRunner by wrapping Execute and
320+
// returning PipelineContext.Current so callers need not import PipelineContext.
321+
func (p *Pipeline) Run(ctx context.Context, data map[string]any) (map[string]any, error) {
322+
pc, err := p.Execute(ctx, data)
323+
if err != nil {
324+
return nil, err
325+
}
326+
return pc.Current, nil
327+
}

0 commit comments

Comments
 (0)