From 8a0f655185f327ad0e341c2c7ae54dfc2b061e7c Mon Sep 17 00:00:00 2001 From: "Cruz Morales (TresPies)" <273886418+DojoGenesis@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:47:31 -0500 Subject: [PATCH] fix(backends): bootstrap ON CONFLICT target follows the 062 unique swap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migration 062 replaced uq_backends_name (name) with uq_backends_scope_name (scope, name); the first-boot pool seeding still inferred ON CONFLICT (name) and died with SQLSTATE 42P10 on every fresh database — empty context_backends, no enabled backend for any role, /health 503 indefinitely. Populated pools return before the INSERT, so live deployments never execute the broken path. Infer on (scope, name): the seeding INSERT leaves scope at its '_global' default, and the double-start idempotency (risk 6.10) keeps the same arbiter. Adds TestBootstrapSeedsEmptyPool (integration): 42P10 on the old code, green on the fix; also pins the populated-table no-op guard. Co-Authored-By: Claude Fable 5 --- go/internal/backends/bootstrap.go | 2 +- .../backends/bootstrap_integration_test.go | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 go/internal/backends/bootstrap_integration_test.go diff --git a/go/internal/backends/bootstrap.go b/go/internal/backends/bootstrap.go index e7c62aba..776ce253 100644 --- a/go/internal/backends/bootstrap.go +++ b/go/internal/backends/bootstrap.go @@ -72,7 +72,7 @@ func Bootstrap(ctx context.Context, q ExecQuerier, in BootstrapInput) (int, erro (name, base_url, protocol, provider_class, trust, locality, roles, model_map, timeouts, num_ctx, priority, enabled) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,true) - ON CONFLICT (name) DO NOTHING`, + ON CONFLICT (scope, name) DO NOTHING`, b.Name, b.Host, string(b.Protocol), b.ProviderClass, string(b.Trust), b.Locality, b.Roles, marshalModelMap(b.ModelMap), timeouts, nullableInt(b.NumCtx), b.Priority) diff --git a/go/internal/backends/bootstrap_integration_test.go b/go/internal/backends/bootstrap_integration_test.go new file mode 100644 index 00000000..9e0841a7 --- /dev/null +++ b/go/internal/backends/bootstrap_integration_test.go @@ -0,0 +1,68 @@ +//go:build integration + +// Integration regression for the first-boot backend-pool seeding path +// (Bootstrap): migration 062 swapped uq_backends_name (name) → +// uq_backends_scope_name (scope, name), but the seeding INSERT still +// inferred ON CONFLICT (name) — SQLSTATE 42P10 on every fresh database, +// context_backends stays empty and every LLM role answers 503. A populated +// pool returns before the INSERT, which is why live deployments never hit +// it: the path only runs on the empty table a fresh install has. +// +// External test package like backends_tenant_integration_test.go (testdb +// imports store which imports backends — an internal test would cycle). +// +// Run with: +// +// go test -tags=integration ./internal/backends/ -run TestBootstrapSeedsEmptyPool -count=1 -v +package backends_test + +import ( + "context" + "testing" + + "github.com/GottZ/ctx/internal/backends" + "github.com/GottZ/ctx/internal/testdb" +) + +func TestBootstrapSeedsEmptyPool(t *testing.T) { + pool := testdb.SetupTestDB(t) + ctx := context.Background() + + in := backends.BootstrapInput{ + Chat: backends.Backend{ + Host: "http://chat.internal:11434", + Protocol: backends.ProtocolOllama, + Model: "chat-model", + }, + Embed: backends.Backend{ + Host: "http://embed.internal:11434", + Protocol: backends.ProtocolOllama, + Model: "embed-model", + }, + } + + inserted, err := backends.Bootstrap(ctx, pool, in) + if err != nil { + t.Fatalf("Bootstrap on empty context_backends: %v", err) + } + if inserted == 0 { + t.Fatal("Bootstrap inserted 0 rows on an empty pool") + } + + var count int + if err := pool.QueryRow(ctx, `SELECT count(*) FROM context_backends`).Scan(&count); err != nil { + t.Fatalf("count context_backends: %v", err) + } + if count != inserted { + t.Fatalf("row count %d != inserted %d", count, inserted) + } + + // Populated table: the guard must return without touching the pool. + again, err := backends.Bootstrap(ctx, pool, in) + if err != nil { + t.Fatalf("Bootstrap on populated pool: %v", err) + } + if again != 0 { + t.Fatalf("second Bootstrap inserted %d rows, want 0", again) + } +}