From 089dfe10300f0cc160bf4000839de78244351dc7 Mon Sep 17 00:00:00 2001 From: Thomas Stromberg Date: Sun, 19 Oct 2025 17:19:49 +0200 Subject: [PATCH] cleanup, lint --- .golangci.yml | 26 ++-- Makefile | 24 +++- go.mod | 6 +- go.sum | 6 + main.go | 390 ++++++++++++++++++++++++++------------------------ main_test.go | 52 ++++--- 6 files changed, 270 insertions(+), 234 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 6a2d4ea..9346056 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -152,10 +152,10 @@ linters: nakedret: # Default: 30 - max-func-lines: 4 + max-func-lines: 7 nestif: - min-complexity: 12 + min-complexity: 15 nolintlint: # Exclude following linters from requiring an explanation. @@ -171,17 +171,13 @@ linters: rules: - name: add-constant severity: warning - disabled: false - exclude: [""] - arguments: - - max-lit-count: "5" - allow-strs: '"","\n"' - allow-ints: "0,1,2,3,24,30,60,100,365,0o600,0o700,0o750,0o755" - allow-floats: "0.0,0.,1.0,1.,2.0,2." + disabled: true + - name: argument-limit + arguments: [9] - name: cognitive-complexity - arguments: [55] + disabled: true # prefer maintidx - name: cyclomatic - arguments: [60] + disabled: true # prefer maintidx - name: function-length arguments: [150, 225] - name: line-length-limit @@ -213,8 +209,14 @@ linters: os-temp-dir: true varnamelen: - max-distance: 40 + max-distance: 75 min-name-length: 2 + check-receivers: false + ignore-names: + - r + - w + - f + - err exclusions: # Default: [] diff --git a/Makefile b/Makefile index 6698ccd..b090010 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,3 @@ - -# BEGIN: lint-install . -# http://github.com/codeGROOVE-dev/lint-install - .PHONY: lint test build test: go test -race ./... @@ -10,6 +6,10 @@ build: mkdir -p out go build -o out/prs . +# BEGIN: lint-install . +# http://github.com/codeGROOVE-dev/lint-install + +.PHONY: lint lint: _lint LINT_ARCH := $(shell uname -m) @@ -28,7 +28,7 @@ LINTERS := FIXERS := GOLANGCI_LINT_CONFIG := $(LINT_ROOT)/.golangci.yml -GOLANGCI_LINT_VERSION ?= v2.4.0 +GOLANGCI_LINT_VERSION ?= v2.5.0 GOLANGCI_LINT_BIN := $(LINT_ROOT)/out/linters/golangci-lint-$(GOLANGCI_LINT_VERSION)-$(LINT_ARCH) $(GOLANGCI_LINT_BIN): mkdir -p $(LINT_ROOT)/out/linters @@ -58,9 +58,19 @@ yamllint-lint: $(YAMLLINT_BIN) PYTHONPATH=$(YAMLLINT_ROOT)/dist $(YAMLLINT_ROOT)/dist/bin/yamllint . .PHONY: _lint $(LINTERS) -_lint: $(LINTERS) +_lint: + @exit_code=0; \ + for target in $(LINTERS); do \ + $(MAKE) $$target || exit_code=1; \ + done; \ + exit $$exit_code .PHONY: fix $(FIXERS) -fix: $(FIXERS) +fix: + @exit_code=0; \ + for target in $(FIXERS); do \ + $(MAKE) $$target || exit_code=1; \ + done; \ + exit $$exit_code # END: lint-install . diff --git a/go.mod b/go.mod index 4736efd..1490a83 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.25.1 require ( github.com/avast/retry-go/v4 v4.6.1 github.com/charmbracelet/lipgloss v1.1.0 - github.com/codeGROOVE-dev/sprinkler v0.0.0-20251001125233-5fa6f0ff4582 - github.com/codeGROOVE-dev/turnclient v0.0.0-20250929203714-61cf2f094fb1 + github.com/codeGROOVE-dev/sprinkler v0.0.0-20251001144140-ed20651ca4e9 + github.com/codeGROOVE-dev/turnclient v0.0.0-20251001151440-a58eb9b17826 golang.org/x/term v0.35.0 ) @@ -17,7 +17,7 @@ require ( github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/clipperhouse/uax29/v2 v2.2.0 // indirect - github.com/codeGROOVE-dev/prx v0.0.0-20250923100916-d2b60be50274 // indirect + github.com/codeGROOVE-dev/prx v0.0.0-20251001143458-17e6b58fb46c // indirect github.com/codeGROOVE-dev/retry v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/go.sum b/go.sum index 3946d85..a89ba8f 100644 --- a/go.sum +++ b/go.sum @@ -16,12 +16,18 @@ github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdoh github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/codeGROOVE-dev/prx v0.0.0-20250923100916-d2b60be50274 h1:9eLzQdOaQEn30279ai3YjNdJOM/efbcYanWC9juAJ+M= github.com/codeGROOVE-dev/prx v0.0.0-20250923100916-d2b60be50274/go.mod h1:7qLbi18baOyS8yO/6/64SBIqtyzSzLFdsDST15NPH3w= +github.com/codeGROOVE-dev/prx v0.0.0-20251001143458-17e6b58fb46c h1:/rrjFoqwFqKNzc1f14vQt6QJ9U5tQ4Uh6U8hgixkSqw= +github.com/codeGROOVE-dev/prx v0.0.0-20251001143458-17e6b58fb46c/go.mod h1:7qLbi18baOyS8yO/6/64SBIqtyzSzLFdsDST15NPH3w= github.com/codeGROOVE-dev/retry v1.2.0 h1:xYpYPX2PQZmdHwuiQAGGzsBm392xIMl4nfMEFApQnu8= github.com/codeGROOVE-dev/retry v1.2.0/go.mod h1:8OgefgV1XP7lzX2PdKlCXILsYKuz6b4ZpHa/20iLi8E= github.com/codeGROOVE-dev/sprinkler v0.0.0-20251001125233-5fa6f0ff4582 h1:IPCaNGRWdyMZKyjnjv+wdSmPmOZtKFD6SVaha5DuCqk= github.com/codeGROOVE-dev/sprinkler v0.0.0-20251001125233-5fa6f0ff4582/go.mod h1:RZ/Te7HkY5upHQlnmf3kV4GHVM0R8AK3U+yPItCZAoQ= +github.com/codeGROOVE-dev/sprinkler v0.0.0-20251001144140-ed20651ca4e9 h1:Nuyy0vMl6YD96N6WwZeqClPa/VlaILJYZoG50ezOAHw= +github.com/codeGROOVE-dev/sprinkler v0.0.0-20251001144140-ed20651ca4e9/go.mod h1:RZ/Te7HkY5upHQlnmf3kV4GHVM0R8AK3U+yPItCZAoQ= github.com/codeGROOVE-dev/turnclient v0.0.0-20250929203714-61cf2f094fb1 h1:lQZoQN9Vo+AzGHGRMAoFewJ07vS24cNIEx2GrL5FX/g= github.com/codeGROOVE-dev/turnclient v0.0.0-20250929203714-61cf2f094fb1/go.mod h1:7lBF4vS6T+D1rNjmJ+CNVrXALQvdwNfBVEy7vhIQtYk= +github.com/codeGROOVE-dev/turnclient v0.0.0-20251001151440-a58eb9b17826 h1:ly6n4spiC6r0IOMl8QfZjv+qUnMHLvo/qErGPVMV3IE= +github.com/codeGROOVE-dev/turnclient v0.0.0-20251001151440-a58eb9b17826/go.mod h1:JXk9gT6Qb496lnTcgpk9h917XaREGa+t6Kvg0YHAQJY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= diff --git a/main.go b/main.go index 504395a..cb71e1e 100644 --- a/main.go +++ b/main.go @@ -326,7 +326,7 @@ func main() { cancel() return } - output := generatePRDisplay(prs, username, *blocked, *verbose, *includeStale, excludedOrgs) + output := generatePRDisplay(prs, username, *blocked, *includeStale, excludedOrgs) if output != "" { fmt.Print(output) } @@ -496,7 +496,18 @@ func fetchPRs(ctx context.Context, query *prQuery, logger *log.Logger, cls *clie enrichPRsParallel(ctx, query.token, prs, logger, cls.http, cls.turn, query.username, query.debug, query.noCache) logger.Printf("INFO: Successfully enriched all %d PRs", len(prs)) - return prs, nil + // Filter out closed/merged PRs (can happen due to GitHub search lag or stale cache) + openPRs := make([]PR, 0, len(prs)) + for i := range prs { + if prs[i].State == "open" { + openPRs = append(openPRs, prs[i]) + } + } + if len(openPRs) != len(prs) { + logger.Printf("Filtered out %d closed/merged PRs", len(prs)-len(openPRs)) + } + + return openPRs, nil } func makeGitHubSearchRequest(ctx context.Context, query, token string, httpClient *http.Client, logger *log.Logger) (*http.Response, error) { @@ -586,23 +597,22 @@ func enrichPRsParallel(ctx context.Context, token string, prs []PR, logger *log. var wg sync.WaitGroup for i := range prs { - wg.Add(1) sem <- struct{}{} // acquire semaphore + pr := &prs[i] - go func(pr *PR, githubToken string) { + wg.Go(func() { defer func() { <-sem // release semaphore - wg.Done() }() // Ignore non-critical errors - let the app continue - if err := enrichPRData(ctx, pr, githubToken, logger, httpClient, turnClient, username, debug, noCache); err != nil { + if err := enrichPRData(ctx, pr, token, logger, httpClient, turnClient, username, debug, noCache); err != nil { if errors.Is(err, context.Canceled) { return } logger.Printf("WARNING: Failed to enrich PR #%d: %v", pr.Number, err) } - }(&prs[i], token) + }) } wg.Wait() @@ -932,7 +942,7 @@ func runWatchMode(ctx context.Context, cfg *watchConfig) { sprinklerLogger = slog.New(slog.NewTextHandler(os.Stderr, nil)) } else { // Discard all logs in non-verbose mode - sprinklerLogger = slog.New(slog.NewTextHandler(io.Discard, nil)) + sprinklerLogger = slog.New(slog.DiscardHandler) } config := client.Config{ @@ -1031,7 +1041,7 @@ func updateDisplay(ctx context.Context, cfg *displayConfig) error { } // Generate display output - output := generatePRDisplay(prs, cfg.username, cfg.blockingOnly, cfg.verbose, cfg.includeStale, cfg.excludedOrgs) + output := generatePRDisplay(prs, cfg.username, cfg.blockingOnly, cfg.includeStale, cfg.excludedOrgs) // Check if display has changed currentHash := fmt.Sprintf("%x", sha256.Sum256([]byte(output))) @@ -1048,55 +1058,123 @@ func updateDisplay(ctx context.Context, cfg *displayConfig) error { return nil } -func generatePRDisplay(prs []PR, username string, blockingOnly, verbose, includeStale bool, excludedOrgs []string) string { - var output strings.Builder +// filterExcludedOrgs removes PRs from excluded organizations. +func filterExcludedOrgs(prs []PR, excludedOrgs []string) []PR { + if len(excludedOrgs) == 0 { + return prs + } - // Filter out excluded orgs - if len(excludedOrgs) > 0 { - var filtered []PR - for i := range prs { - excluded := false - org := orgFromURL(prs[i].HTMLURL) - for _, exc := range excludedOrgs { - if org == exc { - excluded = true - break - } - } - if !excluded { - filtered = append(filtered, prs[i]) + filtered := make([]PR, 0, len(prs)) + for i := range prs { + excluded := false + org := orgFromURL(prs[i].HTMLURL) + for _, exc := range excludedOrgs { + if org == exc { + excluded = true + break } } - prs = filtered + if !excluded { + filtered = append(filtered, prs[i]) + } } + return filtered +} - // Filter stale PRs unless includeStale is true - if !includeStale { - var filtered []PR - staleAge := stalePRDays * 24 * time.Hour - for i := range prs { - stale := false - - // Check if PR is older than 90 days based on UpdatedAt - if time.Since(prs[i].UpdatedAt) > staleAge { - stale = true - } +// filterStalePRs removes PRs that are considered stale. +func filterStalePRs(prs []PR) []PR { + filtered := make([]PR, 0, len(prs)) + staleAge := stalePRDays * 24 * time.Hour + for i := range prs { + stale := false - // Also check TurnResponse tags if available - if !stale && prs[i].TurnResponse != nil { - for _, tag := range prs[i].TurnResponse.Analysis.Tags { - if tag == "stale" { - stale = true - break - } + // Check if PR is older than 90 days based on UpdatedAt + if time.Since(prs[i].UpdatedAt) > staleAge { + stale = true + } + + // Also check TurnResponse tags if available + if !stale && prs[i].TurnResponse != nil { + for _, tag := range prs[i].TurnResponse.Analysis.Tags { + if tag == "stale" { + stale = true + break } } + } - if !stale { - filtered = append(filtered, prs[i]) - } + if !stale { + filtered = append(filtered, prs[i]) } - prs = filtered + } + return filtered +} + +// countBlockingPRs counts how many PRs are blocking on the user. +// Returns blocked (critical) and awaiting (non-critical) counts. +func countBlockingPRs(prs []PR, username string) (blocked, awaiting int) { + for i := range prs { + if isCriticalBlocker(&prs[i], username) { + blocked++ + } else if isBlockingOnUser(&prs[i], username) { + awaiting++ + } + } + return blocked, awaiting +} + +// formatPRListHeader generates a header for a PR list with counts. +func formatPRListHeader(prCount, blocked, awaiting int, isOutgoing bool) string { + var output strings.Builder + + // Proper singular/plural for PRs + prText := "PR" + if prCount != 1 { + prText = "PRs" + } + + // Base text - gray for outgoing + baseText := fmt.Sprintf("incoming - %d %s", prCount, prText) + if isOutgoing { + baseText = fmt.Sprintf("outgoing - %d %s", prCount, prText) + output.WriteString(lipgloss.NewStyle(). + Foreground(lipgloss.Color("#8B8B8B")). + Render(baseText)) + } else { + output.WriteString(baseText) + } + + // Add blocked count + if blocked > 0 { + output.WriteString(", ") + blockText := "blocked on YOU" + output.WriteString(lipgloss.NewStyle(). + Foreground(lipgloss.Color("#E5484D")). + Bold(true). + Render(fmt.Sprintf("%d %s", blocked, blockText))) + } + + // Add awaiting count + if awaiting > 0 { + output.WriteString(", ") + awaitText := "awaiting your input" + output.WriteString(lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FFB224")). + Bold(true). + Render(fmt.Sprintf("%d %s", awaiting, awaitText))) + } + + output.WriteString(":\n") + return output.String() +} + +func generatePRDisplay(prs []PR, username string, blockingOnly, includeStale bool, excludedOrgs []string) string { + var output strings.Builder + + // Filter PRs + prs = filterExcludedOrgs(prs, excludedOrgs) + if !includeStale { + prs = filterStalePRs(prs) } // Sort PRs by most recently updated @@ -1105,55 +1183,15 @@ func generatePRDisplay(prs []PR, username string, blockingOnly, verbose, include // Split into incoming and outgoing incoming, outgoing := categorizePRs(prs, username) - // Count blocking PRs - separate critical and non-critical - inBlocked := 0 // critical actions only - inAwaiting := 0 // non-critical actions - for i := range incoming { - if isCriticalBlocker(&incoming[i], username) { - inBlocked++ - } else if isBlockingOnUser(&incoming[i], username) { - inAwaiting++ - } - } - - outBlocked := 0 // critical actions only - outAwaiting := 0 // non-critical actions - for i := range outgoing { - if isCriticalBlocker(&outgoing[i], username) { - outBlocked++ - } else if isBlockingOnUser(&outgoing[i], username) { - outAwaiting++ - } - } + // Count blocking PRs + inBlocked, inAwaiting := countBlockingPRs(incoming, username) + outBlocked, outAwaiting := countBlockingPRs(outgoing, username) output.WriteString("\n") // Incoming PRs with integrated header if len(incoming) > 0 && (!blockingOnly || inBlocked > 0 || inAwaiting > 0) { - // Header with counts - proper singular/plural - prText := "PR" - if len(incoming) != 1 { - prText = "PRs" - } - output.WriteString(fmt.Sprintf("incoming - %d %s", len(incoming), prText)) - if inBlocked > 0 { - output.WriteString(", ") - blockText := "blocked on YOU" - output.WriteString(lipgloss.NewStyle(). - Foreground(lipgloss.Color("#E5484D")). // Red for critical blocked count - Bold(true). - Render(fmt.Sprintf("%d %s", inBlocked, blockText))) - } - if inAwaiting > 0 { - output.WriteString(", ") - awaitText := "awaiting your input" - output.WriteString(lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFB224")). // Yellow for awaiting input - Bold(true). - Render(fmt.Sprintf("%d %s", inAwaiting, awaitText))) - } - output.WriteString(":\n") - + output.WriteString(formatPRListHeader(len(incoming), inBlocked, inAwaiting, false)) for i := range incoming { if blockingOnly && !isBlockingOnUser(&incoming[i], username) { continue @@ -1167,33 +1205,7 @@ func generatePRDisplay(prs []PR, username string, blockingOnly, verbose, include if len(incoming) > 0 { output.WriteString("\n") } - - // Header with counts - gray color for distinction, proper singular/plural - prText := "PR" - if len(outgoing) != 1 { - prText = "PRs" - } - output.WriteString(lipgloss.NewStyle(). - Foreground(lipgloss.Color("#8B8B8B")). // Gray for outgoing header - Render(fmt.Sprintf("outgoing - %d %s", len(outgoing), prText))) - if outBlocked > 0 { - output.WriteString(", ") - blockText := "blocked on YOU" - output.WriteString(lipgloss.NewStyle(). - Foreground(lipgloss.Color("#E5484D")). - Bold(true). - Render(fmt.Sprintf("%d %s", outBlocked, blockText))) - } - if outAwaiting > 0 { - output.WriteString(", ") - awaitText := "awaiting your input" - output.WriteString(lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFB224")). - Bold(true). - Render(fmt.Sprintf("%d %s", outAwaiting, awaitText))) - } - output.WriteString(":\n") - + output.WriteString(formatPRListHeader(len(outgoing), outBlocked, outAwaiting, true)) for i := range outgoing { if blockingOnly && !isBlockingOnUser(&outgoing[i], username) { continue @@ -1220,6 +1232,76 @@ func terminalWidth() int { return width } +// appendNextActionKinds appends formatted next action kinds to the output. +func appendNextActionKinds(output *strings.Builder, pr *PR, username string) { + if pr.TurnResponse == nil || pr.TurnResponse.Analysis.NextAction == nil { + return + } + + var userActionKinds []string + var otherCriticalKinds []string + var userActionCritical bool + seen := make(map[string]bool) + + // First, collect current user's actions + if userAction, hasUserAction := pr.TurnResponse.Analysis.NextAction[username]; hasUserAction { + kind := string(userAction.Kind) + if !seen[kind] { + userActionKinds = append(userActionKinds, kind) + seen[kind] = true + userActionCritical = userAction.Critical + } + } + + // Then collect critical actions from other users (avoiding duplicates) + for user, action := range pr.TurnResponse.Analysis.NextAction { + if user != username && action.Critical { + kind := string(action.Kind) + if !seen[kind] { + otherCriticalKinds = append(otherCriticalKinds, kind) + seen[kind] = true + } + } + } + + // Display actions if any exist + if len(userActionKinds) == 0 && len(otherCriticalKinds) == 0 { + return + } + + // Dark grey emdash + output.WriteString(lipgloss.NewStyle(). + Foreground(lipgloss.Color("#6B6B6B")). // Dark grey + Render(" — ")) + + // Display user's actions first with appropriate color + if len(userActionKinds) > 0 { + actionText := strings.Join(userActionKinds, " ") + if userActionCritical { + // Red for critical user action + output.WriteString(lipgloss.NewStyle(). + Foreground(lipgloss.Color("#E5484D")). + Render(actionText)) + } else { + // Yellow for non-critical user action + output.WriteString(lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FFB224")). + Render(actionText)) + } + } + + // Display other critical actions in dark grey + if len(otherCriticalKinds) > 0 { + if len(userActionKinds) > 0 { + output.WriteString(" ") // Space between user and other actions + } + actionText := strings.Join(otherCriticalKinds, " ") + output.WriteString(lipgloss.NewStyle(). + Foreground(lipgloss.Color("#6B6B6B")). + Render(actionText)) + } +} + func formatPR(pr *PR, username string) string { var output strings.Builder @@ -1230,19 +1312,20 @@ func formatPR(pr *PR, username string) string { isBlocking := isBlockingOnUser(pr, username) isCritical := isCriticalBlocker(pr, username) - if isCritical { + switch { + case isCritical: // Red triangle for critical blocker output.WriteString(lipgloss.NewStyle(). Foreground(lipgloss.Color("#E5484D")). // Modern red Bold(true). Render("► ")) - } else if isBlocking { + case isBlocking: // Yellow bullet for regular next action output.WriteString(lipgloss.NewStyle(). Foreground(lipgloss.Color("#FFB224")). // Yellow/amber Bold(true). Render("• ")) - } else { + default: output.WriteString(" ") // Just indent, no bullet for non-blocking } @@ -1291,68 +1374,7 @@ func formatPR(pr *PR, username string) string { } // Add NextAction kinds if available - if pr.TurnResponse != nil && pr.TurnResponse.Analysis.NextAction != nil { - var userActionKinds []string - var otherCriticalKinds []string - var userActionCritical bool - seen := make(map[string]bool) - - // First, collect current user's actions - if userAction, hasUserAction := pr.TurnResponse.Analysis.NextAction[username]; hasUserAction { - kind := string(userAction.Kind) - if !seen[kind] { - userActionKinds = append(userActionKinds, kind) - seen[kind] = true - userActionCritical = userAction.Critical - } - } - - // Then collect critical actions from other users (avoiding duplicates) - for user, action := range pr.TurnResponse.Analysis.NextAction { - if user != username && action.Critical { - kind := string(action.Kind) - if !seen[kind] { - otherCriticalKinds = append(otherCriticalKinds, kind) - seen[kind] = true - } - } - } - - // Display actions if any exist - if len(userActionKinds) > 0 || len(otherCriticalKinds) > 0 { - // Dark grey emdash - output.WriteString(lipgloss.NewStyle(). - Foreground(lipgloss.Color("#6B6B6B")). // Dark grey - Render(" — ")) - - // Display user's actions first with appropriate color - if len(userActionKinds) > 0 { - actionText := strings.Join(userActionKinds, " ") - if userActionCritical { - // Red for critical user action - output.WriteString(lipgloss.NewStyle(). - Foreground(lipgloss.Color("#E5484D")). - Render(actionText)) - } else { - // Yellow for non-critical user action - output.WriteString(lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFB224")). - Render(actionText)) - } - } - - // Display other critical actions in dark grey - if len(otherCriticalKinds) > 0 { - if len(userActionKinds) > 0 { - output.WriteString(" ") // Space between user and other actions - } - actionText := strings.Join(otherCriticalKinds, " ") - output.WriteString(lipgloss.NewStyle(). - Foreground(lipgloss.Color("#6B6B6B")). - Render(actionText)) - } - } - } + appendNextActionKinds(&output, pr, username) output.WriteString("\n") return output.String() diff --git a/main_test.go b/main_test.go index 300311a..94287b5 100644 --- a/main_test.go +++ b/main_test.go @@ -248,6 +248,27 @@ func TestCategorizePRs(t *testing.T) { } } +// countPRLines counts PR lines in a section by looking for bullet prefixes. +func countPRLines(output, sectionHeader, nextSection string) int { + lines := strings.Split(output, "\n") + sectionStarted := false + count := 0 + + for _, line := range lines { + if strings.Contains(line, sectionHeader) { + sectionStarted = true + continue + } + if nextSection != "" && strings.Contains(line, nextSection) { + break + } + if sectionStarted && (strings.HasPrefix(line, "• ") || strings.HasPrefix(line, "► ") || strings.HasPrefix(line, " ")) { + count++ + } + } + return count +} + func TestGeneratePRDisplayBlockedFlag(t *testing.T) { username := "alice" @@ -344,7 +365,7 @@ func TestGeneratePRDisplayBlockedFlag(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - output := generatePRDisplay(tt.prs, username, tt.blockingOnly, false, true, nil) + output := generatePRDisplay(tt.prs, username, tt.blockingOnly, true, nil) // Debug output for failing test if tt.name == "blocked mode with only outgoing blocked" { @@ -373,39 +394,14 @@ func TestGeneratePRDisplayBlockedFlag(t *testing.T) { // Count actual PRs shown in each section if tt.expectIncoming { - incomingLines := 0 - lines := strings.Split(output, "\n") - incomingStarted := false - for _, line := range lines { - if strings.Contains(line, "incoming -") { - incomingStarted = true - continue - } - if strings.Contains(line, "outgoing -") { - break - } - if incomingStarted && (strings.HasPrefix(line, "• ") || strings.HasPrefix(line, " ")) { - incomingLines++ - } - } + incomingLines := countPRLines(output, "incoming -", "outgoing -") if incomingLines != tt.expectIncomingCount { t.Errorf("Expected %d incoming PRs shown, got %d", tt.expectIncomingCount, incomingLines) } } if tt.expectOutgoing { - outgoingLines := 0 - lines := strings.Split(output, "\n") - outgoingStarted := false - for _, line := range lines { - if strings.Contains(line, "outgoing -") { - outgoingStarted = true - continue - } - if outgoingStarted && (strings.HasPrefix(line, "• ") || strings.HasPrefix(line, " ")) { - outgoingLines++ - } - } + outgoingLines := countPRLines(output, "outgoing -", "") if outgoingLines != tt.expectOutgoingCount { t.Errorf("Expected %d outgoing PRs shown, got %d", tt.expectOutgoingCount, outgoingLines) }