feat(v0.6): ctask agents check + doctor integration
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.
This commit is contained in:
+158
@@ -0,0 +1,158 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/warrenronsiek/ctask/internal/agent"
|
||||
"github.com/warrenronsiek/ctask/internal/config"
|
||||
"github.com/warrenronsiek/ctask/internal/workspace"
|
||||
)
|
||||
|
||||
var agentsCmd = &cobra.Command{
|
||||
Use: "agents",
|
||||
Short: "Inspect and validate agent configuration",
|
||||
SilenceUsage: true,
|
||||
}
|
||||
|
||||
var agentsCheckCmd = &cobra.Command{
|
||||
Use: "check [workspace]",
|
||||
Short: "Validate the agent configuration for a workspace",
|
||||
Long: `Validate that the configured agent for a workspace can be launched.
|
||||
Does NOT launch the agent. Checks: agent type known, command resolvable
|
||||
on PATH, launch_dir valid, AGENTS.md present, CLAUDE.md shim present
|
||||
(WARN, claude type only). Displays agent.env keys informationally.
|
||||
|
||||
If [workspace] is omitted, checks the most-recently-active workspace.`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
SilenceUsage: true,
|
||||
RunE: runAgentsCheck,
|
||||
}
|
||||
|
||||
func init() {
|
||||
agentsCheckCmd.ValidArgsFunction = completeWorkspaces(completionAny)
|
||||
agentsCmd.AddCommand(agentsCheckCmd)
|
||||
rootCmd.AddCommand(agentsCmd)
|
||||
}
|
||||
|
||||
func runAgentsCheck(cmd *cobra.Command, args []string) error {
|
||||
roots := config.SearchRoots()
|
||||
var ws *workspace.QueryResult
|
||||
if len(args) == 1 {
|
||||
ws = resolveOne(roots, args[0], true)
|
||||
} else {
|
||||
best, err := workspace.MostRecentActive(roots)
|
||||
if err != nil || best == nil {
|
||||
return fmt.Errorf("agents check: no workspace specified and no active workspace found")
|
||||
}
|
||||
ws = best
|
||||
}
|
||||
|
||||
return runAgentsCheckOnWorkspace(cmd.OutOrStdout(), ws)
|
||||
}
|
||||
|
||||
// runAgentsCheckOnWorkspace performs the checks and prints results.
|
||||
// Returns a non-nil error iff any check is FAIL (so doctor can surface
|
||||
// the failure count). Extracted from runAgentsCheck so doctor can reuse.
|
||||
func runAgentsCheckOnWorkspace(out io.Writer, ws *workspace.QueryResult) error {
|
||||
fmt.Fprintf(out, "── Agent Check: %s ─────────────────\n", ws.Meta.Slug)
|
||||
|
||||
failed := 0
|
||||
spec := ws.Meta.Agent
|
||||
|
||||
// 1. Agent type known. (ValidateAgentSpec already enforced this on read,
|
||||
// but we re-display for symmetry. If type is empty, fall through to
|
||||
// default_agent and label as such.)
|
||||
typ := spec.Type
|
||||
typeLabel := typ
|
||||
if typ == "" {
|
||||
typ = config.LoadResolver().DefaultAgent().Value
|
||||
typeLabel = typ + " (from default_agent)"
|
||||
}
|
||||
fmt.Fprintf(out, "Agent type: %s\n", typeLabel)
|
||||
if !agent.IsKnownType(typ) {
|
||||
fmt.Fprintf(out, " [FAIL] unknown agent type %q\n", typ)
|
||||
failed++
|
||||
} else {
|
||||
fmt.Fprintln(out, " [PASS]")
|
||||
}
|
||||
|
||||
// 2. Command resolvable.
|
||||
resolved, rerr := agent.Resolve(spec, typ)
|
||||
if rerr != nil {
|
||||
fmt.Fprintf(out, "Command: [FAIL] %v\n", rerr)
|
||||
failed++
|
||||
} else {
|
||||
path, lerr := exec.LookPath(resolved.Command)
|
||||
fmt.Fprintf(out, "Command: %s", resolved.Command)
|
||||
if lerr != nil {
|
||||
fmt.Fprintf(out, "\n [FAIL] not found on PATH: %v\n", lerr)
|
||||
failed++
|
||||
} else {
|
||||
fmt.Fprintf(out, " (%s)\n", path)
|
||||
fmt.Fprintln(out, " [PASS]")
|
||||
}
|
||||
}
|
||||
|
||||
// 3. launch_dir valid.
|
||||
launchAbs, _, lderr := workspace.ResolveLaunch(ws.Path, ws.Meta.LaunchDir)
|
||||
fmt.Fprintf(out, "Launch dir: %s\n", launchAbs)
|
||||
if lderr != nil {
|
||||
fmt.Fprintf(out, " [FAIL] %v\n", lderr)
|
||||
failed++
|
||||
} else {
|
||||
fmt.Fprintln(out, " [PASS]")
|
||||
}
|
||||
|
||||
// 4. AGENTS.md present.
|
||||
if _, err := os.Stat(filepath.Join(ws.Path, "AGENTS.md")); err == nil {
|
||||
fmt.Fprintln(out, "AGENTS.md: found\n [PASS]")
|
||||
} else {
|
||||
fmt.Fprintln(out, "AGENTS.md: [FAIL] missing")
|
||||
failed++
|
||||
}
|
||||
|
||||
// 5. CLAUDE.md shim — WARN only, claude type only.
|
||||
if typ == "claude" {
|
||||
if _, err := os.Stat(filepath.Join(ws.Path, "CLAUDE.md")); err == nil {
|
||||
fmt.Fprintln(out, "CLAUDE.md: found\n [PASS]")
|
||||
} else {
|
||||
fmt.Fprintln(out, "CLAUDE.md: [WARN] missing (shim is optional)")
|
||||
}
|
||||
}
|
||||
|
||||
// 6. agent.env keys (informational + CTASK_* shadow WARN).
|
||||
if len(spec.Env) > 0 {
|
||||
keys := make([]string, 0, len(spec.Env))
|
||||
for k := range spec.Env {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
fmt.Fprintf(out, "Agent env: %d keys configured (%s)\n", len(keys), strings.Join(keys, ", "))
|
||||
// agent.env merges AFTER ctask's own exported vars, so a key
|
||||
// matching CTASK_* shadows what ctask exports. The user is allowed
|
||||
// to do this (spec §5); we surface the surprise. WARN does not
|
||||
// bump the failed counter.
|
||||
var shadowed []string
|
||||
for _, k := range keys {
|
||||
if strings.HasPrefix(k, "CTASK_") {
|
||||
shadowed = append(shadowed, k)
|
||||
}
|
||||
}
|
||||
if len(shadowed) > 0 {
|
||||
fmt.Fprintf(out, " [WARN] agent.env overrides ctask-exported vars: %s\n",
|
||||
strings.Join(shadowed, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
if failed > 0 {
|
||||
return fmt.Errorf("agents check: %d failures", failed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/warrenronsiek/ctask/internal/config"
|
||||
"github.com/warrenronsiek/ctask/internal/workspace"
|
||||
)
|
||||
|
||||
// captureRootCmd redirects rootCmd's output to a buffer for the duration
|
||||
// of t and restores the process defaults (and clears SetArgs) on cleanup.
|
||||
// rootCmd is a package global; tests that drive it via Execute() must
|
||||
// restore it so later tests are not affected.
|
||||
func captureRootCmd(t *testing.T) *bytes.Buffer {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
rootCmd.SetErr(&buf)
|
||||
t.Cleanup(func() {
|
||||
rootCmd.SetOut(os.Stdout)
|
||||
rootCmd.SetErr(os.Stderr)
|
||||
rootCmd.SetArgs(nil)
|
||||
})
|
||||
return &buf
|
||||
}
|
||||
|
||||
func TestAgentsCheckPassAllChecks(t *testing.T) {
|
||||
tmpRoot := t.TempDir()
|
||||
t.Setenv("CTASK_ROOT", tmpRoot)
|
||||
config.SetConfigPathForTest(t, filepath.Join(t.TempDir(), "no-config.yaml"))
|
||||
|
||||
if _, err := exec.LookPath("go"); err != nil {
|
||||
t.Skip("no command on PATH for fixture")
|
||||
}
|
||||
res, err := workspace.Create(workspace.CreateOpts{
|
||||
Root: tmpRoot,
|
||||
Title: "agents-check-pass",
|
||||
Category: "general",
|
||||
AgentSpec: workspace.AgentSpec{Type: "custom", Command: "go"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
|
||||
buf := captureRootCmd(t)
|
||||
rootCmd.SetArgs([]string{"agents", "check", filepath.Base(res.Path)})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
out := buf.String()
|
||||
for _, want := range []string{"[PASS]", "Agent type:", "Command:", "AGENTS.md:"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("output missing %q\n%s", want, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentsCheckCommandNotFound(t *testing.T) {
|
||||
tmpRoot := t.TempDir()
|
||||
t.Setenv("CTASK_ROOT", tmpRoot)
|
||||
config.SetConfigPathForTest(t, filepath.Join(t.TempDir(), "no-config.yaml"))
|
||||
|
||||
res, _ := workspace.Create(workspace.CreateOpts{
|
||||
Root: tmpRoot,
|
||||
Title: "no-cmd",
|
||||
Category: "general",
|
||||
AgentSpec: workspace.AgentSpec{Type: "custom", Command: "definitely-not-on-path-zzz"},
|
||||
})
|
||||
|
||||
buf := captureRootCmd(t)
|
||||
rootCmd.SetArgs([]string{"agents", "check", filepath.Base(res.Path)})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(buf.String(), "[FAIL]") {
|
||||
t.Errorf("output missing [FAIL]:\n%s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentsCheckMissingAgentsMD(t *testing.T) {
|
||||
tmpRoot := t.TempDir()
|
||||
t.Setenv("CTASK_ROOT", tmpRoot)
|
||||
config.SetConfigPathForTest(t, filepath.Join(t.TempDir(), "no-config.yaml"))
|
||||
|
||||
if _, err := exec.LookPath("go"); err != nil {
|
||||
t.Skip("no PATH command")
|
||||
}
|
||||
res, _ := workspace.Create(workspace.CreateOpts{
|
||||
Root: tmpRoot, Title: "no-agents-md", Category: "general",
|
||||
AgentSpec: workspace.AgentSpec{Type: "custom", Command: "go"},
|
||||
})
|
||||
// Delete the AGENTS.md that workspace.Create just wrote.
|
||||
_ = os.Remove(filepath.Join(res.Path, "AGENTS.md"))
|
||||
|
||||
buf := captureRootCmd(t)
|
||||
rootCmd.SetArgs([]string{"agents", "check", filepath.Base(res.Path)})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(buf.String(), "AGENTS.md: [FAIL]") {
|
||||
t.Errorf("output missing AGENTS.md FAIL:\n%s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentsCheckMissingShimWarnsForClaude(t *testing.T) {
|
||||
tmpRoot := t.TempDir()
|
||||
t.Setenv("CTASK_ROOT", tmpRoot)
|
||||
config.SetConfigPathForTest(t, filepath.Join(t.TempDir(), "no-config.yaml"))
|
||||
|
||||
// Use claude as the type but a known-on-PATH command override so the
|
||||
// command check passes regardless of whether claude is installed.
|
||||
if _, err := exec.LookPath("go"); err != nil {
|
||||
t.Skip("no PATH command")
|
||||
}
|
||||
res, _ := workspace.Create(workspace.CreateOpts{
|
||||
Root: tmpRoot, Title: "claude-no-shim", Category: "general",
|
||||
AgentSpec: workspace.AgentSpec{Type: "claude", Command: "go"},
|
||||
})
|
||||
_ = os.Remove(filepath.Join(res.Path, "CLAUDE.md"))
|
||||
|
||||
buf := captureRootCmd(t)
|
||||
rootCmd.SetArgs([]string{"agents", "check", filepath.Base(res.Path)})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error (WARN should not bump failure count): %v", err)
|
||||
}
|
||||
out := buf.String()
|
||||
if !strings.Contains(out, "CLAUDE.md: [WARN]") {
|
||||
t.Errorf("output missing CLAUDE.md WARN line:\n%s", out)
|
||||
}
|
||||
if strings.Contains(out, "CLAUDE.md: [FAIL]") {
|
||||
t.Errorf("CLAUDE.md missing must be WARN, not FAIL:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentsCheckShimNotRequiredForOpencode(t *testing.T) {
|
||||
tmpRoot := t.TempDir()
|
||||
t.Setenv("CTASK_ROOT", tmpRoot)
|
||||
config.SetConfigPathForTest(t, filepath.Join(t.TempDir(), "no-config.yaml"))
|
||||
|
||||
if _, err := exec.LookPath("go"); err != nil {
|
||||
t.Skip("no PATH command")
|
||||
}
|
||||
res, _ := workspace.Create(workspace.CreateOpts{
|
||||
Root: tmpRoot, Title: "opencode-no-shim-needed", Category: "general",
|
||||
// type: custom is a stand-in for opencode so the test does not
|
||||
// need opencode on PATH. The shim check is gated on type=="claude",
|
||||
// so any non-claude type must NOT emit a CLAUDE.md line.
|
||||
AgentSpec: workspace.AgentSpec{Type: "custom", Command: "go"},
|
||||
})
|
||||
if _, err := os.Stat(filepath.Join(res.Path, "CLAUDE.md")); !os.IsNotExist(err) {
|
||||
t.Fatalf("precondition: CLAUDE.md should not exist for non-claude, got %v", err)
|
||||
}
|
||||
|
||||
buf := captureRootCmd(t)
|
||||
rootCmd.SetArgs([]string{"agents", "check", filepath.Base(res.Path)})
|
||||
_ = rootCmd.Execute()
|
||||
if strings.Contains(buf.String(), "CLAUDE.md:") {
|
||||
t.Errorf("non-claude workspace must not produce a CLAUDE.md row:\n%s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentsCheckCustomNoCommand(t *testing.T) {
|
||||
tmpRoot := t.TempDir()
|
||||
t.Setenv("CTASK_ROOT", tmpRoot)
|
||||
config.SetConfigPathForTest(t, filepath.Join(t.TempDir(), "no-config.yaml"))
|
||||
|
||||
// Create a normal workspace, then build a QueryResult with
|
||||
// agent.type=custom and no command directly — ReadMeta would reject
|
||||
// such a task.yaml, so we exercise the agents-check diagnostic itself.
|
||||
res, _ := workspace.Create(workspace.CreateOpts{
|
||||
Root: tmpRoot, Title: "custom-no-cmd-direct", Category: "general",
|
||||
AgentSpec: workspace.AgentSpec{Type: "claude"},
|
||||
})
|
||||
qr := &workspace.QueryResult{
|
||||
Path: res.Path,
|
||||
Root: tmpRoot,
|
||||
Meta: &workspace.TaskMeta{
|
||||
Slug: res.Meta.Slug,
|
||||
Agent: workspace.AgentSpec{Type: "custom"}, // no command
|
||||
},
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err := runAgentsCheckOnWorkspace(&buf, qr)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(buf.String(), "requires command") {
|
||||
t.Errorf("output must mention requires command:\n%s", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentsCheckShowsEnvKeysInfo(t *testing.T) {
|
||||
tmpRoot := t.TempDir()
|
||||
t.Setenv("CTASK_ROOT", tmpRoot)
|
||||
config.SetConfigPathForTest(t, filepath.Join(t.TempDir(), "no-config.yaml"))
|
||||
|
||||
if _, err := exec.LookPath("go"); err != nil {
|
||||
t.Skip("no PATH command")
|
||||
}
|
||||
res, _ := workspace.Create(workspace.CreateOpts{
|
||||
Root: tmpRoot, Title: "env-keys-info", Category: "general",
|
||||
AgentSpec: workspace.AgentSpec{
|
||||
Type: "custom",
|
||||
Command: "go",
|
||||
Env: map[string]string{
|
||||
"OPENAI_API_KEY": "x",
|
||||
"OPENAI_BASE_URL": "y",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
buf := captureRootCmd(t)
|
||||
rootCmd.SetArgs([]string{"agents", "check", filepath.Base(res.Path)})
|
||||
_ = rootCmd.Execute()
|
||||
out := buf.String()
|
||||
if !strings.Contains(out, "Agent env: 2 keys configured") {
|
||||
t.Errorf("output missing env-keys count:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "OPENAI_API_KEY, OPENAI_BASE_URL") {
|
||||
t.Errorf("output missing sorted key list:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentsCheckWarnsOnCtaskEnvShadow(t *testing.T) {
|
||||
tmpRoot := t.TempDir()
|
||||
t.Setenv("CTASK_ROOT", tmpRoot)
|
||||
config.SetConfigPathForTest(t, filepath.Join(t.TempDir(), "no-config.yaml"))
|
||||
|
||||
if _, err := exec.LookPath("go"); err != nil {
|
||||
t.Skip("no PATH command")
|
||||
}
|
||||
res, _ := workspace.Create(workspace.CreateOpts{
|
||||
Root: tmpRoot, Title: "ctask-env-shadow", Category: "general",
|
||||
AgentSpec: workspace.AgentSpec{
|
||||
Type: "custom",
|
||||
Command: "go",
|
||||
Env: map[string]string{"CTASK_WORKSPACE": "shadowed"},
|
||||
},
|
||||
})
|
||||
|
||||
buf := captureRootCmd(t)
|
||||
rootCmd.SetArgs([]string{"agents", "check", filepath.Base(res.Path)})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("unexpected error (shadow is WARN, not FAIL): %v", err)
|
||||
}
|
||||
if !strings.Contains(buf.String(), "[WARN] agent.env overrides ctask-exported vars") {
|
||||
t.Errorf("output missing CTASK_* shadow WARN:\n%s", buf.String())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/warrenronsiek/ctask/internal/config"
|
||||
"github.com/warrenronsiek/ctask/internal/workspace"
|
||||
)
|
||||
|
||||
// This file swaps process-global os.Stdout and env vars via
|
||||
// runDoctorCapture. Do not call t.Parallel() in this file.
|
||||
|
||||
func TestDoctorIncludesAgentCheck(t *testing.T) {
|
||||
if _, err := exec.LookPath("go"); err != nil {
|
||||
t.Skip("no PATH command")
|
||||
}
|
||||
tmpRoot := t.TempDir()
|
||||
config.SetConfigPathForTest(t, filepath.Join(t.TempDir(), "no-config.yaml"))
|
||||
|
||||
if _, err := workspace.Create(workspace.CreateOpts{
|
||||
Root: tmpRoot,
|
||||
Title: "doctor-agent-test",
|
||||
Category: "general",
|
||||
AgentSpec: workspace.AgentSpec{Type: "custom", Command: "go"},
|
||||
}); err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
|
||||
// doctor returns an error if any check failed; assert on the printed
|
||||
// block, not the exit code.
|
||||
out, _ := runDoctorCapture(t, tmpRoot, "", "", false, false)
|
||||
if !strings.Contains(out, "── Agent Check:") {
|
||||
t.Errorf("doctor output missing Agent Check block:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoctorSkipsAgentCheckWhenNoWorkspace(t *testing.T) {
|
||||
tmpRoot := t.TempDir()
|
||||
config.SetConfigPathForTest(t, filepath.Join(t.TempDir(), "no-config.yaml"))
|
||||
// Do NOT create any workspaces.
|
||||
|
||||
out, _ := runDoctorCapture(t, tmpRoot, "", "", false, false)
|
||||
if !strings.Contains(out, "Agent check: skipped") {
|
||||
t.Errorf("doctor output missing skip line:\n%s", out)
|
||||
}
|
||||
}
|
||||
@@ -197,6 +197,17 @@ func TestCompletionSubcommandViaExecute(t *testing.T) {
|
||||
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 {
|
||||
|
||||
@@ -213,6 +213,11 @@ func runDoctor(cmd *cobra.Command, args []string) error {
|
||||
resolver := config.LoadResolver()
|
||||
checkSettings(resolver, &passed, &failed)
|
||||
|
||||
// Check 11: v0.6 — agent check when a workspace context is resolvable.
|
||||
// Uses the most-recently-active workspace (mirrors `ctask last`); skips
|
||||
// with an INFO line when no active workspace exists anywhere.
|
||||
checkAgentForDoctor(&passed, &failed)
|
||||
|
||||
// Summary
|
||||
fmt.Println()
|
||||
fmt.Printf("%d checks passed, %d failed\n", passed, failed)
|
||||
@@ -364,6 +369,26 @@ func formatSettingSource(s config.ResolvedSetting) string {
|
||||
return base
|
||||
}
|
||||
|
||||
// checkAgentForDoctor runs the same validation as `ctask agents check`
|
||||
// against the most-recently-active workspace. When no active workspace
|
||||
// exists, prints a skip line. FAIL outcomes increment the failed counter
|
||||
// (so doctor's overall exit code reflects agent breakage); WARN does not.
|
||||
func checkAgentForDoctor(passed, failed *int) {
|
||||
_ = passed // reserved for future symmetry with the other checks
|
||||
fmt.Println()
|
||||
roots := config.SearchRoots()
|
||||
best, err := workspace.MostRecentActive(roots)
|
||||
if err != nil || best == nil {
|
||||
fmt.Println("Agent check: skipped (no workspace context)")
|
||||
return
|
||||
}
|
||||
if checkErr := runAgentsCheckOnWorkspace(os.Stdout, best); checkErr != nil {
|
||||
// runAgentsCheckOnWorkspace already printed the [FAIL] lines; just
|
||||
// increment the counter so the summary reflects the breakage.
|
||||
*failed++
|
||||
}
|
||||
}
|
||||
|
||||
// checkTmux reports the three-state tmux check (v0.5.3):
|
||||
// - CTASK_SESSION_MODE != "persistent" -> INFO (direct mode, tmux optional)
|
||||
// - persistent + tmux on PATH + version OK -> two INFO lines
|
||||
|
||||
@@ -19,6 +19,7 @@ func TestNewAgentFlagWritesTypeToTaskYaml(t *testing.T) {
|
||||
prev := runWorkspaceEntry
|
||||
runWorkspaceEntry = func(WorkspaceEntryOptions) error { return nil }
|
||||
t.Cleanup(func() { runWorkspaceEntry = prev })
|
||||
captureRootCmd(t) // restores rootCmd SetArgs/SetOut on cleanup
|
||||
|
||||
rootCmd.SetArgs([]string{"new", "agent-flag-test", "--agent", "claude"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
@@ -46,6 +47,7 @@ func TestNewAgentCustomWithoutCommandRefused(t *testing.T) {
|
||||
prev := runWorkspaceEntry
|
||||
runWorkspaceEntry = func(WorkspaceEntryOptions) error { return nil }
|
||||
t.Cleanup(func() { runWorkspaceEntry = prev })
|
||||
captureRootCmd(t) // restores rootCmd SetArgs/SetOut on cleanup
|
||||
|
||||
rootCmd.SetArgs([]string{"new", "custom-no-cmd", "--agent", "custom"})
|
||||
err := rootCmd.Execute()
|
||||
|
||||
Reference in New Issue
Block a user