feat(v0.3): add Projects filter to ListWorkspaces; fix last/delete
ListOpts gains a Projects bool that filters by EffectiveType. Default behavior (Projects: false) now returns tasks only -- this is a deliberate semantic change that supports the new 'ctask list' (tasks) vs 'ctask list --projects' (projects) spec. The change silently regresses two cmd-level callers that scan for "the most recently updated workspace": cmd/last.go (used by 'ctask last') and cmd/delete.go (used to print the "this was your most recent workspace" note). Both are fixed by unioning a tasks-scan with a projects-scan, so 'last' and 'delete' continue to consider both types. Test helper createTestWorkspaceTyped allows setting an explicit type (or "" to simulate a v0.2 workspace with no type field).
This commit is contained in:
@@ -10,6 +10,11 @@ type ListOpts struct {
|
||||
IncludeArchived bool
|
||||
Category string
|
||||
Limit int
|
||||
|
||||
// Projects, when true, returns project workspaces only. When false (default),
|
||||
// only task workspaces are returned. v0.2 workspaces with no Type field are
|
||||
// treated as tasks via EffectiveType.
|
||||
Projects bool
|
||||
}
|
||||
|
||||
// ListWorkspaces returns workspaces in reverse-chronological order.
|
||||
@@ -19,6 +24,11 @@ func ListWorkspaces(root string, opts ListOpts) ([]QueryResult, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wantType := "task"
|
||||
if opts.Projects {
|
||||
wantType = "project"
|
||||
}
|
||||
|
||||
var filtered []QueryResult
|
||||
for _, ws := range all {
|
||||
if !opts.IncludeArchived && ws.Meta.Status == "archived" {
|
||||
@@ -27,6 +37,9 @@ func ListWorkspaces(root string, opts ListOpts) ([]QueryResult, error) {
|
||||
if opts.Category != "" && ws.Meta.Category != opts.Category {
|
||||
continue
|
||||
}
|
||||
if EffectiveType(ws.Meta) != wantType {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, ws)
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,60 @@ func TestListWorkspaces(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListProjectsFilter(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
createTestWorkspaceTyped(t, root, "general", "2026-04-05_task-a", "active", "task")
|
||||
createTestWorkspaceTyped(t, root, "general", "2026-04-04_task-b", "active", "") // legacy: no type -> task
|
||||
createTestWorkspaceTyped(t, root, "projects", "2026-04-03_proj-a", "active", "project")
|
||||
createTestWorkspaceTyped(t, root, "projects", "2026-04-02_proj-b", "archived", "project")
|
||||
|
||||
// Default: tasks only, active
|
||||
results, err := ListWorkspaces(root, ListOpts{Limit: 20})
|
||||
if err != nil {
|
||||
t.Fatalf("ListWorkspaces: %v", err)
|
||||
}
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("expected 2 active tasks, got %d", len(results))
|
||||
}
|
||||
for _, r := range results {
|
||||
if EffectiveType(r.Meta) != "task" {
|
||||
t.Errorf("non-task in default list: %s (type %q)", r.Meta.Slug, r.Meta.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// --projects: projects only, active
|
||||
results, err = ListWorkspaces(root, ListOpts{Projects: true, Limit: 20})
|
||||
if err != nil {
|
||||
t.Fatalf("ListWorkspaces --projects: %v", err)
|
||||
}
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("expected 1 active project, got %d", len(results))
|
||||
}
|
||||
if results[0].Meta.Slug != "proj-a" {
|
||||
t.Errorf("expected proj-a, got %s", results[0].Meta.Slug)
|
||||
}
|
||||
|
||||
// --projects --all
|
||||
results, err = ListWorkspaces(root, ListOpts{Projects: true, IncludeArchived: true, Limit: 20})
|
||||
if err != nil {
|
||||
t.Fatalf("ListWorkspaces --projects --all: %v", err)
|
||||
}
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("expected 2 projects with --all, got %d", len(results))
|
||||
}
|
||||
|
||||
// --all alone: tasks only with archived
|
||||
results, err = ListWorkspaces(root, ListOpts{IncludeArchived: true, Limit: 20})
|
||||
if err != nil {
|
||||
t.Fatalf("ListWorkspaces --all: %v", err)
|
||||
}
|
||||
for _, r := range results {
|
||||
if EffectiveType(r.Meta) != "task" {
|
||||
t.Errorf("--all alone returned non-task: %s", r.Meta.Slug)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListReverseChronological(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
createTestWorkspace(t, root, "general", "2026-04-01_first", "active")
|
||||
|
||||
@@ -7,8 +7,15 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// helper to create a minimal workspace for query testing
|
||||
// createTestWorkspace creates a minimal task workspace (Type defaults to "task")
|
||||
// for query/list testing.
|
||||
func createTestWorkspace(t *testing.T, root, category, dirName string, status string) {
|
||||
createTestWorkspaceTyped(t, root, category, dirName, status, "task")
|
||||
}
|
||||
|
||||
// createTestWorkspaceTyped creates a minimal workspace of an explicit type
|
||||
// (use "" to simulate a v0.2 workspace with no type field).
|
||||
func createTestWorkspaceTyped(t *testing.T, root, category, dirName, status, taskType string) {
|
||||
t.Helper()
|
||||
dir := filepath.Join(root, category, dirName)
|
||||
os.MkdirAll(dir, 0755)
|
||||
@@ -23,6 +30,7 @@ func createTestWorkspace(t *testing.T, root, category, dirName string, status st
|
||||
UpdatedAt: now,
|
||||
Status: status,
|
||||
Category: category,
|
||||
Type: taskType,
|
||||
Mode: "local",
|
||||
Agent: "claude",
|
||||
WorkspacePath: dir,
|
||||
|
||||
Reference in New Issue
Block a user