bd list --tree: - Use actual parent-child dependencies instead of dotted ID hierarchy - Treat epic dependencies as parent-child relationships - Sort children by priority (P0 first) - Fix tree display in daemon mode with read-only store access bd graph: - Add --all flag to show dependency graph of all open issues - Add --compact flag for tree-style rendering (reduces 44+ lines to 13) - Fix "needs:N" cognitive noise by using semantic colors - Add blocks:N indicator with semantic red coloring bd show: - Tufte-aligned header with status icon, priority, and type badges - Add glamour markdown rendering with auto light/dark mode detection - Cap markdown line width at 100 chars for readability - Mute entire row for closed dependencies (work done, no attention needed) Design system: - Add shared status icons (○ ◐ ● ✓ ❄) with semantic colors - Implement priority colors: P0 red, P1 orange, P2 muted gold, P3-P4 neutral - Add TrueColor profile for distinct hex color rendering - Type badges for epic (purple) and bug (red) Design principles: - Semantic colors only for actionable items - Closed items fade (muted gray) - Icons > text labels for better scanability Co-Authored-By: SageOx <ox@sageox.ai>
186 lines
5.8 KiB
Go
186 lines
5.8 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRenderBasicStyles(t *testing.T) {
|
|
t.Run("semantic wrappers", func(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
got string
|
|
want string
|
|
}{
|
|
{"pass", RenderPass("ok"), PassStyle.Render("ok")},
|
|
{"warn", RenderWarn("careful"), WarnStyle.Render("careful")},
|
|
{"fail", RenderFail("boom"), FailStyle.Render("boom")},
|
|
{"muted", RenderMuted("note"), MutedStyle.Render("note")},
|
|
{"accent", RenderAccent("info"), AccentStyle.Render("info")},
|
|
{"category", RenderCategory("mixed Case"), CategoryStyle.Render("MIXED CASE")},
|
|
{"separator", RenderSeparator(), MutedStyle.Render(SeparatorLight)},
|
|
{"pass icon", RenderPassIcon(), PassStyle.Render(IconPass)},
|
|
{"warn icon", RenderWarnIcon(), WarnStyle.Render(IconWarn)},
|
|
{"fail icon", RenderFailIcon(), FailStyle.Render(IconFail)},
|
|
{"skip icon", RenderSkipIcon(), MutedStyle.Render(IconSkip)},
|
|
{"info icon", RenderInfoIcon(), AccentStyle.Render(IconInfo)},
|
|
{"bold", RenderBold("bold"), BoldStyle.Render("bold")},
|
|
{"command", RenderCommand("bd prime"), CommandStyle.Render("bd prime")},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if tc.got != tc.want {
|
|
t.Fatalf("%s mismatch: got %q want %q", tc.name, tc.got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRenderStatusAndPriority(t *testing.T) {
|
|
statusCases := []struct {
|
|
status string
|
|
want string
|
|
}{
|
|
{"open", StatusOpenStyle.Render("open")},
|
|
{"in_progress", StatusInProgressStyle.Render("in_progress")},
|
|
{"blocked", StatusBlockedStyle.Render("blocked")},
|
|
{"pinned", StatusPinnedStyle.Render("pinned")},
|
|
{"hooked", StatusHookedStyle.Render("hooked")},
|
|
{"closed", StatusClosedStyle.Render("closed")},
|
|
{"custom", StatusOpenStyle.Render("custom")},
|
|
}
|
|
for _, tc := range statusCases {
|
|
if got := RenderStatus(tc.status); got != tc.want {
|
|
t.Fatalf("status %s mismatch: got %q want %q", tc.status, got, tc.want)
|
|
}
|
|
}
|
|
|
|
// RenderPriority now includes the priority icon (●)
|
|
priorityCases := []struct {
|
|
priority int
|
|
want string
|
|
}{
|
|
{0, PriorityP0Style.Render(PriorityIcon + " P0")},
|
|
{1, PriorityP1Style.Render(PriorityIcon + " P1")},
|
|
{2, PriorityP2Style.Render(PriorityIcon + " P2")},
|
|
{3, PriorityP3Style.Render(PriorityIcon + " P3")},
|
|
{4, PriorityP4Style.Render(PriorityIcon + " P4")},
|
|
{5, PriorityIcon + " P5"},
|
|
}
|
|
for _, tc := range priorityCases {
|
|
if got := RenderPriority(tc.priority); got != tc.want {
|
|
t.Fatalf("priority %d mismatch: got %q want %q", tc.priority, got, tc.want)
|
|
}
|
|
}
|
|
|
|
// RenderPriorityCompact returns just "P0" without icon
|
|
if got := RenderPriorityCompact(0); !strings.Contains(got, "P0") {
|
|
t.Fatalf("compact priority should contain P0, got %q", got)
|
|
}
|
|
|
|
if got := RenderPriorityForStatus(0, "closed"); got != "P0" {
|
|
t.Fatalf("closed priority should be plain text, got %q", got)
|
|
}
|
|
if got := RenderPriorityForStatus(1, "open"); got != RenderPriority(1) {
|
|
t.Fatalf("open priority should use styling")
|
|
}
|
|
}
|
|
|
|
func TestRenderTypeVariants(t *testing.T) {
|
|
cases := []struct {
|
|
issueType string
|
|
want string
|
|
}{
|
|
{"bug", TypeBugStyle.Render("bug")},
|
|
{"feature", TypeFeatureStyle.Render("feature")},
|
|
{"task", TypeTaskStyle.Render("task")},
|
|
{"epic", TypeEpicStyle.Render("epic")},
|
|
{"chore", TypeChoreStyle.Render("chore")},
|
|
// Gas Town types (agent, role, rig) have been removed - they now fall through to default
|
|
{"agent", "agent"}, // Falls through to default (no styling)
|
|
{"role", "role"}, // Falls through to default (no styling)
|
|
{"rig", "rig"}, // Falls through to default (no styling)
|
|
{"custom", "custom"},
|
|
}
|
|
for _, tc := range cases {
|
|
if got := RenderType(tc.issueType); got != tc.want {
|
|
t.Fatalf("type %s mismatch: got %q want %q", tc.issueType, got, tc.want)
|
|
}
|
|
}
|
|
|
|
if got := RenderTypeForStatus("bug", "closed"); got != "bug" {
|
|
t.Fatalf("closed type should be plain, got %q", got)
|
|
}
|
|
if got := RenderTypeForStatus("bug", "open"); got != RenderType("bug") {
|
|
t.Fatalf("open type should be styled")
|
|
}
|
|
}
|
|
|
|
func TestRenderIssueCompact(t *testing.T) {
|
|
open := RenderIssueCompact("bd-1", 0, "bug", "in_progress", "ship it")
|
|
wantOpen := fmt.Sprintf("%s [%s] [%s] %s - %s",
|
|
RenderID("bd-1"),
|
|
RenderPriority(0),
|
|
RenderType("bug"),
|
|
RenderStatus("in_progress"),
|
|
"ship it",
|
|
)
|
|
if open != wantOpen {
|
|
t.Fatalf("open issue line mismatch: got %q want %q", open, wantOpen)
|
|
}
|
|
|
|
closed := RenderIssueCompact("bd-2", 2, "task", "closed", "done")
|
|
raw := fmt.Sprintf("%s [P%d] [%s] %s - %s", "bd-2", 2, "task", "closed", "done")
|
|
if closed != StatusClosedStyle.Render(raw) {
|
|
t.Fatalf("closed issue line should be dimmed: got %q", closed)
|
|
}
|
|
}
|
|
|
|
func TestRenderClosedUtilities(t *testing.T) {
|
|
line := "bd-42 closed"
|
|
if got := RenderClosedLine(line); got != StatusClosedStyle.Render(line) {
|
|
t.Fatalf("closed line mismatch: got %q", got)
|
|
}
|
|
|
|
if got := RenderID("bd-5"); got != IDStyle.Render("bd-5") {
|
|
t.Fatalf("RenderID mismatch")
|
|
}
|
|
}
|
|
|
|
func TestRenderCommandAndCategoryAreUppercaseSafe(t *testing.T) {
|
|
got := RenderCategory(" already upper ")
|
|
if !strings.Contains(got, " ALREADY UPPER ") {
|
|
t.Fatalf("category should uppercase input, got %q", got)
|
|
}
|
|
|
|
cmd := RenderCommand("bd prime")
|
|
if !strings.Contains(cmd, "bd prime") {
|
|
t.Fatalf("command output missing text: %q", cmd)
|
|
}
|
|
}
|
|
|
|
func TestIsAgentMode(t *testing.T) {
|
|
// Test default (no env vars) - t.Setenv automatically restores after test
|
|
t.Setenv("BD_AGENT_MODE", "")
|
|
t.Setenv("CLAUDE_CODE", "")
|
|
if IsAgentMode() {
|
|
t.Fatal("expected false with no env vars")
|
|
}
|
|
|
|
// Test BD_AGENT_MODE=1
|
|
t.Setenv("BD_AGENT_MODE", "1")
|
|
t.Setenv("CLAUDE_CODE", "")
|
|
if !IsAgentMode() {
|
|
t.Fatal("expected true with BD_AGENT_MODE=1")
|
|
}
|
|
|
|
// Test CLAUDE_CODE auto-detection
|
|
t.Setenv("BD_AGENT_MODE", "")
|
|
t.Setenv("CLAUDE_CODE", "something")
|
|
if !IsAgentMode() {
|
|
t.Fatal("expected true with CLAUDE_CODE set")
|
|
}
|
|
}
|