From 89bb8df50560e8cb1a845facfd3137c43b060a6b Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 20 Feb 2026 15:44:02 -0800 Subject: [PATCH] Show parsed ticket IDs in /list output --- internal/integrations/slack/slack.go | 43 +++++++++++++++- .../integrations/slack/slack_logic_test.go | 50 +++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/internal/integrations/slack/slack.go b/internal/integrations/slack/slack.go index 0e303fe..9f26f34 100644 --- a/internal/integrations/slack/slack.go +++ b/internal/integrations/slack/slack.go @@ -708,8 +708,9 @@ func renderListItems(api *slack.Client, db *sql.DB, cfg Config, channelID, userI if item.Category != "" { category = fmt.Sprintf(" _%s_", item.Category) } + description := formatItemDescriptionForList(item) text := fmt.Sprintf("*%s*: %s (%s)%s%s", - item.Author, item.Description, item.Status, source, category) + item.Author, description, item.Status, source, category) if canManageItem(item, isManager, user) { editOpt := slack.NewOptionBlockObject( fmt.Sprintf("edit:%d", item.ID), @@ -1337,6 +1338,46 @@ func itemInRange(item WorkItem, from, to time.Time) bool { return !item.ReportedAt.Before(from) && item.ReportedAt.Before(to) } +func formatItemDescriptionForList(item WorkItem) string { + description := strings.TrimSpace(item.Description) + tickets := canonicalTicketList(item.TicketIDs) + if tickets == "" { + return description + } + if leading, ok := leadingTicketPrefix(description); ok && canonicalTicketList(leading) == tickets { + return description + } + return fmt.Sprintf("[%s] %s", tickets, description) +} + +func leadingTicketPrefix(description string) (string, bool) { + description = strings.TrimSpace(description) + if !strings.HasPrefix(description, "[") { + return "", false + } + end := strings.Index(description, "]") + if end <= 1 { + return "", false + } + return description[1:end], true +} + +func canonicalTicketList(ticketIDs string) string { + if strings.TrimSpace(ticketIDs) == "" { + return "" + } + parts := strings.Split(ticketIDs, ",") + cleaned := make([]string, 0, len(parts)) + for _, part := range parts { + p := strings.TrimSpace(part) + if p == "" { + continue + } + cleaned = append(cleaned, p) + } + return strings.Join(cleaned, ",") +} + func canManageItem(item WorkItem, isManager bool, user *slack.User) bool { if isManager { return true diff --git a/internal/integrations/slack/slack_logic_test.go b/internal/integrations/slack/slack_logic_test.go index d7d1f9e..f468b88 100644 --- a/internal/integrations/slack/slack_logic_test.go +++ b/internal/integrations/slack/slack_logic_test.go @@ -97,6 +97,56 @@ func TestMapMRStatusAndReportedAt(t *testing.T) { } } +func TestFormatItemDescriptionForList(t *testing.T) { + tests := []struct { + name string + item WorkItem + want string + }{ + { + name: "prepend single ticket id", + item: WorkItem{ + Description: "Tune query timeout for widget pipeline", + TicketIDs: "7003001", + }, + want: "[7003001] Tune query timeout for widget pipeline", + }, + { + name: "prepend comma list and trim spaces", + item: WorkItem{ + Description: "Improve cache warm-up sequence", + TicketIDs: "7003002, 7003003", + }, + want: "[7003002,7003003] Improve cache warm-up sequence", + }, + { + name: "no duplicate when description already has same prefix", + item: WorkItem{ + Description: "[7003004] Add schema validation guard", + TicketIDs: "7003004", + }, + want: "[7003004] Add schema validation guard", + }, + { + name: "no ticket ids leaves description unchanged", + item: WorkItem{ + Description: "Cleanup stale deployment artifacts", + TicketIDs: "", + }, + want: "Cleanup stale deployment artifacts", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := formatItemDescriptionForList(tt.item) + if got != tt.want { + t.Fatalf("formatItemDescriptionForList() = %q, want %q", got, tt.want) + } + }) + } +} + func TestDeriveBossReportFromTeamReport_FileExists(t *testing.T) { dir := t.TempDir() teamName := "TestTeam"