Skip to content

Commit b1f9e93

Browse files
committed
Enforce cross-project ID uniqueness in handleCreateBead
Before generating a new bead ID, collect all existing IDs from every project store and pass them as the excluded set to CreateExcluding. This prevents duplicate IDs across projects in a multi-store setup. Built with Raymond (Agent Orchestrator)
1 parent 4091f87 commit b1f9e93

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

internal/server/handlers.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,14 @@ func (s *Server) handleCreateBead(w http.ResponseWriter, r *http.Request) {
146146
return
147147
}
148148

149-
created, err := st.Create(b)
149+
excluded := map[string]struct{}{}
150+
for _, proj := range s.provider.Projects() {
151+
for _, existing := range proj.Store.All() {
152+
excluded[existing.ID] = struct{}{}
153+
}
154+
}
155+
156+
created, err := st.CreateExcluding(b, excluded)
150157
if err != nil {
151158
jsonError(w, err.Error(), http.StatusBadRequest)
152159
return

internal/server/handlers_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,3 +460,55 @@ func TestDeleteBead_UnblockedField(t *testing.T) {
460460
t.Fatalf("expected 1 unblocked bead, got %d", len(unblockedList))
461461
}
462462
}
463+
464+
func TestCreateBead_SingleProject_NoRegression(t *testing.T) {
465+
srv := crudServer(t)
466+
467+
b1 := createViaAPI(t, srv, map[string]any{"title": "First"})
468+
b2 := createViaAPI(t, srv, map[string]any{"title": "Second"})
469+
470+
if b1.ID == "" || b2.ID == "" {
471+
t.Fatal("expected non-empty IDs")
472+
}
473+
if b1.ID == b2.ID {
474+
t.Fatalf("expected unique IDs, got %q for both", b1.ID)
475+
}
476+
}
477+
478+
func TestCreateBead_MultiProject_CrossProjectUniqueness(t *testing.T) {
479+
dirA := t.TempDir()
480+
dirB := t.TempDir()
481+
482+
storeA, err := store.Load(filepath.Join(dirA, "a.json"))
483+
if err != nil {
484+
t.Fatalf("Load storeA: %v", err)
485+
}
486+
storeB, err := store.Load(filepath.Join(dirB, "b.json"))
487+
if err != nil {
488+
t.Fatalf("Load storeB: %v", err)
489+
}
490+
491+
// Seed a known ID into store A.
492+
seed := model.Bead{ID: "bd-aaaa", Title: "seed"}
493+
if _, err := storeA.Create(seed); err != nil {
494+
t.Fatalf("seed create: %v", err)
495+
}
496+
497+
p := NewMultiStoreProvider([]ProviderEntry{
498+
{Name: "projA", Token: testToken + "-a", Store: storeA},
499+
{Name: "projB", Token: testToken, Store: storeB},
500+
})
501+
srv, err := New(Config{Port: 0, DataFile: filepath.Join(dirB, "b.json"), LogOutput: io.Discard}, p)
502+
if err != nil {
503+
t.Fatalf("New: %v", err)
504+
}
505+
srv.Store = storeB
506+
507+
// Create many beads via the HTTP API (targeting store B) and verify none get the seeded ID.
508+
for i := range 50 {
509+
b := createViaAPI(t, srv, map[string]any{"title": "bead"})
510+
if b.ID == "bd-aaaa" {
511+
t.Fatalf("bead %d got ID %q which collides with store A seed", i, b.ID)
512+
}
513+
}
514+
}

0 commit comments

Comments
 (0)