diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 2c47f7d86..b9357aa2e 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -15,10 +15,10 @@ builds: - stdjson ldflags: - -s -w - - -X main.version={{ .Version }} - - -X main.gitCommit={{ .ShortCommit }} - - -X main.buildTime={{ .Date }} - - -X main.goVersion={{ .Env.GOVERSION }} + - -X github.com/sipeed/picoclaw/cmd/picoclaw/internal.version={{ .Version }} + - -X github.com/sipeed/picoclaw/cmd/picoclaw/internal.gitCommit={{ .ShortCommit }} + - -X github.com/sipeed/picoclaw/cmd/picoclaw/internal.buildTime={{ .Date }} + - -X github.com/sipeed/picoclaw/cmd/picoclaw/internal.goVersion={{ .Env.GOVERSION }} goos: - linux - windows diff --git a/Makefile b/Makefile index 629140168..fa572df9c 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,8 @@ VERSION?=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") GIT_COMMIT=$(shell git rev-parse --short=8 HEAD 2>/dev/null || echo "dev") BUILD_TIME=$(shell date +%FT%T%z) GO_VERSION=$(shell $(GO) version | awk '{print $$3}') -LDFLAGS=-ldflags "-X github.com/sipeed/picoclaw/cmd/picoclaw/internal.version=$(VERSION) -X github.com/sipeed/picoclaw/cmd/picoclaw/internal.gitCommit=$(GIT_COMMIT) -X github.com/sipeed/picoclaw/cmd/picoclaw/internal.buildTime=$(BUILD_TIME) -X github.com/sipeed/picoclaw/cmd/picoclaw/internal.goVersion=$(GO_VERSION) -s -w" +INTERNAL=github.com/sipeed/picoclaw/cmd/picoclaw/internal +LDFLAGS=-ldflags "-X $(INTERNAL).version=$(VERSION) -X $(INTERNAL).gitCommit=$(GIT_COMMIT) -X $(INTERNAL).buildTime=$(BUILD_TIME) -X $(INTERNAL).goVersion=$(GO_VERSION) -s -w" # Go variables GO?=go diff --git a/cmd/picoclaw/internal/agent/command_test.go b/cmd/picoclaw/internal/agent/command_test.go new file mode 100644 index 000000000..ef8d488b2 --- /dev/null +++ b/cmd/picoclaw/internal/agent/command_test.go @@ -0,0 +1,59 @@ +package agent + +import "testing" + +func TestNewAgentCommand(t *testing.T) { + cmd := NewAgentCommand() + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Use != "agent" { + t.Errorf("expected command name 'agent', got %q", cmd.Use) + } + + if cmd.Short != "Interact with the agent directly" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if len(cmd.Aliases) > 0 { + t.Errorf("expected command to have no aliases, got %d", len(cmd.Aliases)) + } + + if cmd.HasSubCommands() { + t.Error("expected command to have no subcommands") + } + + if cmd.Run != nil { + t.Error("expected command to have nil Run()") + } + + if cmd.RunE == nil { + t.Error("expected command to have non-nil RunE()") + } + + if cmd.PersistentPreRun != nil { + t.Error("expected command to have nil PersistentPreRun()") + } + + if cmd.PersistentPostRun != nil { + t.Error("expected command to have nil PersistentPostRun()") + } + + if !cmd.HasFlags() { + t.Error("expected command to have flags") + } + + if cmd.Flags().Lookup("debug") == nil { + t.Error("expected command to have debug flag") + } + + if cmd.Flags().Lookup("message") == nil { + t.Error("expected command to have message flag") + } + + if cmd.Flags().Lookup("session") == nil { + t.Error("expected command to have session flag") + } +} diff --git a/cmd/picoclaw/internal/auth/command_test.go b/cmd/picoclaw/internal/auth/command_test.go new file mode 100644 index 000000000..0b715e0ce --- /dev/null +++ b/cmd/picoclaw/internal/auth/command_test.go @@ -0,0 +1,87 @@ +package auth + +import "testing" + +func TestNewAuthCommand(t *testing.T) { + cmd := NewAuthCommand() + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Use != "auth" { + t.Errorf("expected command name 'auth', got %q", cmd.Use) + } + + if cmd.Short != "Manage authentication (login, logout, status)" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if len(cmd.Aliases) > 0 { + t.Errorf("expected command to have no aliases, got %d", len(cmd.Aliases)) + } + + if cmd.Run != nil { + t.Error("expected command to have nil Run()") + } + + if cmd.RunE == nil { + t.Error("expected command to have non-nil RunE()") + } + + if cmd.PersistentPreRun != nil { + t.Error("expected command to have nil PersistentPreRun()") + } + + if cmd.PersistentPostRun != nil { + t.Error("expected command to have nil PersistentPostRun()") + } + + if cmd.HasFlags() { + t.Error("expected command to have no flags") + } + + if !cmd.HasSubCommands() { + t.Error("expected command to have subcommands") + } + + allowedCommands := map[string]struct{}{ + "login": {}, + "logout": {}, + "status": {}, + } + + for _, subcmd := range cmd.Commands() { + if _, found := allowedCommands[subcmd.Name()]; !found { + t.Errorf("unexpected subcommand %q", subcmd.Name()) + } + + if len(subcmd.Aliases) > 0 { + t.Errorf("expected subcommand %q to have no aliases, got %d", subcmd.Name(), len(subcmd.Aliases)) + } + + if cmd.Hidden { + t.Errorf("expected subcommand %q to be visible", subcmd.Name()) + } + + if subcmd.HasSubCommands() { + t.Errorf("expected subcommand `%s` to have no subcommands", subcmd.Name()) + } + + if subcmd.Run != nil { + t.Errorf("expected subcommand `%s` to have nil Run()", subcmd.Name()) + } + + if subcmd.RunE == nil { + t.Errorf("expected subcommand `%s` to have non-nil RunE()", subcmd.Name()) + } + + if subcmd.PersistentPreRun != nil { + t.Errorf("expected subcommand `%s` to have nil PersistentPreRun()", subcmd.Name()) + } + + if subcmd.PersistentPostRun != nil { + t.Errorf("expected subcommand `%s` to have nil PersistentPostRun()", subcmd.Name()) + } + } +} diff --git a/cmd/picoclaw/internal/auth/helpers.go b/cmd/picoclaw/internal/auth/helpers.go index a646260ec..c273b71ba 100644 --- a/cmd/picoclaw/internal/auth/helpers.go +++ b/cmd/picoclaw/internal/auth/helpers.go @@ -9,24 +9,6 @@ import ( "github.com/sipeed/picoclaw/pkg/config" ) -func authHelp() { - fmt.Println("\nAuth commands:") - fmt.Println(" login Login via OAuth or paste token") - fmt.Println(" logout Remove stored credentials") - fmt.Println(" status Show current auth status") - fmt.Println() - fmt.Println("Login options:") - fmt.Println(" --provider Provider to login with (openai, anthropic)") - fmt.Println(" --device-code Use device code flow (for headless environments)") - fmt.Println() - fmt.Println("Examples:") - fmt.Println(" picoclaw auth login --provider openai") - fmt.Println(" picoclaw auth login --provider openai --device-code") - fmt.Println(" picoclaw auth login --provider anthropic") - fmt.Println(" picoclaw auth logout --provider openai") - fmt.Println(" picoclaw auth status") -} - func authLoginCmd(provider string, useDeviceCode bool) error { switch provider { case "openai": diff --git a/cmd/picoclaw/internal/auth/login_test.go b/cmd/picoclaw/internal/auth/login_test.go new file mode 100644 index 000000000..84c61df66 --- /dev/null +++ b/cmd/picoclaw/internal/auth/login_test.go @@ -0,0 +1,42 @@ +package auth + +import ( + "testing" + + "github.com/spf13/cobra" +) + +func TestNewLoginSubCommand(t *testing.T) { + cmd := newLoginCommand() + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Short != "Login via OAuth or paste token" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if !cmd.HasFlags() { + t.Error("expected command to have flags") + } + + if cmd.Flags().Lookup("device-code") == nil { + t.Error("expected command to have device-code flag") + } + + hasProviderFlag := cmd.Flags().Lookup("provider") != nil + if !hasProviderFlag { + t.Error("expected command to have provider flag") + } else { + var val []string + var found bool + providerFlag := cmd.Flag("provider") + + val, found = providerFlag.Annotations[cobra.BashCompOneRequiredFlag] + + if !found || val[0] != "true" { + t.Errorf("expected provider flag to be required, got %v", val) + } + } +} diff --git a/cmd/picoclaw/internal/auth/logout_test.go b/cmd/picoclaw/internal/auth/logout_test.go new file mode 100644 index 000000000..9edcf2680 --- /dev/null +++ b/cmd/picoclaw/internal/auth/logout_test.go @@ -0,0 +1,23 @@ +package auth + +import "testing" + +func TestNewLogoutSubcommand(t *testing.T) { + cmd := newLogoutCommand() + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Short != "Remove stored credentials" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if !cmd.HasFlags() { + t.Error("expected command to have flags") + } + + if cmd.Flags().Lookup("provider") == nil { + t.Error("expected command to have provider flag") + } +} diff --git a/cmd/picoclaw/internal/auth/status_test.go b/cmd/picoclaw/internal/auth/status_test.go new file mode 100644 index 000000000..fe065b45b --- /dev/null +++ b/cmd/picoclaw/internal/auth/status_test.go @@ -0,0 +1,19 @@ +package auth + +import "testing" + +func TestNewStatusSubcommand(t *testing.T) { + cmd := newStatusCommand() + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Short != "Show current auth status" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if cmd.HasFlags() { + t.Error("expected command to have no flags") + } +} diff --git a/cmd/picoclaw/internal/cron/add_test.go b/cmd/picoclaw/internal/cron/add_test.go new file mode 100644 index 000000000..4916b5179 --- /dev/null +++ b/cmd/picoclaw/internal/cron/add_test.go @@ -0,0 +1,83 @@ +package cron + +import ( + "testing" + + "github.com/spf13/cobra" +) + +func TestNewAddSubcommand(t *testing.T) { + fn := func() string { return "" } + cmd := newAddCommand(fn) + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Use != "add" { + t.Errorf("expected command name 'add', got %q", cmd.Use) + } + + if cmd.Short != "Add a new scheduled job" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if !cmd.HasFlags() { + t.Error("expected command to have flags") + } + + if cmd.Flags().Lookup("every") == nil { + t.Error("expected command to have every flag") + } + + if cmd.Flags().Lookup("cron") == nil { + t.Error("expected command to have cron flag") + } + + if cmd.Flags().Lookup("deliver") == nil { + t.Error("expected command to have deliver flag") + } + + if cmd.Flags().Lookup("to") == nil { + t.Error("expected command to have to flag") + } + + if cmd.Flags().Lookup("channel") == nil { + t.Error("expected command to have channel flag") + } + + hasNameFlag := cmd.Flags().Lookup("name") != nil + hasMessageFlag := cmd.Flags().Lookup("message") != nil + + if !hasNameFlag { + t.Error("expected command to have name flag") + } + + if !hasMessageFlag { + t.Error("expected command to have message flag") + } + + if hasNameFlag { + var val []string + var found bool + nameFlag := cmd.Flag("name") + + val, found = nameFlag.Annotations[cobra.BashCompOneRequiredFlag] + + if !found || val[0] != "true" { + t.Errorf("expected name flag to be required, got %v", val) + } + } + + if hasMessageFlag { + var val []string + var found bool + messageFlag := cmd.Flag("message") + + val, found = messageFlag.Annotations[cobra.BashCompOneRequiredFlag] + + if !found || val[0] != "true" { + t.Errorf("expected message flag to be required, got %v", val) + } + } +} diff --git a/cmd/picoclaw/internal/cron/command.go b/cmd/picoclaw/internal/cron/command.go index a8975daf9..1bf537e5d 100644 --- a/cmd/picoclaw/internal/cron/command.go +++ b/cmd/picoclaw/internal/cron/command.go @@ -33,8 +33,8 @@ func NewCronCommand() *cobra.Command { newListCommand(func() string { return storePath }), newAddCommand(func() string { return storePath }), newRemoveCommand(func() string { return storePath }), - newEnableCommand(func() string { return storePath }, false), - newEnableCommand(func() string { return storePath }, true), + newEnableCommand(func() string { return storePath }), + newEnableCommand(func() string { return storePath }), ) return cmd diff --git a/cmd/picoclaw/internal/cron/command_test.go b/cmd/picoclaw/internal/cron/command_test.go new file mode 100644 index 000000000..f19ef67d5 --- /dev/null +++ b/cmd/picoclaw/internal/cron/command_test.go @@ -0,0 +1,93 @@ +package cron + +import "testing" + +func TestNewCronCommand(t *testing.T) { + cmd := NewCronCommand() + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Short != "Manage scheduled tasks" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if len(cmd.Aliases) != 1 { + t.Errorf("expected command to have exactly one alias, got %d", len(cmd.Aliases)) + } + + if !cmd.HasAlias("c") { + t.Errorf("expected command to have alias `c`, got %v", cmd.Aliases) + } + + if cmd.HasFlags() { + t.Error("expected command to have no flags") + } + + if cmd.Run != nil { + t.Error("expected command to have nil Run()") + } + + if cmd.RunE == nil { + t.Error("expected command to have non-nil RunE()") + } + + if cmd.PersistentPreRunE == nil { + t.Error("expected command to have non-nil PersistentPreRunE()") + } + + if cmd.PersistentPreRun != nil { + t.Error("expected command to have nil PersistentPreRun()") + } + + if cmd.PersistentPostRun != nil { + t.Error("expected command to have nil PersistentPostRun()") + } + + if !cmd.HasSubCommands() { + t.Error("expected command to have subcommands") + } + + allowedCommands := map[string]struct{}{ + "list": {}, + "add": {}, + "remove": {}, + "enable": {}, + "disable": {}, + } + + for _, subcmd := range cmd.Commands() { + if _, found := allowedCommands[subcmd.Name()]; !found { + t.Errorf("unexpected subcommand %q", subcmd.Name()) + } + + if len(subcmd.Aliases) > 0 { + t.Errorf("expected subcommand %q to have no aliases, got %d", subcmd.Name(), len(subcmd.Aliases)) + } + + if cmd.Hidden { + t.Errorf("expected subcommand %q to be visible", subcmd.Name()) + } + + if subcmd.HasSubCommands() { + t.Errorf("expected subcommand `%s` to have no subcommands", subcmd.Name()) + } + + if subcmd.Run != nil { + t.Errorf("expected subcommand `%s` to have nil Run()", subcmd.Name()) + } + + if subcmd.RunE == nil { + t.Errorf("expected subcommand `%s` to have non-nil RunE()", subcmd.Name()) + } + + if subcmd.PersistentPreRun != nil { + t.Errorf("expected subcommand `%s` to have nil PersistentPreRun()", subcmd.Name()) + } + + if subcmd.PersistentPostRun != nil { + t.Errorf("expected subcommand `%s` to have nil PersistentPostRun()", subcmd.Name()) + } + } +} diff --git a/cmd/picoclaw/internal/cron/disable.go b/cmd/picoclaw/internal/cron/disable.go new file mode 100644 index 000000000..047771ed6 --- /dev/null +++ b/cmd/picoclaw/internal/cron/disable.go @@ -0,0 +1,16 @@ +package cron + +import "github.com/spf13/cobra" + +func newDisableCommand(storePath func() string) *cobra.Command { + return &cobra.Command{ + Use: "disable", + Short: "Disable a job", + Args: cobra.ExactArgs(1), + Example: `picoclaw cron disable 1`, + RunE: func(_ *cobra.Command, args []string) error { + cronEnableCmd(storePath(), true, args[0]) + return nil + }, + } +} diff --git a/cmd/picoclaw/internal/cron/disable_test.go b/cmd/picoclaw/internal/cron/disable_test.go new file mode 100644 index 000000000..fba4fd5da --- /dev/null +++ b/cmd/picoclaw/internal/cron/disable_test.go @@ -0,0 +1,24 @@ +package cron + +import "testing" + +func TestDisableSubcommand(t *testing.T) { + fn := func() string { return "" } + cmd := newDisableCommand(fn) + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Use != "disable" { + t.Errorf("expected command name 'disable', got %q", cmd.Use) + } + + if cmd.Short != "Disable a job" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if !cmd.HasExample() { + t.Error("expected command to have example") + } +} diff --git a/cmd/picoclaw/internal/cron/enable.go b/cmd/picoclaw/internal/cron/enable.go index c922acbb2..5a61d58f3 100644 --- a/cmd/picoclaw/internal/cron/enable.go +++ b/cmd/picoclaw/internal/cron/enable.go @@ -2,20 +2,14 @@ package cron import "github.com/spf13/cobra" -func newEnableCommand(storePath func() string, disable bool) *cobra.Command { - name := "enable" - short := "Enable a job" - if disable { - name = "disable" - short = "Disable a job" - } - +func newEnableCommand(storePath func() string) *cobra.Command { return &cobra.Command{ - Use: name + " ", - Short: short, - Args: cobra.ExactArgs(1), + Use: "enable", + Short: "Enable a job", + Args: cobra.ExactArgs(1), + Example: `picoclaw cron enable 1`, RunE: func(_ *cobra.Command, args []string) error { - cronEnableCmd(storePath(), disable, args[0]) + cronEnableCmd(storePath(), false, args[0]) return nil }, } diff --git a/cmd/picoclaw/internal/cron/enable_test.go b/cmd/picoclaw/internal/cron/enable_test.go new file mode 100644 index 000000000..484e8b207 --- /dev/null +++ b/cmd/picoclaw/internal/cron/enable_test.go @@ -0,0 +1,24 @@ +package cron + +import "testing" + +func TestEnableSubcommand(t *testing.T) { + fn := func() string { return "" } + cmd := newEnableCommand(fn) + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Use != "enable" { + t.Errorf("expected command name 'enable', got %q", cmd.Use) + } + + if cmd.Short != "Enable a job" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if !cmd.HasExample() { + t.Error("expected command to have example") + } +} diff --git a/cmd/picoclaw/internal/cron/list_test.go b/cmd/picoclaw/internal/cron/list_test.go new file mode 100644 index 000000000..8270e4cf6 --- /dev/null +++ b/cmd/picoclaw/internal/cron/list_test.go @@ -0,0 +1,16 @@ +package cron + +import "testing" + +func TestNewListSubcommand(t *testing.T) { + fn := func() string { return "" } + cmd := newListCommand(fn) + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Short != "List all scheduled jobs" { + t.Errorf("expected command short description, got %q", cmd.Short) + } +} diff --git a/cmd/picoclaw/internal/cron/remove.go b/cmd/picoclaw/internal/cron/remove.go index e35796b85..5f1d1a04b 100644 --- a/cmd/picoclaw/internal/cron/remove.go +++ b/cmd/picoclaw/internal/cron/remove.go @@ -4,9 +4,10 @@ import "github.com/spf13/cobra" func newRemoveCommand(storePath func() string) *cobra.Command { cmd := &cobra.Command{ - Use: "remove ", - Short: "Remove a job by ID", - Args: cobra.ExactArgs(1), + Use: "remove", + Short: "Remove a job by ID", + Args: cobra.ExactArgs(1), + Example: `picoclaw cron remove 1`, RunE: func(_ *cobra.Command, args []string) error { cronRemoveCmd(storePath(), args[0]) return nil diff --git a/cmd/picoclaw/internal/cron/remove_test.go b/cmd/picoclaw/internal/cron/remove_test.go new file mode 100644 index 000000000..005702641 --- /dev/null +++ b/cmd/picoclaw/internal/cron/remove_test.go @@ -0,0 +1,20 @@ +package cron + +import "testing" + +func TestNewRemoveSubcommand(t *testing.T) { + fn := func() string { return "" } + cmd := newRemoveCommand(fn) + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Short != "Remove a job by ID" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if !cmd.HasExample() { + t.Error("expected command to have example") + } +} diff --git a/cmd/picoclaw/internal/gateway/command_test.go b/cmd/picoclaw/internal/gateway/command_test.go new file mode 100644 index 000000000..cb8575c53 --- /dev/null +++ b/cmd/picoclaw/internal/gateway/command_test.go @@ -0,0 +1,55 @@ +package gateway + +import "testing" + +func TestNewGatewayCommand(t *testing.T) { + cmd := NewGatewayCommand() + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Use != "gateway" { + t.Errorf("expected command name 'gateway', got %q", cmd.Use) + } + + if cmd.Short != "Start picoclaw gateway" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if len(cmd.Aliases) != 1 { + t.Errorf("expected command to have 1 alias, got %d", len(cmd.Aliases)) + } + + if !cmd.HasAlias("g") { + t.Errorf("expected command to have alias 'g'") + } + + if cmd.Run != nil { + t.Error("expected command to have nil Run()") + } + + if cmd.RunE == nil { + t.Error("expected command to have non-nil RunE()") + } + + if cmd.PersistentPreRun != nil { + t.Error("expected command to have nil PersistentPreRun()") + } + + if cmd.PersistentPostRun != nil { + t.Error("expected command to have nil PersistentPostRun()") + } + + if cmd.HasSubCommands() { + t.Error("expected command to have no subcommands") + } + + if !cmd.HasFlags() { + t.Error("expected command to have flags") + } + + if cmd.Flags().Lookup("debug") == nil { + t.Error("expected command to have debug flag") + } +} diff --git a/cmd/picoclaw/internal/helpers_test.go b/cmd/picoclaw/internal/helpers_test.go new file mode 100644 index 000000000..57743c465 --- /dev/null +++ b/cmd/picoclaw/internal/helpers_test.go @@ -0,0 +1,132 @@ +package internal + +import ( + "path/filepath" + "runtime" + "strings" + "testing" +) + +func TestGetConfigPath(t *testing.T) { + t.Setenv("HOME", "/tmp/home") + + got := GetConfigPath() + want := filepath.Join("/tmp/home", ".picoclaw", "config.json") + + if got != want { + t.Fatalf("GetConfigPath() = %q, want %q", got, want) + } +} + +func TestFormatVersion_NoGitCommit(t *testing.T) { + oldVersion, oldGit := version, gitCommit + + t.Cleanup(func() { + version, gitCommit = oldVersion, oldGit + }) + + version = "1.2.3" + gitCommit = "" + + got := FormatVersion() + want := "1.2.3" + + if got != want { + t.Fatalf("FormatVersion() = %q, want %q", got, want) + } +} + +func TestFormatVersion_WithGitCommit(t *testing.T) { + oldVersion, oldGit := version, gitCommit + + t.Cleanup(func() { + version, gitCommit = oldVersion, oldGit + }) + + version = "1.2.3" + gitCommit = "abc123" + + got := FormatVersion() + want := "1.2.3 (git: abc123)" + + if got != want { + t.Fatalf("FormatVersion() = %q, want %q", got, want) + } +} + +func TestFormatBuildInfo_UsesBuildTimeAndGoVersion_WhenSet(t *testing.T) { + oldBuildTime, oldGoVersion := buildTime, goVersion + + t.Cleanup(func() { + buildTime, goVersion = oldBuildTime, oldGoVersion + }) + + buildTime = "2026-02-20T00:00:00Z" + goVersion = "go1.23.0" + + build, goVer := FormatBuildInfo() + + if build != buildTime { + t.Fatalf("FormatBuildInfo().build = %q, want %q", build, buildTime) + } + + if goVer != goVersion { + t.Fatalf("FormatBuildInfo().goVer = %q, want %q", goVer, goVersion) + } +} + +func TestFormatBuildInfo_EmptyBuildTime_ReturnsEmptyBuild(t *testing.T) { + oldBuildTime, oldGoVersion := buildTime, goVersion + + t.Cleanup(func() { + buildTime, goVersion = oldBuildTime, oldGoVersion + }) + + buildTime = "" + goVersion = "go1.23.0" + + build, goVer := FormatBuildInfo() + + if build != "" { + t.Fatalf("FormatBuildInfo().build = %q, want empty", build) + } + + if goVer != goVersion { + t.Fatalf("FormatBuildInfo().goVer = %q, want %q", goVer, goVersion) + } +} + +func TestFormatBuildInfo_EmptyGoVersion_FallsBackToRuntimeVersion(t *testing.T) { + oldBuildTime, oldGoVersion := buildTime, goVersion + + t.Cleanup(func() { + buildTime, goVersion = oldBuildTime, oldGoVersion + }) + + buildTime = "x" + goVersion = "" + + build, goVer := FormatBuildInfo() + if build != "x" { + t.Fatalf("FormatBuildInfo().build = %q, want %q", build, "x") + } + + if goVer != runtime.Version() { + t.Fatalf("FormatBuildInfo().goVer = %q, want runtime.Version()=%q", goVer, runtime.Version()) + } +} + +func TestGetConfigPath_Windows(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("windows-specific HOME behavior varies; run on windows") + } + + t.Setenv("USERPROFILE", `C:\Users\Test`) + + got := GetConfigPath() + want := filepath.Join(`C:\Users\Test`, ".picoclaw", "config.json") + + if !strings.EqualFold(got, want) { + t.Fatalf("GetConfigPath() = %q, want %q", got, want) + } +} diff --git a/cmd/picoclaw/internal/migrate/command_test.go b/cmd/picoclaw/internal/migrate/command_test.go new file mode 100644 index 000000000..e4ba8d151 --- /dev/null +++ b/cmd/picoclaw/internal/migrate/command_test.go @@ -0,0 +1,79 @@ +package migrate + +import "testing" + +func TestNewMigrateCommand(t *testing.T) { + cmd := NewMigrateCommand() + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Use != "migrate" { + t.Errorf("expected command name 'migrate', got %q", cmd.Use) + } + + if cmd.Short != "Migrate from OpenClaw to PicoClaw" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if len(cmd.Aliases) > 0 { + t.Errorf("expected command to have no aliases, got %d", len(cmd.Aliases)) + } + + if !cmd.HasExample() { + t.Error("expected command to have example") + } + + if cmd.HasSubCommands() { + t.Error("expected command to have no subcommands") + } + + if cmd.Run != nil { + t.Error("expected command to have nil Run()") + } + + if cmd.RunE == nil { + t.Error("expected command to have non-nil RunE()") + } + + if cmd.PersistentPreRun != nil { + t.Error("expected command to have nil PersistentPreRun()") + } + + if cmd.PersistentPostRun != nil { + t.Error("expected command to have nil PersistentPostRun()") + } + + if !cmd.HasFlags() { + t.Error("expected command to have flags") + } + + if cmd.Flags().Lookup("dry-run") == nil { + t.Error("expected command to have dry-run flag") + } + + if cmd.Flags().Lookup("refresh") == nil { + t.Error("expected command to have refresh flag") + } + + if cmd.Flags().Lookup("config-only") == nil { + t.Error("expected command to have config-only flag") + } + + if cmd.Flags().Lookup("workspace-only") == nil { + t.Error("expected command to have workspace-only flag") + } + + if cmd.Flags().Lookup("force") == nil { + t.Error("expected command to have force flag") + } + + if cmd.Flags().Lookup("openclaw-home") == nil { + t.Error("expected command to have openclaw-home flag") + } + + if cmd.Flags().Lookup("picoclaw-home") == nil { + t.Error("expected command to have picoclaw-home flag") + } +} diff --git a/cmd/picoclaw/internal/onboard/command_test.go b/cmd/picoclaw/internal/onboard/command_test.go new file mode 100644 index 000000000..4ef69f5a6 --- /dev/null +++ b/cmd/picoclaw/internal/onboard/command_test.go @@ -0,0 +1,51 @@ +package onboard + +import "testing" + +func TestNewOnboardCommand(t *testing.T) { + cmd := NewOnboardCommand() + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Use != "onboard" { + t.Errorf("expected command name 'onboard', got %q", cmd.Use) + } + + if cmd.Short != "Initialize picoclaw configuration and workspace" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if len(cmd.Aliases) != 1 { + t.Errorf("expected command to have 1 alias, got %d", len(cmd.Aliases)) + } + + if !cmd.HasAlias("o") { + t.Errorf("expected command to have alias 'o'") + } + + if cmd.Run == nil { + t.Error("expected command to have non-nil Run()") + } + + if cmd.RunE != nil { + t.Error("expected command to have nil RunE()") + } + + if cmd.PersistentPreRun != nil { + t.Error("expected command to have nil PersistentPreRun()") + } + + if cmd.PersistentPostRun != nil { + t.Error("expected command to have nil PersistentPostRun()") + } + + if cmd.HasFlags() { + t.Error("expected command to have no flags") + } + + if cmd.HasSubCommands() { + t.Error("expected command to have no subcommands") + } +} diff --git a/cmd/picoclaw/internal/skills/command.go b/cmd/picoclaw/internal/skills/command.go index fa3f37719..3818c4964 100644 --- a/cmd/picoclaw/internal/skills/command.go +++ b/cmd/picoclaw/internal/skills/command.go @@ -12,7 +12,7 @@ import ( func NewSkillsCommand() *cobra.Command { cmd := &cobra.Command{ Use: "skills", - Short: "Manage skills (install, list, remove)", + Short: "Manage skills", RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Help() }, diff --git a/cmd/picoclaw/internal/skills/install_test.go b/cmd/picoclaw/internal/skills/install_test.go new file mode 100644 index 000000000..e2b507b68 --- /dev/null +++ b/cmd/picoclaw/internal/skills/install_test.go @@ -0,0 +1,39 @@ +package skills + +import "testing" + +func TestNewInstallSubcommand(t *testing.T) { + cmd := newInstallCommand(nil) + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Use != "install" { + t.Errorf("expected command name 'install', got %q", cmd.Use) + } + + if cmd.Short != "Install skill from GitHub" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if cmd.Run == nil { + t.Error("expected command to have non-nil Run()") + } + + if !cmd.HasExample() { + t.Error("expected command to have example") + } + + if cmd.HasSubCommands() { + t.Error("expected command to have no subcommands") + } + + if cmd.HasFlags() { + t.Error("expected command to have no flags") + } + + if len(cmd.Aliases) > 0 { + t.Errorf("expected command to have no aliases, got %d", len(cmd.Aliases)) + } +} diff --git a/cmd/picoclaw/internal/skills/installbuiltin_test.go b/cmd/picoclaw/internal/skills/installbuiltin_test.go new file mode 100644 index 000000000..a859ff99f --- /dev/null +++ b/cmd/picoclaw/internal/skills/installbuiltin_test.go @@ -0,0 +1,39 @@ +package skills + +import "testing" + +func TestNewInstallbuiltinSubcommand(t *testing.T) { + cmd := newInstallBuiltinCommand("") + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Use != "install-builtin" { + t.Errorf("expected command name 'install-builtin', got %q", cmd.Use) + } + + if cmd.Short != "Install all builtin skills to workspace" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if cmd.Run == nil { + t.Error("expected command to have non-nil Run()") + } + + if !cmd.HasExample() { + t.Error("expected command to have example") + } + + if cmd.HasSubCommands() { + t.Error("expected command to have no subcommands") + } + + if cmd.HasFlags() { + t.Error("expected command to have no flags") + } + + if len(cmd.Aliases) > 0 { + t.Errorf("expected command to have no aliases, got %d", len(cmd.Aliases)) + } +} diff --git a/cmd/picoclaw/internal/skills/list_test.go b/cmd/picoclaw/internal/skills/list_test.go new file mode 100644 index 000000000..118db4062 --- /dev/null +++ b/cmd/picoclaw/internal/skills/list_test.go @@ -0,0 +1,39 @@ +package skills + +import "testing" + +func TestNewListSubcommand(t *testing.T) { + cmd := newListCommand(nil) + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Use != "list" { + t.Errorf("expected command name 'list', got %q", cmd.Use) + } + + if cmd.Short != "List installed skills" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if cmd.Run == nil { + t.Error("expected command to have non-nil Run()") + } + + if !cmd.HasExample() { + t.Error("expected command to have example") + } + + if cmd.HasSubCommands() { + t.Error("expected command to have no subcommands") + } + + if cmd.HasFlags() { + t.Error("expected command to have no flags") + } + + if len(cmd.Aliases) > 0 { + t.Errorf("expected command to have no aliases, got %d", len(cmd.Aliases)) + } +} diff --git a/cmd/picoclaw/internal/skills/listbuiltin_test.go b/cmd/picoclaw/internal/skills/listbuiltin_test.go new file mode 100644 index 000000000..26fa7249c --- /dev/null +++ b/cmd/picoclaw/internal/skills/listbuiltin_test.go @@ -0,0 +1,39 @@ +package skills + +import "testing" + +func TestNewListbuiltinSubcommand(t *testing.T) { + cmd := newListBuiltinCommand() + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Use != "list-builtin" { + t.Errorf("expected command name 'list-builtin', got %q", cmd.Use) + } + + if cmd.Short != "List available builtin skills" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if cmd.Run == nil { + t.Error("expected command to have non-nil Run()") + } + + if !cmd.HasExample() { + t.Error("expected command to have example") + } + + if cmd.HasSubCommands() { + t.Error("expected command to have no subcommands") + } + + if cmd.HasFlags() { + t.Error("expected command to have no flags") + } + + if len(cmd.Aliases) > 0 { + t.Errorf("expected command to have no aliases, got %d", len(cmd.Aliases)) + } +} diff --git a/cmd/picoclaw/internal/skills/remove_test.go b/cmd/picoclaw/internal/skills/remove_test.go new file mode 100644 index 000000000..54af059e4 --- /dev/null +++ b/cmd/picoclaw/internal/skills/remove_test.go @@ -0,0 +1,47 @@ +package skills + +import "testing" + +func TestNewRemoveSubcommand(t *testing.T) { + cmd := newRemoveCommand(nil) + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Use != "remove" { + t.Errorf("expected command name 'remove', got %q", cmd.Use) + } + + if cmd.Short != "Remove installed skill" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if cmd.Run == nil { + t.Error("expected command to have non-nil Run()") + } + + if !cmd.HasExample() { + t.Error("expected command to have example") + } + + if cmd.HasSubCommands() { + t.Error("expected command to have no subcommands") + } + + if cmd.HasFlags() { + t.Error("expected command to have no flags") + } + + if len(cmd.Aliases) != 2 { + t.Errorf("expected command to have 2 aliases, got %d", len(cmd.Aliases)) + } + + if !cmd.HasAlias("rm") { + t.Errorf("expected command to have alias 'rm'") + } + + if !cmd.HasAlias("uninstall") { + t.Errorf("expected command to have alias 'uninstall'") + } +} diff --git a/cmd/picoclaw/internal/skills/search_test.go b/cmd/picoclaw/internal/skills/search_test.go new file mode 100644 index 000000000..aad179908 --- /dev/null +++ b/cmd/picoclaw/internal/skills/search_test.go @@ -0,0 +1,35 @@ +package skills + +import "testing" + +func TestNewSearchSubcommand(t *testing.T) { + cmd := newSearchCommand(nil) + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Use != "search" { + t.Errorf("expected command name 'search', got %q", cmd.Use) + } + + if cmd.Short != "Search available skills" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if cmd.Run == nil { + t.Error("expected command to have non-nil Run()") + } + + if cmd.HasSubCommands() { + t.Error("expected command to have no subcommands") + } + + if cmd.HasFlags() { + t.Error("expected command to have no flags") + } + + if len(cmd.Aliases) > 0 { + t.Errorf("expected command to have no aliases, got %d", len(cmd.Aliases)) + } +} diff --git a/cmd/picoclaw/internal/skills/show_test.go b/cmd/picoclaw/internal/skills/show_test.go new file mode 100644 index 000000000..534b8be21 --- /dev/null +++ b/cmd/picoclaw/internal/skills/show_test.go @@ -0,0 +1,39 @@ +package skills + +import "testing" + +func TestNewShowSubcommand(t *testing.T) { + cmd := newShowCommand(nil) + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Use != "show" { + t.Errorf("expected command name 'show', got %q", cmd.Use) + } + + if cmd.Short != "Show skill details" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if cmd.Run == nil { + t.Error("expected command to have non-nil Run()") + } + + if !cmd.HasExample() { + t.Error("expected command to have example") + } + + if cmd.HasSubCommands() { + t.Error("expected command to have no subcommands") + } + + if cmd.HasFlags() { + t.Error("expected command to have no flags") + } + + if len(cmd.Aliases) > 0 { + t.Errorf("expected command to have no aliases, got %d", len(cmd.Aliases)) + } +} diff --git a/cmd/picoclaw/internal/skills/skills_test.go b/cmd/picoclaw/internal/skills/skills_test.go new file mode 100644 index 000000000..8ae273c62 --- /dev/null +++ b/cmd/picoclaw/internal/skills/skills_test.go @@ -0,0 +1,47 @@ +package skills + +import "testing" + +func TestNewSkillsCommand(t *testing.T) { + cmd := NewSkillsCommand() + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Use != "skills" { + t.Errorf("expected command name 'skills', got %q", cmd.Use) + } + + if cmd.Short != "Manage skills" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if len(cmd.Aliases) > 0 { + t.Errorf("expected command to have no aliases, got %d", len(cmd.Aliases)) + } + + if cmd.HasFlags() { + t.Error("expected command to have no flags") + } + + if cmd.Run != nil { + t.Error("expected command to have nil Run()") + } + + if cmd.RunE == nil { + t.Error("expected command to have non-nil RunE()") + } + + if cmd.PersistentPreRunE == nil { + t.Error("expected command to have persistent pre-run hook") + } + + if cmd.PersistentPreRun != nil { + t.Error("expected command to have nil PersistentPreRun()") + } + + if cmd.PersistentPostRun != nil { + t.Error("expected command to have nil PersistentPostRun()") + } +} diff --git a/cmd/picoclaw/internal/status/command_test.go b/cmd/picoclaw/internal/status/command_test.go new file mode 100644 index 000000000..ebad99388 --- /dev/null +++ b/cmd/picoclaw/internal/status/command_test.go @@ -0,0 +1,47 @@ +package status + +import "testing" + +func TestNewStatusCommand(t *testing.T) { + cmd := NewStatusCommand() + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Use != "status" { + t.Errorf("expected command name 'status', got %q", cmd.Use) + } + + if len(cmd.Aliases) != 1 { + t.Errorf("expected command to have 1 alias, got %d", len(cmd.Aliases)) + } + + if !cmd.HasAlias("s") { + t.Errorf("expected command to have alias 's'") + } + + if cmd.Short != "Show picoclaw status" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if cmd.HasSubCommands() { + t.Error("expected command to have no subcommands") + } + + if cmd.Run == nil { + t.Error("expected command to have non-nil Run()") + } + + if cmd.RunE != nil { + t.Error("expected command to have nil RunE()") + } + + if cmd.PersistentPreRun != nil { + t.Error("expected command to have nil PersistentPreRun()") + } + + if cmd.PersistentPostRun != nil { + t.Error("expected command to have nil PersistentPostRun()") + } +} diff --git a/cmd/picoclaw/internal/version/command_test.go b/cmd/picoclaw/internal/version/command_test.go new file mode 100644 index 000000000..305d93ff0 --- /dev/null +++ b/cmd/picoclaw/internal/version/command_test.go @@ -0,0 +1,51 @@ +package version + +import "testing" + +func TestNewVersionCommand(t *testing.T) { + cmd := NewVersionCommand() + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Use != "version" { + t.Errorf("expected command name 'version', got %q", cmd.Use) + } + + if len(cmd.Aliases) != 1 { + t.Errorf("expected command to have 1 alias, got %d", len(cmd.Aliases)) + } + + if !cmd.HasAlias("v") { + t.Errorf("expected command to have alias 'v'") + } + + if cmd.HasFlags() { + t.Error("expected command to have no flags") + } + + if cmd.Short != "Show version information" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if cmd.HasSubCommands() { + t.Error("expected command to have no subcommands") + } + + if cmd.Run == nil { + t.Error("expected command to have non-nil Run()") + } + + if cmd.RunE != nil { + t.Error("expected command to have nil RunE()") + } + + if cmd.PersistentPreRun != nil { + t.Error("expected command to have nil PersistentPreRun()") + } + + if cmd.PersistentPostRun != nil { + t.Error("expected command to have nil PersistentPostRun()") + } +} diff --git a/cmd/picoclaw/main_test.go b/cmd/picoclaw/main_test.go new file mode 100644 index 000000000..2969dd0b7 --- /dev/null +++ b/cmd/picoclaw/main_test.go @@ -0,0 +1,71 @@ +package main + +import ( + "testing" +) + +func TestNewPicoclawCommand(t *testing.T) { + cmd := NewPicoclawCommand() + + if cmd == nil { + t.Fatalf("expected non-nil command") + } + + if cmd.Use != "picoclaw" { + t.Errorf("expected command name 'picoclaw', got %q", cmd.Use) + } + + if cmd.Short != "picoclaw — Personal AI Assistant" { + t.Errorf("expected command short description, got %q", cmd.Short) + } + + if !cmd.HasSubCommands() { + t.Error("expected command to have subcommands") + } + + if !cmd.HasAvailableSubCommands() { + t.Error("expected command to have available subcommands") + } + + if cmd.HasFlags() { + t.Error("expected command to have no flags") + } + + if cmd.Run != nil { + t.Error("expected command to have nil Run()") + } + + if cmd.RunE != nil { + t.Error("expected command to have nil RunE()") + } + + if cmd.PersistentPreRun != nil { + t.Error("expected command to have nil PersistentPreRun()") + } + + if cmd.PersistentPostRun != nil { + t.Error("expected command to have nil PersistentPostRun()") + } + + allowedCommands := map[string]struct{}{ + "agent": {}, + "auth": {}, + "cron": {}, + "gateway": {}, + "migrate": {}, + "onboard": {}, + "skills": {}, + "status": {}, + "version": {}, + } + + for _, subcmd := range cmd.Commands() { + if _, found := allowedCommands[subcmd.Name()]; !found { + t.Errorf("unexpected subcommand %q", subcmd.Name()) + } + + if cmd.Hidden { + t.Errorf("expected subcommand %q to be visible", subcmd.Name()) + } + } +}