diff --git a/cmd/info.go b/cmd/info.go index 56849b9..a3616f5 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "os" + "path/filepath" "github.com/spf13/cobra" "github.com/warrenronsiek/ctask/internal/config" @@ -38,6 +39,22 @@ func runInfo(cmd *cobra.Command, args []string) error { fmt.Printf("Updated: %s\n", m.UpdatedAt.Format("2006-01-02 15:04:05")) fmt.Printf("Path: %s\n", ws.Path) + if m.LaunchDir != "" { + // Per spec amendment: stat the expected path directly instead of + // inferring existence from ResolveLaunch's fallback behavior. info + // is a display command, not a launch command — a permission error + // here is the same user-facing outcome as "not a directory", so + // we just report whether the stat succeeded with a directory. + launchAbs := filepath.Join(ws.Path, m.LaunchDir) + dirExists := "no" + if info, err := os.Stat(launchAbs); err == nil && info.IsDir() { + dirExists = "yes" + } + fmt.Printf("Launch dir: %s/\n", m.LaunchDir) + fmt.Printf("Launch path: %s\n", launchAbs) + fmt.Printf("Dir exists: %s\n", dirExists) + } + if m.ArchivedAt != nil { fmt.Printf("Archived: %s\n", m.ArchivedAt.Format("2006-01-02 15:04:05")) } diff --git a/cmd/info_launch_test.go b/cmd/info_launch_test.go new file mode 100644 index 0000000..07c82cf --- /dev/null +++ b/cmd/info_launch_test.go @@ -0,0 +1,112 @@ +package cmd + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/warrenronsiek/ctask/internal/workspace" +) + +// runInfoCapture runs runInfo with CTASK_ROOT set and captures stdout. +func runInfoCapture(t *testing.T, root, query string) (string, error) { + 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) + } + }() + + stdoutR, stdoutW, _ := os.Pipe() + prevStdout := os.Stdout + os.Stdout = stdoutW + defer func() { os.Stdout = prevStdout }() + + err := runInfo(infoCmd, []string{query}) + stdoutW.Close() + var buf bytes.Buffer + buf.ReadFrom(stdoutR) + return buf.String(), err +} + +func TestInfoShowsLaunchFieldsWhenLaunchDirSet(t *testing.T) { + root := t.TempDir() + wsDir := filepath.Join(root, "projects", "2026-04-22_demo") + os.MkdirAll(wsDir, 0755) + os.MkdirAll(filepath.Join(wsDir, "demo"), 0755) + now := time.Now().UTC().Truncate(time.Second) + meta := &workspace.TaskMeta{ + ID: "t", Slug: "demo", Title: "demo", + CreatedAt: now, UpdatedAt: now, + Status: "active", Category: "projects", Type: "project", + Mode: "local", Agent: "claude", LaunchDir: "demo", + } + workspace.WriteMeta(filepath.Join(wsDir, "task.yaml"), meta) + + out, err := runInfoCapture(t, root, "demo") + if err != nil { + t.Fatalf("runInfo: %v", err) + } + for _, want := range []string{"Launch dir:", "Launch path:", "Dir exists:", "demo"} { + if !strings.Contains(out, want) { + t.Errorf("info output missing %q:\n%s", want, out) + } + } + if !strings.Contains(out, "Dir exists: yes") { + t.Errorf("expected 'Dir exists: yes', got:\n%s", out) + } +} + +func TestInfoOmitsLaunchFieldsForTask(t *testing.T) { + root := t.TempDir() + wsDir := filepath.Join(root, "general", "2026-04-22_regular") + os.MkdirAll(wsDir, 0755) + now := time.Now().UTC().Truncate(time.Second) + meta := &workspace.TaskMeta{ + ID: "t", Slug: "regular", Title: "regular", + CreatedAt: now, UpdatedAt: now, + Status: "active", Category: "general", Type: "task", + Mode: "local", Agent: "claude", + } + workspace.WriteMeta(filepath.Join(wsDir, "task.yaml"), meta) + + out, err := runInfoCapture(t, root, "regular") + if err != nil { + t.Fatalf("runInfo: %v", err) + } + for _, shouldAbsent := range []string{"Launch dir:", "Launch path:", "Dir exists:"} { + if strings.Contains(out, shouldAbsent) { + t.Errorf("task info should not show %q:\n%s", shouldAbsent, out) + } + } +} + +func TestInfoShowsDirExistsNoWhenLaunchDirMissing(t *testing.T) { + root := t.TempDir() + wsDir := filepath.Join(root, "projects", "2026-04-22_renamed") + os.MkdirAll(wsDir, 0755) + // Intentionally do NOT create the project subdir. + now := time.Now().UTC().Truncate(time.Second) + meta := &workspace.TaskMeta{ + ID: "t", Slug: "renamed", Title: "renamed", + CreatedAt: now, UpdatedAt: now, + Status: "active", Category: "projects", Type: "project", + Mode: "local", Agent: "claude", LaunchDir: "renamed", + } + workspace.WriteMeta(filepath.Join(wsDir, "task.yaml"), meta) + + out, err := runInfoCapture(t, root, "renamed") + if err != nil { + t.Fatalf("runInfo: %v", err) + } + if !strings.Contains(out, "Dir exists: no") { + t.Errorf("expected 'Dir exists: no' for missing launch dir:\n%s", out) + } +}