feat: workspace metadata model with YAML read/write
TaskMeta struct matching task.yaml schema exactly. Tests for roundtrip, field presence, and archive state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package workspace
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// TaskMeta represents the task.yaml schema. Exactly these fields, no extras in v0.1.
|
||||
type TaskMeta struct {
|
||||
ID string `yaml:"id"`
|
||||
Slug string `yaml:"slug"`
|
||||
Title string `yaml:"title"`
|
||||
CreatedAt time.Time `yaml:"created_at"`
|
||||
UpdatedAt time.Time `yaml:"updated_at"`
|
||||
Status string `yaml:"status"`
|
||||
Category string `yaml:"category"`
|
||||
Mode string `yaml:"mode"`
|
||||
Agent string `yaml:"agent"`
|
||||
WorkspacePath string `yaml:"workspace_path"`
|
||||
ArchivedAt *time.Time `yaml:"archived_at"`
|
||||
}
|
||||
|
||||
// WriteMeta writes a TaskMeta to a YAML file.
|
||||
func WriteMeta(path string, meta *TaskMeta) error {
|
||||
data, err := yaml.Marshal(meta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0644)
|
||||
}
|
||||
|
||||
// ReadMeta reads a TaskMeta from a YAML file.
|
||||
func ReadMeta(path string) (*TaskMeta, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var meta TaskMeta
|
||||
if err := yaml.Unmarshal(data, &meta); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &meta, nil
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package workspace
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWriteAndReadMeta(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "task.yaml")
|
||||
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
meta := &TaskMeta{
|
||||
ID: "20260405-143022",
|
||||
Slug: "arch-notes",
|
||||
Title: "arch notes",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Status: "active",
|
||||
Category: "general",
|
||||
Mode: "local",
|
||||
Agent: "claude",
|
||||
WorkspacePath: "/home/warren/ai-workspaces/general/20260405_arch-notes",
|
||||
ArchivedAt: nil,
|
||||
}
|
||||
|
||||
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.ID != meta.ID {
|
||||
t.Errorf("ID: got %q, want %q", got.ID, meta.ID)
|
||||
}
|
||||
if got.Slug != meta.Slug {
|
||||
t.Errorf("Slug: got %q, want %q", got.Slug, meta.Slug)
|
||||
}
|
||||
if got.Status != "active" {
|
||||
t.Errorf("Status: got %q, want \"active\"", got.Status)
|
||||
}
|
||||
if got.ArchivedAt != nil {
|
||||
t.Errorf("ArchivedAt: expected nil, got %v", got.ArchivedAt)
|
||||
}
|
||||
if !got.CreatedAt.Equal(now) {
|
||||
t.Errorf("CreatedAt: got %v, want %v", got.CreatedAt, now)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaYAMLFieldsPresent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "task.yaml")
|
||||
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
meta := &TaskMeta{
|
||||
ID: "20260405-143022",
|
||||
Slug: "test",
|
||||
Title: "test",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Status: "active",
|
||||
Category: "general",
|
||||
Mode: "local",
|
||||
Agent: "claude",
|
||||
WorkspacePath: "/tmp/test",
|
||||
}
|
||||
|
||||
WriteMeta(path, meta)
|
||||
|
||||
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:"} {
|
||||
if !strings.Contains(content, field) {
|
||||
t.Errorf("missing field %s in YAML output", field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaArchive(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "task.yaml")
|
||||
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
meta := &TaskMeta{
|
||||
ID: "20260405-143022",
|
||||
Slug: "test",
|
||||
Title: "test",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Status: "active",
|
||||
Category: "general",
|
||||
Mode: "local",
|
||||
Agent: "claude",
|
||||
WorkspacePath: "/tmp/test",
|
||||
}
|
||||
|
||||
WriteMeta(path, meta)
|
||||
|
||||
archiveTime := now.Add(time.Hour)
|
||||
meta.Status = "archived"
|
||||
meta.ArchivedAt = &archiveTime
|
||||
WriteMeta(path, meta)
|
||||
|
||||
got, _ := ReadMeta(path)
|
||||
if got.Status != "archived" {
|
||||
t.Errorf("Status: got %q, want \"archived\"", got.Status)
|
||||
}
|
||||
if got.ArchivedAt == nil {
|
||||
t.Fatal("ArchivedAt: expected non-nil")
|
||||
}
|
||||
if !got.ArchivedAt.Equal(archiveTime) {
|
||||
t.Errorf("ArchivedAt: got %v, want %v", got.ArchivedAt, archiveTime)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user