From 2f3e2ad9cf3c31e42c8eeaedc859390f310287cd Mon Sep 17 00:00:00 2001 From: dataCenter430 Date: Tue, 31 Mar 2026 17:31:29 +0200 Subject: [PATCH] fix: lint errors --- cmd/picoclaw/internal/cliui/cliui.go | 8 +- cmd/picoclaw/internal/cliui/cliui_test.go | 177 ++++++++++++++++++++++ cmd/picoclaw/internal/cliui/help_error.go | 3 +- cmd/picoclaw/internal/cliui/onboard.go | 2 +- cmd/picoclaw/internal/cliui/status.go | 28 ++-- cmd/picoclaw/internal/onboard/command.go | 2 +- cmd/picoclaw/internal/status/helpers.go | 59 +++----- cmd/picoclaw/main.go | 5 +- 8 files changed, 222 insertions(+), 62 deletions(-) create mode 100644 cmd/picoclaw/internal/cliui/cliui_test.go diff --git a/cmd/picoclaw/internal/cliui/cliui.go b/cmd/picoclaw/internal/cliui/cliui.go index 30c27e5db..b1ba636c9 100644 --- a/cmd/picoclaw/internal/cliui/cliui.go +++ b/cmd/picoclaw/internal/cliui/cliui.go @@ -96,10 +96,10 @@ func InnerStderrWidth() int { } var ( - accentBlue = lipgloss.Color("#3E5DB9") - accentRed = lipgloss.Color("#D54646") - colorMuted = lipgloss.Color("#6B6B6B") - colorOK = lipgloss.Color("#2E7D32") + accentBlue = lipgloss.Color("#3E5DB9") + accentRed = lipgloss.Color("#D54646") + colorMuted = lipgloss.Color("#6B6B6B") + colorOK = lipgloss.Color("#2E7D32") ) func borderStyle() lipgloss.Style { diff --git a/cmd/picoclaw/internal/cliui/cliui_test.go b/cmd/picoclaw/internal/cliui/cliui_test.go new file mode 100644 index 000000000..fb76e7e01 --- /dev/null +++ b/cmd/picoclaw/internal/cliui/cliui_test.go @@ -0,0 +1,177 @@ +package cliui + +import ( + "testing" + + flag "github.com/spf13/pflag" +) + +func init() { + // Disable ANSI colors in tests so output is predictable plain text. + Init(true) +} + +// --------------------------------------------------------------------------- +// showErrHint +// --------------------------------------------------------------------------- + +func TestShowErrHint(t *testing.T) { + cases := []struct { + msg string + want bool + }{ + // Cobra flag errors — should show hint + {"unknown flag: --foo", true}, + {"unknown shorthand flag: 'f' in -f", true}, + {"flag needs an argument: --output", true}, + {"required flag(s) \"model\" not set", true}, + // Generic invalid-argument errors — should show hint + {"invalid argument \"abc\" for --count", true}, + // usage: in message — should show hint + {"bad input\nusage: picoclaw ...", true}, + // Should NOT false-positive on unrelated "flag" words + {"connection flagged by remote", false}, + {"feature flag not set", false}, + {"please flag this issue", false}, + // Unrelated messages — no hint + {"something went wrong", false}, + {"network timeout", false}, + } + + for _, tc := range cases { + got := showErrHint(tc.msg) + if got != tc.want { + t.Errorf("showErrHint(%q) = %v, want %v", tc.msg, got, tc.want) + } + } +} + +// --------------------------------------------------------------------------- +// styleUsageTokens +// --------------------------------------------------------------------------- + +func TestStyleUsageTokensContainsTokens(t *testing.T) { + cases := []struct { + input string + contains []string // substrings that must appear in plain output + }{ + { + "picoclaw agent ", + []string{"picoclaw agent", ""}, + }, + { + "picoclaw [command] [flags]", + []string{"picoclaw", "[command]", "[flags]"}, + }, + { + "picoclaw", + []string{"picoclaw"}, + }, + { + "cmd [--flag]", + []string{"cmd", "", "[--flag]"}, + }, + } + + for _, tc := range cases { + out := styleUsageTokens(tc.input) + for _, sub := range tc.contains { + if !containsStripped(out, sub) { + t.Errorf("styleUsageTokens(%q): output %q does not contain %q", tc.input, out, sub) + } + } + } +} + +// containsStripped checks whether plain contains sub after stripping ANSI escapes. +// Since Init(true) sets Ascii profile, lipgloss emits no escape codes in tests, +// so this is just a plain substring check. +func containsStripped(plain, sub string) bool { + return len(plain) >= len(sub) && findSubstring(plain, sub) +} + +func findSubstring(s, sub string) bool { + for i := 0; i <= len(s)-len(sub); i++ { + if s[i:i+len(sub)] == sub { + return true + } + } + return false +} + +// --------------------------------------------------------------------------- +// collectFlagRows +// --------------------------------------------------------------------------- + +func TestCollectFlagRows_Empty(t *testing.T) { + fs := flag.NewFlagSet("test", flag.ContinueOnError) + rows := collectFlagRows(fs) + if len(rows) != 0 { + t.Fatalf("expected 0 rows for empty FlagSet, got %d", len(rows)) + } +} + +func TestCollectFlagRows_BasicFlags(t *testing.T) { + fs := flag.NewFlagSet("test", flag.ContinueOnError) + fs.String("output", "", "output file path") + fs.Bool("verbose", false, "enable verbose mode") + fs.Int("count", 1, "number of items") + + rows := collectFlagRows(fs) + + if len(rows) != 3 { + t.Fatalf("expected 3 rows, got %d", len(rows)) + } + + // Rows must be sorted alphabetically by flag name. + names := []string{} + for _, r := range rows { + names = append(names, r[0]) + } + if names[0] > names[1] || names[1] > names[2] { + t.Errorf("rows not sorted: %v", names) + } +} + +func TestCollectFlagRows_Shorthand(t *testing.T) { + fs := flag.NewFlagSet("test", flag.ContinueOnError) + fs.StringP("model", "m", "", "model name") + + rows := collectFlagRows(fs) + if len(rows) != 1 { + t.Fatalf("expected 1 row, got %d", len(rows)) + } + left := rows[0][0] + if !findSubstring(left, "-m") || !findSubstring(left, "--model") { + t.Errorf("expected shorthand and long form in %q", left) + } +} + +func TestCollectFlagRows_HiddenFlagsExcluded(t *testing.T) { + fs := flag.NewFlagSet("test", flag.ContinueOnError) + fs.String("visible", "", "this shows up") + hidden := fs.String("hidden", "", "this should not show up") + _ = hidden + _ = fs.MarkHidden("hidden") + + rows := collectFlagRows(fs) + if len(rows) != 1 { + t.Fatalf("expected 1 row (hidden excluded), got %d", len(rows)) + } + if !findSubstring(rows[0][0], "visible") { + t.Errorf("expected visible flag in rows, got %q", rows[0][0]) + } +} + +func TestCollectFlagRows_UsageInRightColumn(t *testing.T) { + fs := flag.NewFlagSet("test", flag.ContinueOnError) + fs.String("format", "json", "output format: json or text") + + rows := collectFlagRows(fs) + if len(rows) != 1 { + t.Fatalf("expected 1 row, got %d", len(rows)) + } + if rows[0][1] != "output format: json or text" { + t.Errorf("expected usage in right column, got %q", rows[0][1]) + } +} diff --git a/cmd/picoclaw/internal/cliui/help_error.go b/cmd/picoclaw/internal/cliui/help_error.go index b0d6cf336..7972f91d3 100644 --- a/cmd/picoclaw/internal/cliui/help_error.go +++ b/cmd/picoclaw/internal/cliui/help_error.go @@ -67,7 +67,8 @@ func FormatCLIError(msg string, ctx *cobra.Command) string { func showErrHint(msg string) bool { m := strings.ToLower(msg) return strings.Contains(m, "unknown flag") || - strings.Contains(m, "flag") || + strings.Contains(m, "unknown shorthand flag") || + strings.Contains(m, "flag needs an argument") || strings.Contains(m, "invalid") || strings.Contains(m, "required") || strings.Contains(m, "usage:") diff --git a/cmd/picoclaw/internal/cliui/onboard.go b/cmd/picoclaw/internal/cliui/onboard.go index 36d40bc54..e74cf68c6 100644 --- a/cmd/picoclaw/internal/cliui/onboard.go +++ b/cmd/picoclaw/internal/cliui/onboard.go @@ -75,7 +75,7 @@ func printOnboardFancy(logo string, encrypt bool, configPath string) { // Same order as plain output: numbered steps → recommended → chat line. next := titleBarStyle().Render("Next steps") + "\n\n" + - bodyStyle().Width(inner - 4).Render(steps+"\n\n"+rec+"\n\n"+chat) + bodyStyle().Width(inner-4).Render(steps+"\n\n"+rec+"\n\n"+chat) fmt.Println(borderStyle().Width(inner).Render(next)) } diff --git a/cmd/picoclaw/internal/cliui/status.go b/cmd/picoclaw/internal/cliui/status.go index 4ab2c9d7f..f01fe296d 100644 --- a/cmd/picoclaw/internal/cliui/status.go +++ b/cmd/picoclaw/internal/cliui/status.go @@ -7,6 +7,12 @@ import ( "github.com/charmbracelet/lipgloss" ) +// ProviderRow holds one provider's display name and status value. +type ProviderRow struct { + Name string + Val string +} + // StatusReport is a structured status view for PrintStatus. type StatusReport struct { Logo string @@ -16,10 +22,8 @@ type StatusReport struct { ConfigOK bool WorkspacePath string WorkspaceOK bool - Model string - // ProviderNames and ProviderVals same length - ProviderNames []string - ProviderVals []string + Model string + Providers []ProviderRow OAuthLines []string // each full line "provider (method): state" } @@ -45,8 +49,8 @@ func printStatusPlain(r StatusReport) { if r.ConfigOK { fmt.Printf("Model: %s\n", r.Model) - for i := range r.ProviderNames { - fmt.Printf("%s: %s\n", r.ProviderNames[i], r.ProviderVals[i]) + for _, p := range r.Providers { + fmt.Printf("%s: %s\n", p.Name, p.Val) } if len(r.OAuthLines) > 0 { fmt.Println("\nOAuth/Token Auth:") @@ -80,7 +84,7 @@ func printStatusFancy(r StatusReport) { fmt.Println(topBox.Render(head.String())) fmt.Println() - if UseColumnLayout() && len(r.ProviderNames) > 0 && r.ConfigOK { + if UseColumnLayout() && len(r.Providers) > 0 && r.ConfigOK { leftW := (inner - 2) / 2 rightW := inner - leftW - 2 pathsNarrow := pathStatusPanel(r, leftW) @@ -89,7 +93,7 @@ func printStatusFancy(r StatusReport) { fmt.Println(lipgloss.JoinHorizontal(lipgloss.Top, pathsNarrow, gap, prov)) } else { fmt.Println(pathStatusPanel(r, inner)) - if len(r.ProviderNames) > 0 && r.ConfigOK { + if len(r.Providers) > 0 && r.ConfigOK { fmt.Println(providerTablePanel(r, inner)) } } @@ -130,7 +134,7 @@ func statusMark(ok bool) string { } func providerTablePanel(r StatusReport, colW int) string { - if len(r.ProviderNames) == 0 { + if len(r.Providers) == 0 { return "" } keyW := min(22, colW/3) @@ -144,9 +148,9 @@ func providerTablePanel(r StatusReport, colW int) string { var b strings.Builder b.WriteString(titleBarStyle().Render("Providers & local") + "\n\n") - for i := range r.ProviderNames { - k := lipgloss.NewStyle().Foreground(accentBlue).Bold(true).Width(keyW).Render(r.ProviderNames[i]) - v := styleProviderVal(r.ProviderVals[i]).Width(valW).Render(r.ProviderVals[i]) + for _, p := range r.Providers { + k := lipgloss.NewStyle().Foreground(accentBlue).Bold(true).Width(keyW).Render(p.Name) + v := styleProviderVal(p.Val).Width(valW).Render(p.Val) b.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, k, " ", v)) b.WriteString("\n") } diff --git a/cmd/picoclaw/internal/onboard/command.go b/cmd/picoclaw/internal/onboard/command.go index d2318df3b..4be19b2a5 100644 --- a/cmd/picoclaw/internal/onboard/command.go +++ b/cmd/picoclaw/internal/onboard/command.go @@ -6,7 +6,7 @@ import ( "github.com/spf13/cobra" ) -//go:generate bash -lc "rm -rf workspace && cp -r ../../../../workspace ./workspace" +//go:generate cp -r ../../../../workspace . //go:embed workspace var embeddedFiles embed.FS diff --git a/cmd/picoclaw/internal/status/helpers.go b/cmd/picoclaw/internal/status/helpers.go index 3f48b7004..361866275 100644 --- a/cmd/picoclaw/internal/status/helpers.go +++ b/cmd/picoclaw/internal/status/helpers.go @@ -98,53 +98,30 @@ func statusCmd() { } ollamaBase, hasOllama := findProtocolBase("ollama") - status := func(enabled bool) string { + val := func(enabled bool, extra ...string) string { if enabled { + if len(extra) > 0 && extra[0] != "" { + return "✓ " + extra[0] + } return "✓" } return "not set" } - report.ProviderNames = []string{ - "OpenRouter API", - "Anthropic API", - "OpenAI API", - "Gemini API", - "Zhipu API", - "Qwen API", - "Groq API", - "Moonshot API", - "DeepSeek API", - "VolcEngine API", - "Nvidia API", - } - report.ProviderVals = []string{ - status(hasOpenRouter), - status(hasAnthropic), - status(hasOpenAI), - status(hasGemini), - status(hasZhipu), - status(hasQwen), - status(hasGroq), - status(hasMoonshot), - status(hasDeepSeek), - status(hasVolcEngine), - status(hasNvidia), - } - - if hasVLLM { - report.ProviderNames = append(report.ProviderNames, "vLLM / local") - report.ProviderVals = append(report.ProviderVals, "✓ "+vllmBase) - } else { - report.ProviderNames = append(report.ProviderNames, "vLLM / local") - report.ProviderVals = append(report.ProviderVals, "not set") - } - if hasOllama { - report.ProviderNames = append(report.ProviderNames, "Ollama") - report.ProviderVals = append(report.ProviderVals, "✓ "+ollamaBase) - } else { - report.ProviderNames = append(report.ProviderNames, "Ollama") - report.ProviderVals = append(report.ProviderVals, "not set") + report.Providers = []cliui.ProviderRow{ + {"OpenRouter API", val(hasOpenRouter)}, + {"Anthropic API", val(hasAnthropic)}, + {"OpenAI API", val(hasOpenAI)}, + {"Gemini API", val(hasGemini)}, + {"Zhipu API", val(hasZhipu)}, + {"Qwen API", val(hasQwen)}, + {"Groq API", val(hasGroq)}, + {"Moonshot API", val(hasMoonshot)}, + {"DeepSeek API", val(hasDeepSeek)}, + {"VolcEngine API", val(hasVolcEngine)}, + {"Nvidia API", val(hasNvidia)}, + {"vLLM / local", val(hasVLLM, vllmBase)}, + {"Ollama", val(hasOllama, ollamaBase)}, } store, _ := auth.LoadStore() diff --git a/cmd/picoclaw/main.go b/cmd/picoclaw/main.go index 6f528ae94..5ab27d6b3 100644 --- a/cmd/picoclaw/main.go +++ b/cmd/picoclaw/main.go @@ -13,9 +13,9 @@ import ( "github.com/spf13/cobra" "github.com/sipeed/picoclaw/cmd/picoclaw/internal" - "github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/agent" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/auth" + "github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/cron" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/gateway" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/migrate" @@ -40,7 +40,8 @@ func earlyColorDisabled() bool { return true } for i := 1; i < len(os.Args); i++ { - if os.Args[i] == "--no-color" { + arg := os.Args[i] + if arg == "--no-color" || arg == "--no-color=true" || arg == "--no-color=1" { return true } }