feat(activity): add --details/-d flag for full issue information (#1317)
* feat(activity): add --details/-d flag for full issue information Add a new --details (-d) flag to the `bd activity` command that includes full issue information in the output, including comments. * style(activity): simplify --details text output format Remove ASCII tree characters and use cleaner indentation with blank lines between events for better readability.
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/steveyegge/beads/internal/routing"
|
"github.com/steveyegge/beads/internal/routing"
|
||||||
"github.com/steveyegge/beads/internal/rpc"
|
"github.com/steveyegge/beads/internal/rpc"
|
||||||
|
"github.com/steveyegge/beads/internal/types"
|
||||||
"github.com/steveyegge/beads/internal/ui"
|
"github.com/steveyegge/beads/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ var (
|
|||||||
activityLimit int
|
activityLimit int
|
||||||
activityInterval time.Duration
|
activityInterval time.Duration
|
||||||
activityTown bool
|
activityTown bool
|
||||||
|
activityDetails bool
|
||||||
)
|
)
|
||||||
|
|
||||||
// ActivityEvent represents a formatted activity event for output
|
// ActivityEvent represents a formatted activity event for output
|
||||||
@@ -40,6 +42,8 @@ type ActivityEvent struct {
|
|||||||
ParentID string `json:"parent_id,omitempty"`
|
ParentID string `json:"parent_id,omitempty"`
|
||||||
StepCount int `json:"step_count,omitempty"`
|
StepCount int `json:"step_count,omitempty"`
|
||||||
Actor string `json:"actor,omitempty"`
|
Actor string `json:"actor,omitempty"`
|
||||||
|
// Full issue details (populated when --details is used)
|
||||||
|
Issue *types.IssueDetails `json:"issue,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var activityCmd = &cobra.Command{
|
var activityCmd = &cobra.Command{
|
||||||
@@ -67,7 +71,8 @@ Examples:
|
|||||||
bd activity --type update # Only show updates
|
bd activity --type update # Only show updates
|
||||||
bd activity --limit 50 # Show last 50 events
|
bd activity --limit 50 # Show last 50 events
|
||||||
bd activity --town # Aggregated feed from all rigs
|
bd activity --town # Aggregated feed from all rigs
|
||||||
bd activity --follow --town # Stream all rig activity`,
|
bd activity --follow --town # Stream all rig activity
|
||||||
|
bd activity --details --json # Full issue details in JSON output`,
|
||||||
Run: runActivity,
|
Run: runActivity,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +84,7 @@ func init() {
|
|||||||
activityCmd.Flags().IntVar(&activityLimit, "limit", 100, "Maximum number of events to show")
|
activityCmd.Flags().IntVar(&activityLimit, "limit", 100, "Maximum number of events to show")
|
||||||
activityCmd.Flags().DurationVar(&activityInterval, "interval", 500*time.Millisecond, "Polling interval for --follow mode")
|
activityCmd.Flags().DurationVar(&activityInterval, "interval", 500*time.Millisecond, "Polling interval for --follow mode")
|
||||||
activityCmd.Flags().BoolVar(&activityTown, "town", false, "Aggregated feed from all rigs (uses routes.jsonl)")
|
activityCmd.Flags().BoolVar(&activityTown, "town", false, "Aggregated feed from all rigs (uses routes.jsonl)")
|
||||||
|
activityCmd.Flags().BoolVarP(&activityDetails, "details", "d", false, "Include full issue details in output (works best with --json)")
|
||||||
|
|
||||||
rootCmd.AddCommand(activityCmd)
|
rootCmd.AddCommand(activityCmd)
|
||||||
}
|
}
|
||||||
@@ -136,7 +142,11 @@ func runActivityOnce(sinceTime time.Time) {
|
|||||||
if jsonOutput {
|
if jsonOutput {
|
||||||
formatted := make([]ActivityEvent, 0, len(events))
|
formatted := make([]ActivityEvent, 0, len(events))
|
||||||
for _, e := range events {
|
for _, e := range events {
|
||||||
formatted = append(formatted, formatEvent(e))
|
var details *types.IssueDetails
|
||||||
|
if activityDetails {
|
||||||
|
details = fetchIssueDetails(daemonClient, e.IssueID)
|
||||||
|
}
|
||||||
|
formatted = append(formatted, formatEventWithDetails(e, details))
|
||||||
}
|
}
|
||||||
outputJSON(formatted)
|
outputJSON(formatted)
|
||||||
return
|
return
|
||||||
@@ -147,8 +157,15 @@ func runActivityOnce(sinceTime time.Time) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For text output with --details, show full issue info
|
||||||
for _, e := range events {
|
for _, e := range events {
|
||||||
printEvent(e)
|
printEvent(e)
|
||||||
|
if activityDetails {
|
||||||
|
details := fetchIssueDetails(daemonClient, e.IssueID)
|
||||||
|
if details != nil {
|
||||||
|
printEventDetails(details)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,10 +189,20 @@ func runActivityFollow(sinceTime time.Time) {
|
|||||||
events = filterEvents(events)
|
events = filterEvents(events)
|
||||||
for _, e := range events {
|
for _, e := range events {
|
||||||
if jsonOutput {
|
if jsonOutput {
|
||||||
data, _ := json.Marshal(formatEvent(e))
|
var details *types.IssueDetails
|
||||||
|
if activityDetails {
|
||||||
|
details = fetchIssueDetails(daemonClient, e.IssueID)
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(formatEventWithDetails(e, details))
|
||||||
fmt.Println(string(data))
|
fmt.Println(string(data))
|
||||||
} else {
|
} else {
|
||||||
printEvent(e)
|
printEvent(e)
|
||||||
|
if activityDetails {
|
||||||
|
details := fetchIssueDetails(daemonClient, e.IssueID)
|
||||||
|
if details != nil {
|
||||||
|
printEventDetails(details)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if e.Timestamp.After(lastPoll) {
|
if e.Timestamp.After(lastPoll) {
|
||||||
lastPoll = e.Timestamp
|
lastPoll = e.Timestamp
|
||||||
@@ -243,10 +270,20 @@ func runActivityFollow(sinceTime time.Time) {
|
|||||||
newEvents = filterEvents(newEvents)
|
newEvents = filterEvents(newEvents)
|
||||||
for _, e := range newEvents {
|
for _, e := range newEvents {
|
||||||
if jsonOutput {
|
if jsonOutput {
|
||||||
data, _ := json.Marshal(formatEvent(e))
|
var details *types.IssueDetails
|
||||||
|
if activityDetails {
|
||||||
|
details = fetchIssueDetails(daemonClient, e.IssueID)
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(formatEventWithDetails(e, details))
|
||||||
fmt.Println(string(data))
|
fmt.Println(string(data))
|
||||||
} else {
|
} else {
|
||||||
printEvent(e)
|
printEvent(e)
|
||||||
|
if activityDetails {
|
||||||
|
details := fetchIssueDetails(daemonClient, e.IssueID)
|
||||||
|
if details != nil {
|
||||||
|
printEventDetails(details)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if e.Timestamp.After(lastPoll) {
|
if e.Timestamp.After(lastPoll) {
|
||||||
lastPoll = e.Timestamp
|
lastPoll = e.Timestamp
|
||||||
@@ -276,6 +313,25 @@ func fetchMutations(since time.Time) ([]rpc.MutationEvent, error) {
|
|||||||
return mutations, nil
|
return mutations, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fetchIssueDetails retrieves full issue details from the daemon
|
||||||
|
func fetchIssueDetails(client *rpc.Client, issueID string) *types.IssueDetails {
|
||||||
|
if client == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Show(&rpc.ShowArgs{ID: issueID})
|
||||||
|
if err != nil || !resp.Success {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var details types.IssueDetails
|
||||||
|
if err := json.Unmarshal(resp.Data, &details); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &details
|
||||||
|
}
|
||||||
|
|
||||||
// filterEvents applies --mol and --type filters
|
// filterEvents applies --mol and --type filters
|
||||||
func filterEvents(events []rpc.MutationEvent) []rpc.MutationEvent {
|
func filterEvents(events []rpc.MutationEvent) []rpc.MutationEvent {
|
||||||
if activityMol == "" && activityType == "" {
|
if activityMol == "" && activityType == "" {
|
||||||
@@ -299,6 +355,11 @@ func filterEvents(events []rpc.MutationEvent) []rpc.MutationEvent {
|
|||||||
|
|
||||||
// formatEvent converts a mutation event to a formatted activity event
|
// formatEvent converts a mutation event to a formatted activity event
|
||||||
func formatEvent(e rpc.MutationEvent) ActivityEvent {
|
func formatEvent(e rpc.MutationEvent) ActivityEvent {
|
||||||
|
return formatEventWithDetails(e, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatEventWithDetails converts a mutation event to a formatted activity event with optional details
|
||||||
|
func formatEventWithDetails(e rpc.MutationEvent, details *types.IssueDetails) ActivityEvent {
|
||||||
symbol, message := getEventDisplay(e)
|
symbol, message := getEventDisplay(e)
|
||||||
return ActivityEvent{
|
return ActivityEvent{
|
||||||
Timestamp: e.Timestamp,
|
Timestamp: e.Timestamp,
|
||||||
@@ -311,6 +372,7 @@ func formatEvent(e rpc.MutationEvent) ActivityEvent {
|
|||||||
ParentID: e.ParentID,
|
ParentID: e.ParentID,
|
||||||
StepCount: e.StepCount,
|
StepCount: e.StepCount,
|
||||||
Actor: e.Actor,
|
Actor: e.Actor,
|
||||||
|
Issue: details,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,6 +483,48 @@ func printEvent(e rpc.MutationEvent) {
|
|||||||
fmt.Printf("[%s] %s %s\n", timestamp, coloredSymbol, message)
|
fmt.Printf("[%s] %s %s\n", timestamp, coloredSymbol, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// printEventDetails prints full issue details for text output with --details
|
||||||
|
func printEventDetails(details *types.IssueDetails) {
|
||||||
|
fmt.Printf("Status: %s Priority: P%d Type: %s\n",
|
||||||
|
details.Status, details.Priority, details.IssueType)
|
||||||
|
if details.Assignee != "" {
|
||||||
|
fmt.Printf("Assignee: %s\n", details.Assignee)
|
||||||
|
}
|
||||||
|
if len(details.Labels) > 0 {
|
||||||
|
fmt.Printf("Labels: %s\n", strings.Join(details.Labels, ", "))
|
||||||
|
}
|
||||||
|
if details.Description != "" {
|
||||||
|
desc := truncateString(details.Description, 80)
|
||||||
|
desc = strings.ReplaceAll(desc, "\n", " ")
|
||||||
|
fmt.Printf("Description: %s\n", desc)
|
||||||
|
}
|
||||||
|
if len(details.Dependencies) > 0 {
|
||||||
|
deps := make([]string, 0, len(details.Dependencies))
|
||||||
|
for _, d := range details.Dependencies {
|
||||||
|
deps = append(deps, d.ID)
|
||||||
|
}
|
||||||
|
fmt.Printf("Depends on: %s\n", strings.Join(deps, ", "))
|
||||||
|
}
|
||||||
|
if len(details.Dependents) > 0 {
|
||||||
|
dependents := make([]string, 0, len(details.Dependents))
|
||||||
|
for _, d := range details.Dependents {
|
||||||
|
dependents = append(dependents, d.ID)
|
||||||
|
}
|
||||||
|
fmt.Printf("Blocked by: %s\n", strings.Join(dependents, ", "))
|
||||||
|
}
|
||||||
|
if len(details.Comments) > 0 {
|
||||||
|
fmt.Printf("Comments: %d\n", len(details.Comments))
|
||||||
|
for _, c := range details.Comments {
|
||||||
|
text := truncateString(c.Text, 60)
|
||||||
|
text = strings.ReplaceAll(text, "\n", " ")
|
||||||
|
fmt.Printf(" @%s: %s\n", c.Author, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("Created: %s Updated: %s\n",
|
||||||
|
details.CreatedAt.Format("2006-01-02 15:04"), details.UpdatedAt.Format("2006-01-02 15:04"))
|
||||||
|
fmt.Println() // Blank line between events
|
||||||
|
}
|
||||||
|
|
||||||
// parseDurationString parses duration strings like "5m", "1h", "30s", "2d"
|
// parseDurationString parses duration strings like "5m", "1h", "30s", "2d"
|
||||||
func parseDurationString(s string) (time.Duration, error) {
|
func parseDurationString(s string) (time.Duration, error) {
|
||||||
// Try standard Go duration first
|
// Try standard Go duration first
|
||||||
@@ -545,6 +649,16 @@ func fetchTownMutations(daemons []rigDaemon, since time.Time) []rpc.MutationEven
|
|||||||
return events
|
return events
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findDaemonForIssue finds the appropriate daemon client for an issue ID based on prefix
|
||||||
|
func findDaemonForIssue(daemons []rigDaemon, issueID string) *rpc.Client {
|
||||||
|
for _, d := range daemons {
|
||||||
|
if d.client != nil && strings.HasPrefix(issueID, d.prefix) {
|
||||||
|
return d.client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// fetchTownMutationsWithStatus retrieves mutations and returns count of responding daemons
|
// fetchTownMutationsWithStatus retrieves mutations and returns count of responding daemons
|
||||||
func fetchTownMutationsWithStatus(daemons []rigDaemon, since time.Time) ([]rpc.MutationEvent, int) {
|
func fetchTownMutationsWithStatus(daemons []rigDaemon, since time.Time) ([]rpc.MutationEvent, int) {
|
||||||
var allEvents []rpc.MutationEvent
|
var allEvents []rpc.MutationEvent
|
||||||
@@ -613,7 +727,12 @@ func runTownActivityOnce(sinceTime time.Time) {
|
|||||||
if jsonOutput {
|
if jsonOutput {
|
||||||
formatted := make([]ActivityEvent, 0, len(events))
|
formatted := make([]ActivityEvent, 0, len(events))
|
||||||
for _, e := range events {
|
for _, e := range events {
|
||||||
formatted = append(formatted, formatEvent(e))
|
var details *types.IssueDetails
|
||||||
|
if activityDetails {
|
||||||
|
client := findDaemonForIssue(daemons, e.IssueID)
|
||||||
|
details = fetchIssueDetails(client, e.IssueID)
|
||||||
|
}
|
||||||
|
formatted = append(formatted, formatEventWithDetails(e, details))
|
||||||
}
|
}
|
||||||
outputJSON(formatted)
|
outputJSON(formatted)
|
||||||
return
|
return
|
||||||
@@ -626,6 +745,13 @@ func runTownActivityOnce(sinceTime time.Time) {
|
|||||||
|
|
||||||
for _, e := range events {
|
for _, e := range events {
|
||||||
printEvent(e)
|
printEvent(e)
|
||||||
|
if activityDetails {
|
||||||
|
client := findDaemonForIssue(daemons, e.IssueID)
|
||||||
|
details := fetchIssueDetails(client, e.IssueID)
|
||||||
|
if details != nil {
|
||||||
|
printEventDetails(details)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,10 +793,22 @@ func runTownActivityFollow(sinceTime time.Time) {
|
|||||||
|
|
||||||
for _, e := range events {
|
for _, e := range events {
|
||||||
if jsonOutput {
|
if jsonOutput {
|
||||||
data, _ := json.Marshal(formatEvent(e))
|
var details *types.IssueDetails
|
||||||
|
if activityDetails {
|
||||||
|
client := findDaemonForIssue(daemons, e.IssueID)
|
||||||
|
details = fetchIssueDetails(client, e.IssueID)
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(formatEventWithDetails(e, details))
|
||||||
fmt.Println(string(data))
|
fmt.Println(string(data))
|
||||||
} else {
|
} else {
|
||||||
printEvent(e)
|
printEvent(e)
|
||||||
|
if activityDetails {
|
||||||
|
client := findDaemonForIssue(daemons, e.IssueID)
|
||||||
|
details := fetchIssueDetails(client, e.IssueID)
|
||||||
|
if details != nil {
|
||||||
|
printEventDetails(details)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if e.Timestamp.After(lastPoll) {
|
if e.Timestamp.After(lastPoll) {
|
||||||
lastPoll = e.Timestamp
|
lastPoll = e.Timestamp
|
||||||
@@ -722,10 +860,22 @@ func runTownActivityFollow(sinceTime time.Time) {
|
|||||||
|
|
||||||
for _, e := range newEvents {
|
for _, e := range newEvents {
|
||||||
if jsonOutput {
|
if jsonOutput {
|
||||||
data, _ := json.Marshal(formatEvent(e))
|
var details *types.IssueDetails
|
||||||
|
if activityDetails {
|
||||||
|
client := findDaemonForIssue(daemons, e.IssueID)
|
||||||
|
details = fetchIssueDetails(client, e.IssueID)
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(formatEventWithDetails(e, details))
|
||||||
fmt.Println(string(data))
|
fmt.Println(string(data))
|
||||||
} else {
|
} else {
|
||||||
printEvent(e)
|
printEvent(e)
|
||||||
|
if activityDetails {
|
||||||
|
client := findDaemonForIssue(daemons, e.IssueID)
|
||||||
|
details := fetchIssueDetails(client, e.IssueID)
|
||||||
|
if details != nil {
|
||||||
|
printEventDetails(details)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if e.Timestamp.After(lastPoll) {
|
if e.Timestamp.After(lastPoll) {
|
||||||
lastPoll = e.Timestamp
|
lastPoll = e.Timestamp
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveyegge/beads/internal/rpc"
|
"github.com/steveyegge/beads/internal/rpc"
|
||||||
|
"github.com/steveyegge/beads/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestParseDurationString tests the duration parsing function
|
// TestParseDurationString tests the duration parsing function
|
||||||
@@ -303,3 +304,150 @@ func TestFormatEvent(t *testing.T) {
|
|||||||
t.Error("expected non-empty message")
|
t.Error("expected non-empty message")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestFormatEventWithDetails tests that issue details are included when provided
|
||||||
|
func TestFormatEventWithDetails(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
event := rpc.MutationEvent{
|
||||||
|
Type: rpc.MutationCreate,
|
||||||
|
IssueID: "bd-test",
|
||||||
|
Timestamp: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("without details", func(t *testing.T) {
|
||||||
|
result := formatEventWithDetails(event, nil)
|
||||||
|
if result.Issue != nil {
|
||||||
|
t.Error("expected nil issue when no details provided")
|
||||||
|
}
|
||||||
|
if result.IssueID != "bd-test" {
|
||||||
|
t.Errorf("expected issue ID bd-test, got %s", result.IssueID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("with details", func(t *testing.T) {
|
||||||
|
details := &types.IssueDetails{
|
||||||
|
Issue: types.Issue{
|
||||||
|
ID: "bd-test",
|
||||||
|
Title: "Test Issue",
|
||||||
|
Description: "A test description",
|
||||||
|
Status: types.StatusOpen,
|
||||||
|
Priority: 2,
|
||||||
|
IssueType: types.TypeTask,
|
||||||
|
Assignee: "testuser",
|
||||||
|
},
|
||||||
|
Labels: []string{"bug", "urgent"},
|
||||||
|
Dependencies: nil,
|
||||||
|
Dependents: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := formatEventWithDetails(event, details)
|
||||||
|
|
||||||
|
if result.Issue == nil {
|
||||||
|
t.Fatal("expected issue details to be included")
|
||||||
|
}
|
||||||
|
if result.Issue.ID != "bd-test" {
|
||||||
|
t.Errorf("expected issue ID bd-test, got %s", result.Issue.ID)
|
||||||
|
}
|
||||||
|
if result.Issue.Title != "Test Issue" {
|
||||||
|
t.Errorf("expected title 'Test Issue', got %s", result.Issue.Title)
|
||||||
|
}
|
||||||
|
if result.Issue.Assignee != "testuser" {
|
||||||
|
t.Errorf("expected assignee 'testuser', got %s", result.Issue.Assignee)
|
||||||
|
}
|
||||||
|
if len(result.Issue.Labels) != 2 {
|
||||||
|
t.Errorf("expected 2 labels, got %d", len(result.Issue.Labels))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFindDaemonForIssue tests daemon lookup by issue ID prefix
|
||||||
|
func TestFindDaemonForIssue(t *testing.T) {
|
||||||
|
// Create mock daemons (without actual connections)
|
||||||
|
daemons := []rigDaemon{
|
||||||
|
{prefix: "bd-", rig: "beads", client: nil}, // No client
|
||||||
|
{prefix: "app-", rig: "app", client: nil}, // No client
|
||||||
|
{prefix: "test-", rig: "test", client: nil}, // No client
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("no matching prefix returns nil", func(t *testing.T) {
|
||||||
|
result := findDaemonForIssue(daemons, "other-123")
|
||||||
|
if result != nil {
|
||||||
|
t.Error("expected nil for non-matching prefix")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("matching prefix but nil client returns nil", func(t *testing.T) {
|
||||||
|
result := findDaemonForIssue(daemons, "bd-abc123")
|
||||||
|
if result != nil {
|
||||||
|
t.Error("expected nil when daemon client is nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty daemons list returns nil", func(t *testing.T) {
|
||||||
|
result := findDaemonForIssue([]rigDaemon{}, "bd-abc")
|
||||||
|
if result != nil {
|
||||||
|
t.Error("expected nil for empty daemon list")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFetchIssueDetails tests the issue details fetching function
|
||||||
|
func TestFetchIssueDetails(t *testing.T) {
|
||||||
|
t.Run("nil client returns nil", func(t *testing.T) {
|
||||||
|
result := fetchIssueDetails(nil, "bd-123")
|
||||||
|
if result != nil {
|
||||||
|
t.Error("expected nil when client is nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFormatEventWithDetailsIncludesComments verifies comments are preserved in details
|
||||||
|
func TestFormatEventWithDetailsIncludesComments(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
event := rpc.MutationEvent{
|
||||||
|
Type: rpc.MutationUpdate,
|
||||||
|
IssueID: "bd-test",
|
||||||
|
Timestamp: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
details := &types.IssueDetails{
|
||||||
|
Issue: types.Issue{
|
||||||
|
ID: "bd-test",
|
||||||
|
Title: "Test Issue",
|
||||||
|
Status: types.StatusInProgress,
|
||||||
|
},
|
||||||
|
Comments: []*types.Comment{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
IssueID: "bd-test",
|
||||||
|
Author: "alice",
|
||||||
|
Text: "First comment with full text that should not be truncated in JSON",
|
||||||
|
CreatedAt: now.Add(-time.Hour),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
IssueID: "bd-test",
|
||||||
|
Author: "bob",
|
||||||
|
Text: "Second comment",
|
||||||
|
CreatedAt: now.Add(-30 * time.Minute),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := formatEventWithDetails(event, details)
|
||||||
|
|
||||||
|
if result.Issue == nil {
|
||||||
|
t.Fatal("expected issue details to be included")
|
||||||
|
}
|
||||||
|
if len(result.Issue.Comments) != 2 {
|
||||||
|
t.Errorf("expected 2 comments, got %d", len(result.Issue.Comments))
|
||||||
|
}
|
||||||
|
if result.Issue.Comments[0].Author != "alice" {
|
||||||
|
t.Errorf("expected first comment author 'alice', got %s", result.Issue.Comments[0].Author)
|
||||||
|
}
|
||||||
|
// Verify full text is preserved (not truncated) for JSON output
|
||||||
|
expectedText := "First comment with full text that should not be truncated in JSON"
|
||||||
|
if result.Issue.Comments[0].Text != expectedText {
|
||||||
|
t.Errorf("expected full comment text preserved, got %s", result.Issue.Comments[0].Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user