bd sync: 2025-11-24 20:20:43

This commit is contained in:
Steve Yegge
2025-11-24 20:20:43 -08:00
parent cf560a947c
commit b453db8832
4 changed files with 545 additions and 3 deletions

View File

@@ -1,5 +1,5 @@
{"id":"bd-0a43","content_hash":"36ff43c769f6b0d227c892b20c3af9b1092b38e06cc0f31c0d7c313528be716e","title":"Split monolithic sqlite.go into focused files","description":"internal/storage/sqlite/sqlite.go is 1050 lines containing initialization, 20+ CRUD methods, query building, and schema management.\n\nSplit into:\n- store.go: Store struct \u0026 initialization (150 lines)\n- bead_queries.go: Bead CRUD (300 lines)\n- work_queries.go: Work queries (200 lines) \n- stats_queries.go: Statistics (150 lines)\n- schema.go: Schema \u0026 migrations (150 lines)\n- helpers.go: Common utilities (100 lines)\n\nImpact: Impossible to understand at a glance; hard to find specific functionality; high cognitive load\n\nEffort: 6-8 hours","status":"open","priority":0,"issue_type":"task","created_at":"2025-11-16T14:51:16.520465-08:00","updated_at":"2025-11-16T14:51:16.520465-08:00","source_repo":"."}
{"id":"bd-0ae8","content_hash":"6c04d79a886f484083a64428ed161aca2ede32034e38fb7cc0c50600df121d69","title":"Add config/metadata and comment operations to Transaction","description":"Extend Transaction interface with config, metadata, and comment operations.\n\n## Tasks\n1. Add to Transaction interface:\n - SetConfig(ctx, key, value) error\n - GetConfig(ctx, key) (string, error)\n - SetMetadata(ctx, key, value) error\n - GetMetadata(ctx, key) (string, error)\n - AddComment(ctx, issueID, actor, comment) error\n\n2. Implement on sqliteTxStorage:\n - Simple SQL operations using conn\n - Read-your-writes consistency within transaction\n\n## Acceptance Criteria\n- [ ] Config operations work within transaction\n- [ ] Metadata operations work within transaction\n- [ ] AddComment works within transaction\n- [ ] Test: atomic config update with issue creation","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-24T11:37:05.888578-08:00","updated_at":"2025-11-24T11:37:05.888578-08:00","source_repo":".","dependencies":[{"issue_id":"bd-0ae8","depends_on_id":"bd-6pul","type":"parent-child","created_at":"2025-11-24T11:37:05.891105-08:00","created_by":"daemon"}]}
{"id":"bd-0ae8","content_hash":"f14ea6cd007f34727ded2b934f5bc719b2dc74682496d32106e115aa85749a57","title":"Add config/metadata and comment operations to Transaction","description":"Extend Transaction interface with config, metadata, and comment operations.\n\n## Tasks\n1. Add to Transaction interface:\n - SetConfig(ctx, key, value) error\n - GetConfig(ctx, key) (string, error)\n - SetMetadata(ctx, key, value) error\n - GetMetadata(ctx, key) (string, error)\n - AddComment(ctx, issueID, actor, comment) error\n\n2. Implement on sqliteTxStorage:\n - Simple SQL operations using conn\n - Read-your-writes consistency within transaction\n\n## Acceptance Criteria\n- [ ] Config operations work within transaction\n- [ ] Metadata operations work within transaction\n- [ ] AddComment works within transaction\n- [ ] Test: atomic config update with issue creation","status":"closed","priority":2,"issue_type":"task","assignee":"claude","created_at":"2025-11-24T11:37:05.888578-08:00","updated_at":"2025-11-24T20:16:58.599574-08:00","closed_at":"2025-11-24T20:16:58.599574-08:00","source_repo":".","dependencies":[{"issue_id":"bd-0ae8","depends_on_id":"bd-6pul","type":"parent-child","created_at":"2025-11-24T11:37:05.891105-08:00","created_by":"daemon"}]}
{"id":"bd-0fvq","content_hash":"6fb6e394efe3010fd5d9213669417e5f6376017de4187988d5a6fd0d36c80b40","title":"bd doctor should recommend bd prime migration for existing repos","description":"bd doctor should detect old beads integration patterns and recommend migrating to bd prime approach.\n\n## Current behavior\n- bd doctor checks if Claude hooks are installed globally\n- Doesn't check project-level integration (AGENTS.md, CLAUDE.md)\n- Doesn't recommend migration for repos using old patterns\n\n## Desired behavior\nbd doctor should detect and suggest:\n\n1. **Old slash command pattern detected**\n - Check for /beads:* references in AGENTS.md, CLAUDE.md\n - Suggest: These slash commands are deprecated, use bd prime hooks instead\n \n2. **No agent documentation**\n - Check if AGENTS.md or CLAUDE.md exists\n - Suggest: Run 'bd onboard' or 'bd setup claude' to document workflow\n \n3. **Old MCP-only pattern**\n - Check for instructions to use MCP tools but no bd prime hooks\n - Suggest: Add bd prime hooks for better token efficiency\n\n4. **Migration path**\n - Show: 'Run bd setup claude to add SessionStart/PreCompact hooks'\n - Show: 'Update AGENTS.md to reference bd prime instead of slash commands'\n\n## Example output\n\n⚠ Warning: Old beads integration detected in CLAUDE.md\n Found: /beads:* slash command references (deprecated)\n Recommend: Migrate to bd prime hooks for better token efficiency\n Fix: Run 'bd setup claude' and update CLAUDE.md\n\n💡 Tip: bd prime + hooks reduces token usage by 80-99% vs slash commands\n MCP mode: ~50 tokens vs ~10.5k for full MCP scan\n CLI mode: ~1-2k tokens with automatic context recovery\n\n## Benefits\n- Helps existing repos adopt new best practices\n- Clear migration path for users\n- Better token efficiency messaging","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-12T03:20:25.567748-08:00","updated_at":"2025-11-12T03:20:25.567748-08:00","source_repo":"."}
{"id":"bd-0l5n","content_hash":"00fc853350d8dbf1a0cdfe3b7212d312d8c6afe1c29432dce2ada42d306b01be","title":"SECURITY: No authorization check - any agent can deregister others","description":"In deregister_agent (app.py:2882), there is no authorization check to verify the calling agent has permission to deregister another agent. Any agent or external caller can deregister any other agent in a project, allowing malicious or buggy agents to disrupt legitimate agents. Recommendation: Restrict to self-deregistration, add caller_agent verification, or require admin approval.","status":"open","priority":0,"issue_type":"bug","created_at":"2025-11-24T17:13:46.378191-08:00","updated_at":"2025-11-24T17:13:46.378191-08:00","source_repo":"."}
{"id":"bd-1a6j","content_hash":"16f978c58b9988457aeb1eaff37fb17f12e91325549b38be10362a08923e9a2d","title":"Test issue 2","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-07T19:07:12.24632-08:00","updated_at":"2025-11-07T19:07:12.24632-08:00","source_repo":"."}
@@ -54,11 +54,11 @@
{"id":"bd-jgxi","content_hash":"9af80db7f04944bbc55f2d77e3576028c553c936e9d40266ccbf169eec47eb9f","title":"Auto-migrate database on CLI version bump","description":"When CLI is upgraded (e.g., 0.24.0 → 0.24.1), database version becomes stale. Add auto-migration in PersistentPreRun or daemon startup. Check dbVersion != CLIVersion and run bd migrate automatically. Fixes recurring UX issue where bd doctor shows version mismatch after every CLI upgrade.","status":"open","priority":0,"issue_type":"feature","created_at":"2025-11-21T23:16:09.004619-08:00","updated_at":"2025-11-21T23:16:27.229388-08:00","source_repo":".","dependencies":[{"issue_id":"bd-jgxi","depends_on_id":"bd-tbz3","type":"parent-child","created_at":"2025-11-21T23:16:09.005513-08:00","created_by":"daemon"}]}
{"id":"bd-l954","content_hash":"263dd2111cf0353b307f2e47489aa42ecf607e49b1316b54a6497cad9d3722b0","title":"Performance Testing Framework","description":"Add comprehensive performance testing for beads focusing on optimization guidance and validating 10K+ database scale. Uses standard Go tooling, follows existing patterns, minimal complexity.\n\nComponents:\n- Benchmark suite for critical operations at 10K-20K scale\n- Fixture generator for realistic test data (epic hierarchies, cross-links)\n- User diagnostics via bd doctor --perf\n- Always-on profiling integration\n\nGoals:\n- Identify bottlenecks for optimization work\n- Validate performance at 10K+ issue scale\n- Enable users to collect diagnostics for bug reports\n- Support both SQLite and JSONL import paths","status":"open","priority":2,"issue_type":"epic","created_at":"2025-11-13T22:22:11.203467-08:00","updated_at":"2025-11-13T22:22:11.203467-08:00","source_repo":"."}
{"id":"bd-m0w","content_hash":"e8641e225f1d4cf13fbd97c4a83046e3597df180d3ee134125e4a35abc6941cd","title":"Add test coverage for internal/validation package","description":"","design":"Validation package has 1 test file. Critical for data integrity. Target: 80% coverage","acceptance_criteria":"- At least 4 test files\n- Package coverage \u003e= 80%\n- Tests cover all validation rules","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-20T21:21:24.129559-05:00","updated_at":"2025-11-20T21:21:24.129559-05:00","source_repo":".","dependencies":[{"issue_id":"bd-m0w","depends_on_id":"bd-ge7","type":"blocks","created_at":"2025-11-20T21:21:31.350477-05:00","created_by":"daemon"}]}
{"id":"bd-m73k","content_hash":"caa524c6fdd4b3ba26cb3a282116d3afe17b1b721f5e2f0cf70765c986849d69","title":"Add transaction support for atomic multi-operation workflows","description":"Add a transaction-aware API to the Beads Storage interface to support atomic multi-operation workflows. Context from VC project: The VC plan approval code needs to create multiple issues atomically (phases, tasks, dependencies, labels, metadata updates). Currently each storage operation gets its own DB connection making transactions impossible. When approving a mission plan we need to atomically: create 5-10 phase issues, create 20-30 task issues, add dependencies between them, apply labels to all issues, update mission metadata, delete ephemeral plan. If ANY step fails ALL changes should rollback.","design":"Add RunInTransaction wrapper to Storage interface. Create Transaction interface with same methods as Storage but using underlying sql.Tx. Implementation creates transactionStorage struct that wraps tx and uses tx.ExecContext instead of db.ExecContext for all operations.","acceptance_criteria":"WHEN calling RunInTransaction THEN all operations use same transaction. WHEN transaction function returns nil THEN transaction is committed. WHEN transaction function returns error THEN transaction is rolled back. WHEN transaction commits THEN all changes are visible. WHEN transaction rolls back THEN no changes are visible.","status":"open","priority":1,"issue_type":"feature","created_at":"2025-11-24T11:18:17.050954-08:00","updated_at":"2025-11-24T11:18:17.050954-08:00","source_repo":"."}
{"id":"bd-m73k","content_hash":"caa524c6fdd4b3ba26cb3a282116d3afe17b1b721f5e2f0cf70765c986849d69","title":"Add transaction support for atomic multi-operation workflows","description":"Add a transaction-aware API to the Beads Storage interface to support atomic multi-operation workflows. Context from VC project: The VC plan approval code needs to create multiple issues atomically (phases, tasks, dependencies, labels, metadata updates). Currently each storage operation gets its own DB connection making transactions impossible. When approving a mission plan we need to atomically: create 5-10 phase issues, create 20-30 task issues, add dependencies between them, apply labels to all issues, update mission metadata, delete ephemeral plan. If ANY step fails ALL changes should rollback.","design":"Add RunInTransaction wrapper to Storage interface. Create Transaction interface with same methods as Storage but using underlying sql.Tx. Implementation creates transactionStorage struct that wraps tx and uses tx.ExecContext instead of db.ExecContext for all operations.","acceptance_criteria":"WHEN calling RunInTransaction THEN all operations use same transaction. WHEN transaction function returns nil THEN transaction is committed. WHEN transaction function returns error THEN transaction is rolled back. WHEN transaction commits THEN all changes are visible. WHEN transaction rolls back THEN no changes are visible.","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-11-24T11:18:17.050954-08:00","updated_at":"2025-11-24T20:18:52.777806-08:00","closed_at":"2025-11-24T20:18:52.777806-08:00","source_repo":"."}
{"id":"bd-m7ge","content_hash":"bb08f2bcbbdd2e392733d92bff2e46a51000337ac019d306dd6a2983916873c4","title":"Add .beads/README.md during 'bd init' for project documentation and promotion","description":"When 'bd init' is run, automatically generate a .beads/README.md file that:\n\n1. Briefly explains what Beads is (AI-native issue tracking that lives in your repo)\n2. Links to the main repository: https://github.com/steveyegge/beads\n3. Provides a quick reference of essential commands:\n - bd create: Create new issues\n - bd list: View all issues\n - bd update: Modify issue status/details\n - bd show: View issue details\n - bd sync: Sync with git remote\n4. Highlights key benefits for AI coding agents and developers\n5. Encourages developers to try it out\n\nThe README should be enthusiastic and compelling to get open source contributors excited about using Beads for their AI-assisted development workflows.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-16T22:32:50.478681-08:00","updated_at":"2025-11-16T22:32:58.492868-08:00","source_repo":"."}
{"id":"bd-mexx","content_hash":"8fef16c6c30727dda57cbabc54e315e45cedf6c9cff4f87e768729db288ce2e9","title":"Add test for concurrent deregistration race condition","description":"No test verifies behavior when two concurrent deregistration calls race. Add a test that calls deregister_agent twice concurrently to verify idempotency holds under race conditions.","status":"open","priority":3,"issue_type":"task","created_at":"2025-11-24T17:14:17.901397-08:00","updated_at":"2025-11-24T17:14:17.901397-08:00","source_repo":"."}
{"id":"bd-mnap","content_hash":"c15d3c631656fe6d21291f127fc545af93e712b5f3f94cce028513fb743a4fdb","title":"Investigate performance issues in VS Code Copilot (Windows)","description":"Beads unusable in Windows 11 VS Code Copilot chat with Sonnet 4.5.\nSummary event happens every 3-4 turns, taking 3 minutes.\nCopilot summarizes after ~125k tokens despite model supporting 1M.\nLarge context size of beads might be triggering aggressive summarization.\nNeed workaround or optimization for context size.\n","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-20T18:56:30.124918-05:00","updated_at":"2025-11-20T18:56:30.124918-05:00","source_repo":"."}
{"id":"bd-mq1b","content_hash":"16369efa3bc8d99c33ff5f11066ea39f9a3c152b1cfbd98701136382c2fbc514","title":"Add SearchIssues to Transaction for read-your-writes","description":"Add search capability within transaction for read-your-writes consistency.\n\n## Tasks\n1. Add to Transaction interface:\n - SearchIssues(ctx, query, filter) ([]*types.Issue, error)\n\n2. Implement on sqliteTxStorage:\n - Reuse existing search logic with conn\n - Ensure reads see uncommitted writes within same transaction\n\n## Acceptance Criteria\n- [ ] SearchIssues returns issues created in same transaction\n- [ ] Filter logic works correctly within transaction\n- [ ] Test: create issue then search for it in same transaction","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-24T11:37:21.412233-08:00","updated_at":"2025-11-24T11:37:21.412233-08:00","source_repo":".","dependencies":[{"issue_id":"bd-mq1b","depends_on_id":"bd-6pul","type":"parent-child","created_at":"2025-11-24T11:37:21.413628-08:00","created_by":"daemon"}]}
{"id":"bd-mq1b","content_hash":"16369efa3bc8d99c33ff5f11066ea39f9a3c152b1cfbd98701136382c2fbc514","title":"Add SearchIssues to Transaction for read-your-writes","description":"Add search capability within transaction for read-your-writes consistency.\n\n## Tasks\n1. Add to Transaction interface:\n - SearchIssues(ctx, query, filter) ([]*types.Issue, error)\n\n2. Implement on sqliteTxStorage:\n - Reuse existing search logic with conn\n - Ensure reads see uncommitted writes within same transaction\n\n## Acceptance Criteria\n- [ ] SearchIssues returns issues created in same transaction\n- [ ] Filter logic works correctly within transaction\n- [ ] Test: create issue then search for it in same transaction","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-24T11:37:21.412233-08:00","updated_at":"2025-11-24T20:19:25.597788-08:00","source_repo":".","dependencies":[{"issue_id":"bd-mq1b","depends_on_id":"bd-6pul","type":"parent-child","created_at":"2025-11-24T11:37:21.413628-08:00","created_by":"daemon"}]}
{"id":"bd-n4gu","content_hash":"0d06c2ec9303bf472c239b1a95e1b857bfb08f630e8016920aa49fff63716947","title":"Build slot cleanup uses synchronous file I/O in async function","description":"In app.py:2996-3020, deregister_agent uses synchronous file I/O (iterdir, glob, read_text, write_text) in an async function. This can cause latency spikes with many build slot files. Recommendation: Use anyio or aiofiles for async file operations, or run in thread pool.","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-24T17:14:02.06723-08:00","updated_at":"2025-11-24T17:14:02.06723-08:00","source_repo":"."}
{"id":"bd-n4td","content_hash":"1a5222748ad9badd0cdfdcfbe831f96c532deeb41909f9729e111dcbaa119d0d","title":"Add warning when staleness check errors","description":"## Problem\n\nWhen ensureDatabaseFresh() calls CheckStaleness() and it errors (corrupted metadata, permission issues, etc.), we silently proceed with potentially stale data.\n\n**Location:** cmd/bd/staleness.go:27-32\n\n**Scenarios:**\n- Corrupted metadata table\n- Database locked by another process \n- Permission issues reading JSONL file\n- Invalid last_import_time format in DB\n\n## Current Code\n\n```go\nisStale, err := autoimport.CheckStaleness(ctx, store, dbPath)\nif err \\!= nil {\n // If we can't determine staleness, allow operation to proceed\n // (better to show potentially stale data than block user)\n return nil\n}\n```\n\n## Fix\n\n```go\nisStale, err := autoimport.CheckStaleness(ctx, store, dbPath)\nif err \\!= nil {\n fmt.Fprintf(os.Stderr, \"Warning: Could not verify database freshness: %v\\n\", err)\n fmt.Fprintf(os.Stderr, \"Proceeding anyway. Data may be stale.\\n\\n\")\n return nil\n}\n```\n\n## Impact\nMedium - users should know when staleness check fails\n\n## Effort\nEasy - 5 minutes","status":"open","priority":2,"issue_type":"bug","created_at":"2025-11-20T20:16:34.889997-05:00","updated_at":"2025-11-20T20:16:34.889997-05:00","source_repo":".","dependencies":[{"issue_id":"bd-n4td","depends_on_id":"bd-2q6d","type":"blocks","created_at":"2025-11-20T20:18:20.154723-05:00","created_by":"stevey"}]}
{"id":"bd-nq41","content_hash":"33f9cfe6a0ef5200dcd5016317b43b1568ff9dc7303537d956bdab02029f6c63","title":"Fix Homebrew warning about Ruby file location","description":"Homebrew warning: Found Ruby file outside steveyegge/beads tap formula directory.\nWarning points to: /opt/homebrew/Library/Taps/steveyegge/homebrew-beads/bd.rb\nIt should likely be inside a Formula/ directory or similar structure expected by Homebrew taps.\n","status":"open","priority":2,"issue_type":"chore","created_at":"2025-11-20T18:56:21.226579-05:00","updated_at":"2025-11-20T18:56:21.226579-05:00","source_repo":"."}

View File

@@ -796,3 +796,91 @@ func (t *sqliteTxStorage) RemoveLabel(ctx context.Context, issueID, label, actor
return nil
}
// SetConfig sets a configuration value within the transaction.
func (t *sqliteTxStorage) SetConfig(ctx context.Context, key, value string) error {
_, err := t.conn.ExecContext(ctx, `
INSERT INTO config (key, value) VALUES (?, ?)
ON CONFLICT (key) DO UPDATE SET value = excluded.value
`, key, value)
if err != nil {
return fmt.Errorf("failed to set config: %w", err)
}
return nil
}
// GetConfig gets a configuration value within the transaction.
// This enables read-your-writes semantics for config values.
func (t *sqliteTxStorage) GetConfig(ctx context.Context, key string) (string, error) {
var value string
err := t.conn.QueryRowContext(ctx, `SELECT value FROM config WHERE key = ?`, key).Scan(&value)
if err == sql.ErrNoRows {
return "", nil
}
if err != nil {
return "", fmt.Errorf("failed to get config: %w", err)
}
return value, nil
}
// SetMetadata sets a metadata value within the transaction.
func (t *sqliteTxStorage) SetMetadata(ctx context.Context, key, value string) error {
_, err := t.conn.ExecContext(ctx, `
INSERT INTO metadata (key, value) VALUES (?, ?)
ON CONFLICT (key) DO UPDATE SET value = excluded.value
`, key, value)
if err != nil {
return fmt.Errorf("failed to set metadata: %w", err)
}
return nil
}
// GetMetadata gets a metadata value within the transaction.
// This enables read-your-writes semantics for metadata values.
func (t *sqliteTxStorage) GetMetadata(ctx context.Context, key string) (string, error) {
var value string
err := t.conn.QueryRowContext(ctx, `SELECT value FROM metadata WHERE key = ?`, key).Scan(&value)
if err == sql.ErrNoRows {
return "", nil
}
if err != nil {
return "", fmt.Errorf("failed to get metadata: %w", err)
}
return value, nil
}
// AddComment adds a comment to an issue within the transaction.
func (t *sqliteTxStorage) AddComment(ctx context.Context, issueID, actor, comment string) error {
// Update issue updated_at timestamp first to verify issue exists
now := time.Now()
res, err := t.conn.ExecContext(ctx, `
UPDATE issues SET updated_at = ? WHERE id = ?
`, now, issueID)
if err != nil {
return fmt.Errorf("failed to update timestamp: %w", err)
}
rows, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rows == 0 {
return fmt.Errorf("issue %s not found", issueID)
}
// Insert comment event
_, err = t.conn.ExecContext(ctx, `
INSERT INTO events (issue_id, event_type, actor, comment)
VALUES (?, ?, ?, ?)
`, issueID, types.EventCommented, actor, comment)
if err != nil {
return fmt.Errorf("failed to add comment: %w", err)
}
// Mark issue as dirty for incremental export
if err := markDirty(ctx, t.conn, issueID); err != nil {
return fmt.Errorf("failed to mark issue dirty: %w", err)
}
return nil
}

View File

@@ -847,6 +847,449 @@ func TestTransactionAtomicPlanApproval(t *testing.T) {
}
}
// TestTransactionSetConfig tests setting a config value within a transaction.
func TestTransactionSetConfig(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
return tx.SetConfig(ctx, "test.key", "test-value")
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify config was set
value, err := store.GetConfig(ctx, "test.key")
if err != nil {
t.Fatalf("GetConfig failed: %v", err)
}
if value != "test-value" {
t.Errorf("expected 'test-value', got %q", value)
}
}
// TestTransactionGetConfig tests reading config within a transaction (read-your-writes).
func TestTransactionGetConfig(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Set config
if err := tx.SetConfig(ctx, "test.key", "test-value"); err != nil {
return err
}
// Read it back within same transaction
value, err := tx.GetConfig(ctx, "test.key")
if err != nil {
return err
}
if value != "test-value" {
t.Errorf("expected 'test-value' within transaction, got %q", value)
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
}
// TestTransactionConfigRollback tests that config changes are rolled back on error.
func TestTransactionConfigRollback(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
if err := tx.SetConfig(ctx, "test.key", "test-value"); err != nil {
return err
}
return &testError{msg: "intentional rollback"}
})
if err == nil {
t.Error("expected error from transaction")
}
// Verify config was NOT set (rolled back)
value, err := store.GetConfig(ctx, "test.key")
if err != nil {
t.Fatalf("GetConfig failed: %v", err)
}
if value != "" {
t.Errorf("expected empty value after rollback, got %q", value)
}
}
// TestTransactionSetMetadata tests setting a metadata value within a transaction.
func TestTransactionSetMetadata(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
return tx.SetMetadata(ctx, "test.metadata", "metadata-value")
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify metadata was set
value, err := store.GetMetadata(ctx, "test.metadata")
if err != nil {
t.Fatalf("GetMetadata failed: %v", err)
}
if value != "metadata-value" {
t.Errorf("expected 'metadata-value', got %q", value)
}
}
// TestTransactionGetMetadata tests reading metadata within a transaction (read-your-writes).
func TestTransactionGetMetadata(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Set metadata
if err := tx.SetMetadata(ctx, "test.metadata", "metadata-value"); err != nil {
return err
}
// Read it back within same transaction
value, err := tx.GetMetadata(ctx, "test.metadata")
if err != nil {
return err
}
if value != "metadata-value" {
t.Errorf("expected 'metadata-value' within transaction, got %q", value)
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
}
// TestTransactionMetadataRollback tests that metadata changes are rolled back on error.
func TestTransactionMetadataRollback(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
if err := tx.SetMetadata(ctx, "test.metadata", "metadata-value"); err != nil {
return err
}
return &testError{msg: "intentional rollback"}
})
if err == nil {
t.Error("expected error from transaction")
}
// Verify metadata was NOT set (rolled back)
value, err := store.GetMetadata(ctx, "test.metadata")
if err != nil {
t.Fatalf("GetMetadata failed: %v", err)
}
if value != "" {
t.Errorf("expected empty value after rollback, got %q", value)
}
}
// TestTransactionAddComment tests adding a comment within a transaction.
func TestTransactionAddComment(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Create issue first
issue := &types.Issue{
Title: "Test Issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, issue, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Add comment in transaction
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
return tx.AddComment(ctx, issue.ID, "commenter", "This is a test comment")
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify comment exists via events
events, err := store.GetEvents(ctx, issue.ID, 10)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
found := false
for _, e := range events {
if e.EventType == types.EventCommented && e.Comment != nil && *e.Comment == "This is a test comment" {
found = true
break
}
}
if !found {
t.Error("expected comment event to exist")
}
}
// TestTransactionAddCommentToCreatedIssue tests adding a comment to an issue created in the same transaction.
func TestTransactionAddCommentToCreatedIssue(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
var issueID string
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Create issue
issue := &types.Issue{
Title: "Test Issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
if err := tx.CreateIssue(ctx, issue, "test-actor"); err != nil {
return err
}
issueID = issue.ID
// Add comment to the issue we just created
return tx.AddComment(ctx, issue.ID, "commenter", "Comment on new issue")
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify both issue and comment exist
issue, err := store.GetIssue(ctx, issueID)
if err != nil || issue == nil {
t.Error("expected issue to exist")
}
events, err := store.GetEvents(ctx, issueID, 10)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
found := false
for _, e := range events {
if e.EventType == types.EventCommented && e.Comment != nil && *e.Comment == "Comment on new issue" {
found = true
break
}
}
if !found {
t.Error("expected comment event to exist")
}
}
// TestTransactionAddCommentNonexistentIssue tests that adding a comment to a nonexistent issue fails.
func TestTransactionAddCommentNonexistentIssue(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
return tx.AddComment(ctx, "nonexistent-id", "commenter", "This should fail")
})
if err == nil {
t.Error("expected error when commenting on nonexistent issue")
}
}
// TestTransactionCommentRollback tests that comments are rolled back on error.
func TestTransactionCommentRollback(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Create issue first
issue := &types.Issue{
Title: "Test Issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, issue, "test-actor"); err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
if err := tx.AddComment(ctx, issue.ID, "commenter", "This comment should be rolled back"); err != nil {
return err
}
return &testError{msg: "intentional rollback"}
})
if err == nil {
t.Error("expected error from transaction")
}
// Verify comment was NOT added (rolled back)
events, err := store.GetEvents(ctx, issue.ID, 10)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
for _, e := range events {
if e.EventType == types.EventCommented {
t.Error("expected no comment events after rollback")
}
}
}
// TestTransactionAtomicConfigWithIssue tests atomically creating an issue and setting config.
func TestTransactionAtomicConfigWithIssue(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
var issueID string
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
// Create issue
issue := &types.Issue{
Title: "Test Issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
if err := tx.CreateIssue(ctx, issue, "test-actor"); err != nil {
return err
}
issueID = issue.ID
// Set config referencing the issue
if err := tx.SetConfig(ctx, "last_created_issue", issue.ID); err != nil {
return err
}
// Set metadata
if err := tx.SetMetadata(ctx, "import_marker", "test-import-123"); err != nil {
return err
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify all three operations succeeded
issue, err := store.GetIssue(ctx, issueID)
if err != nil || issue == nil {
t.Error("expected issue to exist")
}
configValue, err := store.GetConfig(ctx, "last_created_issue")
if err != nil {
t.Fatalf("GetConfig failed: %v", err)
}
if configValue != issueID {
t.Errorf("expected config value %q, got %q", issueID, configValue)
}
metadataValue, err := store.GetMetadata(ctx, "import_marker")
if err != nil {
t.Fatalf("GetMetadata failed: %v", err)
}
if metadataValue != "test-import-123" {
t.Errorf("expected metadata value 'test-import-123', got %q", metadataValue)
}
}
// TestTransactionConfigOverwrite tests that SetConfig overwrites existing values.
func TestTransactionConfigOverwrite(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
// Set initial value
if err := store.SetConfig(ctx, "test.key", "initial"); err != nil {
t.Fatalf("SetConfig failed: %v", err)
}
// Overwrite in transaction
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
return tx.SetConfig(ctx, "test.key", "updated")
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
// Verify overwrite
value, err := store.GetConfig(ctx, "test.key")
if err != nil {
t.Fatalf("GetConfig failed: %v", err)
}
if value != "updated" {
t.Errorf("expected 'updated', got %q", value)
}
}
// TestTransactionGetConfigNonexistent tests getting a nonexistent config key returns empty string.
func TestTransactionGetConfigNonexistent(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
value, err := tx.GetConfig(ctx, "nonexistent.key")
if err != nil {
return err
}
if value != "" {
t.Errorf("expected empty string for nonexistent key, got %q", value)
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
}
// TestTransactionGetMetadataNonexistent tests getting a nonexistent metadata key returns empty string.
func TestTransactionGetMetadataNonexistent(t *testing.T) {
ctx := context.Background()
store, cleanup := setupTestDB(t)
defer cleanup()
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
value, err := tx.GetMetadata(ctx, "nonexistent.metadata")
if err != nil {
return err
}
if value != "" {
t.Errorf("expected empty string for nonexistent metadata, got %q", value)
}
return nil
})
if err != nil {
t.Fatalf("RunInTransaction failed: %v", err)
}
}
// testError is a simple error type for testing
type testError struct {
msg string

View File

@@ -61,6 +61,17 @@ type Transaction interface {
// Label operations
AddLabel(ctx context.Context, issueID, label, actor string) error
RemoveLabel(ctx context.Context, issueID, label, actor string) error
// Config operations (for atomic config + issue workflows)
SetConfig(ctx context.Context, key, value string) error
GetConfig(ctx context.Context, key string) (string, error)
// Metadata operations (for internal state like import hashes)
SetMetadata(ctx context.Context, key, value string) error
GetMetadata(ctx context.Context, key string) (string, error)
// Comment operations
AddComment(ctx context.Context, issueID, actor, comment string) error
}
// Storage defines the interface for issue storage backends