test(v0.3): add MostRecentActive helper with focused unit coverage
Extracts the cross-type "most recently updated active workspace" selector that previously lived inline in cmd/last and cmd/delete. The helper takes only a root path and returns (nil, nil) when nothing matches, which keeps the call sites trivial. Eight focused tests cover: - tasks-only fixture - projects-only fixture - project most-recent vs older task - task most-recent vs older project - legacy v0.2 workspace (no Type field) winning, treated as task - newest workspace archived, older active project wins - empty root returns (nil, nil) - all archived returns (nil, nil) A new createTestWorkspaceFull helper takes an explicit UpdatedAt timestamp so the "which is newest" assertions don't depend on wall-clock ordering. cmd/last and cmd/delete are migrated onto the helper in the next commit.
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
package workspace
|
||||
|
||||
// MostRecentActive returns the active workspace with the latest UpdatedAt
|
||||
// timestamp under root, considering both tasks and projects.
|
||||
//
|
||||
// Returns (nil, nil) if no active workspaces exist (this is not an error
|
||||
// condition; callers decide how to surface "nothing to do").
|
||||
//
|
||||
// Legacy v0.2 workspaces (no Type field) are included as tasks via
|
||||
// EffectiveType. Archived workspaces are always excluded.
|
||||
func MostRecentActive(root string) (*QueryResult, error) {
|
||||
results, err := ListWorkspaces(root, ListOpts{
|
||||
IncludeArchived: false,
|
||||
Limit: 0,
|
||||
Type: TypeAny,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(results) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
best := results[0]
|
||||
for _, r := range results[1:] {
|
||||
if r.Meta.UpdatedAt.After(best.Meta.UpdatedAt) {
|
||||
best = r
|
||||
}
|
||||
}
|
||||
return &best, nil
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package workspace
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// All test timestamps are anchored to a fixed base date so the assertions
|
||||
// don't depend on wall-clock time.
|
||||
var baseT = time.Date(2026, 4, 1, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
func TestMostRecentActiveTasksOnly(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
createTestWorkspaceFull(t, root, "general", "2026-04-01_old", "active", "task", baseT)
|
||||
createTestWorkspaceFull(t, root, "general", "2026-04-02_new", "active", "task", baseT.Add(2*time.Hour))
|
||||
|
||||
got, err := MostRecentActive(root)
|
||||
if err != nil {
|
||||
t.Fatalf("MostRecentActive: %v", err)
|
||||
}
|
||||
if got == nil {
|
||||
t.Fatal("expected a result, got nil")
|
||||
}
|
||||
if got.Meta.Slug != "new" {
|
||||
t.Errorf("slug: got %q, want \"new\"", got.Meta.Slug)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMostRecentActiveProjectsOnly(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
createTestWorkspaceFull(t, root, "projects", "2026-04-01_old-proj", "active", "project", baseT)
|
||||
createTestWorkspaceFull(t, root, "projects", "2026-04-02_new-proj", "active", "project", baseT.Add(2*time.Hour))
|
||||
|
||||
got, err := MostRecentActive(root)
|
||||
if err != nil {
|
||||
t.Fatalf("MostRecentActive: %v", err)
|
||||
}
|
||||
if got == nil {
|
||||
t.Fatal("expected a result, got nil")
|
||||
}
|
||||
if got.Meta.Slug != "new-proj" {
|
||||
t.Errorf("slug: got %q, want \"new-proj\"", got.Meta.Slug)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMostRecentActiveProjectWinsOverOlderTask(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
createTestWorkspaceFull(t, root, "general", "2026-04-01_old-task", "active", "task", baseT)
|
||||
createTestWorkspaceFull(t, root, "projects", "2026-04-02_new-proj", "active", "project", baseT.Add(2*time.Hour))
|
||||
|
||||
got, err := MostRecentActive(root)
|
||||
if err != nil {
|
||||
t.Fatalf("MostRecentActive: %v", err)
|
||||
}
|
||||
if got == nil || got.Meta.Slug != "new-proj" {
|
||||
t.Fatalf("expected new-proj, got %+v", got)
|
||||
}
|
||||
if EffectiveType(got.Meta) != "project" {
|
||||
t.Errorf("EffectiveType: got %q, want \"project\"", EffectiveType(got.Meta))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMostRecentActiveTaskWinsOverOlderProject(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
createTestWorkspaceFull(t, root, "projects", "2026-04-01_old-proj", "active", "project", baseT)
|
||||
createTestWorkspaceFull(t, root, "general", "2026-04-02_new-task", "active", "task", baseT.Add(2*time.Hour))
|
||||
|
||||
got, err := MostRecentActive(root)
|
||||
if err != nil {
|
||||
t.Fatalf("MostRecentActive: %v", err)
|
||||
}
|
||||
if got == nil || got.Meta.Slug != "new-task" {
|
||||
t.Fatalf("expected new-task, got %+v", got)
|
||||
}
|
||||
if EffectiveType(got.Meta) != "task" {
|
||||
t.Errorf("EffectiveType: got %q, want \"task\"", EffectiveType(got.Meta))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMostRecentActiveLegacyWorkspaceCounts(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
// Most recent has no Type field at all (v0.2 era).
|
||||
createTestWorkspaceFull(t, root, "projects", "2026-04-01_old-proj", "active", "project", baseT)
|
||||
createTestWorkspaceFull(t, root, "general", "2026-04-02_legacy", "active", "", baseT.Add(2*time.Hour))
|
||||
|
||||
got, err := MostRecentActive(root)
|
||||
if err != nil {
|
||||
t.Fatalf("MostRecentActive: %v", err)
|
||||
}
|
||||
if got == nil || got.Meta.Slug != "legacy" {
|
||||
t.Fatalf("expected legacy, got %+v", got)
|
||||
}
|
||||
// EffectiveType should fall back to "task".
|
||||
if EffectiveType(got.Meta) != "task" {
|
||||
t.Errorf("legacy EffectiveType: got %q, want \"task\"", EffectiveType(got.Meta))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMostRecentActiveExcludesArchived(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
// Newest is archived; should be skipped in favor of the older active project.
|
||||
createTestWorkspaceFull(t, root, "general", "2026-04-03_newest-archived", "archived", "task", baseT.Add(3*time.Hour))
|
||||
createTestWorkspaceFull(t, root, "projects", "2026-04-02_active-proj", "active", "project", baseT.Add(2*time.Hour))
|
||||
createTestWorkspaceFull(t, root, "general", "2026-04-01_old-task", "active", "task", baseT)
|
||||
|
||||
got, err := MostRecentActive(root)
|
||||
if err != nil {
|
||||
t.Fatalf("MostRecentActive: %v", err)
|
||||
}
|
||||
if got == nil || got.Meta.Slug != "active-proj" {
|
||||
t.Fatalf("expected active-proj (newest non-archived), got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMostRecentActiveReturnsNilWhenEmpty(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
got, err := MostRecentActive(root)
|
||||
if err != nil {
|
||||
t.Fatalf("MostRecentActive: %v", err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Errorf("expected nil for empty root, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMostRecentActiveReturnsNilWhenAllArchived(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
createTestWorkspaceFull(t, root, "general", "2026-04-01_arch-task", "archived", "task", baseT)
|
||||
createTestWorkspaceFull(t, root, "projects", "2026-04-02_arch-proj", "archived", "project", baseT.Add(time.Hour))
|
||||
|
||||
got, err := MostRecentActive(root)
|
||||
if err != nil {
|
||||
t.Fatalf("MostRecentActive: %v", err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Errorf("expected nil when all workspaces archived, got %+v", got)
|
||||
}
|
||||
}
|
||||
@@ -16,18 +16,25 @@ func createTestWorkspace(t *testing.T, root, category, dirName string, status st
|
||||
// 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()
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
createTestWorkspaceFull(t, root, category, dirName, status, taskType, now)
|
||||
}
|
||||
|
||||
// createTestWorkspaceFull creates a workspace with an explicit UpdatedAt
|
||||
// timestamp -- used by tests that exercise "most recently updated" logic.
|
||||
func createTestWorkspaceFull(t *testing.T, root, category, dirName, status, taskType string, updatedAt time.Time) {
|
||||
t.Helper()
|
||||
dir := filepath.Join(root, category, dirName)
|
||||
os.MkdirAll(dir, 0755)
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
// Extract slug from dirName (skip "YYYY-MM-DD_")
|
||||
slug := dirName[11:]
|
||||
meta := &TaskMeta{
|
||||
ID: "test",
|
||||
Slug: slug,
|
||||
Title: slug,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
CreatedAt: updatedAt,
|
||||
UpdatedAt: updatedAt,
|
||||
Status: status,
|
||||
Category: category,
|
||||
Type: taskType,
|
||||
|
||||
Reference in New Issue
Block a user