From 26dc313f16a2827b0f7a4651f495f36f669cea73 Mon Sep 17 00:00:00 2001 From: Claudomator Agent Date: Mon, 16 Mar 2026 21:02:07 +0000 Subject: feat: expose project field in API and CLI - POST /api/tasks now reads and stores the project field from request body - GET /api/tasks/{id} returns project in response (via Task struct json tags) - list command: adds PROJECT column to tabwriter output - status command: prints Project line when non-empty - Tests: TestProject_RoundTrip (API), TestListTasks_ShowsProject, TestStatusCmd_ShowsProject (CLI) Co-Authored-By: Claude Sonnet 4.6 --- internal/api/server.go | 2 + internal/api/server_test.go | 43 ++++++++++++++++++ internal/cli/list.go | 6 +-- internal/cli/project_test.go | 102 +++++++++++++++++++++++++++++++++++++++++++ internal/cli/status.go | 3 ++ 5 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 internal/cli/project_test.go (limited to 'internal') diff --git a/internal/api/server.go b/internal/api/server.go index f640aba..48440e1 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -423,6 +423,7 @@ func (s *Server) handleCreateTask(w http.ResponseWriter, r *http.Request) { Name string `json:"name"` Description string `json:"description"` ElaborationInput string `json:"elaboration_input"` + Project string `json:"project"` Agent task.AgentConfig `json:"agent"` Claude task.AgentConfig `json:"claude"` // legacy alias Timeout string `json:"timeout"` @@ -446,6 +447,7 @@ func (s *Server) handleCreateTask(w http.ResponseWriter, r *http.Request) { Name: input.Name, Description: input.Description, ElaborationInput: input.ElaborationInput, + Project: input.Project, Agent: input.Agent, Priority: task.Priority(input.Priority), Tags: input.Tags, diff --git a/internal/api/server_test.go b/internal/api/server_test.go index 83f83f4..696aca3 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -398,6 +398,49 @@ func TestCreateTask_ValidationFailure(t *testing.T) { } } +func TestProject_RoundTrip(t *testing.T) { + srv, _ := testServer(t) + + payload := `{ + "name": "Project Task", + "project": "test-project", + "agent": { + "type": "claude", + "instructions": "do the thing", + "model": "sonnet" + } + }` + req := httptest.NewRequest("POST", "/api/tasks", bytes.NewBufferString(payload)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusCreated { + t.Fatalf("create: want 201, got %d; body: %s", w.Code, w.Body.String()) + } + + var created task.Task + json.NewDecoder(w.Body).Decode(&created) + if created.Project != "test-project" { + t.Errorf("create response: project want 'test-project', got %q", created.Project) + } + + // GET the task and verify project is persisted + req2 := httptest.NewRequest("GET", "/api/tasks/"+created.ID, nil) + w2 := httptest.NewRecorder() + srv.Handler().ServeHTTP(w2, req2) + + if w2.Code != http.StatusOK { + t.Fatalf("get: want 200, got %d; body: %s", w2.Code, w2.Body.String()) + } + + var fetched task.Task + json.NewDecoder(w2.Body).Decode(&fetched) + if fetched.Project != "test-project" { + t.Errorf("get response: project want 'test-project', got %q", fetched.Project) + } +} + func TestListTasks_Empty(t *testing.T) { srv, _ := testServer(t) diff --git a/internal/cli/list.go b/internal/cli/list.go index 3425388..ab80868 100644 --- a/internal/cli/list.go +++ b/internal/cli/list.go @@ -49,10 +49,10 @@ func listTasks(state string) error { } w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) - fmt.Fprintln(w, "ID\tNAME\tSTATE\tPRIORITY\tCREATED") + fmt.Fprintln(w, "ID\tNAME\tPROJECT\tSTATE\tPRIORITY\tCREATED") for _, t := range tasks { - fmt.Fprintf(w, "%.8s\t%s\t%s\t%s\t%s\n", - t.ID, t.Name, t.State, t.Priority, t.CreatedAt.Format("2006-01-02 15:04")) + fmt.Fprintf(w, "%.8s\t%s\t%s\t%s\t%s\t%s\n", + t.ID, t.Name, t.Project, t.State, t.Priority, t.CreatedAt.Format("2006-01-02 15:04")) } w.Flush() return nil diff --git a/internal/cli/project_test.go b/internal/cli/project_test.go new file mode 100644 index 0000000..c62e181 --- /dev/null +++ b/internal/cli/project_test.go @@ -0,0 +1,102 @@ +package cli + +import ( + "bytes" + "io" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/thepeterstone/claudomator/internal/config" + "github.com/thepeterstone/claudomator/internal/storage" + "github.com/thepeterstone/claudomator/internal/task" +) + +func makeProjectTask(t *testing.T, dir string) *task.Task { + t.Helper() + db, err := storage.Open(filepath.Join(dir, "test.db")) + if err != nil { + t.Fatalf("storage.Open: %v", err) + } + defer db.Close() + + now := time.Now().UTC() + tk := &task.Task{ + ID: "proj-task-id", + Name: "Project Task", + Project: "test-project", + Agent: task.AgentConfig{Type: "claude", Instructions: "do it", Model: "sonnet"}, + Priority: task.PriorityNormal, + Tags: []string{}, + DependsOn: []string{}, + Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "exponential"}, + State: task.StatePending, + CreatedAt: now, + UpdatedAt: now, + } + if err := db.CreateTask(tk); err != nil { + t.Fatalf("CreateTask: %v", err) + } + return tk +} + +func captureStdout(fn func()) string { + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + fn() + + w.Close() + os.Stdout = old + var buf bytes.Buffer + io.Copy(&buf, r) + return buf.String() +} + +func withDB(t *testing.T, dbPath string, fn func()) { + t.Helper() + origCfg := cfg + if cfg == nil { + cfg = &config.Config{} + } + cfg.DBPath = dbPath + defer func() { cfg = origCfg }() + fn() +} + +func TestListTasks_ShowsProject(t *testing.T) { + dir := t.TempDir() + dbPath := filepath.Join(dir, "test.db") + makeProjectTask(t, dir) + + withDB(t, dbPath, func() { + out := captureStdout(func() { + if err := listTasks(""); err != nil { + t.Fatalf("listTasks: %v", err) + } + }) + if !strings.Contains(out, "test-project") { + t.Errorf("list output missing project 'test-project':\n%s", out) + } + }) +} + +func TestStatusCmd_ShowsProject(t *testing.T) { + dir := t.TempDir() + dbPath := filepath.Join(dir, "test.db") + tk := makeProjectTask(t, dir) + + withDB(t, dbPath, func() { + out := captureStdout(func() { + if err := showStatus(tk.ID); err != nil { + t.Fatalf("showStatus: %v", err) + } + }) + if !strings.Contains(out, "test-project") { + t.Errorf("status output missing project 'test-project':\n%s", out) + } + }) +} diff --git a/internal/cli/status.go b/internal/cli/status.go index 16b88b0..77a30d5 100644 --- a/internal/cli/status.go +++ b/internal/cli/status.go @@ -39,6 +39,9 @@ func showStatus(id string) error { fmt.Printf("State: %s\n", t.State) fmt.Printf("Priority: %s\n", t.Priority) fmt.Printf("Model: %s\n", t.Agent.Model) + if t.Project != "" { + fmt.Printf("Project: %s\n", t.Project) + } if t.Description != "" { fmt.Printf("Description: %s\n", t.Description) } -- cgit v1.2.3