Files
beads/internal/storage/sqlite/test_helpers.go
beads/crew/dave b362b36824 feat: add session_id field to issue close/update mutations (bd-tksk)
Adds closed_by_session tracking for entity CV building per Gas Town
decision 009-session-events-architecture.md.

Changes:
- Add ClosedBySession field to Issue struct
- Add closed_by_session column to issues table (migration 034)
- Add --session flag to bd close command
- Support CLAUDE_SESSION_ID env var as fallback
- Add --session flag to bd update for status=closed
- Display closed_by_session in bd show output
- Update Storage interface to include session parameter in CloseIssue

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Executed-By: beads/crew/dave
Rig: beads
Role: crew
2025-12-31 13:14:15 -08:00

193 lines
5.7 KiB
Go

package sqlite
import (
"context"
"testing"
"github.com/steveyegge/beads/internal/types"
)
// testEnv provides a test environment with common setup and helpers.
// Use newTestEnv(t) to create a test environment with automatic cleanup.
type testEnv struct {
t *testing.T
Store *SQLiteStorage
Ctx context.Context
}
// newTestEnv creates a new test environment with a configured store.
// The store is automatically cleaned up when the test completes.
func newTestEnv(t *testing.T) *testEnv {
t.Helper()
store := newTestStore(t, "")
return &testEnv{
t: t,
Store: store,
Ctx: context.Background(),
}
}
// CreateIssue creates a test issue with the given title and defaults.
// Returns the created issue with ID populated.
func (e *testEnv) CreateIssue(title string) *types.Issue {
e.t.Helper()
return e.CreateIssueWith(title, types.StatusOpen, 2, types.TypeTask)
}
// CreateIssueWith creates a test issue with specified attributes.
func (e *testEnv) CreateIssueWith(title string, status types.Status, priority int, issueType types.IssueType) *types.Issue {
e.t.Helper()
issue := &types.Issue{
Title: title,
Status: status,
Priority: priority,
IssueType: issueType,
}
if err := e.Store.CreateIssue(e.Ctx, issue, "test-user"); err != nil {
e.t.Fatalf("CreateIssue(%q) failed: %v", title, err)
}
return issue
}
// CreateIssueWithAssignee creates a test issue with an assignee.
func (e *testEnv) CreateIssueWithAssignee(title, assignee string) *types.Issue {
e.t.Helper()
issue := &types.Issue{
Title: title,
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeTask,
Assignee: assignee,
}
if err := e.Store.CreateIssue(e.Ctx, issue, "test-user"); err != nil {
e.t.Fatalf("CreateIssue(%q) failed: %v", title, err)
}
return issue
}
// CreateEpic creates an epic issue.
func (e *testEnv) CreateEpic(title string) *types.Issue {
e.t.Helper()
return e.CreateIssueWith(title, types.StatusOpen, 1, types.TypeEpic)
}
// CreateBug creates a bug issue.
func (e *testEnv) CreateBug(title string, priority int) *types.Issue {
e.t.Helper()
return e.CreateIssueWith(title, types.StatusOpen, priority, types.TypeBug)
}
// AddDep adds a blocking dependency (issue depends on dependsOn).
func (e *testEnv) AddDep(issue, dependsOn *types.Issue) {
e.t.Helper()
e.AddDepType(issue, dependsOn, types.DepBlocks)
}
// AddDepType adds a dependency with the specified type.
func (e *testEnv) AddDepType(issue, dependsOn *types.Issue, depType types.DependencyType) {
e.t.Helper()
dep := &types.Dependency{
IssueID: issue.ID,
DependsOnID: dependsOn.ID,
Type: depType,
}
if err := e.Store.AddDependency(e.Ctx, dep, "test-user"); err != nil {
e.t.Fatalf("AddDependency(%s -> %s) failed: %v", issue.ID, dependsOn.ID, err)
}
}
// AddParentChild adds a parent-child dependency (child belongs to parent).
func (e *testEnv) AddParentChild(child, parent *types.Issue) {
e.t.Helper()
e.AddDepType(child, parent, types.DepParentChild)
}
// Close closes the issue with the given reason.
func (e *testEnv) Close(issue *types.Issue, reason string) {
e.t.Helper()
if err := e.Store.CloseIssue(e.Ctx, issue.ID, reason, "test-user", ""); err != nil {
e.t.Fatalf("CloseIssue(%s) failed: %v", issue.ID, err)
}
}
// GetReadyWork gets ready work with the given filter.
func (e *testEnv) GetReadyWork(filter types.WorkFilter) []*types.Issue {
e.t.Helper()
ready, err := e.Store.GetReadyWork(e.Ctx, filter)
if err != nil {
e.t.Fatalf("GetReadyWork failed: %v", err)
}
return ready
}
// GetReadyIDs returns a map of issue IDs that are ready (open status).
func (e *testEnv) GetReadyIDs() map[string]bool {
e.t.Helper()
ready := e.GetReadyWork(types.WorkFilter{Status: types.StatusOpen})
ids := make(map[string]bool)
for _, issue := range ready {
ids[issue.ID] = true
}
return ids
}
// AssertReady asserts that the issue is in the ready work list.
func (e *testEnv) AssertReady(issue *types.Issue) {
e.t.Helper()
ids := e.GetReadyIDs()
if !ids[issue.ID] {
e.t.Errorf("expected %s (%s) to be ready, but it was blocked", issue.ID, issue.Title)
}
}
// AssertBlocked asserts that the issue is NOT in the ready work list.
func (e *testEnv) AssertBlocked(issue *types.Issue) {
e.t.Helper()
ids := e.GetReadyIDs()
if ids[issue.ID] {
e.t.Errorf("expected %s (%s) to be blocked, but it was ready", issue.ID, issue.Title)
}
}
// newTestStore creates a SQLiteStorage with issue_prefix configured (bd-166)
// This prevents "database not initialized" errors in tests
//
// Test Isolation Pattern (bd-2e80):
// By default, uses "file::memory:?mode=memory&cache=private" for proper test isolation.
// The standard ":memory:" creates a SHARED database across all tests in the same process,
// which can cause test interference and flaky behavior. The private mode ensures each
// test gets its own isolated in-memory database.
//
// To override (e.g., for file-based tests), pass a custom dbPath:
// - For temp files: t.TempDir()+"/test.db"
// - For shared memory (not recommended): ":memory:"
func newTestStore(t *testing.T, dbPath string) *SQLiteStorage {
t.Helper()
// Default to temp file for test isolation
// File-based databases are more reliable than in-memory for connection pool scenarios
if dbPath == "" {
dbPath = t.TempDir() + "/test.db"
}
ctx := context.Background()
store, err := New(ctx, dbPath)
if err != nil {
t.Fatalf("Failed to create test database: %v", err)
}
t.Cleanup(func() {
if cerr := store.Close(); cerr != nil {
t.Fatalf("Failed to close test database: %v", cerr)
}
})
// CRITICAL (bd-166): Set issue_prefix to prevent "database not initialized" errors
if err := store.SetConfig(ctx, "issue_prefix", "bd"); err != nil {
_ = store.Close()
t.Fatalf("Failed to set issue_prefix: %v", err)
}
return store
}