refactor(api): merge review-queue into /api/activities, rename resolve to assign
Removes the unused GET /api/activities list/detail endpoints, moves the review-queue list and resolve/unlock/unassign actions under /api/activities, renames resolve to assign end-to-end, and drops the now-unused store.ReviewQueue helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -197,7 +197,7 @@ func TestWorkoutKindUpdate_RejectsInvertedPaceRange(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReviewQueueResolve(t *testing.T) {
|
||||
func TestActivitiesAssign(t *testing.T) {
|
||||
s, db, userID := newTestServer(t)
|
||||
ctx := newCtx()
|
||||
|
||||
@@ -229,9 +229,9 @@ func TestReviewQueueResolve(t *testing.T) {
|
||||
}
|
||||
|
||||
router := s.Router()
|
||||
rec := doJSON(t, router, http.MethodGet, "/api/review-queue/", nil)
|
||||
rec := doJSON(t, router, http.MethodGet, "/api/activities/", nil)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("review queue status = %d", rec.Code)
|
||||
t.Fatalf("activities list status = %d", rec.Code)
|
||||
}
|
||||
var page struct {
|
||||
Items []map[string]any `json:"items"`
|
||||
@@ -240,43 +240,43 @@ func TestReviewQueueResolve(t *testing.T) {
|
||||
}
|
||||
json.Unmarshal(rec.Body.Bytes(), &page)
|
||||
if len(page.Items) != 1 || page.Total != 1 {
|
||||
t.Fatalf("expected 1 item in review queue (total=1), got %d items, total=%d: %s", len(page.Items), page.Total, rec.Body.String())
|
||||
t.Fatalf("expected 1 item in activities list (total=1), got %d items, total=%d: %s", len(page.Items), page.Total, rec.Body.String())
|
||||
}
|
||||
laps, _ := page.Items[0]["laps"].([]any)
|
||||
if len(laps) != 1 {
|
||||
t.Fatalf("expected 1 lap in review queue item, got %d: %s", len(laps), rec.Body.String())
|
||||
t.Fatalf("expected 1 lap in activities list item, got %d: %s", len(laps), rec.Body.String())
|
||||
}
|
||||
samples, _ := page.Items[0]["samples"].([]any)
|
||||
if len(samples) != 1 {
|
||||
t.Fatalf("expected 1 sample in review queue item, got %d: %s", len(samples), rec.Body.String())
|
||||
t.Fatalf("expected 1 sample in activities list item, got %d: %s", len(samples), rec.Body.String())
|
||||
}
|
||||
|
||||
rec = doJSON(t, router, http.MethodPost, "/api/review-queue/"+itoa(activityID)+"/resolve", map[string]any{"workout_kind_id": kindID})
|
||||
rec = doJSON(t, router, http.MethodPost, "/api/activities/"+itoa(activityID)+"/assign", map[string]any{"workout_kind_id": kindID})
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("resolve status = %d, body = %s", rec.Code, rec.Body.String())
|
||||
t.Fatalf("assign status = %d, body = %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
// The activity stays listed after resolving -- the Activities page shows
|
||||
// every activity, locked or not, not just ones still needing review.
|
||||
rec = doJSON(t, router, http.MethodGet, "/api/review-queue/", nil)
|
||||
rec = doJSON(t, router, http.MethodGet, "/api/activities/", nil)
|
||||
json.Unmarshal(rec.Body.Bytes(), &page)
|
||||
if len(page.Items) != 1 || page.Total != 1 {
|
||||
t.Fatalf("expected activity to remain listed after resolve, got %d items, total=%d", len(page.Items), page.Total)
|
||||
t.Fatalf("expected activity to remain listed after assign, got %d items, total=%d", len(page.Items), page.Total)
|
||||
}
|
||||
if page.Items[0]["AssignmentSource"] != store.AssignmentSourceManual {
|
||||
t.Fatalf("expected AssignmentSource=manual after resolve, got %v", page.Items[0]["AssignmentSource"])
|
||||
t.Fatalf("expected AssignmentSource=manual after assign, got %v", page.Items[0]["AssignmentSource"])
|
||||
}
|
||||
if got := page.Items[0]["WorkoutKindID"]; got != float64(kindID) {
|
||||
t.Fatalf("expected WorkoutKindID=%d after resolve, got %v", kindID, got)
|
||||
t.Fatalf("expected WorkoutKindID=%d after assign, got %v", kindID, got)
|
||||
}
|
||||
|
||||
// Unlocking reverts the source to rule_engine but keeps the same kind,
|
||||
// so a later reclassify pass is free to change it again.
|
||||
rec = doJSON(t, router, http.MethodPost, "/api/review-queue/"+itoa(activityID)+"/unlock", nil)
|
||||
rec = doJSON(t, router, http.MethodPost, "/api/activities/"+itoa(activityID)+"/unlock", nil)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("unlock status = %d, body = %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
rec = doJSON(t, router, http.MethodGet, "/api/review-queue/", nil)
|
||||
rec = doJSON(t, router, http.MethodGet, "/api/activities/", nil)
|
||||
json.Unmarshal(rec.Body.Bytes(), &page)
|
||||
if page.Items[0]["AssignmentSource"] != store.AssignmentSourceRuleEngine {
|
||||
t.Fatalf("expected AssignmentSource=rule_engine after unlock, got %v", page.Items[0]["AssignmentSource"])
|
||||
@@ -286,18 +286,18 @@ func TestReviewQueueResolve(t *testing.T) {
|
||||
}
|
||||
|
||||
// Unlocking an already-unlocked activity is rejected.
|
||||
rec = doJSON(t, router, http.MethodPost, "/api/review-queue/"+itoa(activityID)+"/unlock", nil)
|
||||
rec = doJSON(t, router, http.MethodPost, "/api/activities/"+itoa(activityID)+"/unlock", nil)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400 unlocking a non-manual assignment, got %d", rec.Code)
|
||||
}
|
||||
|
||||
// Manually unassigning clears the kind back to Unclassified and locks
|
||||
// that decision (source=manual), same as resolving to a specific kind.
|
||||
rec = doJSON(t, router, http.MethodPost, "/api/review-queue/"+itoa(activityID)+"/unassign", nil)
|
||||
rec = doJSON(t, router, http.MethodPost, "/api/activities/"+itoa(activityID)+"/unassign", nil)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("unassign status = %d, body = %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
rec = doJSON(t, router, http.MethodGet, "/api/review-queue/", nil)
|
||||
rec = doJSON(t, router, http.MethodGet, "/api/activities/", nil)
|
||||
json.Unmarshal(rec.Body.Bytes(), &page)
|
||||
if page.Items[0]["AssignmentSource"] != store.AssignmentSourceManual {
|
||||
t.Fatalf("expected AssignmentSource=manual after unassign, got %v", page.Items[0]["AssignmentSource"])
|
||||
@@ -307,7 +307,7 @@ func TestReviewQueueResolve(t *testing.T) {
|
||||
}
|
||||
|
||||
// It also shows up under the Unclassified filter now.
|
||||
rec = doJSON(t, router, http.MethodGet, "/api/review-queue/?unclassified=true", nil)
|
||||
rec = doJSON(t, router, http.MethodGet, "/api/activities/?unclassified=true", nil)
|
||||
json.Unmarshal(rec.Body.Bytes(), &page)
|
||||
if len(page.Items) != 1 || page.Total != 1 {
|
||||
t.Fatalf("expected unassigned activity to show up as unclassified, got %d items, total=%d", len(page.Items), page.Total)
|
||||
@@ -315,18 +315,18 @@ func TestReviewQueueResolve(t *testing.T) {
|
||||
|
||||
// Unassigning locks it, so unlocking works again (reverting to
|
||||
// rule_engine sourcing with no kind, i.e. needs_review).
|
||||
rec = doJSON(t, router, http.MethodPost, "/api/review-queue/"+itoa(activityID)+"/unlock", nil)
|
||||
rec = doJSON(t, router, http.MethodPost, "/api/activities/"+itoa(activityID)+"/unlock", nil)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("unlock after unassign status = %d, body = %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
rec = doJSON(t, router, http.MethodGet, "/api/review-queue/", nil)
|
||||
rec = doJSON(t, router, http.MethodGet, "/api/activities/", nil)
|
||||
json.Unmarshal(rec.Body.Bytes(), &page)
|
||||
if page.Items[0]["AssignmentSource"] != store.AssignmentSourceRuleEngine {
|
||||
t.Fatalf("expected AssignmentSource=rule_engine after unlock, got %v", page.Items[0]["AssignmentSource"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestReviewQueue_PaginatesByCursor(t *testing.T) {
|
||||
func TestActivities_PaginatesByCursor(t *testing.T) {
|
||||
s, db, userID := newTestServer(t)
|
||||
ctx := newCtx()
|
||||
|
||||
@@ -356,7 +356,7 @@ func TestReviewQueue_PaginatesByCursor(t *testing.T) {
|
||||
}
|
||||
getPage := func(query string) page {
|
||||
t.Helper()
|
||||
rec := doJSON(t, router, http.MethodGet, "/api/review-queue/"+query, nil)
|
||||
rec := doJSON(t, router, http.MethodGet, "/api/activities/"+query, nil)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
@@ -404,7 +404,7 @@ func TestReviewQueue_PaginatesByCursor(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReviewQueue_FiltersByKindAndUnclassifiedStayPaginated(t *testing.T) {
|
||||
func TestActivities_FiltersByKindAndUnclassifiedStayPaginated(t *testing.T) {
|
||||
s, db, userID := newTestServer(t)
|
||||
ctx := newCtx()
|
||||
|
||||
@@ -448,7 +448,7 @@ func TestReviewQueue_FiltersByKindAndUnclassifiedStayPaginated(t *testing.T) {
|
||||
}
|
||||
getPage := func(query string) page {
|
||||
t.Helper()
|
||||
rec := doJSON(t, router, http.MethodGet, "/api/review-queue/"+query, nil)
|
||||
rec := doJSON(t, router, http.MethodGet, "/api/activities/"+query, nil)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
@@ -491,7 +491,7 @@ func TestReviewQueue_FiltersByKindAndUnclassifiedStayPaginated(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveReview_RejectsRaceKind(t *testing.T) {
|
||||
func TestAssignActivity_RejectsRaceKind(t *testing.T) {
|
||||
s, db, userID := newTestServer(t)
|
||||
ctx := newCtx()
|
||||
|
||||
@@ -504,9 +504,9 @@ func TestResolveReview_RejectsRaceKind(t *testing.T) {
|
||||
t.Fatalf("GetWorkoutKindByName(Race): ok=%v err=%v", ok, err)
|
||||
}
|
||||
|
||||
rec := doJSON(t, s.Router(), http.MethodPost, "/api/review-queue/"+itoa(activityID)+"/resolve", map[string]any{"workout_kind_id": raceKind.ID})
|
||||
rec := doJSON(t, s.Router(), http.MethodPost, "/api/activities/"+itoa(activityID)+"/assign", map[string]any{"workout_kind_id": raceKind.ID})
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("resolve to Race status = %d, want 400, body = %s", rec.Code, rec.Body.String())
|
||||
t.Fatalf("assign to Race status = %d, want 400, body = %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,57 +577,6 @@ func TestReclassifyAll_SkipsManualAndRaceAssignments(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListActivities_ReportsLockedForManualAndRaceAssignments(t *testing.T) {
|
||||
s, db, userID := newTestServer(t)
|
||||
ctx := newCtx()
|
||||
|
||||
easyID, err := db.CreateWorkoutKind(ctx, userID, store.WorkoutKind{Name: "Test Easy Locked", RuleJSON: `{"match":"all","conditions":[]}`, IsActive: true})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateWorkoutKind: %v", err)
|
||||
}
|
||||
raceKind, ok, err := db.GetWorkoutKindByName(ctx, userID, "Race")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("GetWorkoutKindByName(Race): ok=%v err=%v", ok, err)
|
||||
}
|
||||
|
||||
ruleEngineActivity, _ := db.UpsertActivity(ctx, userID, store.Activity{GarminActivityID: 1, StartTimeUTC: "2026-07-01 06:00:00", RawJSON: "{}"})
|
||||
manualActivity, _ := db.UpsertActivity(ctx, userID, store.Activity{GarminActivityID: 2, StartTimeUTC: "2026-07-02 06:00:00", RawJSON: "{}"})
|
||||
raceActivity, _ := db.UpsertActivity(ctx, userID, store.Activity{GarminActivityID: 3, EventTypeKey: "race", StartTimeUTC: "2026-07-03 06:00:00", RawJSON: "{}"})
|
||||
|
||||
db.InsertKindAssignment(ctx, userID, store.KindAssignment{ActivityID: ruleEngineActivity, WorkoutKindID: &easyID, AssignmentSource: store.AssignmentSourceRuleEngine, Status: store.AssignmentStatusAssigned, CandidateKindsJSON: "[]"})
|
||||
db.InsertKindAssignment(ctx, userID, store.KindAssignment{ActivityID: manualActivity, WorkoutKindID: &easyID, AssignmentSource: store.AssignmentSourceManual, Status: store.AssignmentStatusAssigned, CandidateKindsJSON: "[]"})
|
||||
db.InsertKindAssignment(ctx, userID, store.KindAssignment{ActivityID: raceActivity, WorkoutKindID: &raceKind.ID, AssignmentSource: store.AssignmentSourceRuleEngine, Status: store.AssignmentStatusAssigned, CandidateKindsJSON: "[]"})
|
||||
|
||||
rec := doJSON(t, s.Router(), http.MethodGet, "/api/activities/", nil)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, body = %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var items []activityListItem
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &items); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
locked := map[int64]bool{}
|
||||
kindName := map[int64]string{}
|
||||
for _, it := range items {
|
||||
locked[it.ID] = it.Locked
|
||||
if it.WorkoutKindName != nil {
|
||||
kindName[it.ID] = *it.WorkoutKindName
|
||||
}
|
||||
}
|
||||
if locked[ruleEngineActivity] {
|
||||
t.Errorf("rule-engine activity should not be locked")
|
||||
}
|
||||
if !locked[manualActivity] {
|
||||
t.Errorf("manual activity should be locked")
|
||||
}
|
||||
if !locked[raceActivity] {
|
||||
t.Errorf("race activity should be locked")
|
||||
}
|
||||
if kindName[raceActivity] != "Race" {
|
||||
t.Errorf("race activity workout_kind_name = %q, want %q", kindName[raceActivity], "Race")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncReset_DeletesActivitiesAndBackfillEndpointIsGone(t *testing.T) {
|
||||
s, db, userID := newTestServer(t)
|
||||
ctx := newCtx()
|
||||
|
||||
Reference in New Issue
Block a user