feat(v0.3): add Type field to TaskMeta with backward-compat helper
TaskMeta now records type: task or type: project. EffectiveType returns 'task' for missing/empty/unknown values and for nil meta, so v0.2 workspaces continue to read as tasks without any migration. The field is placed between Category and Mode in the YAML output. Tests cover the round-trip and the legacy no-type-field case.
This commit is contained in:
@@ -7,7 +7,9 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// TaskMeta represents the task.yaml schema. Exactly these fields, no extras in v0.1.
|
||||
// TaskMeta represents the task.yaml schema.
|
||||
// The Type field is v0.3+; older workspaces without this field are treated as "task"
|
||||
// (see EffectiveType). Field order in this struct is the YAML output order.
|
||||
type TaskMeta struct {
|
||||
ID string `yaml:"id"`
|
||||
Slug string `yaml:"slug"`
|
||||
@@ -16,12 +18,28 @@ type TaskMeta struct {
|
||||
UpdatedAt time.Time `yaml:"updated_at"`
|
||||
Status string `yaml:"status"`
|
||||
Category string `yaml:"category"`
|
||||
Type string `yaml:"type"`
|
||||
Mode string `yaml:"mode"`
|
||||
Agent string `yaml:"agent"`
|
||||
WorkspacePath string `yaml:"workspace_path"`
|
||||
ArchivedAt *time.Time `yaml:"archived_at"`
|
||||
}
|
||||
|
||||
// EffectiveType returns "task" or "project". An empty or missing Type field
|
||||
// is treated as "task" for backward compatibility with v0.2 workspaces.
|
||||
// A nil meta is also treated as "task".
|
||||
func EffectiveType(m *TaskMeta) string {
|
||||
if m == nil {
|
||||
return "task"
|
||||
}
|
||||
switch m.Type {
|
||||
case "project":
|
||||
return "project"
|
||||
default:
|
||||
return "task"
|
||||
}
|
||||
}
|
||||
|
||||
// WriteMeta writes a TaskMeta to a YAML file.
|
||||
func WriteMeta(path string, meta *TaskMeta) error {
|
||||
data, err := yaml.Marshal(meta)
|
||||
|
||||
@@ -76,13 +76,94 @@ func TestMetaYAMLFieldsPresent(t *testing.T) {
|
||||
data, _ := os.ReadFile(path)
|
||||
content := string(data)
|
||||
|
||||
for _, field := range []string{"id:", "slug:", "title:", "created_at:", "updated_at:", "status:", "category:", "mode:", "agent:", "workspace_path:", "archived_at:"} {
|
||||
for _, field := range []string{"id:", "slug:", "title:", "created_at:", "updated_at:", "status:", "category:", "type:", "mode:", "agent:", "workspace_path:", "archived_at:"} {
|
||||
if !strings.Contains(content, field) {
|
||||
t.Errorf("missing field %s in YAML output", field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaTypeRoundTrip(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "task.yaml")
|
||||
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
meta := &TaskMeta{
|
||||
ID: "20260410-120000",
|
||||
Slug: "billing",
|
||||
Title: "billing",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Status: "active",
|
||||
Category: "projects",
|
||||
Type: "project",
|
||||
Mode: "local",
|
||||
Agent: "claude",
|
||||
WorkspacePath: "/tmp/billing",
|
||||
}
|
||||
if err := WriteMeta(path, meta); err != nil {
|
||||
t.Fatalf("WriteMeta: %v", err)
|
||||
}
|
||||
got, err := ReadMeta(path)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadMeta: %v", err)
|
||||
}
|
||||
if got.Type != "project" {
|
||||
t.Errorf("Type: got %q, want \"project\"", got.Type)
|
||||
}
|
||||
if EffectiveType(got) != "project" {
|
||||
t.Errorf("EffectiveType: got %q, want \"project\"", EffectiveType(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaTypeMissingDefaultsToTask(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "task.yaml")
|
||||
|
||||
// Write a YAML file that does NOT have a type field (simulating an old v0.2 workspace).
|
||||
old := []byte(`id: legacy
|
||||
slug: legacy-task
|
||||
title: legacy task
|
||||
created_at: 2026-04-01T00:00:00Z
|
||||
updated_at: 2026-04-01T00:00:00Z
|
||||
status: active
|
||||
category: general
|
||||
mode: local
|
||||
agent: claude
|
||||
workspace_path: /tmp/legacy
|
||||
archived_at: null
|
||||
`)
|
||||
if err := os.WriteFile(path, old, 0644); err != nil {
|
||||
t.Fatalf("write legacy yaml: %v", err)
|
||||
}
|
||||
|
||||
meta, err := ReadMeta(path)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadMeta: %v", err)
|
||||
}
|
||||
if meta.Type != "" {
|
||||
t.Errorf("legacy Type field: got %q, want empty string", meta.Type)
|
||||
}
|
||||
if EffectiveType(meta) != "task" {
|
||||
t.Errorf("EffectiveType for legacy: got %q, want \"task\"", EffectiveType(meta))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveTypeEmptyDefaultsToTask(t *testing.T) {
|
||||
if got := EffectiveType(&TaskMeta{Type: ""}); got != "task" {
|
||||
t.Errorf("empty Type EffectiveType: got %q, want \"task\"", got)
|
||||
}
|
||||
if got := EffectiveType(&TaskMeta{Type: "task"}); got != "task" {
|
||||
t.Errorf("explicit task EffectiveType: got %q", got)
|
||||
}
|
||||
if got := EffectiveType(&TaskMeta{Type: "project"}); got != "project" {
|
||||
t.Errorf("explicit project EffectiveType: got %q", got)
|
||||
}
|
||||
if got := EffectiveType(nil); got != "task" {
|
||||
t.Errorf("nil EffectiveType: got %q, want \"task\"", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaArchive(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "task.yaml")
|
||||
|
||||
Reference in New Issue
Block a user