From a162aec0b2191760d3c5fea61b21ece84dacee51 Mon Sep 17 00:00:00 2001 From: typebasedio Date: Wed, 22 Apr 2026 21:33:52 -0400 Subject: [PATCH] fix(v0.5.1): use local time for workspace directory prefix and info display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workspace directory names (YYYY-MM-DD_slug) and the YYYYMMDD-HHMMSS ID field now use local time so users see their wall-clock date rather than UTC — the prior behavior caused evening-EST creations to appear under tomorrow's date for several hours every day. ctask info's Created/Updated/Archived lines also convert to local for display. Stored timestamps in task.yaml, session logs, the lease, the manifest, and the session summary all continue to use UTC. Only user-facing surfaces change. Co-Authored-By: Claude Opus 4.7 (1M context) --- cmd/info.go | 9 ++++-- cmd/info_launch_test.go | 50 +++++++++++++++++++++++++++++++ internal/workspace/create.go | 10 +++++-- internal/workspace/create_test.go | 38 +++++++++++++++++++++++ 4 files changed, 101 insertions(+), 6 deletions(-) diff --git a/cmd/info.go b/cmd/info.go index a3616f5..1d30079 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -35,8 +35,11 @@ func runInfo(cmd *cobra.Command, args []string) error { fmt.Printf("Status: %s\n", m.Status) fmt.Printf("Mode: %s\n", m.Mode) fmt.Printf("Agent: %s\n", m.Agent) - fmt.Printf("Created: %s\n", m.CreatedAt.Format("2006-01-02 15:04:05")) - fmt.Printf("Updated: %s\n", m.UpdatedAt.Format("2006-01-02 15:04:05")) + // v0.5.1: display timestamps in local time. task.yaml stores UTC; + // info converts for friendliness so the shown time matches the user's + // wall clock. + fmt.Printf("Created: %s\n", m.CreatedAt.Local().Format("2006-01-02 15:04:05")) + fmt.Printf("Updated: %s\n", m.UpdatedAt.Local().Format("2006-01-02 15:04:05")) fmt.Printf("Path: %s\n", ws.Path) if m.LaunchDir != "" { @@ -56,7 +59,7 @@ func runInfo(cmd *cobra.Command, args []string) error { } if m.ArchivedAt != nil { - fmt.Printf("Archived: %s\n", m.ArchivedAt.Format("2006-01-02 15:04:05")) + fmt.Printf("Archived: %s\n", m.ArchivedAt.Local().Format("2006-01-02 15:04:05")) } // List contents diff --git a/cmd/info_launch_test.go b/cmd/info_launch_test.go index 07c82cf..f22b166 100644 --- a/cmd/info_launch_test.go +++ b/cmd/info_launch_test.go @@ -88,6 +88,56 @@ func TestInfoOmitsLaunchFieldsForTask(t *testing.T) { } } +func TestInfoFormatsTimestampsInLocalZone(t *testing.T) { + // v0.5.1: info must display Created/Updated/Archived in the local zone + // so the displayed time matches the user's wall clock. The stored + // timestamp stays UTC in task.yaml (unambiguous), but the display + // converts for friendliness. + root := t.TempDir() + wsDir := filepath.Join(root, "general", "2026-04-22_tz-test") + os.MkdirAll(wsDir, 0755) + + // Pick a UTC moment that crosses a date boundary in most western zones. + fixedUTC := time.Date(2026, 4, 23, 1, 22, 50, 0, time.UTC) + archived := fixedUTC.Add(time.Hour) + meta := &workspace.TaskMeta{ + ID: "t", Slug: "tz-test", Title: "tz-test", + CreatedAt: fixedUTC, UpdatedAt: fixedUTC, + ArchivedAt: &archived, + Status: "archived", Category: "general", Type: "task", + Mode: "local", Agent: "claude", + } + workspace.WriteMeta(filepath.Join(wsDir, "task.yaml"), meta) + + // infoAll must be true because the workspace is archived. + prevAll := infoAll + infoAll = true + defer func() { infoAll = prevAll }() + + out, err := runInfoCapture(t, root, "tz-test") + if err != nil { + t.Fatalf("runInfo: %v", err) + } + + localCreated := fixedUTC.Local().Format("2006-01-02 15:04:05") + utcCreated := fixedUTC.Format("2006-01-02 15:04:05") + localArchived := archived.Local().Format("2006-01-02 15:04:05") + + if !strings.Contains(out, "Created: "+localCreated) { + t.Errorf("expected Created line to show local time %q:\n%s", localCreated, out) + } + if !strings.Contains(out, "Updated: "+localCreated) { + t.Errorf("expected Updated line to show local time %q:\n%s", localCreated, out) + } + if !strings.Contains(out, "Archived: "+localArchived) { + t.Errorf("expected Archived line to show local time %q:\n%s", localArchived, out) + } + // Only meaningful when local != UTC (skips when the test host is UTC). + if localCreated != utcCreated && strings.Contains(out, "Created: "+utcCreated) { + t.Errorf("Created line must not show UTC time %q when local differs (%q):\n%s", utcCreated, localCreated, out) + } +} + func TestInfoShowsDirExistsNoWhenLaunchDirMissing(t *testing.T) { root := t.TempDir() wsDir := filepath.Join(root, "projects", "2026-04-22_renamed") diff --git a/internal/workspace/create.go b/internal/workspace/create.go index 2c0fd5f..3b74433 100644 --- a/internal/workspace/create.go +++ b/internal/workspace/create.go @@ -62,9 +62,13 @@ func Create(opts CreateOpts) (*CreateResult, error) { slug = "task" } - now := time.Now().UTC() - date := now.Format("2006-01-02") - id := now.Format("20060102-150405") + // v0.5.1: directory prefix and ID use local time so users see their + // wall-clock date rather than UTC. Stored task.yaml timestamps still + // use UTC (see meta below) for unambiguous machine-readable format. + nowLocal := time.Now() + date := nowLocal.Format("2006-01-02") + id := nowLocal.Format("20060102-150405") + now := nowLocal.UTC() categoryDir := opts.Root if !opts.SkipCategoryDir { diff --git a/internal/workspace/create_test.go b/internal/workspace/create_test.go index 52ec018..fb8ae9d 100644 --- a/internal/workspace/create_test.go +++ b/internal/workspace/create_test.go @@ -5,6 +5,7 @@ import ( "path/filepath" "strings" "testing" + "time" ) func TestCreateWorkspace(t *testing.T) { @@ -372,6 +373,43 @@ func TestCreateProjectSubdirMatchesSuffixedSlug(t *testing.T) { } } +func TestCreateDirectoryPrefixUsesLocalDate(t *testing.T) { + // v0.5.1: directory prefix must use local-time date so users see + // their wall-clock date rather than UTC date on evening EST runs. + root := t.TempDir() + expected := time.Now().Format("2006-01-02") // LOCAL — no .UTC() + res, err := Create(CreateOpts{ + Root: root, Title: "tz-check", Category: "general", + Mode: "local", Agent: "claude", + }) + if err != nil { + t.Fatalf("Create: %v", err) + } + dirBase := filepath.Base(res.Path) + if !strings.HasPrefix(dirBase, expected) { + t.Errorf("directory %q should start with local-date prefix %q", dirBase, expected) + } +} + +func TestCreateStoresTimestampsInUTC(t *testing.T) { + // v0.5.1: even though the directory prefix is local, stored task.yaml + // timestamps must remain UTC (unambiguous machine-readable format). + root := t.TempDir() + res, err := Create(CreateOpts{ + Root: root, Title: "tz-check-stored", Category: "general", + Mode: "local", Agent: "claude", + }) + if err != nil { + t.Fatalf("Create: %v", err) + } + if got := res.Meta.CreatedAt.Location(); got != time.UTC { + t.Errorf("CreatedAt location: got %v, want UTC", got) + } + if got := res.Meta.UpdatedAt.Location(); got != time.UTC { + t.Errorf("UpdatedAt location: got %v, want UTC", got) + } +} + func TestCreateTaskDoesNotScaffoldSubdir(t *testing.T) { root := t.TempDir() res, err := Create(CreateOpts{