Files
BountyBoard/internal/store/store_integration_test.go
etalon f87b954f27 feat: atomization pipeline with job queue, atomizer client, WS hub, and board UI (phase 7)
- internal/extsvc: circuit breaker (§11.16) + retrying bearer JSON client
- atomizer client: §5.1 contract incl. defensive coefficient normalization
  and extension coefficient range (0,2]
- persisted jobs collection: atomic claim, panic recovery, exponential
  backoff (max 3), stale-running requeue, shutdown-safe bookkeeping
- subdivide (async 202, atomizing status, re-run replaces unpublished
  children after confirm) and extend (sibling task, source budget)
- failure path reverts status and notifies the consultant on final attempt
- PATCH task editing with bounty recompute, budget cascade to descendants
- publish single + bulk; task detail endpoint role-scoped
- coder/websocket hub: multiplexed channels, origin check, 30s heartbeats;
  client ws.js with reconnect + 15s polling fallback
- consultant atomization board: tree by root, coefficient sliders with live
  per-parent sum indicator, bounty preview, modals, shimmer, atomizer-down
  gating via /api/v1/service-health
- fix: tasks external-ref unique index is partial, not sparse (compound
  sparse indexed every task through customerId and broke child inserts)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 19:17:09 +02:00

158 lines
4.2 KiB
Go

//go:build integration
package store
import (
"context"
"errors"
"testing"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"bountyboard/internal/testutil"
"bountyboard/internal/ulid"
)
func TestEnsureIndexesIsIdempotent(t *testing.T) {
db := testutil.MongoDB(t)
ctx := context.Background()
if err := EnsureIndexes(ctx, db); err != nil {
t.Fatalf("first run: %v", err)
}
if err := EnsureIndexes(ctx, db); err != nil {
t.Fatalf("second run must be a no-op: %v", err)
}
// Spot-check a few critical indexes actually exist.
checks := map[string]string{
"users": "email_1",
"tasks": "uniq_external_ref",
"sessions": "expiresAt_1",
}
for coll, wantName := range checks {
cur, err := db.Collection(coll).Indexes().List(ctx)
if err != nil {
t.Fatal(err)
}
var specs []bson.M
if err := cur.All(ctx, &specs); err != nil {
t.Fatal(err)
}
found := false
for _, s := range specs {
if s["name"] == wantName {
found = true
// the upsert key must be unique and partial (external-only)
if coll == "tasks" {
if s["unique"] != true || s["partialFilterExpression"] == nil {
t.Errorf("%s/%s: want unique partial, got %v", coll, wantName, s)
}
}
}
}
if !found {
t.Errorf("%s: index %s not found in %v", coll, wantName, specs)
}
}
}
func TestUpdateVersioned(t *testing.T) {
db := testutil.MongoDB(t)
ctx := context.Background()
coll := db.Collection("tasks")
id := ulid.New()
if err := InsertOne(ctx, coll, id, bson.M{"title": "original", "status": "imported"}); err != nil {
t.Fatal(err)
}
// Happy path bumps version and updatedAt.
if err := UpdateVersioned(ctx, coll, id, 1, bson.M{"$set": bson.M{"title": "edited"}}); err != nil {
t.Fatalf("update at correct version: %v", err)
}
var doc struct {
Title string `bson:"title"`
Version int64 `bson:"version"`
UpdatedAt time.Time `bson:"updatedAt"`
CreatedAt time.Time `bson:"createdAt"`
}
if err := coll.FindOne(ctx, bson.M{"_id": id}).Decode(&doc); err != nil {
t.Fatal(err)
}
if doc.Title != "edited" || doc.Version != 2 {
t.Fatalf("after update: title=%q version=%d", doc.Title, doc.Version)
}
if !doc.UpdatedAt.After(doc.CreatedAt) {
t.Error("updatedAt must advance")
}
// Stale version loses.
err := UpdateVersioned(ctx, coll, id, 1, bson.M{"$set": bson.M{"title": "stale write"}})
if !errors.Is(err, ErrVersionConflict) {
t.Fatalf("want ErrVersionConflict, got %v", err)
}
// Missing document is distinguished from a conflict.
err = UpdateVersioned(ctx, coll, ulid.New(), 1, bson.M{"$set": bson.M{"title": "x"}})
if !errors.Is(err, ErrNotFound) {
t.Fatalf("want ErrNotFound, got %v", err)
}
// Non-$set operators pass through; version still bumps.
if err := UpdateVersioned(ctx, coll, id, 2, bson.M{
"$push": bson.M{"timeline": bson.M{"event": "published"}},
}); err != nil {
t.Fatalf("push update: %v", err)
}
var doc2 struct {
Version int64 `bson:"version"`
Timeline []bson.M `bson:"timeline"`
}
if err := coll.FindOne(ctx, bson.M{"_id": id}).Decode(&doc2); err != nil {
t.Fatal(err)
}
if doc2.Version != 3 || len(doc2.Timeline) != 1 {
t.Fatalf("after push: version=%d timeline=%d", doc2.Version, len(doc2.Timeline))
}
// Touching version inside the update is a programming error.
if err := UpdateVersioned(ctx, coll, id, 3, bson.M{"$inc": bson.M{"version": 5}}); err == nil {
t.Fatal("must reject updates that touch version")
}
}
func TestUpdateVersionedConcurrentWriters(t *testing.T) {
db := testutil.MongoDB(t)
ctx := context.Background()
coll := db.Collection("tasks")
id := ulid.New()
if err := InsertOne(ctx, coll, id, bson.M{"n": 0}); err != nil {
t.Fatal(err)
}
// Two writers race at the same version: exactly one wins.
results := make(chan error, 2)
for i := 0; i < 2; i++ {
go func(i int) {
results <- UpdateVersioned(ctx, coll, id, 1, bson.M{"$set": bson.M{"n": i + 1}})
}(i)
}
var conflicts, oks int
for i := 0; i < 2; i++ {
switch err := <-results; {
case err == nil:
oks++
case errors.Is(err, ErrVersionConflict):
conflicts++
default:
t.Fatalf("unexpected error: %v", err)
}
}
if oks != 1 || conflicts != 1 {
t.Fatalf("oks=%d conflicts=%d, want exactly one of each", oks, conflicts)
}
}