Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
511440c
Set up moq infrastructure and testutil package
newhook Jan 30, 2026
6fb5c09
Generate moq mocks for CLI wrapper interfaces
newhook Jan 30, 2026
2a16047
Convert cachemanager mock from testify/mock to moq-style
newhook Jan 30, 2026
b93dae4
Convert process tests to use moq-generated mocks
newhook Jan 30, 2026
248843d
Convert task/planner tests to use moq-generated mocks
newhook Jan 30, 2026
b09382d
Convert linear/fetcher tests to use moq-style mock
newhook Jan 30, 2026
5f027c7
Replace work/import_pr_test.go local mocks with shared testutil mocks
newhook Jan 30, 2026
78c98cb
Add tests for git package using moq mocks
newhook Jan 30, 2026
e236245
Add tests for worktree package using moq mocks
newhook Jan 30, 2026
dfec84c
Add tests for mise package using moq mocks
newhook Jan 30, 2026
39daaae
Add tests for beads client caching logic
newhook Jan 30, 2026
ea2c6a8
Update CLAUDE.md with mock generation documentation
newhook Jan 30, 2026
ffb6375
Add tests for control plane using moq mocks
newhook Jan 30, 2026
59668bb
Fix gosec and unparam linter warnings
newhook Jan 30, 2026
f720e28
Convert all test files to use testify assertions
newhook Jan 30, 2026
403f866
Refactor setupControlPlane to return a struct instead of multiple values
newhook Jan 30, 2026
5644552
Remove empty tests that only contained documentation comments
newhook Jan 30, 2026
7b96024
Move moq mocks from testutil to respective package directories
newhook Jan 30, 2026
6268096
Remove meta-tests that only validate moq mock behavior
newhook Jan 30, 2026
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
113 changes: 113 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ go test ./...
- `internal/worktree/` - Git worktree operations
- `internal/logging/` - Structured logging using slog
- `internal/procmon/` - Database-backed process monitoring with heartbeats
- `internal/testutil/` - Shared test utilities and moq-generated mocks

## External Dependencies

Expand Down Expand Up @@ -119,6 +120,118 @@ tail -f .co/debug.log
cat .co/debug.log | jq .
```

## Mock Generation

The project uses [moq](https://github.com/matryer/moq) for generating test mocks. Mocks are stored in `internal/testutil/` and use the function-field pattern for easy customization per-test.

### Installing moq

moq is installed automatically via mise:

```bash
mise install # Installs all tools including moq
```

The tool is defined in `mise.toml`:
```toml
"go:github.com/matryer/moq" = "latest"
```

### Regenerating Mocks

After modifying interfaces or adding new `//go:generate` directives:

```bash
mise run generate
```

This runs `go generate ./...` to regenerate all mocks.

### Adding a New Mock

1. Add a `//go:generate` directive to the interface file:
```go
//go:generate moq -out ../testutil/mock_interface.go -pkg testutil . InterfaceName
```

2. Run `mise run generate` to create the mock

3. Use the mock in tests:
```go
mock := &testutil.InterfaceNameMock{
MethodNameFunc: func(ctx context.Context, arg string) error {
return nil
},
}
```

### Available Mocks

Mocks are generated for interfaces in:
- `internal/git/` - Git CLI operations
- `internal/worktree/` - Git worktree operations
- `internal/mise/` - Mise tool operations
- `internal/zellij/` - Zellij session management
- `internal/beads/` - Beads CLI and reader interfaces
- `internal/github/` - GitHub API client
- `internal/claude/` - Claude runner
- `internal/process/` - Process lister/killer
- `internal/task/` - Complexity estimator
- `internal/linear/` - Linear API client
- `internal/beads/cachemanager/` - Cache manager
- `internal/feedback/` - PR feedback processor
- `internal/control/` - Orchestrator spawner, work destroyer (test-local mocks to avoid import cycle)

### Testing Best Practices

**Configuring mock behavior per-test:**
```go
mock := &testutil.GitOperationsMock{
BranchExistsFunc: func(ctx context.Context, repoPath, branchName string) bool {
return branchName == "main" // Returns true only for "main"
},
}
```

**Tracking and verifying calls:**
```go
mock := &testutil.GitOperationsMock{
FetchPRRefFunc: func(ctx context.Context, repoPath string, prNumber int, localBranch string) error {
return nil
},
}

_ = mock.FetchPRRef(ctx, "/repo", 123, "pr-123")

// Verify call count
calls := mock.FetchPRRefCalls()
if len(calls) != 1 {
t.Errorf("expected 1 call, got %d", len(calls))
}

// Verify call arguments
if calls[0].PrNumber != 123 {
t.Errorf("expected prNumber 123, got %d", calls[0].PrNumber)
}
```

**Nil functions return zero values:**
```go
mock := &testutil.GitOperationsMock{} // No functions set

// Returns false (zero value for bool) when BranchExistsFunc is nil
mock.BranchExists(ctx, "/repo", "any") // returns false

// Returns nil, nil when ListBranchesFunc is nil
branches, err := mock.ListBranches(ctx, "/repo") // branches=nil, err=nil
```

**Compile-time interface verification:**
```go
// Ensure mock implements the interface at compile time
var _ git.Operations = (*testutil.GitOperationsMock)(nil)
```

## Database Migrations

The project uses a SQLite database (`tracking.db`) with schema migrations.
Expand Down
64 changes: 64 additions & 0 deletions internal/beads/cachemanager/mock_cache_manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package cachemanager

import (
"context"
"time"
)

// CacheManagerMock is a mock implementation of CacheManager for testing.
// This uses the function-field pattern consistent with moq-generated mocks.
// Note: moq doesn't support generic interfaces, so this is hand-written.
type CacheManagerMock[K comparable, V any] struct {
GetFunc func(ctx context.Context, key K) (V, bool)
GetMultipleFunc func(ctx context.Context, keys []K) (map[K]V, bool)
GetWithRefreshFunc func(ctx context.Context, key K, ttl time.Duration) (V, bool)
SetFunc func(ctx context.Context, key K, value V, ttl time.Duration)
DeleteFunc func(ctx context.Context, keys ...K) error
FlushFunc func(ctx context.Context) error
}

// Compile-time check that CacheManagerMock implements CacheManager.
var _ CacheManager[string, any] = (*CacheManagerMock[string, any])(nil)

func (m *CacheManagerMock[K, V]) Get(ctx context.Context, key K) (V, bool) {
if m.GetFunc != nil {
return m.GetFunc(ctx, key)
}
var zero V
return zero, false
}

func (m *CacheManagerMock[K, V]) GetMultiple(ctx context.Context, keys []K) (map[K]V, bool) {
if m.GetMultipleFunc != nil {
return m.GetMultipleFunc(ctx, keys)
}
return nil, false
}

func (m *CacheManagerMock[K, V]) GetWithRefresh(ctx context.Context, key K, ttl time.Duration) (V, bool) {
if m.GetWithRefreshFunc != nil {
return m.GetWithRefreshFunc(ctx, key, ttl)
}
var zero V
return zero, false
}

func (m *CacheManagerMock[K, V]) Set(ctx context.Context, key K, value V, ttl time.Duration) {
if m.SetFunc != nil {
m.SetFunc(ctx, key, value, ttl)
}
}

func (m *CacheManagerMock[K, V]) Delete(ctx context.Context, keys ...K) error {
if m.DeleteFunc != nil {
return m.DeleteFunc(ctx, keys...)
}
return nil
}

func (m *CacheManagerMock[K, V]) Flush(ctx context.Context) error {
if m.FlushFunc != nil {
return m.FlushFunc(ctx)
}
return nil
}
42 changes: 0 additions & 42 deletions internal/beads/cachemanager/mock_cache_manager_test.go

This file was deleted.

2 changes: 2 additions & 0 deletions internal/beads/cli.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package beads

//go:generate moq -stub -out ../testutil/beads_cli_mock.go -pkg testutil . CLI:BeadsCLIMock Reader:BeadsReaderMock

import (
"context"
)
Expand Down
Loading