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
This commit is contained in:
beads/crew/dave
2025-12-31 13:13:49 -08:00
committed by Steve Yegge
parent 7c9b975436
commit b362b36824
42 changed files with 165 additions and 82 deletions
+19 -5
View File
@@ -443,6 +443,14 @@ func (m *MemoryStorage) UpdateIssue(ctx context.Context, id string, updates map[
}
issue.ExternalRef = nil
}
case "close_reason":
if v, ok := value.(string); ok {
issue.CloseReason = v
}
case "closed_by_session":
if v, ok := value.(string); ok {
issue.ClosedBySession = v
}
}
}
@@ -467,11 +475,17 @@ func (m *MemoryStorage) UpdateIssue(ctx context.Context, id string, updates map[
return nil
}
// CloseIssue closes an issue with a reason
func (m *MemoryStorage) CloseIssue(ctx context.Context, id string, reason string, actor string) error {
return m.UpdateIssue(ctx, id, map[string]interface{}{
"status": string(types.StatusClosed),
}, actor)
// CloseIssue closes an issue with a reason.
// The session parameter tracks which Claude Code session closed the issue (can be empty).
func (m *MemoryStorage) CloseIssue(ctx context.Context, id string, reason string, actor string, session string) error {
updates := map[string]interface{}{
"status": string(types.StatusClosed),
"close_reason": reason,
}
if session != "" {
updates["closed_by_session"] = session
}
return m.UpdateIssue(ctx, id, updates, actor)
}
// CreateTombstone converts an existing issue to a tombstone record.
@@ -515,7 +515,7 @@ func TestMemoryStorage_GetStaleIssues_FilteringAndLimit(t *testing.T) {
t.Fatalf("CreateIssue %s: %v", iss.ID, err)
}
}
if err := store.CloseIssue(ctx, closed.ID, "done", "actor"); err != nil {
if err := store.CloseIssue(ctx, closed.ID, "done", "actor", ""); err != nil {
t.Fatalf("CloseIssue: %v", err)
}
@@ -555,10 +555,10 @@ func TestMemoryStorage_Statistics_EpicsEligibleForClosure_Counting(t *testing.T)
t.Fatalf("CreateIssue %s: %v", iss.ID, err)
}
}
if err := store.CloseIssue(ctx, c1.ID, "done", "actor"); err != nil {
if err := store.CloseIssue(ctx, c1.ID, "done", "actor", ""); err != nil {
t.Fatalf("CloseIssue c1: %v", err)
}
if err := store.CloseIssue(ctx, c2.ID, "done", "actor"); err != nil {
if err := store.CloseIssue(ctx, c2.ID, "done", "actor", ""); err != nil {
t.Fatalf("CloseIssue c2: %v", err)
}
// Parent-child deps: child -> epic.
@@ -851,7 +851,7 @@ func TestMemoryStorage_UpdateIssue_CoversMoreFields(t *testing.T) {
}
// Status closed when already closed should not clear ClosedAt.
if err := store.CloseIssue(ctx, iss.ID, "done", "actor"); err != nil {
if err := store.CloseIssue(ctx, iss.ID, "done", "actor", ""); err != nil {
t.Fatalf("CloseIssue: %v", err)
}
closedOnce, _ := store.GetIssue(ctx, iss.ID)
@@ -881,7 +881,7 @@ func TestMemoryStorage_CountEpicsEligibleForClosure_CoversBranches(t *testing.T)
t.Fatalf("CreateIssue %s: %v", iss.ID, err)
}
}
if err := store.CloseIssue(ctx, epClosed.ID, "done", "actor"); err != nil {
if err := store.CloseIssue(ctx, epClosed.ID, "done", "actor", ""); err != nil {
t.Fatalf("CloseIssue: %v", err)
}
// Child -> ep1 (eligible once child is closed).
@@ -898,7 +898,7 @@ func TestMemoryStorage_CountEpicsEligibleForClosure_CoversBranches(t *testing.T)
store.mu.Unlock()
// Close child to make ep1 eligible.
if err := store.CloseIssue(ctx, c.ID, "done", "actor"); err != nil {
if err := store.CloseIssue(ctx, c.ID, "done", "actor", ""); err != nil {
t.Fatalf("CloseIssue child: %v", err)
}
+5 -5
View File
@@ -320,7 +320,7 @@ func TestCloseIssue(t *testing.T) {
}
// Close it
if err := store.CloseIssue(ctx, issue.ID, "Completed", "test-user"); err != nil {
if err := store.CloseIssue(ctx, issue.ID, "Completed", "test-user", ""); err != nil {
t.Fatalf("CloseIssue failed: %v", err)
}
@@ -733,7 +733,7 @@ func TestStatistics(t *testing.T) {
}
// Close the one marked as closed
if issue.Status == types.StatusClosed {
if err := store.CloseIssue(ctx, issue.ID, "Done", "test-user"); err != nil {
if err := store.CloseIssue(ctx, issue.ID, "Done", "test-user", ""); err != nil {
t.Fatalf("CloseIssue failed: %v", err)
}
}
@@ -786,7 +786,7 @@ func TestStatistics_BlockedAndReadyCounts(t *testing.T) {
}
// Close the closedBlocker properly
if err := store.CloseIssue(ctx, closedBlocker.ID, "Done", "test"); err != nil {
if err := store.CloseIssue(ctx, closedBlocker.ID, "Done", "test", ""); err != nil {
t.Fatalf("CloseIssue failed: %v", err)
}
@@ -878,7 +878,7 @@ func TestStatistics_EpicsEligibleForClosure(t *testing.T) {
// Close the children properly
for _, child := range []*types.Issue{child1, child2} {
if err := store.CloseIssue(ctx, child.ID, "Done", "test"); err != nil {
if err := store.CloseIssue(ctx, child.ID, "Done", "test", ""); err != nil {
t.Fatalf("CloseIssue failed: %v", err)
}
}
@@ -926,7 +926,7 @@ func TestStatistics_TombstonesExcludedFromTotal(t *testing.T) {
}
// Close the closed issue properly
if err := store.CloseIssue(ctx, issues[1].ID, "Done", "test"); err != nil {
if err := store.CloseIssue(ctx, issues[1].ID, "Done", "test", ""); err != nil {
t.Fatalf("CloseIssue failed: %v", err)
}
+12 -12
View File
@@ -126,7 +126,7 @@ func TestCacheInvalidationOnStatusChange(t *testing.T) {
}
// Close the blocker
if err := store.CloseIssue(ctx, blocker.ID, "Done", "test-user"); err != nil {
if err := store.CloseIssue(ctx, blocker.ID, "Done", "test-user", ""); err != nil {
t.Fatalf("CloseIssue failed: %v", err)
}
@@ -186,7 +186,7 @@ func TestCacheConsistencyAcrossOperations(t *testing.T) {
}
// Operation 3: Close blocker1
store.CloseIssue(ctx, blocker1.ID, "Done", "test-user")
store.CloseIssue(ctx, blocker1.ID, "Done", "test-user", "")
cached = getCachedBlockedIssues(t, store)
if cached[blocked1.ID] || !cached[blocked2.ID] {
@@ -322,7 +322,7 @@ func TestDeepHierarchyCacheCorrectness(t *testing.T) {
}
// Close the blocker and verify all become unblocked
store.CloseIssue(ctx, blocker.ID, "Done", "test-user")
store.CloseIssue(ctx, blocker.ID, "Done", "test-user", "")
cached = getCachedBlockedIssues(t, store)
if len(cached) != 0 {
@@ -359,7 +359,7 @@ func TestMultipleBlockersInCache(t *testing.T) {
}
// Close one blocker - should still be blocked
store.CloseIssue(ctx, blocker1.ID, "Done", "test-user")
store.CloseIssue(ctx, blocker1.ID, "Done", "test-user", "")
cached = getCachedBlockedIssues(t, store)
if !cached[blocked.ID] {
@@ -367,7 +367,7 @@ func TestMultipleBlockersInCache(t *testing.T) {
}
// Close the second blocker - should be unblocked
store.CloseIssue(ctx, blocker2.ID, "Done", "test-user")
store.CloseIssue(ctx, blocker2.ID, "Done", "test-user", "")
cached = getCachedBlockedIssues(t, store)
if cached[blocked.ID] {
@@ -400,7 +400,7 @@ func TestConditionalBlocksCache(t *testing.T) {
}
// Close A with SUCCESS (no failure keywords) - B should STILL be blocked
store.CloseIssue(ctx, issueA.ID, "Completed successfully", "test-user")
store.CloseIssue(ctx, issueA.ID, "Completed successfully", "test-user", "")
cached = getCachedBlockedIssues(t, store)
if !cached[issueB.ID] {
@@ -411,7 +411,7 @@ func TestConditionalBlocksCache(t *testing.T) {
store.UpdateIssue(ctx, issueA.ID, map[string]interface{}{"status": types.StatusOpen}, "test-user")
// Close A with FAILURE - B should now be UNBLOCKED
store.CloseIssue(ctx, issueA.ID, "Task failed due to timeout", "test-user")
store.CloseIssue(ctx, issueA.ID, "Task failed due to timeout", "test-user", "")
cached = getCachedBlockedIssues(t, store)
if cached[issueB.ID] {
@@ -451,7 +451,7 @@ func TestConditionalBlocksVariousFailureKeywords(t *testing.T) {
store.AddDependency(ctx, dep, "test-user")
// Close A with failure reason
store.CloseIssue(ctx, issueA.ID, "Closed: "+reason, "test-user")
store.CloseIssue(ctx, issueA.ID, "Closed: "+reason, "test-user", "")
cached := getCachedBlockedIssues(t, store)
if cached[issueB.ID] {
@@ -501,7 +501,7 @@ func TestWaitsForAllChildren(t *testing.T) {
}
// Close first child - waiter should still be blocked (second child still open)
store.CloseIssue(ctx, child1.ID, "Done", "test-user")
store.CloseIssue(ctx, child1.ID, "Done", "test-user", "")
cached = getCachedBlockedIssues(t, store)
if !cached[waiter.ID] {
@@ -509,7 +509,7 @@ func TestWaitsForAllChildren(t *testing.T) {
}
// Close second child - waiter should now be unblocked
store.CloseIssue(ctx, child2.ID, "Done", "test-user")
store.CloseIssue(ctx, child2.ID, "Done", "test-user", "")
cached = getCachedBlockedIssues(t, store)
if cached[waiter.ID] {
@@ -557,7 +557,7 @@ func TestWaitsForAnyChildren(t *testing.T) {
}
// Close first child - waiter should now be unblocked (any-children gate satisfied)
store.CloseIssue(ctx, child1.ID, "Done", "test-user")
store.CloseIssue(ctx, child1.ID, "Done", "test-user", "")
cached = getCachedBlockedIssues(t, store)
if cached[waiter.ID] {
@@ -636,7 +636,7 @@ func TestWaitsForDynamicChildrenAdded(t *testing.T) {
}
// Close the child - waiter should be unblocked again
store.CloseIssue(ctx, child.ID, "Done", "test-user")
store.CloseIssue(ctx, child.ID, "Done", "test-user", "")
cached = getCachedBlockedIssues(t, store)
if cached[waiter.ID] {
+1 -1
View File
@@ -57,7 +57,7 @@ func (h *epicTestHelper) addParentChildDependency(childID, parentID string) {
}
func (h *epicTestHelper) closeIssue(id, reason string) {
if err := h.store.CloseIssue(h.ctx, id, reason, "test-user"); err != nil {
if err := h.store.CloseIssue(h.ctx, id, reason, "test-user", ""); err != nil {
h.t.Fatalf("CloseIssue (%s) failed: %v", id, err)
}
}
+1 -1
View File
@@ -311,7 +311,7 @@ func TestEventTypesInHistory(t *testing.T) {
t.Fatalf("AddLabel failed: %v", err)
}
err = store.CloseIssue(ctx, issue.ID, "Done", "test-user")
err = store.CloseIssue(ctx, issue.ID, "Done", "test-user", "")
if err != nil {
t.Fatalf("CloseIssue failed: %v", err)
}
+2 -2
View File
@@ -179,7 +179,7 @@ func TestDuplicateOf(t *testing.T) {
}
// Close the duplicate
if err := store.CloseIssue(ctx, duplicate.ID, "Closed as duplicate", "test"); err != nil {
if err := store.CloseIssue(ctx, duplicate.ID, "Closed as duplicate", "test", ""); err != nil {
t.Fatalf("Failed to close duplicate: %v", err)
}
@@ -251,7 +251,7 @@ func TestSupersededBy(t *testing.T) {
}
// Close old version
if err := store.CloseIssue(ctx, oldVersion.ID, "Superseded by v2", "test"); err != nil {
if err := store.CloseIssue(ctx, oldVersion.ID, "Superseded by v2", "test", ""); err != nil {
t.Fatalf("Failed to close old version: %v", err)
}
+2
View File
@@ -50,6 +50,7 @@ var migrationsList = []Migration{
{"mol_type_column", migrations.MigrateMolTypeColumn},
{"hooked_status_migration", migrations.MigrateHookedStatus},
{"event_fields", migrations.MigrateEventFields},
{"closed_by_session_column", migrations.MigrateClosedBySessionColumn},
}
// MigrationInfo contains metadata about a migration for inspection
@@ -107,6 +108,7 @@ func getMigrationDescription(name string) string {
"mol_type_column": "Adds mol_type column for molecule type classification (swarm/patrol/work)",
"hooked_status_migration": "Migrates blocked hooked issues to in_progress status",
"event_fields": "Adds event fields (event_kind, actor, target, payload) for operational state change beads",
"closed_by_session_column": "Adds closed_by_session column for tracking which Claude Code session closed an issue",
}
if desc, ok := descriptions[name]; ok {
@@ -0,0 +1,34 @@
package migrations
import (
"database/sql"
"fmt"
)
// MigrateClosedBySessionColumn adds the closed_by_session column to the issues table.
// This tracks which Claude Code session closed the issue, enabling work attribution
// for entity CV building. See Gas Town decision 009-session-events-architecture.md.
func MigrateClosedBySessionColumn(db *sql.DB) error {
// Check if column already exists
var columnExists bool
err := db.QueryRow(`
SELECT COUNT(*) > 0
FROM pragma_table_info('issues')
WHERE name = 'closed_by_session'
`).Scan(&columnExists)
if err != nil {
return fmt.Errorf("failed to check closed_by_session column: %w", err)
}
if columnExists {
return nil
}
// Add the closed_by_session column
_, err = db.Exec(`ALTER TABLE issues ADD COLUMN closed_by_session TEXT DEFAULT ''`)
if err != nil {
return fmt.Errorf("failed to add closed_by_session column: %w", err)
}
return nil
}
+7 -4
View File
@@ -664,6 +664,8 @@ var allowedUpdateFields = map[string]bool{
"estimated_minutes": true,
"external_ref": true,
"closed_at": true,
"close_reason": true,
"closed_by_session": true,
// Messaging fields
"sender": true,
"wisp": true, // Database column is 'ephemeral', mapped in UpdateIssue
@@ -1070,8 +1072,9 @@ func (s *SQLiteStorage) ResetCounter(ctx context.Context, prefix string) error {
return nil
}
// CloseIssue closes an issue with a reason
func (s *SQLiteStorage) CloseIssue(ctx context.Context, id string, reason string, actor string) error {
// CloseIssue closes an issue with a reason.
// The session parameter tracks which Claude Code session closed the issue (can be empty).
func (s *SQLiteStorage) CloseIssue(ctx context.Context, id string, reason string, actor string, session string) error {
now := time.Now()
// Update with special event handling
@@ -1086,9 +1089,9 @@ func (s *SQLiteStorage) CloseIssue(ctx context.Context, id string, reason string
// 2. events.comment - for audit history (when was it closed, by whom)
// Keep both in sync. If refactoring, consider deriving one from the other.
result, err := tx.ExecContext(ctx, `
UPDATE issues SET status = ?, closed_at = ?, updated_at = ?, close_reason = ?
UPDATE issues SET status = ?, closed_at = ?, updated_at = ?, close_reason = ?, closed_by_session = ?
WHERE id = ?
`, types.StatusClosed, now, now, reason, id)
`, types.StatusClosed, now, now, reason, session, id)
if err != nil {
return fmt.Errorf("failed to close issue: %w", err)
}
+5 -5
View File
@@ -515,7 +515,7 @@ func TestDeepHierarchyBlocking(t *testing.T) {
}
// Now close the blocker and verify all levels become ready
store.CloseIssue(ctx, blocker.ID, "Done", "test-user")
store.CloseIssue(ctx, blocker.ID, "Done", "test-user", "")
ready, err = store.GetReadyWork(ctx, types.WorkFilter{Status: types.StatusOpen})
if err != nil {
@@ -564,7 +564,7 @@ func TestGetReadyWorkIncludesInProgress(t *testing.T) {
store.UpdateIssue(ctx, issue3.ID, map[string]interface{}{"status": types.StatusInProgress}, "test-user")
store.CreateIssue(ctx, issue4, "test-user")
store.CreateIssue(ctx, issue5, "test-user")
store.CloseIssue(ctx, issue5.ID, "Done", "test-user")
store.CloseIssue(ctx, issue5.ID, "Done", "test-user", "")
// Add dependency: issue3 blocks on issue4
store.AddDependency(ctx, &types.Dependency{IssueID: issue3.ID, DependsOnID: issue4.ID, Type: types.DepBlocks}, "test-user")
@@ -1018,7 +1018,7 @@ func TestGetReadyWorkExternalDeps(t *testing.T) {
}
// Close the capability issue
if err := externalStore.CloseIssue(ctx, capabilityIssue.ID, "Shipped", "test-user"); err != nil {
if err := externalStore.CloseIssue(ctx, capabilityIssue.ID, "Shipped", "test-user", ""); err != nil {
t.Fatalf("failed to close capability issue: %v", err)
}
@@ -1246,7 +1246,7 @@ func TestGetBlockedIssuesFiltersExternalDeps(t *testing.T) {
}
// Close the capability issue
if err := externalStore.CloseIssue(ctx, capabilityIssue.ID, "Shipped", "test-user"); err != nil {
if err := externalStore.CloseIssue(ctx, capabilityIssue.ID, "Shipped", "test-user", ""); err != nil {
t.Fatalf("failed to close capability issue: %v", err)
}
@@ -1371,7 +1371,7 @@ func TestGetBlockedIssuesPartialExternalDeps(t *testing.T) {
if err := externalStore.AddLabel(ctx, cap1Issue.ID, "provides:cap1", "test-user"); err != nil {
t.Fatalf("failed to add provides label: %v", err)
}
if err := externalStore.CloseIssue(ctx, cap1Issue.ID, "Shipped", "test-user"); err != nil {
if err := externalStore.CloseIssue(ctx, cap1Issue.ID, "Shipped", "test-user", ""); err != nil {
t.Fatalf("failed to close cap1 issue: %v", err)
}
+1
View File
@@ -19,6 +19,7 @@ CREATE TABLE IF NOT EXISTS issues (
created_by TEXT DEFAULT '',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
closed_at DATETIME,
closed_by_session TEXT DEFAULT '',
external_ref TEXT,
compaction_level INTEGER DEFAULT 0,
compacted_at DATETIME,
+1 -1
View File
@@ -180,7 +180,7 @@ func BenchmarkBulkCloseIssues(b *testing.B) {
for i := 0; i < b.N; i++ {
for j, issue := range issues {
if err := store.CloseIssue(ctx, issue.ID, "Bulk closed", "bench"); err != nil {
if err := store.CloseIssue(ctx, issue.ID, "Bulk closed", "bench", ""); err != nil {
b.Fatalf("CloseIssue failed: %v", err)
}
// Re-open for next iteration (except last one)
+6 -6
View File
@@ -640,7 +640,7 @@ func TestCloseIssue(t *testing.T) {
t.Fatalf("CreateIssue failed: %v", err)
}
err = store.CloseIssue(ctx, issue.ID, "Done", "test-user")
err = store.CloseIssue(ctx, issue.ID, "Done", "test-user", "")
if err != nil {
t.Fatalf("CloseIssue failed: %v", err)
}
@@ -717,7 +717,7 @@ func TestClosedAtInvariant(t *testing.T) {
}
// Close the issue
err = store.CloseIssue(ctx, issue.ID, "Done", "test-user")
err = store.CloseIssue(ctx, issue.ID, "Done", "test-user", "")
if err != nil {
t.Fatalf("CloseIssue failed: %v", err)
}
@@ -812,7 +812,7 @@ func TestSearchIssues(t *testing.T) {
}
// Close the third issue
if issue.Title == "Another bug" {
err = store.CloseIssue(ctx, issue.ID, "Done", "test-user")
err = store.CloseIssue(ctx, issue.ID, "Done", "test-user", "")
if err != nil {
t.Fatalf("CloseIssue failed: %v", err)
}
@@ -977,7 +977,7 @@ func TestGetStatistics(t *testing.T) {
}
// Close the one that should be closed
if issue.Title == "Closed task" {
err = store.CloseIssue(ctx, issue.ID, "Done", "test-user")
err = store.CloseIssue(ctx, issue.ID, "Done", "test-user", "")
if err != nil {
t.Fatalf("CloseIssue failed: %v", err)
}
@@ -1532,7 +1532,7 @@ func TestConvoyReactiveCompletion(t *testing.T) {
}
// Close first issue - convoy should still be open
err = store.CloseIssue(ctx, issue1.ID, "Done", "test-user")
err = store.CloseIssue(ctx, issue1.ID, "Done", "test-user", "")
if err != nil {
t.Fatalf("CloseIssue issue1 failed: %v", err)
}
@@ -1546,7 +1546,7 @@ func TestConvoyReactiveCompletion(t *testing.T) {
}
// Close second issue - convoy should auto-close now
err = store.CloseIssue(ctx, issue2.ID, "Done", "test-user")
err = store.CloseIssue(ctx, issue2.ID, "Done", "test-user", "")
if err != nil {
t.Fatalf("CloseIssue issue2 failed: %v", err)
}
+1 -1
View File
@@ -105,7 +105,7 @@ func (e *testEnv) AddParentChild(child, parent *types.Issue) {
// 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 {
if err := e.Store.CloseIssue(e.Ctx, issue.ID, reason, "test-user", ""); err != nil {
e.t.Fatalf("CloseIssue(%s) failed: %v", issue.ID, err)
}
}
+1 -1
View File
@@ -72,7 +72,7 @@ func TestCreateTombstone(t *testing.T) {
}
// Close the issue to set closed_at
if err := store.CloseIssue(ctx, "bd-closed-1", "closing for test", "tester"); err != nil {
if err := store.CloseIssue(ctx, "bd-closed-1", "closing for test", "tester", ""); err != nil {
t.Fatalf("Failed to close issue: %v", err)
}
+4 -3
View File
@@ -523,13 +523,14 @@ func applyUpdatesToIssue(issue *types.Issue, updates map[string]interface{}) {
// CloseIssue closes an issue within the transaction.
// NOTE: close_reason is stored in both issues table and events table - see SQLiteStorage.CloseIssue.
func (t *sqliteTxStorage) CloseIssue(ctx context.Context, id string, reason string, actor string) error {
// The session parameter tracks which Claude Code session closed the issue (can be empty).
func (t *sqliteTxStorage) CloseIssue(ctx context.Context, id string, reason string, actor string, session string) error {
now := time.Now()
result, err := t.conn.ExecContext(ctx, `
UPDATE issues SET status = ?, closed_at = ?, updated_at = ?, close_reason = ?
UPDATE issues SET status = ?, closed_at = ?, updated_at = ?, close_reason = ?, closed_by_session = ?
WHERE id = ?
`, types.StatusClosed, now, now, reason, id)
`, types.StatusClosed, now, now, reason, session, id)
if err != nil {
return fmt.Errorf("failed to close issue: %w", err)
}
+1 -1
View File
@@ -254,7 +254,7 @@ func TestTransactionCloseIssue(t *testing.T) {
// Close in transaction
err := store.RunInTransaction(ctx, func(tx storage.Transaction) error {
return tx.CloseIssue(ctx, issue.ID, "Done", "test-actor")
return tx.CloseIssue(ctx, issue.ID, "Done", "test-actor", "")
})
if err != nil {
+2 -2
View File
@@ -50,7 +50,7 @@ type Transaction interface {
CreateIssue(ctx context.Context, issue *types.Issue, actor string) error
CreateIssues(ctx context.Context, issues []*types.Issue, actor string) error
UpdateIssue(ctx context.Context, id string, updates map[string]interface{}, actor string) error
CloseIssue(ctx context.Context, id string, reason string, actor string) error
CloseIssue(ctx context.Context, id string, reason string, actor string, session string) error
DeleteIssue(ctx context.Context, id string) error
GetIssue(ctx context.Context, id string) (*types.Issue, error) // For read-your-writes within transaction
SearchIssues(ctx context.Context, query string, filter types.IssueFilter) ([]*types.Issue, error) // For read-your-writes within transaction
@@ -83,7 +83,7 @@ type Storage interface {
GetIssue(ctx context.Context, id string) (*types.Issue, error)
GetIssueByExternalRef(ctx context.Context, externalRef string) (*types.Issue, error)
UpdateIssue(ctx context.Context, id string, updates map[string]interface{}, actor string) error
CloseIssue(ctx context.Context, id string, reason string, actor string) error
CloseIssue(ctx context.Context, id string, reason string, actor string, session string) error
DeleteIssue(ctx context.Context, id string) error
SearchIssues(ctx context.Context, query string, filter types.IssueFilter) ([]*types.Issue, error)
+2 -2
View File
@@ -35,7 +35,7 @@ func (m *mockStorage) GetIssueByExternalRef(ctx context.Context, externalRef str
func (m *mockStorage) UpdateIssue(ctx context.Context, id string, updates map[string]interface{}, actor string) error {
return nil
}
func (m *mockStorage) CloseIssue(ctx context.Context, id string, reason string, actor string) error {
func (m *mockStorage) CloseIssue(ctx context.Context, id string, reason string, actor string, session string) error {
return nil
}
func (m *mockStorage) DeleteIssue(ctx context.Context, id string) error {
@@ -213,7 +213,7 @@ func (m *mockTransaction) CreateIssues(ctx context.Context, issues []*types.Issu
func (m *mockTransaction) UpdateIssue(ctx context.Context, id string, updates map[string]interface{}, actor string) error {
return nil
}
func (m *mockTransaction) CloseIssue(ctx context.Context, id string, reason string, actor string) error {
func (m *mockTransaction) CloseIssue(ctx context.Context, id string, reason string, actor string, session string) error {
return nil
}
func (m *mockTransaction) DeleteIssue(ctx context.Context, id string) error {