summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorClaudomator Agent <agent@claudomator>2026-03-16 21:02:07 +0000
committerClaudomator Agent <agent@claudomator>2026-03-16 21:02:07 +0000
commit26dc313f16a2827b0f7a4651f495f36f669cea73 (patch)
treeda8fcda11d986cf01b7cd75cee7abc6894287327 /internal
parentb8381507ff61c7fb69a91490a9fd58403da8c0fa (diff)
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 <noreply@anthropic.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/api/server.go2
-rw-r--r--internal/api/server_test.go43
-rw-r--r--internal/cli/list.go6
-rw-r--r--internal/cli/project_test.go102
-rw-r--r--internal/cli/status.go3
5 files changed, 153 insertions, 3 deletions
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)
}