--- phase: 03-test-infrastructure plan: 01 type: execute wave: 1 depends_on: [] files_modified: - backend/database/database.go - backend/database/testhelper.go autonomous: true requirements: - TEST-01 - PERF-04 must_haves: truths: - "Production SQLite connection applies synchronous=NORMAL, cache_size=-8000, and mmap_size=67108864 PRAGMAs at database open" - "NewTestDB(t) returns a clean in-memory SQLite DB with the same migrations and PRAGMAs as production NewDB()" - "Each test gets an isolated database instance — no shared state between test functions" - "Tests using NewTestDB pass with -race flag enabled" artifacts: - path: "backend/database/database.go" provides: "Shared applyPRAGMAs function + production PRAGMA application in NewDB" contains: "applyPRAGMAs" - path: "backend/database/testhelper.go" provides: "NewTestDB test helper for in-memory SQLite with production-mirror setup" exports: ["NewTestDB"] key_links: - from: "backend/database/testhelper.go" to: "backend/database/database.go" via: "shared applyPRAGMAs function" pattern: "applyPRAGMAs\\(" - from: "backend/database/testhelper.go" to: "backend/database/database.go" via: "shared schema application (schemas embed + runMigrations)" pattern: "schemas\\.ReadDir|runMigrations" --- Create a production-mirroring SQLite test helper and apply performance PRAGMAs to the production database connection. Purpose: Establish the test foundation that all subsequent test phases (4-5) depend on. Tests need real database instances with identical configuration to production — same PRAGMAs, same migrations, same constraints — so test results are trustworthy. Output: Modified `database.go` with shared PRAGMA function + production PRAGMAs applied, and new `testhelper.go` with `NewTestDB(t)`. @/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md @/home/caleb/.config/opencode/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @backend/database/database.go From backend/database/database.go: ```go // DB wraps the SQLite database connection and queries. type DB struct { db *sql.DB Ctx context.Context Queries *sqlcgen.Queries logger *slog.Logger } // NewDB opens the database and applies schema migrations. func NewDB(logger *slog.Logger) (*DB, error) // BeginTx starts a new database transaction. func (d *DB) BeginTx() (*sql.Tx, error) // ExecContext executes a query without returning any rows. func (d *DB) ExecContext(query string, args ...any) (sql.Result, error) // QueryContext executes a query that returns rows. func (d *DB) QueryContext(query string, args ...any) (*sql.Rows, error) ``` From backend/database/database.go (internal): ```go //go:embed sql/schemas/*.sql var schemas embed.FS func runMigrations(ctx context.Context, db *sql.DB, logger *slog.Logger) error func isDuplicateColumnErr(err error) bool ``` Task 1: Extract shared applyPRAGMAs and add production PRAGMAs to NewDB backend/database/database.go In `backend/database/database.go`: 1. Create an unexported `applyPRAGMAs(ctx context.Context, db *sql.DB) error` function that executes these PRAGMAs in order: - `PRAGMA foreign_keys = ON` (already exists in NewDB — extract it) - `PRAGMA synchronous = NORMAL` - `PRAGMA cache_size = -8000` - `PRAGMA mmap_size = 67108864` Use a slice of PRAGMA strings and loop over them with `db.ExecContext`. Wrap errors with `fmt.Errorf("could not apply PRAGMA %q: %w", pragma, err)`. 2. Modify `NewDB()` to call `applyPRAGMAs(dbCtx, db)` instead of the inline `PRAGMA foreign_keys = ON` exec. Insert the call right after `db.SetMaxOpenConns(1)` — PRAGMAs before schema creation, per CONTEXT.md decision. 3. Remove the standalone `foreign_keys` PRAGMA block that currently exists in `NewDB()` (lines 58-65) since it's now handled by `applyPRAGMAs`. 4. Add a doc comment on `applyPRAGMAs`: `// applyPRAGMAs configures SQLite connection settings. Called by both NewDB and NewTestDB to ensure identical behavior.` Follow existing conventions: error wrapping with `fmt.Errorf`, blank line after early returns (`nlreturn`), keep lines under 100 chars (`golines`). cd /mnt/vault/dev/golang/yellowjacket && go build -tags webkit2_41 ./backend/database/ && go vet -tags webkit2_41 ./backend/database/ - `applyPRAGMAs` function exists in `database.go` with all 4 PRAGMAs (foreign_keys, synchronous, cache_size, mmap_size) - `NewDB()` calls `applyPRAGMAs` instead of inline foreign_keys PRAGMA - Package compiles and passes vet Task 2: Create NewTestDB helper in testhelper.go backend/database/testhelper.go Create `backend/database/testhelper.go` with: 1. Package declaration: `package database` 2. Imports: `context`, `database/sql`, `fmt`, `io/fs`, `log/slog`, `path`, `testing`, `modernc.org/sqlite` (blank import for driver), and `yellowjacket/backend/database/sql/sqlcgen`. 3. Exported function `NewTestDB(t *testing.T) *DB`: - Call `t.Helper()` at the start - Open in-memory SQLite: `sql.Open("sqlite", ":memory:?_busy_timeout=5000&_journal_mode=WAL")` - If open fails, `t.Fatalf("could not open test database: %v", err)` - `db.SetMaxOpenConns(1)` — same as production - Create context: `ctx := t.Context()` (use `t.Context()` per usetesting linter) - Call `applyPRAGMAs(ctx, db)` — if error, `t.Fatalf("could not apply PRAGMAs: %v", err)` - Apply schemas: iterate `schemas.ReadDir("sql/schemas")`, read each .sql file, `db.ExecContext(ctx, string(sqlContent))` — mirror the exact loop from `NewDB()`. If error, `t.Fatalf`. - Call `runMigrations(ctx, db, slog.Default())` — if error, `t.Fatalf("could not run migrations: %v", err)` - Do NOT run orphan cleanup query (CONTEXT.md decision: "test DBs start empty, no orphans to clean") - Create queries: `queries := sqlcgen.New(db)` - Register cleanup: `t.Cleanup(func() { db.Close() })` - Return `&DB{db: db, Ctx: ctx, Queries: queries, logger: slog.Default()}` 4. Add doc comment: `// NewTestDB returns an in-memory SQLite database that mirrors the production setup (PRAGMAs + all migrations). The database is automatically closed when the test completes via t.Cleanup.` Note: Do NOT expose raw `*sql.DB` — tests use `DB.ExecContext()` / `DB.Queries` like production code (per CONTEXT.md decision). No functional options. No error return — failures are fatal via `t.Fatalf`. Follow conventions: `t.Helper()`, `t.Context()`, blank import comment, doc comments ending with period, `nlreturn` spacing. cd /mnt/vault/dev/golang/yellowjacket && go build -tags webkit2_41 ./backend/database/ && go vet -tags webkit2_41 ./backend/database/ && go test -tags webkit2_41 -race -count=1 -run TestNewTestDB ./backend/database/ 2>&1 || echo "No test yet — build+vet passed" - `backend/database/testhelper.go` exists with exported `NewTestDB(t *testing.T) *DB` - Function opens `:memory:` DB, applies PRAGMAs via shared `applyPRAGMAs`, applies schemas, runs migrations - No orphan cleanup, no health check, no error return, no functional options - Cleanup registered via `t.Cleanup()` - Package compiles, passes vet, and passes `-race` flag After both tasks complete, run the full verification: ```bash # 1. Build the database package go build -tags webkit2_41 ./backend/database/ # 2. Vet the database package go vet -tags webkit2_41 ./backend/database/ # 3. Run all existing tests with race detector to confirm no regressions make test # 4. Verify applyPRAGMAs is called from both NewDB and NewTestDB grep -n "applyPRAGMAs" backend/database/database.go backend/database/testhelper.go # 5. Verify production PRAGMAs are all present grep -c "PRAGMA" backend/database/database.go ``` 1. `backend/database/database.go` has a shared `applyPRAGMAs` function with all 4 PRAGMAs 2. `NewDB()` calls `applyPRAGMAs` (no more inline foreign_keys PRAGMA) 3. `backend/database/testhelper.go` exports `NewTestDB(t *testing.T) *DB` 4. `NewTestDB` uses `:memory:` with same connection params, calls `applyPRAGMAs` + schema loop + `runMigrations` 5. `NewTestDB` registers `t.Cleanup(func() { db.Close() })` 6. `make test` passes (all existing tests green, race detector clean) 7. No orphan cleanup in `NewTestDB`, no health check, no functional options After completion, create `.planning/phases/03-test-infrastructure/03-01-SUMMARY.md`