package handlers import ( "bytes" "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/port" "github.com/orchard9/rdev/internal/sdlc" "github.com/orchard9/rdev/internal/service" ) // testSDLCExecutor implements port.SDLCExecutor for handler tests. type testSDLCExecutor struct { state *sdlc.State classification *sdlc.Classification features []*sdlc.Feature feature *sdlc.Feature artifacts map[sdlc.ArtifactType]*sdlc.Artifact tasks []sdlc.Task task *sdlc.Task blocked []port.BlockedInfo ready []port.ReadyInfo approval []port.ApprovalInfo err error } func (m *testSDLCExecutor) GetState(_ context.Context, _ string) (*sdlc.State, error) { return m.state, m.err } func (m *testSDLCExecutor) GetNext(_ context.Context, _, _ string) (*sdlc.Classification, error) { return m.classification, m.err } func (m *testSDLCExecutor) ListFeatures(_ context.Context, _ string) ([]*sdlc.Feature, error) { return m.features, m.err } func (m *testSDLCExecutor) GetFeature(_ context.Context, _, _ string) (*sdlc.Feature, error) { return m.feature, m.err } func (m *testSDLCExecutor) CreateFeature(_ context.Context, _, slug, title string) (*sdlc.Feature, error) { if m.err != nil { return nil, m.err } return &sdlc.Feature{Slug: slug, Title: title, Phase: sdlc.PhaseDraft}, nil } func (m *testSDLCExecutor) TransitionFeature(_ context.Context, _, _ string, _ sdlc.FeaturePhase) error { return m.err } func (m *testSDLCExecutor) BlockFeature(_ context.Context, _, _, _ string) error { return m.err } func (m *testSDLCExecutor) UnblockFeature(_ context.Context, _, _ string) error { return m.err } func (m *testSDLCExecutor) DeleteFeature(_ context.Context, _, _ string) error { return m.err } func (m *testSDLCExecutor) GetArtifactStatus(_ context.Context, _, _ string) (map[sdlc.ArtifactType]*sdlc.Artifact, error) { return m.artifacts, m.err } func (m *testSDLCExecutor) ApproveArtifact(_ context.Context, _, _ string, _ sdlc.ArtifactType) error { return m.err } func (m *testSDLCExecutor) RejectArtifact(_ context.Context, _, _ string, _ sdlc.ArtifactType) error { return m.err } func (m *testSDLCExecutor) ListTasks(_ context.Context, _, _ string) ([]sdlc.Task, error) { return m.tasks, m.err } func (m *testSDLCExecutor) AddTask(_ context.Context, _, _, title string) (*sdlc.Task, error) { if m.err != nil { return nil, m.err } if m.task != nil { return m.task, nil } return &sdlc.Task{ID: "task-001", Title: title, Status: sdlc.TaskPending}, nil } func (m *testSDLCExecutor) StartTask(_ context.Context, _, _, _ string) error { return m.err } func (m *testSDLCExecutor) CompleteTask(_ context.Context, _, _, _ string) error { return m.err } func (m *testSDLCExecutor) BlockTask(_ context.Context, _, _, _ string) error { return m.err } func (m *testSDLCExecutor) QueryBlocked(_ context.Context, _ string) ([]port.BlockedInfo, error) { return m.blocked, m.err } func (m *testSDLCExecutor) QueryReady(_ context.Context, _ string) ([]port.ReadyInfo, error) { return m.ready, m.err } func (m *testSDLCExecutor) QueryNeedsApproval(_ context.Context, _ string) ([]port.ApprovalInfo, error) { return m.approval, m.err } func (m *testSDLCExecutor) CreateBranch(_ context.Context, _, slug string) (*sdlc.BranchManifest, error) { if m.err != nil { return nil, m.err } return &sdlc.BranchManifest{Name: "feature/" + slug, Feature: slug}, nil } func (m *testSDLCExecutor) GetBranchStatus(_ context.Context, _, slug string) (*port.BranchStatus, error) { if m.err != nil { return nil, m.err } return &port.BranchStatus{ Branch: &sdlc.BranchManifest{Name: "feature/" + slug, Feature: slug}, Checklist: nil, Ready: true, }, nil } func (m *testSDLCExecutor) SyncBranch(_ context.Context, _, _ string) error { return m.err } func (m *testSDLCExecutor) MergeFeature(_ context.Context, _, _, _ string) error { return m.err } func (m *testSDLCExecutor) ArchiveFeature(_ context.Context, _, _ string) error { return m.err } // testSDLCProjectRepo implements port.ProjectRepository for handler tests. type testSDLCProjectRepo struct { project *domain.Project } func (m *testSDLCProjectRepo) Get(_ context.Context, _ domain.ProjectID) (*domain.Project, error) { if m.project == nil { return nil, domain.ErrProjectNotFound } return m.project, nil } func (m *testSDLCProjectRepo) List(_ context.Context) ([]domain.Project, error) { return nil, nil } func (m *testSDLCProjectRepo) Exists(_ context.Context, _ domain.ProjectID) (bool, error) { return m.project != nil, nil } func (m *testSDLCProjectRepo) Register(_ context.Context, _ *domain.Project) error { return nil } func (m *testSDLCProjectRepo) Unregister(_ context.Context, _ domain.ProjectID) error { return nil } func (m *testSDLCProjectRepo) RefreshStatus(_ context.Context) error { return nil } func setupSDLCHandler(exec *testSDLCExecutor) (*SDLCHandler, *chi.Mux) { repo := &testSDLCProjectRepo{ project: &domain.Project{ID: "test-project", PodName: "test-pod"}, } svc := service.NewSDLCService(exec, repo, service.SDLCServiceConfig{}) handler := NewSDLCHandler(svc, nil) r := chi.NewRouter() r.Use(testAdminAuth) handler.Mount(r) return handler, r } func TestSDLCHandler_GetState(t *testing.T) { exec := &testSDLCExecutor{ state: &sdlc.State{Version: 1}, } _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/state", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_GetState_NotInitialized(t *testing.T) { exec := &testSDLCExecutor{ err: sdlc.ErrNotInitialized, } _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/state", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("expected status 404, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_GetState_ProjectNotFound(t *testing.T) { exec := &testSDLCExecutor{} repo := &testSDLCProjectRepo{project: nil} svc := service.NewSDLCService(exec, repo, service.SDLCServiceConfig{}) handler := NewSDLCHandler(svc, nil) r := chi.NewRouter() r.Use(testAdminAuth) handler.Mount(r) req := httptest.NewRequest(http.MethodGet, "/projects/nonexistent/sdlc/state", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("expected status 404, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_CreateFeature(t *testing.T) { exec := &testSDLCExecutor{} _, router := setupSDLCHandler(exec) body, _ := json.Marshal(CreateFeatureRequest{Slug: "auth-flow", Title: "Auth Flow"}) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Errorf("expected status 201, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_CreateFeature_MissingFields(t *testing.T) { exec := &testSDLCExecutor{} _, router := setupSDLCHandler(exec) body, _ := json.Marshal(CreateFeatureRequest{}) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_CreateFeature_AlreadyExists(t *testing.T) { exec := &testSDLCExecutor{err: sdlc.ErrFeatureExists} _, router := setupSDLCHandler(exec) body, _ := json.Marshal(CreateFeatureRequest{Slug: "auth-flow", Title: "Auth Flow"}) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_TransitionFeature(t *testing.T) { exec := &testSDLCExecutor{} _, router := setupSDLCHandler(exec) body, _ := json.Marshal(TransitionFeatureRequest{Phase: "specified"}) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/transition", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_TransitionFeature_InvalidPhase(t *testing.T) { exec := &testSDLCExecutor{} _, router := setupSDLCHandler(exec) body, _ := json.Marshal(TransitionFeatureRequest{Phase: "not-a-phase"}) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/transition", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_TransitionFeature_InvalidTransition(t *testing.T) { exec := &testSDLCExecutor{err: sdlc.ErrInvalidTransition} _, router := setupSDLCHandler(exec) body, _ := json.Marshal(TransitionFeatureRequest{Phase: "review"}) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/transition", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_BlockFeature(t *testing.T) { exec := &testSDLCExecutor{} _, router := setupSDLCHandler(exec) body, _ := json.Marshal(BlockFeatureRequest{Reason: "needs API key"}) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/block", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_BlockFeature_MissingReason(t *testing.T) { exec := &testSDLCExecutor{} _, router := setupSDLCHandler(exec) body, _ := json.Marshal(BlockFeatureRequest{}) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/block", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_DeleteFeature(t *testing.T) { exec := &testSDLCExecutor{} _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodDelete, "/projects/test-project/sdlc/features/auth-flow", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusNoContent { t.Errorf("expected status 204, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_DeleteFeature_NotFound(t *testing.T) { exec := &testSDLCExecutor{err: sdlc.ErrFeatureNotFound} _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodDelete, "/projects/test-project/sdlc/features/nonexistent", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("expected status 404, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_ApproveArtifact(t *testing.T) { exec := &testSDLCExecutor{} _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/artifacts/spec/approve", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_ApproveArtifact_InvalidType(t *testing.T) { exec := &testSDLCExecutor{} _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/artifacts/invalid/approve", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_AddTask(t *testing.T) { exec := &testSDLCExecutor{} _, router := setupSDLCHandler(exec) body, _ := json.Marshal(AddTaskRequest{Title: "Add login form"}) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/tasks", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Errorf("expected status 201, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_AddTask_MissingTitle(t *testing.T) { exec := &testSDLCExecutor{} _, router := setupSDLCHandler(exec) body, _ := json.Marshal(AddTaskRequest{}) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/tasks", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_QueryBlocked(t *testing.T) { exec := &testSDLCExecutor{ blocked: []port.BlockedInfo{ {Slug: "auth", Phase: "implementation", Blockers: []string{"needs API key"}}, }, } _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/query/blocked", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_InternalError(t *testing.T) { exec := &testSDLCExecutor{err: errors.New("something unexpected")} _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/state", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusInternalServerError { t.Errorf("expected status 500, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_GetNext(t *testing.T) { exec := &testSDLCExecutor{ classification: &sdlc.Classification{ Feature: "auth-flow", CurrentPhase: sdlc.PhaseDraft, RuleMatched: "needs-spec", Action: sdlc.ActionCreateSpec, }, } _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/next?feature=auth-flow", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_GetNext_FeatureNotFound(t *testing.T) { exec := &testSDLCExecutor{err: sdlc.ErrFeatureNotFound} _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/next?feature=nonexistent", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("expected status 404, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_ListFeatures(t *testing.T) { exec := &testSDLCExecutor{ features: []*sdlc.Feature{ {Slug: "auth-flow", Title: "Auth Flow", Phase: sdlc.PhaseDraft}, {Slug: "payments", Title: "Payments", Phase: sdlc.PhaseImplementation}, }, } _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/features", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_ListFeatures_Empty(t *testing.T) { exec := &testSDLCExecutor{features: nil} _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/features", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } // Should return empty array, not null if w.Body.String() == "null" { t.Error("expected empty array, not null") } } func TestSDLCHandler_GetFeature(t *testing.T) { exec := &testSDLCExecutor{ feature: &sdlc.Feature{ Slug: "auth-flow", Title: "Auth Flow", Phase: sdlc.PhaseDraft, }, } _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/features/auth-flow", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_GetFeature_NotFound(t *testing.T) { exec := &testSDLCExecutor{err: sdlc.ErrFeatureNotFound} _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/features/nonexistent", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("expected status 404, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_UnblockFeature(t *testing.T) { exec := &testSDLCExecutor{} _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/unblock", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_UnblockFeature_NotFound(t *testing.T) { exec := &testSDLCExecutor{err: sdlc.ErrFeatureNotFound} _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/nonexistent/unblock", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("expected status 404, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_RejectArtifact(t *testing.T) { exec := &testSDLCExecutor{} _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/artifacts/spec/reject", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_RejectArtifact_InvalidType(t *testing.T) { exec := &testSDLCExecutor{} _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/artifacts/invalid/reject", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_GetArtifactStatus(t *testing.T) { exec := &testSDLCExecutor{ artifacts: map[sdlc.ArtifactType]*sdlc.Artifact{ sdlc.ArtifactSpec: sdlc.NewArtifact(sdlc.ArtifactSpec), sdlc.ArtifactDesign: sdlc.NewArtifact(sdlc.ArtifactDesign), }, } _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/features/auth-flow/artifacts", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_ListTasks(t *testing.T) { exec := &testSDLCExecutor{ tasks: []sdlc.Task{ {ID: "task-001", Title: "Setup auth", Status: sdlc.TaskPending}, {ID: "task-002", Title: "Add login", Status: sdlc.TaskInProgress}, }, } _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/features/auth-flow/tasks", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_StartTask(t *testing.T) { exec := &testSDLCExecutor{} _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/tasks/task-001/start", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_StartTask_NotFound(t *testing.T) { exec := &testSDLCExecutor{err: sdlc.ErrTaskNotFound} _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/tasks/nonexistent/start", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("expected status 404, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_CompleteTask(t *testing.T) { exec := &testSDLCExecutor{} _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/tasks/task-001/complete", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_BlockTask(t *testing.T) { exec := &testSDLCExecutor{} _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodPost, "/projects/test-project/sdlc/features/auth-flow/tasks/task-001/block", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_QueryReady(t *testing.T) { exec := &testSDLCExecutor{ ready: []port.ReadyInfo{ {Slug: "auth", Phase: "ready", Action: "IMPLEMENT_TASK"}, }, } _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/query/ready", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } } func TestSDLCHandler_QueryNeedsApproval(t *testing.T) { exec := &testSDLCExecutor{ approval: []port.ApprovalInfo{ {Slug: "auth", Phase: "draft", Message: "spec requires approval"}, }, } _, router := setupSDLCHandler(exec) req := httptest.NewRequest(http.MethodGet, "/projects/test-project/sdlc/query/needs-approval", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } }