f87b954f27
- 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>
158 lines
4.2 KiB
Go
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)
|
|
}
|
|
}
|