testing

package
v0.0.0-...-f1f85be Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 18, 2026 License: MIT Imports: 9 Imported by: 0

README

State Machine Testing Helpers

The testing package provides rich test utilities that make testing state machines intuitive, comprehensive, and maintainable.

Features

  • TestEngine - Enhanced engine wrapper with execution tracing and assertions
  • Test Fixtures - Mock clients, common configs, and test data helpers
  • Test Scenarios - Pre-built scenarios for common patterns
  • Fluent Matchers - Readable assertion DSL
  • Execution Tracing - Record and inspect state machine execution

Installation

import smtesting "github.com/amp-labs/server/builder-mcp/statemachine/testing"

Quick Start

Basic Test with TestEngine
func TestMyWorkflow(t *testing.T) {
    // Create config
    config := &statemachine.Config{
        Name:         "test",
        InitialState: "start",
        FinalStates:  []string{"end"},
        States: []statemachine.StateConfig{
            {Name: "start", Type: "action"},
            {Name: "end", Type: "final"},
        },
        Transitions: []statemachine.TransitionConfig{
            {From: "start", To: "end", Condition: "always"},
        },
    }

    // Create test engine
    engine := smtesting.NewTestEngine(t, config)

    // Execute
    ctx := context.Background()
    smCtx := smtesting.CreateTestContext(map[string]any{
        "input": "test data",
    })

    err := engine.Execute(ctx, smCtx)
    require.NoError(t, err)

    // Assert execution
    engine.AssertStateVisited("start")
    engine.AssertTransitionTaken("start", "end")
    engine.AssertFinalState("end")
}
Using Common Test Configs
func TestLinearWorkflow(t *testing.T) {
    // Use pre-built config
    config := smtesting.CommonTestConfigs.Linear()
    engine := smtesting.NewTestEngine(t, config)

    ctx := context.Background()
    smCtx := smtesting.CreateTestContext(nil)

    err := engine.Execute(ctx, smCtx)
    require.NoError(t, err)
}

TestEngine

The TestEngine wraps the state machine engine with testing capabilities:

Creating a TestEngine
config := &statemachine.Config{...}
engine := smtesting.NewTestEngine(t, config)
Assertions
Assert State Visit
engine.AssertStateVisited("my_state")
// Fails test if state was not visited
Assert Transition
engine.AssertTransitionTaken("state_a", "state_b")
// Fails test if transition did not occur
Assert Final State
engine.AssertFinalState("complete")
// Fails test if final state doesn't match
Assert Context Values
engine.AssertContextValue("result", "success")
// Fails test if context value doesn't match
Assert Execution Time
engine.AssertExecutionTime(100 * time.Millisecond)
// Fails test if execution took longer
Execution Trace

Access the full execution trace for custom assertions:

trace := engine.GetTrace()

for _, entry := range trace {
    fmt.Printf("State: %s, Duration: %s\n", entry.State, entry.Duration)
    if entry.Error != nil {
        fmt.Printf("Error: %v\n", entry.Error)
    }
}

Test Fixtures

Mock Sampling Client
responses := map[string]string{
    "What is the capital of France?": "Paris",
    "What is 2+2?": "4",
}

mockClient := smtesting.NewMockSamplingClient(responses)
response, _ := mockClient.Sample("What is the capital of France?")
// response = "Paris"
Mock Elicitation Client
responses := map[string]any{
    "Choose a color": "blue",
    "Enter a number": 42,
}

mockClient := smtesting.NewMockElicitationClient(responses)
answer, _ := mockClient.Elicit("Choose a color")
// answer = "blue"
Create Test Context
ctx := smtesting.CreateTestContext(map[string]any{
    "user_id": "123",
    "account": "acme",
})

// Context has test-session and test-project IDs
Common Test Configs

Pre-built configs for common patterns:

// Linear: start -> middle -> end
config := smtesting.CommonTestConfigs.Linear()

// Branching: start -> success | failure
config := smtesting.CommonTestConfigs.Branching()

// Loop: start -> retry (loop) -> complete
config := smtesting.CommonTestConfigs.Loop()

// Complex: Multiple states with error handling
config := smtesting.CommonTestConfigs.Complex()

Test Scenarios

Test scenarios encapsulate complete test cases:

Using Built-in Scenarios
func TestScenarios(t *testing.T) {
    scenarios := []smtesting.TestScenario{
        smtesting.LinearWorkflowScenario(),
        smtesting.BranchingWorkflowScenario(),
        smtesting.RetryScenario(),
        smtesting.ErrorRecoveryScenario(),
    }

    for _, scenario := range scenarios {
        smtesting.RunScenario(t, scenario)
    }
}
Creating Custom Scenarios
scenario := smtesting.TestScenario{
    Name: "My Custom Scenario",
    Config: &statemachine.Config{...},
    InitialContext: map[string]any{
        "input": "test",
    },
    MockResponses: map[string]any{
        "question": "answer",
    },
    Assertions: []smtesting.Assertion{
        // Custom assertions
    },
}

smtesting.RunScenario(t, scenario)

Fluent Matchers

Matchers provide a readable assertion DSL:

Basic Matchers
matcher := smtesting.StateWasVisited("process")
matched, err := matcher.Match(engine)
// matched = true if state was visited
Available Matchers
// State matchers
smtesting.StateWasVisited("state_name")
smtesting.TransitionWasTaken("from", "to")

// Context matchers
smtesting.ContextContains("key", "value")

// Execution matchers
smtesting.ExecutionCompleted()
smtesting.ExecutionFailed()
smtesting.ExecutionTookLessThan(100 * time.Millisecond)
Combining Matchers
// All must pass
matcher := smtesting.All(
    smtesting.StateWasVisited("start"),
    smtesting.StateWasVisited("end"),
    smtesting.ExecutionCompleted(),
)

// At least one must pass
matcher := smtesting.Any(
    smtesting.StateWasVisited("success"),
    smtesting.StateWasVisited("failure"),
)
Using Matchers in Tests
func TestWithMatchers(t *testing.T) {
    engine := smtesting.NewTestEngine(t, config)

    ctx := context.Background()
    smCtx := smtesting.CreateTestContext(nil)

    _ = engine.Execute(ctx, smCtx)

    // Use matchers
    matchers := []smtesting.Matcher{
        smtesting.StateWasVisited("start"),
        smtesting.TransitionWasTaken("start", "complete"),
        smtesting.ExecutionCompleted(),
    }

    for _, matcher := range matchers {
        matched, err := matcher.Match(engine)
        require.True(t, matched, matcher.Description())
        require.NoError(t, err)
    }
}

Testing Patterns

Unit Testing Actions
func TestSingleAction(t *testing.T) {
    config := &statemachine.Config{
        Name:         "action_test",
        InitialState: "test_action",
        FinalStates:  []string{"test_action"},
        States: []statemachine.StateConfig{
            {
                Name: "test_action",
                Type: "action",
                Actions: []statemachine.ActionConfig{
                    {Type: "my_action", Name: "test"},
                },
            },
        },
    }

    engine := smtesting.NewTestEngine(t, config)
    ctx := context.Background()
    smCtx := smtesting.CreateTestContext(map[string]any{
        "input": "test",
    })

    err := engine.Execute(ctx, smCtx)
    require.NoError(t, err)

    // Assert action effects
    engine.AssertContextValue("output", "expected")
}
Integration Testing Complete Workflows
func TestCompleteWorkflow(t *testing.T) {
    config := smtesting.CommonTestConfigs.Complex()
    engine := smtesting.NewTestEngine(t, config)

    ctx := context.Background()
    smCtx := smtesting.CreateTestContext(map[string]any{
        "valid": true,
        "success": true,
    })

    err := engine.Execute(ctx, smCtx)
    require.NoError(t, err)

    // Verify complete path
    engine.AssertStateVisited("init")
    engine.AssertStateVisited("validate")
    engine.AssertStateVisited("process")
    engine.AssertFinalState("success")
}
Testing Error Scenarios
func TestErrorHandling(t *testing.T) {
    config := &statemachine.Config{...}
    engine := smtesting.NewTestEngine(t, config)

    ctx := context.Background()
    smCtx := smtesting.CreateTestContext(map[string]any{
        "force_error": true,
    })

    err := engine.Execute(ctx, smCtx)
    // Depending on config, might expect error or error recovery

    // Verify error handling path
    engine.AssertStateVisited("error_handler")
}
Performance Testing
func TestPerformance(t *testing.T) {
    config := smtesting.CommonTestConfigs.Complex()
    engine := smtesting.NewTestEngine(t, config)

    ctx := context.Background()
    smCtx := smtesting.CreateTestContext(nil)

    start := time.Now()
    err := engine.Execute(ctx, smCtx)
    duration := time.Since(start)

    require.NoError(t, err)
    assert.Less(t, duration, 100*time.Millisecond,
        "execution should complete quickly")
}
Snapshot Testing
func TestExecutionSnapshot(t *testing.T) {
    engine := smtesting.NewTestEngine(t, config)

    ctx := context.Background()
    smCtx := smtesting.CreateTestContext(nil)

    _ = engine.Execute(ctx, smCtx)

    // Get execution trace
    trace := engine.GetTrace()

    // Compare with golden snapshot
    // (would use snapshot testing library)
    compareWithSnapshot(t, trace, "workflow_execution.json")
}

Best Practices

  1. Use common configs - Leverage CommonTestConfigs for standard patterns
  2. Test edge cases - Use scenarios to test error paths and edge cases
  3. Assert liberally - Use multiple assertions to catch regressions
  4. Mock external services - Use mock clients for deterministic tests
  5. Check execution traces - Inspect traces for unexpected behavior
  6. Performance test - Use AssertExecutionTime for critical workflows
  7. Snapshot tests - Use execution traces for regression detection

Advanced Usage

Custom Assertions
func assertCustomBehavior(t *testing.T, engine *smtesting.TestEngine) {
    trace := engine.GetTrace()

    // Custom validation logic
    for _, entry := range trace {
        if entry.Duration > 50*time.Millisecond {
            t.Errorf("State %s took too long: %s", entry.State, entry.Duration)
        }
    }
}
Test Helpers
func createTestEngine(t *testing.T, modifications ...func(*statemachine.Config)) *smtesting.TestEngine {
    config := smtesting.CommonTestConfigs.Linear()

    for _, modify := range modifications {
        modify(config)
    }

    return smtesting.NewTestEngine(t, config)
}

See Also

Documentation

Overview

Package testing provides testing utilities for state machine workflows.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrNoMockResponseForPrompt   = errors.New("no mock response for prompt")
	ErrNoMockResponseForQuestion = errors.New("no mock response for question")
)

Test helper errors.

View Source
var (
	ErrNoExecutionTrace          = errors.New("no execution trace available")
	ErrExecutionCompletedNoError = errors.New("execution completed without error")
	ErrNoMatchersPassed          = errors.New("no matchers passed")
	ErrStateNotVisited           = errors.New("state was not visited")
	ErrTransitionNotTaken        = errors.New("transition was not taken")
	ErrContextKeyNotExist        = errors.New("context key does not exist")
	ErrContextValueMismatch      = errors.New("context value mismatch")
	ErrExecutionTooSlow          = errors.New("execution exceeded time limit")
)

Matcher errors.

View Source
var CommonTestConfigs = struct {
	Linear    func() *statemachine.Config
	Branching func() *statemachine.Config
	Loop      func() *statemachine.Config
	Complex   func() *statemachine.Config
}{
	Linear: func() *statemachine.Config {
		return &statemachine.Config{
			Name:         "linear",
			InitialState: "start",
			FinalStates:  []string{"end"},
			States: []statemachine.StateConfig{
				{Name: "start", Type: "action", Actions: []statemachine.ActionConfig{{Type: "noop", Name: "test"}}},
				{Name: "middle", Type: "action", Actions: []statemachine.ActionConfig{{Type: "noop", Name: "test"}}},
				{Name: "end", Type: "final"},
			},
			Transitions: []statemachine.TransitionConfig{
				{From: "start", To: "middle", Condition: "always"},
				{From: "middle", To: "end", Condition: "always"},
			},
		}
	},
	Branching: func() *statemachine.Config {
		return &statemachine.Config{
			Name:         "branching",
			InitialState: "start",
			FinalStates:  []string{"success", "failure"},
			States: []statemachine.StateConfig{
				{Name: "start", Type: "action", Actions: []statemachine.ActionConfig{{Type: "noop", Name: "test"}}},
				{Name: "success", Type: "final"},
				{Name: "failure", Type: "final"},
			},
			Transitions: []statemachine.TransitionConfig{
				{From: "start", To: "success", Condition: "result.success"},
				{From: "start", To: "failure", Condition: "result.failure"},
			},
		}
	},
	Loop: func() *statemachine.Config {
		return &statemachine.Config{
			Name:         "loop",
			InitialState: "start",
			FinalStates:  []string{"complete"},
			States: []statemachine.StateConfig{
				{Name: "start", Type: "action", Actions: []statemachine.ActionConfig{{Type: "noop", Name: "test"}}},
				{Name: "retry", Type: "action", Actions: []statemachine.ActionConfig{{Type: "noop", Name: "test"}}},
				{Name: "complete", Type: "final"},
			},
			Transitions: []statemachine.TransitionConfig{
				{From: "start", To: "retry", Condition: "always"},
				{From: "retry", To: "retry", Condition: "attempts < 3"},
				{From: "retry", To: "complete", Condition: "attempts >= 3"},
			},
		}
	},
	Complex: func() *statemachine.Config {
		return &statemachine.Config{
			Name:         "complex",
			InitialState: "init",
			FinalStates:  []string{"success", "failure"},
			States: []statemachine.StateConfig{
				{Name: "init", Type: "action", Actions: []statemachine.ActionConfig{{Type: "noop", Name: "test"}}},
				{Name: "validate", Type: "action", Actions: []statemachine.ActionConfig{{Type: "noop", Name: "test"}}},
				{Name: "process", Type: "action", Actions: []statemachine.ActionConfig{{Type: "noop", Name: "test"}}},
				{Name: "retry", Type: "action", Actions: []statemachine.ActionConfig{{Type: "noop", Name: "test"}}},
				{Name: "success", Type: "final"},
				{Name: "failure", Type: "final"},
			},
			Transitions: []statemachine.TransitionConfig{
				{From: "init", To: "validate", Condition: "always"},
				{From: "validate", To: "process", Condition: "valid"},
				{From: "validate", To: "failure", Condition: "!valid"},
				{From: "process", To: "success", Condition: "success"},
				{From: "process", To: "retry", Condition: "retryable"},
				{From: "retry", To: "process", Condition: "attempts < 3"},
				{From: "retry", To: "failure", Condition: "attempts >= 3"},
			},
		}
	},
}

CommonTestConfigs provides frequently used test configurations.

Functions

func CreateTestConfig

func CreateTestConfig(name string, initialState string, finalStates []string) *statemachine.Config

CreateTestConfig creates a simple test config.

func CreateTestContext

func CreateTestContext(data map[string]any) *statemachine.Context

CreateTestContext creates a context with test data.

func LoadTestConfig

func LoadTestConfig(name string) (*statemachine.Config, error)

LoadTestConfig loads a config from the testdata directory.

func RunScenario

func RunScenario(t *testing.T, scenario TestScenario)

RunScenario executes a test scenario and validates results.

func SaveTestConfig

func SaveTestConfig(name string, config *statemachine.Config) error

SaveTestConfig saves a config to the testdata directory.

Types

type Assertion

type Assertion struct {
	Name   string
	Passed bool
	Error  error
}

Assertion represents a test assertion.

type Matcher

type Matcher interface {
	Match(engine *TestEngine) (bool, error)
	Description() string
}

Matcher defines an assertion matcher interface.

func All

func All(matchers ...Matcher) Matcher

All creates a matcher that requires all sub-matchers to pass.

func Any

func Any(matchers ...Matcher) Matcher

Any creates a matcher that requires at least one sub-matcher to pass.

func ContextContains

func ContextContains(key string, value any) Matcher

ContextContains creates a matcher that checks context values.

func ExecutionCompleted

func ExecutionCompleted() Matcher

ExecutionCompleted creates a matcher that checks if execution completed successfully.

func ExecutionFailed

func ExecutionFailed() Matcher

ExecutionFailed creates a matcher that checks if execution failed.

func ExecutionTookLessThan

func ExecutionTookLessThan(duration time.Duration) Matcher

ExecutionTookLessThan creates a matcher that checks execution duration.

func StateWasVisited

func StateWasVisited(name string) Matcher

StateWasVisited creates a matcher that checks if a state was visited.

func TransitionWasTaken

func TransitionWasTaken(from, to string) Matcher

TransitionWasTaken creates a matcher that checks if a transition occurred.

type MockElicitationClient

type MockElicitationClient struct {
	// contains filtered or unexported fields
}

MockElicitationClient creates a mock elicitation client for testing.

func NewMockElicitationClient

func NewMockElicitationClient(responses map[string]any) *MockElicitationClient

NewMockElicitationClient creates a mock elicitation client with predefined responses.

func (*MockElicitationClient) Elicit

func (m *MockElicitationClient) Elicit(question string) (any, error)

Elicit returns a predefined response for the given question.

type MockSamplingClient

type MockSamplingClient struct {
	// contains filtered or unexported fields
}

MockSamplingClient creates a mock sampling client for testing.

func NewMockSamplingClient

func NewMockSamplingClient(responses map[string]string) *MockSamplingClient

NewMockSamplingClient creates a mock sampling client with predefined responses.

func (*MockSamplingClient) Sample

func (m *MockSamplingClient) Sample(prompt string) (string, error)

Sample returns a predefined response for the given prompt.

type TestEngine

type TestEngine struct {
	*statemachine.Engine
	// contains filtered or unexported fields
}

TestEngine wraps Engine with testing utilities.

func NewTestEngine

func NewTestEngine(t *testing.T, config *statemachine.Config) *TestEngine

NewTestEngine creates a test engine for a config with default factory.

func NewTestEngineWithFactory

func NewTestEngineWithFactory(
	t *testing.T, config *statemachine.Config, factory *statemachine.ActionFactory,
) *TestEngine

NewTestEngineWithFactory creates a test engine for a config with a custom factory.

func (*TestEngine) AssertContextValue

func (te *TestEngine) AssertContextValue(key string, expected any)

AssertContextValue checks context value at end of execution.

func (*TestEngine) AssertExecutionTime

func (te *TestEngine) AssertExecutionTime(maxDuration time.Duration)

AssertExecutionTime checks total execution time.

func (*TestEngine) AssertFinalState

func (te *TestEngine) AssertFinalState(expected string)

AssertFinalState checks the final state matches expected.

func (*TestEngine) AssertStateVisited

func (te *TestEngine) AssertStateVisited(stateName string)

AssertStateVisited checks if a state was visited during execution.

func (*TestEngine) AssertTransitionTaken

func (te *TestEngine) AssertTransitionTaken(from, to string)

AssertTransitionTaken checks if a specific transition occurred.

func (*TestEngine) Execute

func (te *TestEngine) Execute(ctx context.Context, smCtx *statemachine.Context) error

Execute runs the state machine and records execution trace.

func (*TestEngine) GetAssertions

func (te *TestEngine) GetAssertions() []Assertion

GetAssertions returns all assertions made.

func (*TestEngine) GetTrace

func (te *TestEngine) GetTrace() []TraceEntry

GetTrace returns the execution trace for inspection.

type TestScenario

type TestScenario struct {
	Name           string
	Config         *statemachine.Config
	InitialContext map[string]any
	MockResponses  map[string]any
	Assertions     []Assertion
}

TestScenario represents a complete test scenario for a state machine.

func BranchingWorkflowScenario

func BranchingWorkflowScenario() TestScenario

BranchingWorkflowScenario creates a scenario for testing branching logic.

func ErrorRecoveryScenario

func ErrorRecoveryScenario() TestScenario

ErrorRecoveryScenario creates a scenario for testing error recovery.

func LinearWorkflowScenario

func LinearWorkflowScenario() TestScenario

LinearWorkflowScenario creates a scenario for testing linear workflows.

func RetryScenario

func RetryScenario() TestScenario

RetryScenario creates a scenario for testing retry logic.

type TraceEntry

type TraceEntry struct {
	Timestamp time.Time
	State     string
	Action    string
	Duration  time.Duration
	Error     error
	Context   map[string]any // Snapshot of context
}

TraceEntry records a single step in execution.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL