diff --git a/internal/http/skills_evolution.go b/internal/http/skills_evolution.go index c650e44d..3025fe51 100644 --- a/internal/http/skills_evolution.go +++ b/internal/http/skills_evolution.go @@ -267,15 +267,20 @@ func (h *SkillsHandler) handleSuggestionStatus(w http.ResponseWriter, r *http.Re writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid suggestion id"}) return } - updated, err := h.evolutionStore.UpdateSuggestionStatus(r.Context(), suggestionID, status, "user", store.ActorIDFromContext(r.Context())) + sg, err := h.evolutionStore.GetSuggestion(r.Context(), suggestionID) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } - if updated.SkillID != skillID { + if sg == nil || sg.SkillID != skillID { writeJSON(w, http.StatusNotFound, map[string]string{"error": "suggestion not found"}) return } + updated, err := h.evolutionStore.UpdateSuggestionStatus(r.Context(), suggestionID, status, "user", store.ActorIDFromContext(r.Context())) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } h.logSkillActivity(r, action, skillID, map[string]any{"suggestion_id": suggestionID.String()}) writeJSON(w, http.StatusOK, updated) } diff --git a/internal/http/skills_evolution_test.go b/internal/http/skills_evolution_test.go index c2d59573..032bedcb 100644 --- a/internal/http/skills_evolution_test.go +++ b/internal/http/skills_evolution_test.go @@ -3,6 +3,7 @@ package http import ( "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" "os" @@ -67,8 +68,45 @@ func TestApplySkillSuggestionPatchCreatesNewReferenceFile(t *testing.T) { } } +func TestHandleSuggestionStatusDoesNotUpdateDifferentSkillSuggestion(t *testing.T) { + handler, skillStore, ctx, root := newTestUploadHandler(t) + requestSkillID := skillStore.seedCustomSkill("request-skill", filepath.Join(root, "skills-store", "request-skill", "1"), "active", nil) + ownerSkillID := skillStore.seedCustomSkill("owner-skill", filepath.Join(root, "skills-store", "owner-skill", "1"), "active", nil) + suggestionID := uuid.New() + evolution := &skillEvolutionStoreStub{ + suggestions: map[uuid.UUID]*store.SkillImprovementSuggestion{ + suggestionID: { + ID: suggestionID, + SkillID: ownerSkillID, + Status: store.SkillSuggestionStatusPending, + SuggestionType: "skill_reference_add", + }, + }, + } + handler.SetEvolutionStore(evolution, nil) + + req := httptest.NewRequest(http.MethodPost, "/v1/skills/"+requestSkillID.String()+"/evolution/suggestions/"+suggestionID.String()+"/approve", nil).WithContext(ctx) + req.SetPathValue("id", requestSkillID.String()) + req.SetPathValue("suggestionID", suggestionID.String()) + w := httptest.NewRecorder() + + handler.handleApproveSkillSuggestion(w, req) + + if w.Code != http.StatusNotFound { + t.Fatalf("status = %d, body = %s, want 404", w.Code, w.Body.String()) + } + if evolution.statusUpdates != 0 { + t.Fatalf("status updates = %d, want 0", evolution.statusUpdates) + } + if got := evolution.suggestions[suggestionID].Status; got != store.SkillSuggestionStatusPending { + t.Fatalf("suggestion status = %q, want pending", got) + } +} + type skillEvolutionStoreStub struct { - versions []store.SkillVersion + versions []store.SkillVersion + suggestions map[uuid.UUID]*store.SkillImprovementSuggestion + statusUpdates int } func (s *skillEvolutionStoreStub) GetSettings(context.Context, uuid.UUID) (*store.SkillEvolutionSettings, error) { @@ -99,12 +137,33 @@ func (s *skillEvolutionStoreStub) ListSuggestions(context.Context, uuid.UUID, st return nil, nil } -func (s *skillEvolutionStoreStub) GetSuggestion(context.Context, uuid.UUID) (*store.SkillImprovementSuggestion, error) { - return nil, nil +func (s *skillEvolutionStoreStub) GetSuggestion(_ context.Context, id uuid.UUID) (*store.SkillImprovementSuggestion, error) { + if s.suggestions == nil { + return nil, nil + } + sg, ok := s.suggestions[id] + if !ok { + return nil, nil + } + copied := *sg + return &copied, nil } -func (s *skillEvolutionStoreStub) UpdateSuggestionStatus(context.Context, uuid.UUID, string, string, string) (*store.SkillImprovementSuggestion, error) { - return nil, nil +func (s *skillEvolutionStoreStub) UpdateSuggestionStatus(_ context.Context, id uuid.UUID, status, actorType, actorID string) (*store.SkillImprovementSuggestion, error) { + s.statusUpdates++ + if s.suggestions == nil { + return nil, fmt.Errorf("suggestion not found") + } + sg, ok := s.suggestions[id] + if !ok { + return nil, fmt.Errorf("suggestion not found") + } + sg.Status = status + sg.ReviewedByActorType = actorType + sg.ReviewedByActorID = actorID + now := time.Now() + sg.ReviewedAt = &now + return sg, nil } func (s *skillEvolutionStoreStub) MarkSuggestionApplied(_ context.Context, id uuid.UUID, version int, actorType, actorID string) (*store.SkillImprovementSuggestion, error) { diff --git a/internal/store/sqlitestore/schema.sql b/internal/store/sqlitestore/schema.sql index b2bd44c7..c7df01dd 100644 --- a/internal/store/sqlitestore/schema.sql +++ b/internal/store/sqlitestore/schema.sql @@ -2104,7 +2104,7 @@ CREATE INDEX IF NOT EXISTS idx_browser_cookies_expires_at ON browser_cookies (expires_at); -- ============================================================ --- Skill self-evolution (migration 000078 / SQLite schema 47) +-- Skill self-evolution (migration 000079 / SQLite schema 48) -- ============================================================ CREATE TABLE IF NOT EXISTS skill_evolution_settings (