diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-04-03 08:44:02 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-04-03 08:44:02 +0000 |
| commit | 1271ba1d329c8b16062600dfafdec1d06c735c2e (patch) | |
| tree | 6f42961322830ce8c0518de82b9240f4803e0057 | |
| parent | 5aa6a15ffdf68a8dbe12eb0fdfff93deafb9da10 (diff) | |
feat: require repository_url on tasks; fix UpdateTask to persist it; fix cascade-retry test race
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | internal/api/server_test.go | 19 | ||||
| -rw-r--r-- | internal/storage/db.go | 19 | ||||
| -rw-r--r-- | internal/task/validator.go | 3 | ||||
| -rw-r--r-- | internal/task/validator_test.go | 1 | ||||
| -rwxr-xr-x | scripts/deploy | 5 |
5 files changed, 36 insertions, 11 deletions
diff --git a/internal/api/server_test.go b/internal/api/server_test.go index 4b45f25..67a2fc4 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -217,6 +217,7 @@ func TestGeminiLogs_ParsedCorrectly(t *testing.T) { tk := createTestTask(t, srv, `{ "name": "Gemini Log Test Task", "description": "Test Gemini log parsing", + "repository_url": "https://github.com/user/repo", "agent": { "type": "gemini", "instructions": "generate some output", @@ -363,6 +364,7 @@ func TestCreateTask_Success(t *testing.T) { payload := `{ "name": "API Task", "description": "Created via API", + "repository_url": "https://github.com/user/repo", "agent": { "type": "claude", "instructions": "do the thing", @@ -422,6 +424,7 @@ func TestProject_RoundTrip(t *testing.T) { payload := `{ "name": "Project Task", "project": "test-project", + "repository_url": "https://github.com/user/repo", "agent": { "type": "claude", "instructions": "do the thing", @@ -496,6 +499,7 @@ func TestListTasks_WithTasks(t *testing.T) { for i := 0; i < 3; i++ { tk := &task.Task{ ID: fmt.Sprintf("lt-%d", i), Name: fmt.Sprintf("T%d", i), + RepositoryURL: "https://github.com/user/repo", Agent: task.AgentConfig{Type: "claude", Instructions: "x"}, Priority: task.PriorityNormal, Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "linear"}, Tags: []string{}, DependsOn: []string{}, State: task.StatePending, @@ -533,6 +537,7 @@ func createTaskWithState(t *testing.T, store *storage.DB, id string, state task. tk := &task.Task{ ID: id, Name: "test-task-" + id, + RepositoryURL: "https://github.com/user/repo", Agent: task.AgentConfig{Type: "claude", Instructions: "do something"}, Priority: task.PriorityNormal, Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "linear"}, @@ -911,6 +916,7 @@ func TestRunTask_ManualRunIgnoresRetryLimit(t *testing.T) { tk := &task.Task{ ID: "retry-limit-manual", Name: "Retry Limit Task", + RepositoryURL: "https://github.com/user/repo", Agent: task.AgentConfig{Instructions: "do something"}, Priority: task.PriorityNormal, Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "linear"}, @@ -948,6 +954,7 @@ func TestRunTask_WithinRetryLimit_Returns202(t *testing.T) { tk := &task.Task{ ID: "retry-within-1", Name: "Retry Within Task", + RepositoryURL: "https://github.com/user/repo", Agent: task.AgentConfig{Instructions: "do something"}, Priority: task.PriorityNormal, Retry: task.RetryConfig{MaxAttempts: 3, Backoff: "linear"}, @@ -995,7 +1002,7 @@ func TestDeleteTask_Success(t *testing.T) { srv, store := testServer(t) // Create a task to delete. - created := createTestTask(t, srv, `{"name":"Delete Me","agent":{"type":"claude","instructions":"x","model":"sonnet"}}`) + created := createTestTask(t, srv, `{"name":"Delete Me","repository_url":"https://github.com/user/repo","agent":{"type":"claude","instructions":"x","model":"sonnet"}}`) req := httptest.NewRequest("DELETE", "/api/tasks/"+created.ID, nil) w := httptest.NewRecorder() @@ -1030,6 +1037,7 @@ func TestDeleteTask_RunningTaskRejected(t *testing.T) { tk := &task.Task{ ID: "running-task-del", Name: "Running Task", + RepositoryURL: "https://github.com/user/repo", Agent: task.AgentConfig{Instructions: "x", Model: "sonnet"}, Priority: task.PriorityNormal, Tags: []string{}, @@ -1584,6 +1592,7 @@ func TestRunTask_AgentTimesOut_TaskSetToTimedOut(t *testing.T) { tk := &task.Task{ ID: "async-timeout-1", Name: "timeout-test", + RepositoryURL: "https://github.com/user/repo", Agent: task.AgentConfig{Type: "claude", Instructions: "do something"}, Priority: task.PriorityNormal, Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "linear"}, @@ -2016,7 +2025,13 @@ func TestProjects_CRUD(t *testing.T) { } func TestHandleRunTask_CascadesRetryToFailedDeps(t *testing.T) { - srv, store := testServer(t) + // Use a blocking runner so tasks stay QUEUED long enough to assert state. + block := make(chan struct{}) + t.Cleanup(func() { close(block) }) + srv, store := testServerWithRunner(t, &mockRunner{onRun: func(*task.Task, *storage.Execution) error { + <-block + return nil + }}) now := time.Now().UTC() diff --git a/internal/storage/db.go b/internal/storage/db.go index ee5ee77..4ae0ab1 100644 --- a/internal/storage/db.go +++ b/internal/storage/db.go @@ -19,6 +19,10 @@ func Open(path string) (*DB, error) { if err != nil { return nil, fmt.Errorf("opening database: %w", err) } + // SQLite only allows one concurrent writer. Limiting to one open connection + // prevents "database is locked" errors when multiple goroutines write + // simultaneously via database/sql's connection pool. + db.SetMaxOpenConns(1) s := &DB{db: db} if err := s.migrate(); err != nil { db.Close() @@ -334,9 +338,10 @@ func (s *DB) RejectTask(id, comment string) error { // TaskUpdate holds the fields that UpdateTask may change. type TaskUpdate struct { - Name string - Description string - Config task.AgentConfig + Name string + Description string + RepositoryURL string + Config task.AgentConfig Priority task.Priority TimeoutNS int64 Retry task.RetryConfig @@ -375,13 +380,11 @@ func (s *DB) UpdateTask(id string, u TaskUpdate) error { now := time.Now().UTC() result, err := s.db.Exec(` UPDATE tasks - SET name = ?, description = ?, config_json = ?, priority = ?, timeout_ns = ?, + SET name = ?, description = ?, repository_url = ?, config_json = ?, priority = ?, timeout_ns = ?, retry_json = ?, tags_json = ?, depends_on_json = ?, state = ?, updated_at = ? WHERE id = ?`, - u.Name, u.Description, string(configJSON), string(u.Priority), u.TimeoutNS, - string(retryJSON), string(tagsJSON), string(depsJSON), string(task.StatePending), now, - id, - ) + u.Name, u.Description, u.RepositoryURL, configJSON, string(u.Priority), u.TimeoutNS, + retryJSON, tagsJSON, depsJSON, string(task.StatePending), now, id) if err != nil { return err } diff --git a/internal/task/validator.go b/internal/task/validator.go index 003fab9..43e482e 100644 --- a/internal/task/validator.go +++ b/internal/task/validator.go @@ -29,6 +29,9 @@ func Validate(t *Task) error { if t.Name == "" { ve.Add("name is required") } + if t.RepositoryURL == "" { + ve.Add("repository_url is required") + } if t.Agent.Instructions == "" { ve.Add("agent.instructions is required") } diff --git a/internal/task/validator_test.go b/internal/task/validator_test.go index c0ab986..2c6735c 100644 --- a/internal/task/validator_test.go +++ b/internal/task/validator_test.go @@ -9,6 +9,7 @@ func validTask() *Task { return &Task{ ID: "test-id", Name: "Valid Task", + RepositoryURL: "https://github.com/user/repo", Agent: AgentConfig{ Type: "claude", Instructions: "do something", diff --git a/scripts/deploy b/scripts/deploy index 1a08fc5..2161535 100755 --- a/scripts/deploy +++ b/scripts/deploy @@ -45,7 +45,10 @@ mkdir -p "${SITE_DIR}/scripts" find "${REPO_DIR}/scripts" -maxdepth 1 -type f -exec cp -p {} "${SITE_DIR}/scripts/" \; echo "==> Installing to /usr/local/bin..." -cp "${BIN_DIR}/claudomator" /usr/local/bin/claudomator +install -m 755 "${BIN_DIR}/claudomator" /usr/local/bin/claudomator + +echo "==> Verifying system CLI version..." +/usr/local/bin/claudomator version echo "==> Fixing permissions..." "${REPO_DIR}/scripts/fix-permissions" |
