diff options
Diffstat (limited to 'internal/storage')
| -rw-r--r-- | internal/storage/db.go | 129 | ||||
| -rw-r--r-- | internal/storage/db_test.go | 183 |
2 files changed, 8 insertions, 304 deletions
diff --git a/internal/storage/db.go b/internal/storage/db.go index e03a902..22a3d7b 100644 --- a/internal/storage/db.go +++ b/internal/storage/db.go @@ -123,21 +123,6 @@ func (s *DB) migrate() error { created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL )`, - `CREATE TABLE IF NOT EXISTS stories ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - project_id TEXT NOT NULL DEFAULT '', - branch_name TEXT NOT NULL DEFAULT '', - deploy_config TEXT NOT NULL DEFAULT '', - validation_json TEXT NOT NULL DEFAULT '', - status TEXT NOT NULL DEFAULT 'PENDING', - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL - )`, - `ALTER TABLE tasks ADD COLUMN story_id TEXT`, - `ALTER TABLE tasks ADD COLUMN acceptance_criteria TEXT NOT NULL DEFAULT ''`, - `ALTER TABLE tasks ADD COLUMN checker_for_task_id TEXT NOT NULL DEFAULT ''`, - `ALTER TABLE tasks ADD COLUMN checker_report TEXT NOT NULL DEFAULT ''`, `ALTER TABLE executions ADD COLUMN tokens_in INTEGER`, `ALTER TABLE executions ADD COLUMN tokens_out INTEGER`, `ALTER TABLE executions ADD COLUMN agent TEXT`, @@ -198,12 +183,11 @@ func (s *DB) CreateTask(t *task.Task) error { defer tx.Rollback() //nolint:errcheck if _, err = tx.Exec(` - INSERT INTO tasks (id, name, description, elaboration_input, project, repository_url, config_json, priority, timeout_ns, retry_json, tags_json, depends_on_json, parent_task_id, state, created_at, updated_at, story_id, acceptance_criteria, checker_for_task_id, checker_report) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + INSERT INTO tasks (id, name, description, elaboration_input, project, repository_url, config_json, priority, timeout_ns, retry_json, tags_json, depends_on_json, parent_task_id, state, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, t.ID, t.Name, t.Description, t.ElaborationInput, t.Project, t.RepositoryURL, string(configJSON), string(t.Priority), t.Timeout.Duration.Nanoseconds(), string(retryJSON), string(tagsJSON), string(depsJSON), - t.ParentTaskID, string(t.State), t.CreatedAt.UTC(), t.UpdatedAt.UTC(), t.StoryID, - t.AcceptanceCriteria, t.CheckerForTaskID, t.CheckerReport, + t.ParentTaskID, string(t.State), t.CreatedAt.UTC(), t.UpdatedAt.UTC(), ); err != nil { return err } @@ -230,13 +214,13 @@ func (s *DB) CreateTask(t *task.Task) error { // GetTask retrieves a task by ID. func (s *DB) GetTask(id string) (*task.Task, error) { - row := s.db.QueryRow(`SELECT id, name, description, elaboration_input, project, repository_url, config_json, priority, timeout_ns, retry_json, tags_json, depends_on_json, parent_task_id, state, created_at, updated_at, rejection_comment, question_json, summary, interactions_json, story_id, acceptance_criteria, checker_for_task_id, checker_report FROM tasks WHERE id = ?`, id) + row := s.db.QueryRow(`SELECT id, name, description, elaboration_input, project, repository_url, config_json, priority, timeout_ns, retry_json, tags_json, depends_on_json, parent_task_id, state, created_at, updated_at, rejection_comment, question_json, summary, interactions_json FROM tasks WHERE id = ?`, id) return scanTask(row) } // ListTasks returns tasks matching the given filter. func (s *DB) ListTasks(filter TaskFilter) ([]*task.Task, error) { - query := `SELECT id, name, description, elaboration_input, project, repository_url, config_json, priority, timeout_ns, retry_json, tags_json, depends_on_json, parent_task_id, state, created_at, updated_at, rejection_comment, question_json, summary, interactions_json, story_id, acceptance_criteria, checker_for_task_id, checker_report FROM tasks WHERE 1=1` + query := `SELECT id, name, description, elaboration_input, project, repository_url, config_json, priority, timeout_ns, retry_json, tags_json, depends_on_json, parent_task_id, state, created_at, updated_at, rejection_comment, question_json, summary, interactions_json FROM tasks WHERE 1=1` var args []interface{} if filter.State != "" { @@ -272,7 +256,7 @@ func (s *DB) ListTasks(filter TaskFilter) ([]*task.Task, error) { // ListSubtasks returns all tasks whose parent_task_id matches the given ID. func (s *DB) ListSubtasks(parentID string) ([]*task.Task, error) { - rows, err := s.db.Query(`SELECT id, name, description, elaboration_input, project, repository_url, config_json, priority, timeout_ns, retry_json, tags_json, depends_on_json, parent_task_id, state, created_at, updated_at, rejection_comment, question_json, summary, interactions_json, story_id, acceptance_criteria, checker_for_task_id, checker_report FROM tasks WHERE parent_task_id = ? ORDER BY created_at ASC`, parentID) + rows, err := s.db.Query(`SELECT id, name, description, elaboration_input, project, repository_url, config_json, priority, timeout_ns, retry_json, tags_json, depends_on_json, parent_task_id, state, created_at, updated_at, rejection_comment, question_json, summary, interactions_json FROM tasks WHERE parent_task_id = ? ORDER BY created_at ASC`, parentID) if err != nil { return nil, err } @@ -342,7 +326,7 @@ func (s *DB) ResetTaskForRetry(id string) (*task.Task, error) { } defer tx.Rollback() //nolint:errcheck - t, err := scanTask(tx.QueryRow(`SELECT id, name, description, elaboration_input, project, repository_url, config_json, priority, timeout_ns, retry_json, tags_json, depends_on_json, parent_task_id, state, created_at, updated_at, rejection_comment, question_json, summary, interactions_json, story_id, acceptance_criteria, checker_for_task_id, checker_report FROM tasks WHERE id = ?`, id)) + t, err := scanTask(tx.QueryRow(`SELECT id, name, description, elaboration_input, project, repository_url, config_json, priority, timeout_ns, retry_json, tags_json, depends_on_json, parent_task_id, state, created_at, updated_at, rejection_comment, question_json, summary, interactions_json FROM tasks WHERE id = ?`, id)) if err != nil { if err == sql.ErrNoRows { return nil, fmt.Errorf("task %q not found", id) @@ -987,24 +971,6 @@ func (s *DB) UpdateTaskSummary(taskID, summary string) error { return tx.Commit() } -// UpdateTaskCheckerReport sets the checker_report field on a task. -func (s *DB) UpdateTaskCheckerReport(id, report string) error { - now := time.Now().UTC() - _, err := s.db.Exec(`UPDATE tasks SET checker_report = ?, updated_at = ? WHERE id = ?`, report, now, id) - return err -} - -// GetCheckerTask returns the checker task for the given checked task ID, -// or nil if no checker task exists. -func (s *DB) GetCheckerTask(checkedTaskID string) (*task.Task, error) { - row := s.db.QueryRow(`SELECT id, name, description, elaboration_input, project, repository_url, config_json, priority, timeout_ns, retry_json, tags_json, depends_on_json, parent_task_id, state, created_at, updated_at, rejection_comment, question_json, summary, interactions_json, story_id, acceptance_criteria, checker_for_task_id, checker_report FROM tasks WHERE checker_for_task_id = ? LIMIT 1`, checkedTaskID) - t, err := scanTask(row) - if err == sql.ErrNoRows { - return nil, nil - } - return t, err -} - // AppendTaskInteraction appends a Q&A interaction to the task's interaction history. func (s *DB) AppendTaskInteraction(taskID string, interaction task.Interaction) error { tx, err := s.db.Begin() @@ -1102,17 +1068,12 @@ func scanTask(row scanner) (*task.Task, error) { questionJSON sql.NullString summary sql.NullString interactionsJSON sql.NullString - storyID sql.NullString - acceptanceCriteria sql.NullString - checkerForTaskID sql.NullString - checkerReport sql.NullString ) err := row.Scan( &t.ID, &t.Name, &t.Description, &elaborationInput, &project, &repositoryURL, &configJSON, &priority, &timeoutNS, &retryJSON, &tagsJSON, &depsJSON, &parentTaskID, &state, &t.CreatedAt, &t.UpdatedAt, - &rejectionComment, &questionJSON, &summary, &interactionsJSON, &storyID, - &acceptanceCriteria, &checkerForTaskID, &checkerReport, + &rejectionComment, &questionJSON, &summary, &interactionsJSON, ) t.ParentTaskID = parentTaskID.String t.ElaborationInput = elaborationInput.String @@ -1121,10 +1082,6 @@ func scanTask(row scanner) (*task.Task, error) { t.RejectionComment = rejectionComment.String t.QuestionJSON = questionJSON.String t.Summary = summary.String - t.StoryID = storyID.String - t.AcceptanceCriteria = acceptanceCriteria.String - t.CheckerForTaskID = checkerForTaskID.String - t.CheckerReport = checkerReport.String if err != nil { return nil, err } @@ -1396,73 +1353,3 @@ func (s *DB) UpsertProject(p *task.Project) error { return err } -// CreateStory inserts a new story. -func (s *DB) CreateStory(st *task.Story) error { - now := time.Now().UTC() - _, err := s.db.Exec( - `INSERT INTO stories (id, name, project_id, branch_name, deploy_config, validation_json, status, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - st.ID, st.Name, st.ProjectID, st.BranchName, st.DeployConfig, st.ValidationJSON, string(st.Status), now, now, - ) - return err -} - -// GetStory retrieves a story by ID. -func (s *DB) GetStory(id string) (*task.Story, error) { - row := s.db.QueryRow(`SELECT id, name, project_id, branch_name, deploy_config, validation_json, status, created_at, updated_at FROM stories WHERE id = ?`, id) - st := &task.Story{} - var status string - if err := row.Scan(&st.ID, &st.Name, &st.ProjectID, &st.BranchName, &st.DeployConfig, &st.ValidationJSON, &status, &st.CreatedAt, &st.UpdatedAt); err != nil { - return nil, err - } - st.Status = task.StoryState(status) - return st, nil -} - -// ListStories returns all stories ordered by creation time descending. -func (s *DB) ListStories() ([]*task.Story, error) { - rows, err := s.db.Query(`SELECT id, name, project_id, branch_name, deploy_config, validation_json, status, created_at, updated_at FROM stories ORDER BY created_at DESC`) - if err != nil { - return nil, err - } - defer rows.Close() - var stories []*task.Story - for rows.Next() { - st := &task.Story{} - var status string - if err := rows.Scan(&st.ID, &st.Name, &st.ProjectID, &st.BranchName, &st.DeployConfig, &st.ValidationJSON, &status, &st.CreatedAt, &st.UpdatedAt); err != nil { - return nil, err - } - st.Status = task.StoryState(status) - stories = append(stories, st) - } - return stories, rows.Err() -} - -// UpdateStoryStatus updates the status of a story. -func (s *DB) UpdateStoryStatus(id string, status task.StoryState) error { - now := time.Now().UTC() - _, err := s.db.Exec(`UPDATE stories SET status = ?, updated_at = ? WHERE id = ?`, string(status), now, id) - return err -} - -// ListTasksByStory returns all tasks associated with a story, ordered by creation time ascending. -func (s *DB) ListTasksByStory(storyID string) ([]*task.Task, error) { - rows, err := s.db.Query( - `SELECT id, name, description, elaboration_input, project, repository_url, config_json, priority, timeout_ns, retry_json, tags_json, depends_on_json, parent_task_id, state, created_at, updated_at, rejection_comment, question_json, summary, interactions_json, story_id, acceptance_criteria, checker_for_task_id, checker_report FROM tasks WHERE story_id = ? ORDER BY created_at ASC`, - storyID, - ) - if err != nil { - return nil, err - } - defer rows.Close() - var tasks []*task.Task - for rows.Next() { - t, err := scanTaskRows(rows) - if err != nil { - return nil, err - } - tasks = append(tasks, t) - } - return tasks, rows.Err() -} diff --git a/internal/storage/db_test.go b/internal/storage/db_test.go index 4d744a4..09bbdfc 100644 --- a/internal/storage/db_test.go +++ b/internal/storage/db_test.go @@ -1256,187 +1256,4 @@ func TestUpdateProject(t *testing.T) { } } -func TestCreateStory(t *testing.T) { - db := testDB(t) - st := &task.Story{ - ID: "story-1", - Name: "My Story", - Status: task.StoryPending, - } - if err := db.CreateStory(st); err != nil { - t.Fatalf("CreateStory: %v", err) - } -} - -func TestGetStory(t *testing.T) { - db := testDB(t) - st := &task.Story{ - ID: "story-2", - Name: "Get Story", - ProjectID: "proj-1", - Status: task.StoryPending, - } - if err := db.CreateStory(st); err != nil { - t.Fatalf("CreateStory: %v", err) - } - got, err := db.GetStory("story-2") - if err != nil { - t.Fatalf("GetStory: %v", err) - } - if got.Name != "Get Story" { - t.Errorf("Name: want 'Get Story', got %q", got.Name) - } - if got.ProjectID != "proj-1" { - t.Errorf("ProjectID: want 'proj-1', got %q", got.ProjectID) - } - if got.Status != task.StoryPending { - t.Errorf("Status: want PENDING, got %q", got.Status) - } -} - -func TestListStories(t *testing.T) { - db := testDB(t) - for _, name := range []string{"A", "B", "C"} { - if err := db.CreateStory(&task.Story{ID: name, Name: name, Status: task.StoryPending}); err != nil { - t.Fatalf("CreateStory %s: %v", name, err) - } - } - stories, err := db.ListStories() - if err != nil { - t.Fatalf("ListStories: %v", err) - } - if len(stories) != 3 { - t.Errorf("want 3 stories, got %d", len(stories)) - } -} - -func TestUpdateStoryStatus(t *testing.T) { - db := testDB(t) - st := &task.Story{ID: "story-upd", Name: "Upd", Status: task.StoryPending} - if err := db.CreateStory(st); err != nil { - t.Fatalf("CreateStory: %v", err) - } - if err := db.UpdateStoryStatus("story-upd", task.StoryInProgress); err != nil { - t.Fatalf("UpdateStoryStatus: %v", err) - } - got, _ := db.GetStory("story-upd") - if got.Status != task.StoryInProgress { - t.Errorf("Status: want IN_PROGRESS, got %q", got.Status) - } -} - -func TestListTasksByStory(t *testing.T) { - db := testDB(t) - now := time.Now().UTC() - - if err := db.CreateStory(&task.Story{ID: "story-tasks", Name: "S", Status: task.StoryPending}); err != nil { - t.Fatalf("CreateStory: %v", err) - } - - makeTask := func(id string) *task.Task { - return &task.Task{ - ID: id, - Name: id, - StoryID: "story-tasks", - Agent: task.AgentConfig{Type: "claude"}, - Priority: task.PriorityNormal, - Tags: []string{}, - DependsOn: []string{}, - Retry: task.RetryConfig{MaxAttempts: 1}, - State: task.StatePending, - CreatedAt: now, - UpdatedAt: now, - } - } - - if err := db.CreateTask(makeTask("t1")); err != nil { - t.Fatal(err) - } - if err := db.CreateTask(makeTask("t2")); err != nil { - t.Fatal(err) - } - - tasks, err := db.ListTasksByStory("story-tasks") - if err != nil { - t.Fatalf("ListTasksByStory: %v", err) - } - if len(tasks) != 2 { - t.Errorf("want 2 tasks, got %d", len(tasks)) - } - for _, tk := range tasks { - if tk.StoryID != "story-tasks" { - t.Errorf("task %s: StoryID want 'story-tasks', got %q", tk.ID, tk.StoryID) - } - } -} - -func TestUpdateTaskCheckerReport(t *testing.T) { - db := testDB(t) - tk := &task.Task{ - ID: "cr-1", Name: "orig", RepositoryURL: "https://github.com/x/y", - Agent: task.AgentConfig{Type: "claude", Instructions: "x"}, - Priority: task.PriorityNormal, - Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "linear"}, - Tags: []string{}, DependsOn: []string{}, - State: task.StatePending, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), - } - if err := db.CreateTask(tk); err != nil { - t.Fatalf("CreateTask: %v", err) - } - if err := db.UpdateTaskCheckerReport("cr-1", "Tests failed: missing endpoint"); err != nil { - t.Fatalf("UpdateTaskCheckerReport: %v", err) - } - got, err := db.GetTask("cr-1") - if err != nil { - t.Fatalf("GetTask: %v", err) - } - if got.CheckerReport != "Tests failed: missing endpoint" { - t.Errorf("expected checker report, got %q", got.CheckerReport) - } -} - -func TestGetCheckerTask(t *testing.T) { - db := testDB(t) - checked := &task.Task{ - ID: "chk-orig", Name: "orig", RepositoryURL: "https://github.com/x/y", - Agent: task.AgentConfig{Type: "claude", Instructions: "x"}, - Priority: task.PriorityNormal, - Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "linear"}, - Tags: []string{}, DependsOn: []string{}, - State: task.StatePending, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), - } - if err := db.CreateTask(checked); err != nil { - t.Fatalf("CreateTask checked: %v", err) - } - checker := &task.Task{ - ID: "chk-checker", Name: "Check: orig", CheckerForTaskID: "chk-orig", - RepositoryURL: "https://github.com/x/y", - Agent: task.AgentConfig{Type: "claude", Instructions: "validate"}, - Priority: task.PriorityNormal, - Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "linear"}, - Tags: []string{}, DependsOn: []string{}, - State: task.StatePending, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), - } - if err := db.CreateTask(checker); err != nil { - t.Fatalf("CreateTask checker: %v", err) - } - - // Should find the checker task. - got, err := db.GetCheckerTask("chk-orig") - if err != nil { - t.Fatalf("GetCheckerTask: %v", err) - } - if got == nil || got.ID != "chk-checker" { - t.Errorf("expected checker task ID chk-checker, got %v", got) - } - - // Should return nil when no checker exists. - none, err := db.GetCheckerTask("nonexistent") - if err != nil { - t.Fatalf("GetCheckerTask nonexistent: %v", err) - } - if none != nil { - t.Errorf("expected nil for task with no checker, got %v", none) - } -} |
