diff --git a/internal/config/config.go b/internal/config/config.go index 62a0d3f..6c6b1b6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -59,6 +59,35 @@ func ResolveProjectRoot() string { return expandPath(v) } +// SearchRoots returns the deduplicated list of workspace roots that all query +// and listing operations must consult. Always includes CTASK_ROOT; also +// includes CTASK_PROJECT_ROOT when set and different from CTASK_ROOT. +func SearchRoots() []string { + taskRoot := ResolveRoot() + roots := []string{taskRoot} + + projRoot := ResolveProjectRoot() + if projRoot == "" { + return roots + } + + if samePath(taskRoot, projRoot) { + return roots + } + return append(roots, projRoot) +} + +// samePath reports whether two absolute paths refer to the same directory +// on the current platform. Uses case-insensitive compare on Windows. +func samePath(a, b string) bool { + ac := filepath.Clean(a) + bc := filepath.Clean(b) + if runtime.GOOS == "windows" { + return strings.EqualFold(ac, bc) + } + return ac == bc +} + // EnvVars returns the environment variables to export into child sessions. // taskType must be "task" or "project"; an empty value defaults to "task". func EnvVars(slug, mode, root, workspace, category, taskType string) map[string]string { diff --git a/internal/config/config_roots_test.go b/internal/config/config_roots_test.go new file mode 100644 index 0000000..179e018 --- /dev/null +++ b/internal/config/config_roots_test.go @@ -0,0 +1,53 @@ +package config + +import ( + "os" + "path/filepath" + "testing" +) + +func TestSearchRootsUnsetProjectRoot(t *testing.T) { + os.Unsetenv("CTASK_PROJECT_ROOT") + os.Setenv("CTASK_ROOT", t.TempDir()) + defer os.Unsetenv("CTASK_ROOT") + + roots := SearchRoots() + if len(roots) != 1 { + t.Fatalf("expected 1 root, got %d: %v", len(roots), roots) + } + if roots[0] != ResolveRoot() { + t.Errorf("root mismatch: got %q, want %q", roots[0], ResolveRoot()) + } +} + +func TestSearchRootsDistinctProjectRoot(t *testing.T) { + taskRoot := t.TempDir() + projRoot := t.TempDir() + os.Setenv("CTASK_ROOT", taskRoot) + os.Setenv("CTASK_PROJECT_ROOT", projRoot) + defer os.Unsetenv("CTASK_ROOT") + defer os.Unsetenv("CTASK_PROJECT_ROOT") + + roots := SearchRoots() + if len(roots) != 2 { + t.Fatalf("expected 2 roots, got %d: %v", len(roots), roots) + } + absTask, _ := filepath.Abs(taskRoot) + absProj, _ := filepath.Abs(projRoot) + if roots[0] != absTask || roots[1] != absProj { + t.Errorf("roots: got %v, want [%q, %q]", roots, absTask, absProj) + } +} + +func TestSearchRootsSameRootDedupes(t *testing.T) { + d := t.TempDir() + os.Setenv("CTASK_ROOT", d) + os.Setenv("CTASK_PROJECT_ROOT", d) + defer os.Unsetenv("CTASK_ROOT") + defer os.Unsetenv("CTASK_PROJECT_ROOT") + + roots := SearchRoots() + if len(roots) != 1 { + t.Fatalf("expected 1 root (dedup), got %d: %v", len(roots), roots) + } +}