diff --git a/internal/server/job_action.go b/internal/server/job_action.go new file mode 100644 index 0000000..de73c26 --- /dev/null +++ b/internal/server/job_action.go @@ -0,0 +1,15 @@ +package server + +import "github.com/Neokil/AutoPR/internal/serverstate" + +// JobAction identifies the type of background job the daemon should execute. +type JobAction = serverstate.JobAction + +const ( + jobRun JobAction = "run" + jobAction JobAction = "action" + jobMoveToState JobAction = "move_to_state" + jobCleanup JobAction = "cleanup_ticket" + jobCleanupDone JobAction = "cleanup_done" + jobCleanupAll JobAction = "cleanup_all" +) diff --git a/internal/server/jobs.go b/internal/server/jobs.go index b34d1cd..27f89d3 100644 --- a/internal/server/jobs.go +++ b/internal/server/jobs.go @@ -34,7 +34,7 @@ func (s *server) setJobStatus(job serverstate.JobRecord, status, errMsg string) RepoPath: stringPtr(job.RepoPath), TicketNumber: stringPtr(job.TicketNumber), JobId: stringPtr(job.ID), - Action: stringPtr(job.Action), + Action: stringPtr(string(job.Action)), Scope: stringPtr(job.Scope), Status: stringPtr(status), Error: stringPtr(strings.TrimSpace(errMsg)), diff --git a/internal/server/server.go b/internal/server/server.go index 3e107bf..a7c74b6 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -22,13 +22,6 @@ import ( ) const ( - jobRun = "run" - jobAction = "action" - jobMoveToState = "move_to_state" - jobCleanup = "cleanup_ticket" - jobCleanupDone = "cleanup_done" - jobCleanupAll = "cleanup_all" - jobQueueSize = 256 httpReadHeaderTimeout = 30 * time.Second sectionMatchLen = 3 // full match + 2 capture groups diff --git a/internal/server/strict_api.go b/internal/server/strict_api.go index c86dc9e..9b185e2 100644 --- a/internal/server/strict_api.go +++ b/internal/server/strict_api.go @@ -385,7 +385,7 @@ func (s *server) RunTicket(ctx context.Context, request api.RunTicketRequestObje return acceptedRunTicket(s.enqueueJob(jobRun, repoID, repoRoot, request.Id, enqueueOptions{})) } -func (s *server) enqueueJob(action, repoID, repoPath, ticket string, opts enqueueOptions) (api.ActionAcceptedResponse, int, error) { +func (s *server) enqueueJob(action JobAction, repoID, repoPath, ticket string, opts enqueueOptions) (api.ActionAcceptedResponse, int, error) { if action == jobRun && strings.TrimSpace(ticket) != "" { queueErr := s.ensureQueuedTicket(repoID, repoPath, ticket) if queueErr != nil { @@ -408,7 +408,7 @@ func (s *server) enqueueJob(action, repoID, repoPath, ticket string, opts enqueu RepoPath: stringPtr(repoPath), TicketNumber: stringPtr(ticket), JobId: stringPtr(job.ID), - Action: stringPtr(action), + Action: stringPtr(string(action)), Scope: stringPtr(opts.scope), Status: stringPtr("queued"), }) @@ -417,7 +417,7 @@ func (s *server) enqueueJob(action, repoID, repoPath, ticket string, opts enqueu return api.ActionAcceptedResponse{ Status: "accepted", JobId: job.ID, - Action: action, + Action: string(action), RepoId: repoID, RepoPath: repoPath, TicketNumber: stringPtr(ticket), @@ -430,7 +430,7 @@ func (s *server) enqueueJob(action, repoID, repoPath, ticket string, opts enqueu RepoPath: stringPtr(repoPath), TicketNumber: stringPtr(ticket), JobId: stringPtr(job.ID), - Action: stringPtr(action), + Action: stringPtr(string(action)), Scope: stringPtr(opts.scope), Status: stringPtr("failed"), Error: stringPtr("job queue is full"), diff --git a/internal/server/strict_api_test.go b/internal/server/strict_api_test.go new file mode 100644 index 0000000..5e5118a --- /dev/null +++ b/internal/server/strict_api_test.go @@ -0,0 +1,53 @@ +package server //nolint:testpackage // needs access to unexported enqueueJob and queuedJob internals + +import ( + "net/http" + "testing" + + "github.com/Neokil/AutoPR/internal/api" + "github.com/Neokil/AutoPR/internal/serverstate" +) + +func TestEnqueueJobPreservesActionValue(t *testing.T) { + t.Parallel() + + statePath := t.TempDir() + "/state.json" + store, err := serverstate.NewStore(statePath) + if err != nil { + t.Fatalf("NewStore() error = %v", err) + } + + srv := &server{ + meta: store, + jobs: make(chan queuedJob, 1), + subscribers: map[string]chan api.ServerEvent{}, + } + + resp, code, err := srv.enqueueJob(jobCleanupAll, "repo-1", "/tmp/repo", "", enqueueOptions{scope: "all"}) + if err != nil { + t.Fatalf("enqueueJob() error = %v", err) + } + if code != http.StatusAccepted { + t.Fatalf("enqueueJob() code = %d, want %d", code, http.StatusAccepted) + } + if resp.Action != string(jobCleanupAll) { + t.Fatalf("resp.Action = %q, want %q", resp.Action, jobCleanupAll) + } + + stored, ok := store.GetJob(resp.JobId) + if !ok { + t.Fatalf("GetJob(%q) did not find stored job", resp.JobId) + } + if stored.Action != jobCleanupAll { + t.Fatalf("stored.Action = %q, want %q", stored.Action, jobCleanupAll) + } + + select { + case queued := <-srv.jobs: + if queued.record.Action != jobCleanupAll { + t.Fatalf("queued.record.Action = %q, want %q", queued.record.Action, jobCleanupAll) + } + default: + t.Fatal("expected queued job in channel") + } +} diff --git a/internal/server/transport.go b/internal/server/transport.go index f0fd470..d78dd40 100644 --- a/internal/server/transport.go +++ b/internal/server/transport.go @@ -37,7 +37,7 @@ func toTicketStateResponse(state workflowstate.State) api.TicketStateResponse { func toJobResponse(job serverstate.JobRecord) api.JobStatusResponse { return api.JobStatusResponse{ Id: job.ID, - Action: job.Action, + Action: string(job.Action), RepoId: job.RepoID, RepoPath: job.RepoPath, TicketNumber: stringPtr(job.TicketNumber), diff --git a/internal/serverstate/job_action.go b/internal/serverstate/job_action.go new file mode 100644 index 0000000..77f6714 --- /dev/null +++ b/internal/serverstate/job_action.go @@ -0,0 +1,4 @@ +package serverstate + +// JobAction identifies the type of background job the daemon should execute. +type JobAction string diff --git a/internal/serverstate/store.go b/internal/serverstate/store.go index ace842c..d430cf0 100644 --- a/internal/serverstate/store.go +++ b/internal/serverstate/store.go @@ -46,7 +46,7 @@ type Data struct { // JobRecord represents a single background job and its lifecycle timestamps. type JobRecord struct { ID string `json:"id"` - Action string `json:"action"` + Action JobAction `json:"action"` RepoID string `json:"repo_id"` RepoPath string `json:"repo_path"` TicketNumber string `json:"ticket_number,omitempty"` @@ -231,7 +231,7 @@ func (s *Store) ListTickets(repoID string) []TicketRecord { } // NewJob creates a new queued job record and persists it. -func (s *Store) NewJob(action, repoID, repoPath, ticketNumber, scope string) (JobRecord, error) { +func (s *Store) NewJob(action JobAction, repoID, repoPath, ticketNumber, scope string) (JobRecord, error) { s.mu.Lock() defer s.mu.Unlock() id, err := randomID() diff --git a/internal/serverstate/store_test.go b/internal/serverstate/store_test.go new file mode 100644 index 0000000..56fff84 --- /dev/null +++ b/internal/serverstate/store_test.go @@ -0,0 +1,45 @@ +package serverstate_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/Neokil/AutoPR/internal/serverstate" +) + +func TestNewJobPersistsAndReloadsTypedAction(t *testing.T) { + t.Parallel() + + statePath := filepath.Join(t.TempDir(), "state.json") + store, err := serverstate.NewStore(statePath) + if err != nil { + t.Fatalf("NewStore() error = %v", err) + } + + job, err := store.NewJob(serverstate.JobAction("cleanup_all"), "repo-1", "/tmp/repo", "SC-1", "all") + if err != nil { + t.Fatalf("NewJob() error = %v", err) + } + + raw, err := os.ReadFile(statePath) + if err != nil { + t.Fatalf("ReadFile() error = %v", err) + } + if !strings.Contains(string(raw), `"action": "cleanup_all"`) { + t.Fatalf("persisted state missing action string: %s", string(raw)) + } + + reloaded, err := serverstate.NewStore(statePath) + if err != nil { + t.Fatalf("reloaded NewStore() error = %v", err) + } + stored, ok := reloaded.GetJob(job.ID) + if !ok { + t.Fatalf("GetJob(%q) did not find reloaded job", job.ID) + } + if stored.Action != serverstate.JobAction("cleanup_all") { + t.Fatalf("stored.Action = %q, want %q", stored.Action, serverstate.JobAction("cleanup_all")) + } +}