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
23 changes: 3 additions & 20 deletions internal/pipeline/composition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,9 @@ import (

"github.com/recinq/wave/internal/event"
"github.com/recinq/wave/internal/manifest"
"github.com/recinq/wave/internal/testutil"
)

// testEmitter collects events for assertions.
type testEmitter struct {
events []event.Event
}

func (e *testEmitter) Emit(ev event.Event) {
e.events = append(e.events, ev)
}

func (e *testEmitter) hasState(state string) bool {
for _, ev := range e.events {
if ev.State == state {
return true
}
}
return false
}

func TestCompositionExecutor_SubPipeline(t *testing.T) {
// This test verifies that executeSubPipeline resolves input templates.
// Full sub-pipeline execution requires pipeline files on disk, so we
Expand Down Expand Up @@ -318,7 +301,7 @@ func containsSubstr(s, substr string) bool {
}

func TestCompositionExecutor_Execute_Gate_Auto(t *testing.T) {
emitter := &testEmitter{}
emitter := testutil.NewEventCollector()
m := &manifest.Manifest{}

ce := NewCompositionExecutor(nil, emitter, nil, m, "test", ".wave/pipelines", false)
Expand All @@ -339,7 +322,7 @@ func TestCompositionExecutor_Execute_Gate_Auto(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}

if !emitter.hasState(event.StateGateResolved) {
if !emitter.HasEventWithState(event.StateGateResolved) {
t.Error("expected gate_resolved event")
}
}
29 changes: 15 additions & 14 deletions internal/pipeline/concurrency_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import (

"github.com/recinq/wave/internal/adapter"
"github.com/recinq/wave/internal/manifest"
"github.com/recinq/wave/internal/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestConcurrencyExecutor_BasicExecution(t *testing.T) {
var callCount atomic.Int32
collector := newTestEventCollector()
collector := testutil.NewEventCollector()
mockAdapter := adapter.NewMockAdapter(
adapter.WithStdoutJSON(`{"status": "success"}`),
adapter.WithTokensUsed(1000),
Expand All @@ -32,7 +33,7 @@ func TestConcurrencyExecutor_BasicExecution(t *testing.T) {
)

tmpDir := t.TempDir()
m := createTestManifest(tmpDir)
m := testutil.CreateTestManifest(tmpDir)

p := &Pipeline{
Metadata: PipelineMetadata{Name: "concurrency-test"},
Expand Down Expand Up @@ -72,7 +73,7 @@ func TestConcurrencyExecutor_BasicExecution(t *testing.T) {
}

func TestConcurrencyExecutor_FailFast(t *testing.T) {
collector := newTestEventCollector()
collector := testutil.NewEventCollector()
callCount := &atomic.Int32{}

failAdapter := &concurrencyFailAdapter{
Expand All @@ -85,7 +86,7 @@ func TestConcurrencyExecutor_FailFast(t *testing.T) {
)

tmpDir := t.TempDir()
m := createTestManifest(tmpDir)
m := testutil.CreateTestManifest(tmpDir)

p := &Pipeline{
Metadata: PipelineMetadata{Name: "failfast-test"},
Expand Down Expand Up @@ -113,7 +114,7 @@ func TestConcurrencyExecutor_FailFast(t *testing.T) {
func TestConcurrencyExecutor_MaxConcurrencyCap(t *testing.T) {
var maxConcurrent atomic.Int32
var currentConcurrent atomic.Int32
collector := newTestEventCollector()
collector := testutil.NewEventCollector()

// This adapter tracks max concurrent execution
trackingAdapter := &concurrencyConcurrentTracker{
Expand All @@ -126,7 +127,7 @@ func TestConcurrencyExecutor_MaxConcurrencyCap(t *testing.T) {
)

tmpDir := t.TempDir()
m := createTestManifest(tmpDir)
m := testutil.CreateTestManifest(tmpDir)
// Set max_concurrency to 5
m.Runtime.MaxConcurrency = 5

Expand Down Expand Up @@ -159,7 +160,7 @@ func TestConcurrencyExecutor_MaxConcurrencyCap(t *testing.T) {
}

func TestConcurrencyExecutor_SingleAgent(t *testing.T) {
collector := newTestEventCollector()
collector := testutil.NewEventCollector()
mockAdapter := adapter.NewMockAdapter(
adapter.WithStdoutJSON(`{"status": "success"}`),
adapter.WithTokensUsed(1000),
Expand All @@ -170,7 +171,7 @@ func TestConcurrencyExecutor_SingleAgent(t *testing.T) {
)

tmpDir := t.TempDir()
m := createTestManifest(tmpDir)
m := testutil.CreateTestManifest(tmpDir)

p := &Pipeline{
Metadata: PipelineMetadata{Name: "single-test"},
Expand All @@ -195,7 +196,7 @@ func TestConcurrencyExecutor_SingleAgent(t *testing.T) {
}

func TestConcurrencyExecutor_ZeroConcurrency(t *testing.T) {
collector := newTestEventCollector()
collector := testutil.NewEventCollector()
mockAdapter := adapter.NewMockAdapter(
adapter.WithStdoutJSON(`{"status": "success"}`),
adapter.WithTokensUsed(1000),
Expand All @@ -206,7 +207,7 @@ func TestConcurrencyExecutor_ZeroConcurrency(t *testing.T) {
)

tmpDir := t.TempDir()
m := createTestManifest(tmpDir)
m := testutil.CreateTestManifest(tmpDir)

p := &Pipeline{
Metadata: PipelineMetadata{Name: "zero-test"},
Expand All @@ -231,7 +232,7 @@ func TestConcurrencyExecutor_ZeroConcurrency(t *testing.T) {
}

func TestConcurrencyExecutor_ResultAggregation(t *testing.T) {
collector := newTestEventCollector()
collector := testutil.NewEventCollector()
mockAdapter := adapter.NewMockAdapter(
adapter.WithStdoutJSON(`{"status": "success"}`),
adapter.WithTokensUsed(1000),
Expand All @@ -242,7 +243,7 @@ func TestConcurrencyExecutor_ResultAggregation(t *testing.T) {
)

tmpDir := t.TempDir()
m := createTestManifest(tmpDir)
m := testutil.CreateTestManifest(tmpDir)

step := &Step{
ID: "agg-step",
Expand Down Expand Up @@ -288,7 +289,7 @@ func TestConcurrencyExecutor_ResultAggregation(t *testing.T) {
}

func TestConcurrencyExecutor_WorkspaceIsolation(t *testing.T) {
collector := newTestEventCollector()
collector := testutil.NewEventCollector()
mockAdapter := adapter.NewMockAdapter(
adapter.WithStdoutJSON(`{"status": "success"}`),
adapter.WithTokensUsed(1000),
Expand All @@ -299,7 +300,7 @@ func TestConcurrencyExecutor_WorkspaceIsolation(t *testing.T) {
)

tmpDir := t.TempDir()
m := createTestManifest(tmpDir)
m := testutil.CreateTestManifest(tmpDir)

step := &Step{
ID: "iso-step",
Expand Down
69 changes: 23 additions & 46 deletions internal/pipeline/contract_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/recinq/wave/internal/adapter"
"github.com/recinq/wave/internal/manifest"
"github.com/recinq/wave/internal/security"
"github.com/recinq/wave/internal/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -108,30 +109,6 @@ func (a *contractTestArtifactWritingAdapter) Run(ctx context.Context, cfg adapte
}, nil
}

// createContractTestManifest creates a manifest with configurable workspace root
func createContractTestManifest(workspaceRoot string) *manifest.Manifest {
return &manifest.Manifest{
Metadata: manifest.Metadata{Name: "test-project"},
Adapters: map[string]manifest.Adapter{
"claude": {Binary: "claude", Mode: "headless"},
},
Personas: map[string]manifest.Persona{
"navigator": {
Adapter: "claude",
Temperature: 0.1,
},
"craftsman": {
Adapter: "claude",
Temperature: 0.7,
},
},
Runtime: manifest.Runtime{
WorkspaceRoot: workspaceRoot,
DefaultTimeoutMin: 5,
},
}
}

// ============================================================================
// Test 1: JSON Schema Contract Produces Valid JSON
// ============================================================================
Expand Down Expand Up @@ -171,12 +148,12 @@ func TestContractIntegration_JSONSchemaProducesValidJSON(t *testing.T) {
"step1": validArtifact,
})

collector := newTestEventCollector()
collector := testutil.NewEventCollector()
executor := NewDefaultPipelineExecutor(mockAdapter,
WithEmitter(collector),
)

m := createContractTestManifest(tmpDir)
m := testutil.CreateTestManifest(tmpDir)

p := &Pipeline{
Metadata: PipelineMetadata{Name: "json-schema-test"},
Expand Down Expand Up @@ -253,12 +230,12 @@ func TestContractIntegration_JSONSchemaValidationFailure(t *testing.T) {
"step1": invalidArtifact,
})

collector := newTestEventCollector()
collector := testutil.NewEventCollector()
executor := NewDefaultPipelineExecutor(mockAdapter,
WithEmitter(collector),
)

m := createContractTestManifest(tmpDir)
m := testutil.CreateTestManifest(tmpDir)

p := &Pipeline{
Metadata: PipelineMetadata{Name: "json-schema-fail-test"},
Expand Down Expand Up @@ -344,7 +321,7 @@ func TestContractIntegration_SchemaInjectedIntoPrompt(t *testing.T) {
executor.inputSanitizer = security.NewInputSanitizer(*securityConfig, securityLogger)
executor.securityLogger = securityLogger

m := createContractTestManifest(tmpDir)
m := testutil.CreateTestManifest(tmpDir)

p := &Pipeline{
Metadata: PipelineMetadata{Name: "schema-injection-test"},
Expand Down Expand Up @@ -405,7 +382,7 @@ func TestContractIntegration_InlineSchemaInjectedIntoPrompt(t *testing.T) {
)

executor := NewDefaultPipelineExecutor(capturingAdapter)
m := createContractTestManifest(tmpDir)
m := testutil.CreateTestManifest(tmpDir)

p := &Pipeline{
Metadata: PipelineMetadata{Name: "inline-schema-test"},
Expand Down Expand Up @@ -583,12 +560,12 @@ func TestContractIntegration_ValidatorChecksOutput(t *testing.T) {
"validate-step": tt.artifact,
})

collector := newTestEventCollector()
collector := testutil.NewEventCollector()
executor := NewDefaultPipelineExecutor(mockAdapter,
WithEmitter(collector),
)

m := createContractTestManifest(tmpDir)
m := testutil.CreateTestManifest(tmpDir)

p := &Pipeline{
Metadata: PipelineMetadata{Name: "validation-test"},
Expand Down Expand Up @@ -677,12 +654,12 @@ func TestContractIntegration_ArtifactHandoverBetweenSteps(t *testing.T) {
"implement": step2Artifact,
})

collector := newTestEventCollector()
collector := testutil.NewEventCollector()
executor := NewDefaultPipelineExecutor(mockAdapter,
WithEmitter(collector),
)

m := createContractTestManifest(tmpDir)
m := testutil.CreateTestManifest(tmpDir)

p := &Pipeline{
Metadata: PipelineMetadata{Name: "handover-test"},
Expand Down Expand Up @@ -787,12 +764,12 @@ func TestContractIntegration_MultiStepArtifactChain(t *testing.T) {
"step-c": `{"data": "from-step-c"}`,
})

collector := newTestEventCollector()
collector := testutil.NewEventCollector()
executor := NewDefaultPipelineExecutor(mockAdapter,
WithEmitter(collector),
)

m := createContractTestManifest(tmpDir)
m := testutil.CreateTestManifest(tmpDir)

// Create a chain: A -> B -> C, each passing artifacts
p := &Pipeline{
Expand Down Expand Up @@ -899,12 +876,12 @@ func TestContractIntegration_SoftFailureContinues(t *testing.T) {
"soft-step": `{"other_field": "value"}`,
})

collector := newTestEventCollector()
collector := testutil.NewEventCollector()
executor := NewDefaultPipelineExecutor(mockAdapter,
WithEmitter(collector),
)

m := createContractTestManifest(tmpDir)
m := testutil.CreateTestManifest(tmpDir)

p := &Pipeline{
Metadata: PipelineMetadata{Name: "soft-fail-test"},
Expand Down Expand Up @@ -958,7 +935,7 @@ func TestContractIntegration_InputTemplateReplacement(t *testing.T) {
)

executor := NewDefaultPipelineExecutor(capturingAdapter)
m := createContractTestManifest(tmpDir)
m := testutil.CreateTestManifest(tmpDir)

testInput := "build feature XYZ"

Expand Down Expand Up @@ -1010,12 +987,12 @@ func TestContractIntegration_RetryOnContractFailure(t *testing.T) {
workspaceDir: tmpDir,
}

collector := newTestEventCollector()
collector := testutil.NewEventCollector()
executor := NewDefaultPipelineExecutor(retryAdapter,
WithEmitter(collector),
)

m := createContractTestManifest(tmpDir)
m := testutil.CreateTestManifest(tmpDir)

p := &Pipeline{
Metadata: PipelineMetadata{Name: "retry-contract-test"},
Expand Down Expand Up @@ -1130,7 +1107,7 @@ func TestContractIntegration_PersonaContractSeparation(t *testing.T) {
"implement": `{"code": "func main() {}"}`,
})

capturingCollector := newTestEventCollector()
capturingCollector := testutil.NewEventCollector()
executor := NewDefaultPipelineExecutor(mockAdapter,
WithEmitter(capturingCollector),
)
Expand Down Expand Up @@ -1247,12 +1224,12 @@ func TestContractIntegration_CustomSourcePath(t *testing.T) {
filename: "custom-output.json",
}

collector := newTestEventCollector()
collector := testutil.NewEventCollector()
executor := NewDefaultPipelineExecutor(customAdapter,
WithEmitter(collector),
)

m := createContractTestManifest(tmpDir)
m := testutil.CreateTestManifest(tmpDir)

p := &Pipeline{
Metadata: PipelineMetadata{Name: "custom-source-test"},
Expand Down Expand Up @@ -1334,12 +1311,12 @@ func TestContractIntegration_DiamondDependencyWithContracts(t *testing.T) {
"step-d": `{"step": "D"}`,
})

collector := newTestEventCollector()
collector := testutil.NewEventCollector()
executor := NewDefaultPipelineExecutor(mockAdapter,
WithEmitter(collector),
)

m := createContractTestManifest(tmpDir)
m := testutil.CreateTestManifest(tmpDir)

p := &Pipeline{
Metadata: PipelineMetadata{Name: "diamond-test"},
Expand Down
Loading
Loading