package cli import ( "bytes" "context" "path/filepath" "strconv" "strings" "testing" "time" "github.com/openclaw/gitcrawl/internal/config" "github.com/openclaw/gitcrawl/internal/store" ) func TestCLIAppCommandCoveragePaths(t *testing.T) { ctx := context.Background() configPath := seedGHShimRepo(t, ctx) cfg, err := config.Load(configPath) if err != nil { t.Fatalf("load config: %v", err) } st, err := store.Open(ctx, cfg.DBPath) if err != nil { t.Fatalf("open store: %v", err) } repo, err := st.RepositoryByFullName(ctx, "openclaw/openclaw") if err != nil { t.Fatalf("repo: %v", err) } threads, err := st.ListThreadsFiltered(ctx, store.ThreadListOptions{RepoID: repo.ID, IncludeClosed: true, Numbers: []int{10, 12}}) if err != nil { t.Fatalf("threads: %v", err) } if len(threads) != 2 { t.Fatalf("seed threads = %+v", threads) } result, err := st.SaveDurableClusters(ctx, repo.ID, []store.DurableClusterInput{{ StableKey: "cli:10,12", StableSlug: "cli-10-12", RepresentativeThreadID: threads[0].ID, Title: "CLI command cluster", Members: []store.DurableClusterMemberInput{ {ThreadID: threads[0].ID, Role: "canonical"}, {ThreadID: threads[1].ID, Role: "member"}, }, }}) if err != nil { t.Fatalf("save cluster: %v", err) } if _, err := st.RecordRun(ctx, store.RunRecord{RepoID: repo.ID, Kind: "sync", Scope: "open", Status: "success", StartedAt: "2026-05-08T01:00:00Z", FinishedAt: "2026-05-08T01:00:01Z", StatsJSON: "{}"}); err != nil { t.Fatalf("record run: %v", err) } clusterID, err := st.ClusterIDForThreadNumber(ctx, repo.ID, 10, true) if err != nil { t.Fatalf("cluster id: %v", err) } if result.RunID == 0 { t.Fatal("cluster run id should be non-zero") } if err := st.Close(); err != nil { t.Fatalf("close store: %v", err) } commands := [][]string{ {"--config", configPath, "--json", "configure", "--summary-model", "gpt-test", "--embed-model", "embed-test", "--embedding-basis", "title_original"}, {"--config", configPath, "--json", "metadata"}, {"--config", configPath, "--json", "status"}, {"--config", configPath, "--json", "threads", "openclaw/openclaw", "--numbers", "https://github.com/openclaw/openclaw/issues/10,https://github.com/openclaw/openclaw/pull/12", "--include-closed", "--limit", "2"}, {"--config", configPath, "--json", "runs", "openclaw/openclaw", "--kind", "sync", "--limit", "1"}, {"--config", configPath, "--json", "clusters", "openclaw/openclaw", "--include-closed", "--sort", "oldest", "--min-size", "1", "--limit", "5"}, {"--config", configPath, "--json", "durable-clusters", "openclaw/openclaw", "--include-closed", "--sort", "size", "--min-size", "1", "--limit", "5"}, {"--config", configPath, "--json", "cluster-detail", "openclaw/openclaw", "--id", strconv.FormatInt(clusterID, 10), "--member-limit", "2", "--body-chars", "10", "--include-closed"}, {"--config", configPath, "--json", "close-thread", "openclaw/openclaw", "--number", "https://github.com/openclaw/openclaw/issues/10", "--reason", "covered"}, {"--config", configPath, "--json", "reopen-thread", "openclaw/openclaw", "--number", "10"}, {"--config", configPath, "--json", "close-cluster", "openclaw/openclaw", "--id", strconv.FormatInt(clusterID, 10), "--reason", "covered"}, {"--config", configPath, "--json", "reopen-cluster", "openclaw/openclaw", "--id", strconv.FormatInt(clusterID, 10)}, {"--config", configPath, "--json", "exclude-cluster-member", "openclaw/openclaw", "--id", strconv.FormatInt(clusterID, 10), "--number", "12", "--reason", "covered"}, {"--config", configPath, "--json", "include-cluster-member", "openclaw/openclaw", "--id", strconv.FormatInt(clusterID, 10), "--number", "12", "--reason", "covered"}, {"--config", configPath, "--json", "set-cluster-canonical", "openclaw/openclaw", "--id", strconv.FormatInt(clusterID, 10), "--number", "12", "--reason", "covered"}, } for _, args := range commands { app := New() var stdout, stderr bytes.Buffer app.Stdout = &stdout app.Stderr = &stderr if err := app.Run(ctx, args); err != nil { t.Fatalf("%v failed: %v\nstdout=%s\nstderr=%s", args, err, stdout.String(), stderr.String()) } if stdout.Len() == 0 { t.Fatalf("%v produced no output", args) } } if clusterID <= 0 { t.Fatalf("cluster id = %d", clusterID) } } func TestCLIAppHumanAndLogOutputE2E(t *testing.T) { ctx := context.Background() configPath := seedGHShimRepo(t, ctx) textCommands := [][]string{ {"--config", configPath, "version"}, {"--config", configPath, "metadata"}, {"--config", configPath, "status"}, {"--config", configPath, "doctor"}, {"--config", configPath, "help", "portable"}, {"--config", configPath, "help", "tui"}, } for _, args := range textCommands { app := New() var stdout bytes.Buffer app.Stdout = &stdout if err := app.Run(ctx, args); err != nil { t.Fatalf("%v failed: %v", args, err) } if strings.TrimSpace(stdout.String()) == "" { t.Fatalf("%v produced no text output", args) } } logCommands := [][]string{ {"--config", configPath, "--format", "log", "configure", "--summary-model", "gpt-log"}, {"--config", configPath, "--format", "log", "doctor"}, } for _, args := range logCommands { app := New() var stdout bytes.Buffer app.Stdout = &stdout if err := app.Run(ctx, args); err != nil { t.Fatalf("%v failed: %v", args, err) } if !strings.Contains(stdout.String(), "=") { t.Fatalf("%v log output = %q", args, stdout.String()) } } jsonVersion := New() var jsonOut bytes.Buffer jsonVersion.Stdout = &jsonOut if err := jsonVersion.Run(ctx, []string{"--config", configPath, "--format", "json", "version"}); err != nil { t.Fatalf("json version: %v", err) } if !strings.Contains(jsonOut.String(), `"version"`) { t.Fatalf("json version output = %q", jsonOut.String()) } } func TestCLIAppVectorFallbackCoveragePaths(t *testing.T) { ctx := context.Background() dir := t.TempDir() configPath := filepath.Join(dir, "config.toml") dbPath := filepath.Join(dir, "gitcrawl.db") app := New() if err := app.Run(ctx, []string{"--config", configPath, "init", "--db", dbPath}); err != nil { t.Fatalf("init: %v", err) } repoID, firstID, secondID := seedCommandFlowStore(t, dbPath) st, err := store.Open(ctx, dbPath) if err != nil { t.Fatalf("open store: %v", err) } now := time.Now().UTC().Format(time.RFC3339Nano) for _, vector := range []store.ThreadVector{ {ThreadID: firstID, Basis: "other_basis", Model: "other-model", Dimensions: 2, ContentHash: "v1", Vector: []float64{1, 0}, CreatedAt: now, UpdatedAt: now}, {ThreadID: secondID, Basis: "other_basis", Model: "other-model", Dimensions: 2, ContentHash: "v2", Vector: []float64{0.95, 0.05}, CreatedAt: now, UpdatedAt: now}, } { if err := st.UpsertThreadVector(ctx, vector); err != nil { t.Fatalf("upsert vector: %v", err) } } if err := st.Close(); err != nil { t.Fatalf("close store: %v", err) } configure := New() if err := configure.Run(ctx, []string{"--config", configPath, "configure", "--embed-model", "missing-model", "--embedding-basis", "missing-basis"}); err != nil { t.Fatalf("configure: %v", err) } for _, args := range [][]string{ {"--config", configPath, "--json", "neighbors", "openclaw/openclaw", "--number", "101", "--limit", "1", "--threshold", "0.99"}, {"--config", configPath, "--json", "cluster", "openclaw/openclaw", "--threshold", "0.5", "--min-size", "2", "--limit", "2"}, {"--config", configPath, "--json", "refresh", "openclaw/openclaw", "--no-sync", "--no-embed", "--threshold", "0.5", "--min-size", "2"}, {"--config", configPath, "--json", "search", "openclaw/openclaw", "--query", "gateway", "--mode", ""}, } { run := New() var stdout bytes.Buffer run.Stdout = &stdout if err := run.Run(ctx, args); err != nil { t.Fatalf("%v failed: %v\n%s", args, err, stdout.String()) } } if repoID == 0 { t.Fatal("seed repo id should be non-zero") } } func TestCLIAppUsageBranches(t *testing.T) { ctx := context.Background() configPath := filepath.Join(t.TempDir(), "config.toml") cases := [][]string{ {"--format", "yaml", "status"}, {"serve"}, {"unknown"}, {"configure", "--bad"}, {"metadata", "extra"}, {"status", "extra"}, {"portable"}, {"portable", "unknown"}, {"portable", "prune", "extra"}, {"portable", "prune", "--body-chars", "bad"}, {"threads"}, {"threads", "bad-repo"}, {"threads", "openclaw/openclaw", "--numbers", "bad"}, {"threads", "openclaw/openclaw", "--limit", "bad"}, {"runs"}, {"runs", "openclaw/openclaw", "--limit", "bad"}, {"cluster-detail", "openclaw/openclaw", "--id", "bad"}, {"close-thread", "openclaw/openclaw"}, {"reopen-thread", "openclaw/openclaw", "--number", "bad"}, {"close-cluster", "openclaw/openclaw"}, {"reopen-cluster", "openclaw/openclaw", "--id", "bad"}, {"exclude-cluster-member", "openclaw/openclaw", "--id", "1"}, {"include-cluster-member", "openclaw/openclaw", "--id", "bad", "--number", "1"}, {"set-cluster-canonical", "openclaw/openclaw", "--id", "1", "--number", "bad"}, {"sync", "openclaw/openclaw", "--with", "bad"}, {"refresh"}, {"refresh", "openclaw/openclaw", "--no-sync", "--no-embed", "--no-cluster"}, {"refresh", "bad-repo"}, {"refresh", "openclaw/openclaw", "--limit", "bad"}, {"refresh", "openclaw/openclaw", "--threshold", "bad"}, {"refresh", "openclaw/openclaw", "--threshold", "2"}, {"refresh", "openclaw/openclaw", "--min-size", "bad"}, {"refresh", "openclaw/openclaw", "--k", "bad"}, {"search"}, {"search", "openclaw/openclaw"}, {"search", "bad-repo", "--query", "x"}, {"search", "openclaw/openclaw", "--query", "x", "--limit", "bad"}, {"search", "openclaw/openclaw", "--query", "x", "--mode", "bad"}, {"neighbors"}, {"neighbors", "bad-repo"}, {"neighbors", "openclaw/openclaw"}, {"neighbors", "openclaw/openclaw", "--number", "bad"}, {"neighbors", "openclaw/openclaw", "--number", "1", "--limit", "bad"}, {"neighbors", "openclaw/openclaw", "--number", "1", "--threshold", "bad"}, {"cluster"}, {"cluster", "bad-repo"}, {"cluster", "openclaw/openclaw", "--threshold", "bad"}, {"cluster", "openclaw/openclaw", "--threshold", "2"}, {"cluster", "openclaw/openclaw", "--min-size", "bad"}, {"cluster", "openclaw/openclaw", "--max-cluster-size", "bad"}, {"cluster", "openclaw/openclaw", "--limit", "bad"}, {"embed"}, {"embed", "bad-repo"}, {"embed", "openclaw/openclaw", "--number", "bad"}, {"embed", "openclaw/openclaw", "--limit", "bad"}, {"tui", "one", "two"}, {"tui", "--sort", "bad"}, } for _, args := range cases { app := New() app.Stdout = &bytes.Buffer{} app.Stderr = &bytes.Buffer{} full := append([]string{"--config", configPath}, args...) if err := app.Run(ctx, full); err == nil { t.Fatalf("%v succeeded, want error", args) } } }