feat(v0.4.1): add seed directory checks to ctask doctor

Replaces the always-informational seed-dir block with three-state checks:
INFO when the env var is unset (defaults will be used), PASS when set and
the path exists, FAIL when set but the directory is missing. Only the
configured-but-missing state counts as a failed check and raises the
doctor exit code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 17:56:27 -04:00
parent b4f35231d4
commit 57c6c909d3
3 changed files with 144 additions and 18 deletions
+41 -17
View File
@@ -178,23 +178,27 @@ func runDoctor(cmd *cobra.Command, args []string) error {
failed++
}
// Informational checks (do not affect pass/fail counters).
fmt.Println()
fmt.Println("Seed directories (informational):")
seedDir := config.ResolveSeedDir()
if info, err := os.Stat(seedDir); err == nil && info.IsDir() {
fmt.Printf(" [INFO] General seed directory: %s (present)\n", seedDir)
} else {
fmt.Printf(" [INFO] General seed directory: %s (not present)\n", seedDir)
}
projectSeedDir := config.ResolveProjectSeedDir()
if info, err := os.Stat(projectSeedDir); err == nil && info.IsDir() {
fmt.Printf(" [INFO] Project seed directory: %s (present)\n", projectSeedDir)
} else {
fmt.Printf(" [INFO] Project seed directory: %s (not present)\n", projectSeedDir)
}
// Checks 6 + 7: Seed directory configuration.
// Three states per variable:
// - env not set -> informational (built-in defaults will be used)
// - env set, path exists -> pass
// - env set, path missing -> fail
checkSeedDir(
"General seed directory",
os.Getenv("CTASK_SEED_DIR"),
config.ResolveSeedDir(),
"CTASK_SEED_DIR",
&passed,
&failed,
)
checkSeedDir(
"Project seed directory",
os.Getenv("CTASK_SEED_PROJECT_DIR"),
config.ResolveProjectSeedDir(),
"CTASK_SEED_PROJECT_DIR",
&passed,
&failed,
)
// Summary
fmt.Println()
@@ -205,3 +209,23 @@ func runDoctor(cmd *cobra.Command, args []string) error {
}
return nil
}
// checkSeedDir prints one of the three seed-directory doctor states.
// envValue is the raw value of the CTASK_SEED_* variable (empty means unset).
// resolved is the absolute path returned by the config resolver (used only
// when envValue is non-empty; for the "not configured" state the built-in
// default path is not displayed).
func checkSeedDir(label, envValue, resolved, envName string, passed, failed *int) {
if envValue == "" {
fmt.Printf(" [INFO] %s: not configured (using built-in defaults)\n", label)
return
}
if info, err := os.Stat(resolved); err == nil && info.IsDir() {
fmt.Printf(" [PASS] %s: %s\n", label, resolved)
*passed++
return
}
fmt.Printf(" [FAIL] %s configured but not found: %s\n", label, resolved)
fmt.Printf(" Fix: create the directory or unset %s to use built-in defaults.\n", envName)
*failed++
}
+100
View File
@@ -0,0 +1,100 @@
package cmd
import (
"bytes"
"os"
"strings"
"testing"
)
// This file swaps process-global os.Stdout and env vars. Do not call
// t.Parallel() in this file.
// runDoctorCapture invokes runDoctor with a given CTASK_ROOT (and optional
// seed env vars) and returns stdout and the returned error.
func runDoctorCapture(t *testing.T, root string, seedDir, seedProjDir string, seedSet, seedProjSet bool) (string, error) {
t.Helper()
prevRoot := os.Getenv("CTASK_ROOT")
prevSeed := os.Getenv("CTASK_SEED_DIR")
prevSeedProj := os.Getenv("CTASK_SEED_PROJECT_DIR")
os.Setenv("CTASK_ROOT", root)
if seedSet {
os.Setenv("CTASK_SEED_DIR", seedDir)
} else {
os.Unsetenv("CTASK_SEED_DIR")
}
if seedProjSet {
os.Setenv("CTASK_SEED_PROJECT_DIR", seedProjDir)
} else {
os.Unsetenv("CTASK_SEED_PROJECT_DIR")
}
defer func() {
if prevRoot == "" {
os.Unsetenv("CTASK_ROOT")
} else {
os.Setenv("CTASK_ROOT", prevRoot)
}
if prevSeed == "" {
os.Unsetenv("CTASK_SEED_DIR")
} else {
os.Setenv("CTASK_SEED_DIR", prevSeed)
}
if prevSeedProj == "" {
os.Unsetenv("CTASK_SEED_PROJECT_DIR")
} else {
os.Setenv("CTASK_SEED_PROJECT_DIR", prevSeedProj)
}
}()
stdoutR, stdoutW, _ := os.Pipe()
prevStdout := os.Stdout
os.Stdout = stdoutW
defer func() { os.Stdout = prevStdout }()
err := runDoctor(doctorCmd, nil)
stdoutW.Close()
var buf bytes.Buffer
buf.ReadFrom(stdoutR)
return buf.String(), err
}
func TestDoctorSeedDirNotConfigured(t *testing.T) {
root := t.TempDir()
out, _ := runDoctorCapture(t, root, "", "", false, false)
if !strings.Contains(out, "[INFO] General seed directory: not configured") {
t.Errorf("expected 'not configured' INFO line, got:\n%s", out)
}
if !strings.Contains(out, "[INFO] Project seed directory: not configured") {
t.Errorf("expected project 'not configured' INFO line, got:\n%s", out)
}
}
func TestDoctorSeedDirConfiguredAndExists(t *testing.T) {
root := t.TempDir()
seedDir := t.TempDir()
seedProjDir := t.TempDir()
out, _ := runDoctorCapture(t, root, seedDir, seedProjDir, true, true)
if !strings.Contains(out, "[PASS] General seed directory: "+seedDir) {
t.Errorf("expected PASS line for general seed dir, got:\n%s", out)
}
if !strings.Contains(out, "[PASS] Project seed directory: "+seedProjDir) {
t.Errorf("expected PASS line for project seed dir, got:\n%s", out)
}
}
func TestDoctorSeedDirConfiguredButMissingFails(t *testing.T) {
root := t.TempDir()
missing := root + string(os.PathSeparator) + "no-such-seed-dir"
out, err := runDoctorCapture(t, root, missing, "", true, false)
if err == nil {
t.Fatal("expected non-nil error (failed check), got nil")
}
if !strings.Contains(out, "[FAIL] General seed directory configured but not found: "+missing) {
t.Errorf("expected FAIL line for missing seed dir, got:\n%s", out)
}
}