Import beads' UX design system into gastown: - Add internal/ui/ package with Ayu theme colors and semantic styling - styles.go: AdaptiveColor definitions for light/dark mode - terminal.go: TTY detection, NO_COLOR/CLICOLOR support - markdown.go: Glamour rendering with agent mode bypass - pager.go: Smart paging with GT_PAGER support - Add colorized help output (internal/cmd/help.go) - Group headers in accent color - Command names styled for scannability - Flag types and defaults muted - Add gt thanks command (internal/cmd/thanks.go) - Contributor display with same logic as bd thanks - Styled with Ayu theme colors - Update gt doctor to match bd doctor UX - Category grouping (Core, Infrastructure, Rig, Patrol, etc.) - Semantic icons (✓ ⚠ ✖) with Ayu colors - Tree connectors for detail lines - Summary line with pass/warn/fail counts - Warnings section at end with numbered issues - Migrate existing styles to use ui package - internal/style/style.go uses ui.ColorPass etc. - internal/tui/feed/styles.go uses ui package colors Co-Authored-By: SageOx <ox@sageox.ai>
365 lines
8.4 KiB
Go
365 lines
8.4 KiB
Go
package doctor
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
)
|
|
|
|
// mockCheck is a test check that can be configured to return any status.
|
|
type mockCheck struct {
|
|
BaseCheck
|
|
status CheckStatus
|
|
fixable bool
|
|
fixError error
|
|
fixCount int
|
|
}
|
|
|
|
func newMockCheck(name string, status CheckStatus) *mockCheck {
|
|
return &mockCheck{
|
|
BaseCheck: BaseCheck{
|
|
CheckName: name,
|
|
CheckDescription: "Test check: " + name,
|
|
},
|
|
status: status,
|
|
}
|
|
}
|
|
|
|
func (m *mockCheck) Run(ctx *CheckContext) *CheckResult {
|
|
return &CheckResult{
|
|
Name: m.CheckName,
|
|
Status: m.status,
|
|
Message: "mock result",
|
|
}
|
|
}
|
|
|
|
func (m *mockCheck) CanFix() bool {
|
|
return m.fixable
|
|
}
|
|
|
|
func (m *mockCheck) Fix(ctx *CheckContext) error {
|
|
m.fixCount++
|
|
if m.fixError != nil {
|
|
return m.fixError
|
|
}
|
|
// Simulate successful fix by changing status
|
|
m.status = StatusOK
|
|
return nil
|
|
}
|
|
|
|
func TestCheckStatus_String(t *testing.T) {
|
|
tests := []struct {
|
|
status CheckStatus
|
|
want string
|
|
}{
|
|
{StatusOK, "OK"},
|
|
{StatusWarning, "Warning"},
|
|
{StatusError, "Error"},
|
|
{CheckStatus(99), "Unknown"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := tt.status.String()
|
|
if got != tt.want {
|
|
t.Errorf("CheckStatus(%d).String() = %q, want %q", tt.status, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckContext_RigPath(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ctx CheckContext
|
|
wantPath string
|
|
}{
|
|
{
|
|
name: "empty rig name",
|
|
ctx: CheckContext{TownRoot: "/town"},
|
|
wantPath: "",
|
|
},
|
|
{
|
|
name: "with rig name",
|
|
ctx: CheckContext{TownRoot: "/town", RigName: "myrig"},
|
|
wantPath: "/town/myrig",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := tt.ctx.RigPath()
|
|
if got != tt.wantPath {
|
|
t.Errorf("RigPath() = %q, want %q", got, tt.wantPath)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewReport(t *testing.T) {
|
|
r := NewReport()
|
|
|
|
if r.Timestamp.IsZero() {
|
|
t.Error("NewReport() should set Timestamp")
|
|
}
|
|
if len(r.Checks) != 0 {
|
|
t.Error("NewReport() should have empty Checks slice")
|
|
}
|
|
if r.Summary.Total != 0 {
|
|
t.Error("NewReport() should have zero Total")
|
|
}
|
|
}
|
|
|
|
func TestReport_Add(t *testing.T) {
|
|
r := NewReport()
|
|
|
|
// Add an OK result
|
|
r.Add(&CheckResult{Name: "test1", Status: StatusOK})
|
|
if r.Summary.Total != 1 || r.Summary.OK != 1 {
|
|
t.Errorf("After adding OK: Total=%d, OK=%d", r.Summary.Total, r.Summary.OK)
|
|
}
|
|
|
|
// Add a warning
|
|
r.Add(&CheckResult{Name: "test2", Status: StatusWarning})
|
|
if r.Summary.Total != 2 || r.Summary.Warnings != 1 {
|
|
t.Errorf("After adding Warning: Total=%d, Warnings=%d", r.Summary.Total, r.Summary.Warnings)
|
|
}
|
|
|
|
// Add an error
|
|
r.Add(&CheckResult{Name: "test3", Status: StatusError})
|
|
if r.Summary.Total != 3 || r.Summary.Errors != 1 {
|
|
t.Errorf("After adding Error: Total=%d, Errors=%d", r.Summary.Total, r.Summary.Errors)
|
|
}
|
|
}
|
|
|
|
func TestReport_HasErrors(t *testing.T) {
|
|
r := NewReport()
|
|
if r.HasErrors() {
|
|
t.Error("Empty report should not have errors")
|
|
}
|
|
|
|
r.Add(&CheckResult{Status: StatusOK})
|
|
if r.HasErrors() {
|
|
t.Error("Report with only OK should not have errors")
|
|
}
|
|
|
|
r.Add(&CheckResult{Status: StatusWarning})
|
|
if r.HasErrors() {
|
|
t.Error("Report with only OK/Warning should not have errors")
|
|
}
|
|
|
|
r.Add(&CheckResult{Status: StatusError})
|
|
if !r.HasErrors() {
|
|
t.Error("Report with Error should have errors")
|
|
}
|
|
}
|
|
|
|
func TestReport_HasWarnings(t *testing.T) {
|
|
r := NewReport()
|
|
if r.HasWarnings() {
|
|
t.Error("Empty report should not have warnings")
|
|
}
|
|
|
|
r.Add(&CheckResult{Status: StatusOK})
|
|
if r.HasWarnings() {
|
|
t.Error("Report with only OK should not have warnings")
|
|
}
|
|
|
|
r.Add(&CheckResult{Status: StatusWarning})
|
|
if !r.HasWarnings() {
|
|
t.Error("Report with Warning should have warnings")
|
|
}
|
|
}
|
|
|
|
func TestReport_IsHealthy(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
results []CheckStatus
|
|
want bool
|
|
}{
|
|
{"empty", nil, true},
|
|
{"all OK", []CheckStatus{StatusOK, StatusOK}, true},
|
|
{"has warning", []CheckStatus{StatusOK, StatusWarning}, false},
|
|
{"has error", []CheckStatus{StatusOK, StatusError}, false},
|
|
{"mixed", []CheckStatus{StatusOK, StatusWarning, StatusError}, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r := NewReport()
|
|
for _, status := range tt.results {
|
|
r.Add(&CheckResult{Status: status})
|
|
}
|
|
if got := r.IsHealthy(); got != tt.want {
|
|
t.Errorf("IsHealthy() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReport_Print(t *testing.T) {
|
|
r := NewReport()
|
|
r.Add(&CheckResult{
|
|
Name: "TestCheck",
|
|
Status: StatusOK,
|
|
Message: "All good",
|
|
})
|
|
r.Add(&CheckResult{
|
|
Name: "WarningCheck",
|
|
Status: StatusWarning,
|
|
Message: "Minor issue",
|
|
FixHint: "Run fix command",
|
|
})
|
|
|
|
var buf bytes.Buffer
|
|
r.Print(&buf, false)
|
|
|
|
output := buf.String()
|
|
if output == "" {
|
|
t.Error("Print() should produce output")
|
|
}
|
|
// Basic checks that key elements are present
|
|
if !bytes.Contains(buf.Bytes(), []byte("TestCheck")) {
|
|
t.Error("Output should contain check name")
|
|
}
|
|
// New summary format: "✓ N passed ⚠ N warnings ✖ N failed"
|
|
if !bytes.Contains(buf.Bytes(), []byte("1 passed")) {
|
|
t.Error("Output should contain summary with passed count")
|
|
}
|
|
if !bytes.Contains(buf.Bytes(), []byte("1 warnings")) {
|
|
t.Error("Output should contain summary with warnings count")
|
|
}
|
|
}
|
|
|
|
func TestNewDoctor(t *testing.T) {
|
|
d := NewDoctor()
|
|
if d == nil {
|
|
t.Fatal("NewDoctor() returned nil")
|
|
}
|
|
if len(d.Checks()) != 0 {
|
|
t.Error("NewDoctor() should have no checks registered")
|
|
}
|
|
}
|
|
|
|
func TestDoctor_Register(t *testing.T) {
|
|
d := NewDoctor()
|
|
|
|
check1 := newMockCheck("check1", StatusOK)
|
|
check2 := newMockCheck("check2", StatusOK)
|
|
|
|
d.Register(check1)
|
|
if len(d.Checks()) != 1 {
|
|
t.Error("Register() should add one check")
|
|
}
|
|
|
|
d.Register(check2)
|
|
if len(d.Checks()) != 2 {
|
|
t.Error("Register() should add another check")
|
|
}
|
|
}
|
|
|
|
func TestDoctor_RegisterAll(t *testing.T) {
|
|
d := NewDoctor()
|
|
|
|
check1 := newMockCheck("check1", StatusOK)
|
|
check2 := newMockCheck("check2", StatusOK)
|
|
check3 := newMockCheck("check3", StatusOK)
|
|
|
|
d.RegisterAll(check1, check2, check3)
|
|
if len(d.Checks()) != 3 {
|
|
t.Errorf("RegisterAll() should add 3 checks, got %d", len(d.Checks()))
|
|
}
|
|
}
|
|
|
|
func TestDoctor_Run(t *testing.T) {
|
|
d := NewDoctor()
|
|
d.Register(newMockCheck("ok", StatusOK))
|
|
d.Register(newMockCheck("warn", StatusWarning))
|
|
d.Register(newMockCheck("error", StatusError))
|
|
|
|
ctx := &CheckContext{TownRoot: "/test"}
|
|
report := d.Run(ctx)
|
|
|
|
if report.Summary.Total != 3 {
|
|
t.Errorf("Run() Total = %d, want 3", report.Summary.Total)
|
|
}
|
|
if report.Summary.OK != 1 {
|
|
t.Errorf("Run() OK = %d, want 1", report.Summary.OK)
|
|
}
|
|
if report.Summary.Warnings != 1 {
|
|
t.Errorf("Run() Warnings = %d, want 1", report.Summary.Warnings)
|
|
}
|
|
if report.Summary.Errors != 1 {
|
|
t.Errorf("Run() Errors = %d, want 1", report.Summary.Errors)
|
|
}
|
|
}
|
|
|
|
func TestDoctor_Fix(t *testing.T) {
|
|
d := NewDoctor()
|
|
|
|
okCheck := newMockCheck("ok", StatusOK)
|
|
d.Register(okCheck)
|
|
|
|
fixableCheck := newMockCheck("fixable", StatusError)
|
|
fixableCheck.fixable = true
|
|
d.Register(fixableCheck)
|
|
|
|
unfixableCheck := newMockCheck("unfixable", StatusError)
|
|
unfixableCheck.fixable = false
|
|
d.Register(unfixableCheck)
|
|
|
|
ctx := &CheckContext{TownRoot: "/test"}
|
|
report := d.Fix(ctx)
|
|
|
|
// OK check should remain OK
|
|
if report.Checks[0].Status != StatusOK {
|
|
t.Error("OK check should remain OK")
|
|
}
|
|
|
|
// Fixable check should be fixed
|
|
if fixableCheck.fixCount != 1 {
|
|
t.Error("Fixable check should have Fix() called once")
|
|
}
|
|
if report.Checks[1].Status != StatusOK {
|
|
t.Error("Fixable check should be OK after fix")
|
|
}
|
|
|
|
// Unfixable check should remain error
|
|
if unfixableCheck.fixCount != 0 {
|
|
t.Error("Unfixable check should not have Fix() called")
|
|
}
|
|
if report.Checks[2].Status != StatusError {
|
|
t.Error("Unfixable check should remain Error")
|
|
}
|
|
}
|
|
|
|
func TestBaseCheck(t *testing.T) {
|
|
b := &BaseCheck{
|
|
CheckName: "test",
|
|
CheckDescription: "Test description",
|
|
}
|
|
|
|
if b.Name() != "test" {
|
|
t.Errorf("Name() = %q, want %q", b.Name(), "test")
|
|
}
|
|
if b.Description() != "Test description" {
|
|
t.Errorf("Description() = %q, want %q", b.Description(), "Test description")
|
|
}
|
|
if b.CanFix() {
|
|
t.Error("BaseCheck.CanFix() should return false")
|
|
}
|
|
if err := b.Fix(nil); err != ErrCannotFix {
|
|
t.Errorf("BaseCheck.Fix() should return ErrCannotFix, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestFixableCheck(t *testing.T) {
|
|
f := &FixableCheck{
|
|
BaseCheck: BaseCheck{
|
|
CheckName: "fixable",
|
|
CheckDescription: "Fixable check",
|
|
},
|
|
}
|
|
|
|
if !f.CanFix() {
|
|
t.Error("FixableCheck.CanFix() should return true")
|
|
}
|
|
}
|