diff --git a/cmd/bd/cli_fast_test.go b/cmd/bd/cli_fast_test.go index 5a64a930..560ff771 100644 --- a/cmd/bd/cli_fast_test.go +++ b/cmd/bd/cli_fast_test.go @@ -312,6 +312,94 @@ func TestCLI_UpdateLabels(t *testing.T) { } } +func TestCLI_UpdateEphemeral(t *testing.T) { + if testing.Short() { + t.Skip("skipping slow CLI test in short mode") + } + // Note: Not using t.Parallel() because inProcessMutex serializes execution anyway + tmpDir := setupCLITestDB(t) + out := runBDInProcess(t, tmpDir, "create", "Issue for ephemeral testing", "-p", "2", "--json") + + var issue map[string]interface{} + if err := json.Unmarshal([]byte(out), &issue); err != nil { + t.Fatalf("Failed to parse create output: %v", err) + } + id := issue["id"].(string) + + // Mark as ephemeral + runBDInProcess(t, tmpDir, "update", id, "--ephemeral") + + out = runBDInProcess(t, tmpDir, "show", id, "--json") + var updated []map[string]interface{} + if err := json.Unmarshal([]byte(out), &updated); err != nil { + t.Fatalf("Failed to parse show output: %v", err) + } + if updated[0]["ephemeral"] != true { + t.Errorf("Expected ephemeral to be true after --ephemeral, got: %v", updated[0]["ephemeral"]) + } +} + +func TestCLI_UpdatePersistent(t *testing.T) { + if testing.Short() { + t.Skip("skipping slow CLI test in short mode") + } + // Note: Not using t.Parallel() because inProcessMutex serializes execution anyway + tmpDir := setupCLITestDB(t) + + // Create ephemeral issue directly + out := runBDInProcess(t, tmpDir, "create", "Ephemeral issue", "-p", "2", "--ephemeral", "--json") + + var issue map[string]interface{} + if err := json.Unmarshal([]byte(out), &issue); err != nil { + t.Fatalf("Failed to parse create output: %v", err) + } + id := issue["id"].(string) + + // Verify it's ephemeral + out = runBDInProcess(t, tmpDir, "show", id, "--json") + var initial []map[string]interface{} + if err := json.Unmarshal([]byte(out), &initial); err != nil { + t.Fatalf("Failed to parse show output: %v", err) + } + if initial[0]["ephemeral"] != true { + t.Fatalf("Expected issue to be ephemeral initially, got: %v", initial[0]["ephemeral"]) + } + + // Promote to persistent + runBDInProcess(t, tmpDir, "update", id, "--persistent") + + out = runBDInProcess(t, tmpDir, "show", id, "--json") + var updated []map[string]interface{} + if err := json.Unmarshal([]byte(out), &updated); err != nil { + t.Fatalf("Failed to parse show output after persistent: %v", err) + } + if updated[0]["ephemeral"] == true { + t.Errorf("Expected ephemeral to be false after --persistent, got: %v", updated[0]["ephemeral"]) + } +} + +func TestCLI_UpdateEphemeralMutualExclusion(t *testing.T) { + if testing.Short() { + t.Skip("skipping slow CLI test in short mode") + } + // Note: Not using t.Parallel() because inProcessMutex serializes execution anyway + tmpDir := setupCLITestDB(t) + out := runBDInProcess(t, tmpDir, "create", "Issue for mutual exclusion test", "-p", "2", "--json") + + var issue map[string]interface{} + json.Unmarshal([]byte(out), &issue) + id := issue["id"].(string) + + // Both flags should error + _, stderr, err := runBDInProcessAllowError(t, tmpDir, "update", id, "--ephemeral", "--persistent") + if err == nil { + t.Errorf("Expected error when both flags specified, got none") + } + if !strings.Contains(stderr, "cannot specify both") { + t.Errorf("Expected mutual exclusion error message, got: %v", stderr) + } +} + func TestCLI_Close(t *testing.T) { if testing.Short() { t.Skip("skipping slow CLI test in short mode") diff --git a/cmd/bd/update.go b/cmd/bd/update.go index 117ddbc4..53606559 100644 --- a/cmd/bd/update.go +++ b/cmd/bd/update.go @@ -165,6 +165,19 @@ create, update, show, or close operation).`, updates["defer_until"] = t } } + // Ephemeral/persistent flags + // Note: storage layer uses "wisp" field name, maps to "ephemeral" column + ephemeralChanged := cmd.Flags().Changed("ephemeral") + persistentChanged := cmd.Flags().Changed("persistent") + if ephemeralChanged && persistentChanged { + FatalErrorRespectJSON("cannot specify both --ephemeral and --persistent flags") + } + if ephemeralChanged { + updates["wisp"] = true + } + if persistentChanged { + updates["wisp"] = false + } // Get claim flag claimFlag, _ := cmd.Flags().GetBool("claim") @@ -278,6 +291,10 @@ create, update, show, or close operation).`, empty := "" updateArgs.DeferUntil = &empty } + // Ephemeral/persistent + if wisp, ok := updates["wisp"].(bool); ok { + updateArgs.Ephemeral = &wisp + } // Set claim flag for atomic claim operation updateArgs.Claim = claimFlag @@ -613,6 +630,9 @@ func init() { updateCmd.Flags().String("defer", "", "Defer until date (empty to clear). Issue hidden from bd ready until then") // Gate fields (bd-z6kw) updateCmd.Flags().String("await-id", "", "Set gate await_id (e.g., GitHub run ID for gh:run gates)") + // Ephemeral/persistent flags + updateCmd.Flags().Bool("ephemeral", false, "Mark issue as ephemeral (wisp) - not exported to JSONL") + updateCmd.Flags().Bool("persistent", false, "Mark issue as persistent (promote wisp to regular issue)") updateCmd.ValidArgsFunction = issueIDCompletion rootCmd.AddCommand(updateCmd) } diff --git a/internal/rpc/server_issues_epics.go b/internal/rpc/server_issues_epics.go index 05fffd80..25ee8ed3 100644 --- a/internal/rpc/server_issues_epics.go +++ b/internal/rpc/server_issues_epics.go @@ -92,7 +92,7 @@ func updatesFromArgs(a UpdateArgs) (map[string]interface{}, error) { u["sender"] = *a.Sender } if a.Ephemeral != nil { - u["ephemeral"] = *a.Ephemeral + u["wisp"] = *a.Ephemeral // Storage API uses "wisp", maps to "ephemeral" column } if a.RepliesTo != nil { u["replies_to"] = *a.RepliesTo