0f96d202c7
ctask agents check [workspace] validates that a workspace's agent configuration can be launched without launching it: agent type known, command resolvable on PATH, launch_dir valid, AGENTS.md present, CLAUDE.md shim present (WARN only, claude type only). agent.env keys are displayed informationally, with a WARN line when any key shadows a ctask-exported CTASK_* var. Returns non-zero when any check FAILs. ctask doctor includes the same sweep when a workspace context is resolvable — the most-recently-active workspace via workspace.MostRecentActive. When no active workspace exists, doctor shows "Agent check: skipped (no workspace context)" without bumping the failure counter. runAgentsCheckOnWorkspace is shared between the standalone command and the doctor integration. TestCompletionSubcommandViaExecute is made order-independent: cobra's default completion command captures the root output writer on the first Execute() in the process, and the new agents-check tests now run an Execute() earlier in the suite.
242 lines
6.9 KiB
Go
242 lines
6.9 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/warrenronsiek/ctask/internal/workspace"
|
|
)
|
|
|
|
// completionTestEnv mirrors listTestEnv but is duplicated here so the
|
|
// completion tests are self-contained and can vary the fixtures.
|
|
func completionTestEnv(t *testing.T) string {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
mk := func(category, dirName, status, taskType string) {
|
|
dir := filepath.Join(root, category, dirName)
|
|
os.MkdirAll(dir, 0755)
|
|
now := time.Now().UTC().Truncate(time.Second)
|
|
slug := dirName[11:]
|
|
meta := &workspace.TaskMeta{
|
|
ID: "t", Slug: slug, Title: slug,
|
|
CreatedAt: now, UpdatedAt: now,
|
|
Status: status, Category: category, Type: taskType,
|
|
Mode: "local", Agent: workspace.AgentSpec{Type: "claude"},
|
|
}
|
|
if status == "archived" {
|
|
meta.ArchivedAt = &now
|
|
}
|
|
workspace.WriteMeta(filepath.Join(dir, "task.yaml"), meta)
|
|
}
|
|
mk("general", "2026-04-05_alpha-active", "active", "task")
|
|
mk("general", "2026-04-04_beta-archived", "archived", "task")
|
|
mk("projects", "2026-04-03_gamma-active", "active", "project")
|
|
mk("projects", "2026-04-02_delta-archived", "archived", "project")
|
|
return root
|
|
}
|
|
|
|
// callCompletion invokes a ValidArgsFunction directly under a CTASK_ROOT
|
|
// override, returning the candidate list (sorted for stable comparisons)
|
|
// and the shell directive.
|
|
func callCompletion(t *testing.T, root string, fn func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective)) ([]string, cobra.ShellCompDirective) {
|
|
t.Helper()
|
|
prev := os.Getenv("CTASK_ROOT")
|
|
os.Setenv("CTASK_ROOT", root)
|
|
defer func() {
|
|
if prev == "" {
|
|
os.Unsetenv("CTASK_ROOT")
|
|
} else {
|
|
os.Setenv("CTASK_ROOT", prev)
|
|
}
|
|
}()
|
|
candidates, dir := fn(nil, nil, "")
|
|
sort.Strings(candidates)
|
|
return candidates, dir
|
|
}
|
|
|
|
func TestCompleteWorkspacesActiveOnly(t *testing.T) {
|
|
root := completionTestEnv(t)
|
|
got, _ := callCompletion(t, root, completeWorkspaces(completionActive))
|
|
want := []string{
|
|
"2026-04-03_gamma-active",
|
|
"2026-04-05_alpha-active",
|
|
}
|
|
if !equalStringSlices(got, want) {
|
|
t.Errorf("active filter:\nwant %v\ngot %v", want, got)
|
|
}
|
|
}
|
|
|
|
func TestCompleteWorkspacesArchivedOnly(t *testing.T) {
|
|
root := completionTestEnv(t)
|
|
got, _ := callCompletion(t, root, completeWorkspaces(completionArchived))
|
|
want := []string{
|
|
"2026-04-02_delta-archived",
|
|
"2026-04-04_beta-archived",
|
|
}
|
|
if !equalStringSlices(got, want) {
|
|
t.Errorf("archived filter:\nwant %v\ngot %v", want, got)
|
|
}
|
|
}
|
|
|
|
func TestCompleteWorkspacesAny(t *testing.T) {
|
|
root := completionTestEnv(t)
|
|
got, _ := callCompletion(t, root, completeWorkspaces(completionAny))
|
|
want := []string{
|
|
"2026-04-02_delta-archived",
|
|
"2026-04-03_gamma-active",
|
|
"2026-04-04_beta-archived",
|
|
"2026-04-05_alpha-active",
|
|
}
|
|
if !equalStringSlices(got, want) {
|
|
t.Errorf("any filter:\nwant %v\ngot %v", want, got)
|
|
}
|
|
}
|
|
|
|
func TestCompleteWorkspacesSecondArgReturnsNoCompletion(t *testing.T) {
|
|
// Once the user has typed the first positional arg, completion of further
|
|
// args must not enumerate workspaces.
|
|
root := completionTestEnv(t)
|
|
prev := os.Getenv("CTASK_ROOT")
|
|
os.Setenv("CTASK_ROOT", root)
|
|
defer func() {
|
|
if prev == "" {
|
|
os.Unsetenv("CTASK_ROOT")
|
|
} else {
|
|
os.Setenv("CTASK_ROOT", prev)
|
|
}
|
|
}()
|
|
candidates, dir := completeWorkspaces(completionAny)(nil, []string{"already-typed"}, "")
|
|
if len(candidates) != 0 {
|
|
t.Errorf("expected no candidates after first positional arg, got: %v", candidates)
|
|
}
|
|
if dir != cobra.ShellCompDirectiveNoFileComp {
|
|
t.Errorf("expected ShellCompDirectiveNoFileComp, got: %v", dir)
|
|
}
|
|
}
|
|
|
|
// genCompletion calls the shell-specific generator that Cobra's auto-injected
|
|
// `ctask completion <shell>` subcommand uses internally. Bypasses rootCmd
|
|
// argument-routing state so the tests are independent.
|
|
func genCompletion(shell string) (string, error) {
|
|
var buf bytes.Buffer
|
|
var err error
|
|
switch shell {
|
|
case "bash":
|
|
err = rootCmd.GenBashCompletionV2(&buf, true)
|
|
case "zsh":
|
|
err = rootCmd.GenZshCompletion(&buf)
|
|
case "fish":
|
|
err = rootCmd.GenFishCompletion(&buf, true)
|
|
case "powershell":
|
|
err = rootCmd.GenPowerShellCompletionWithDesc(&buf)
|
|
}
|
|
return buf.String(), err
|
|
}
|
|
|
|
func TestCompletionBashGenerates(t *testing.T) {
|
|
out, err := genCompletion("bash")
|
|
if err != nil {
|
|
t.Fatalf("GenBashCompletionV2: %v", err)
|
|
}
|
|
if len(out) == 0 {
|
|
t.Fatal("expected non-empty bash completion script")
|
|
}
|
|
if !strings.Contains(out, "ctask") {
|
|
t.Errorf("bash completion script should mention 'ctask':\n%s", truncate(out, 200))
|
|
}
|
|
}
|
|
|
|
func TestCompletionPowerShellGenerates(t *testing.T) {
|
|
out, err := genCompletion("powershell")
|
|
if err != nil {
|
|
t.Fatalf("GenPowerShellCompletionWithDesc: %v", err)
|
|
}
|
|
if len(out) == 0 {
|
|
t.Fatal("expected non-empty powershell completion script")
|
|
}
|
|
if !strings.Contains(out, "ctask") {
|
|
t.Errorf("powershell completion script should mention 'ctask':\n%s", truncate(out, 200))
|
|
}
|
|
}
|
|
|
|
func TestCompletionZshGenerates(t *testing.T) {
|
|
out, err := genCompletion("zsh")
|
|
if err != nil {
|
|
t.Fatalf("GenZshCompletion: %v", err)
|
|
}
|
|
if len(out) == 0 {
|
|
t.Fatal("expected non-empty zsh completion script")
|
|
}
|
|
}
|
|
|
|
func TestCompletionFishGenerates(t *testing.T) {
|
|
out, err := genCompletion("fish")
|
|
if err != nil {
|
|
t.Fatalf("GenFishCompletion: %v", err)
|
|
}
|
|
if len(out) == 0 {
|
|
t.Fatal("expected non-empty fish completion script")
|
|
}
|
|
}
|
|
|
|
// TestCompletionSubcommandViaExecute verifies that the user-facing path
|
|
// `ctask completion bash` actually runs end-to-end through Cobra. Cobra
|
|
// adds the `completion` subcommand lazily on first Execute(), so a Find()
|
|
// before any Execute() returns "unknown command" — exercising the real
|
|
// path is the right test.
|
|
func TestCompletionSubcommandViaExecute(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
defer rootCmd.SetOut(os.Stdout)
|
|
defer rootCmd.SetErr(os.Stderr)
|
|
|
|
// Cobra's default `completion` command captures the root's output
|
|
// writer once, when it is first created on the first Execute() anywhere
|
|
// in the process (see InitDefaultCompletionCmd). Drop any previously
|
|
// created instance so the Execute() below re-creates it bound to buf —
|
|
// keeps this test independent of which other test ran Execute() first.
|
|
for _, c := range rootCmd.Commands() {
|
|
if c.Name() == "completion" {
|
|
rootCmd.RemoveCommand(c)
|
|
}
|
|
}
|
|
|
|
rootCmd.SetArgs([]string{"completion", "bash"})
|
|
defer rootCmd.SetArgs(nil)
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("rootCmd.Execute(\"completion\", \"bash\"): %v", err)
|
|
}
|
|
if buf.Len() == 0 {
|
|
t.Fatal("expected non-empty bash completion via Execute")
|
|
}
|
|
if !strings.Contains(buf.String(), "ctask") {
|
|
t.Errorf("end-to-end bash completion should mention 'ctask'")
|
|
}
|
|
}
|
|
|
|
func equalStringSlices(a, b []string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func truncate(s string, n int) string {
|
|
if len(s) <= n {
|
|
return s
|
|
}
|
|
return s[:n]
|
|
}
|