From aaf966872ab5148c40d37121f97035f2d766eac3 Mon Sep 17 00:00:00 2001 From: BaaRai <130491490+BaaRai@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:43:29 +0200 Subject: [PATCH] feat: each test package runs on its own database agent, db, server and evidence serialized on one shared test database via an advisory lock, while evidence, llmrec and traffic-style packages could still run against it in parallel; under full-suite load that window produced a real SQLSTATE 40P01 deadlock in the blackboard direct-source test. TestMain in every PG-using package now rewrites ARTEX_PG_DSN to a per-package database (created automatically by ensureDatabase), so packages neither contend nor need to serialize; the advisory lock stays as an in-package safety net. llmrec gains the TestMain it lacked, which also gives its requires-isolated-DSN tests a truly isolated database inside ./... runs. --- agent/testmain_test.go | 4 ++++ db/db.go | 21 +++++++++++++++++++++ db/testmain_test.go | 4 ++++ evidence/testmain_test.go | 3 +++ llmrec/testmain_test.go | 16 ++++++++++++++++ server/testmain_test.go | 4 ++++ 6 files changed, 52 insertions(+) create mode 100644 llmrec/testmain_test.go diff --git a/agent/testmain_test.go b/agent/testmain_test.go index ccf25bb..aa088a7 100644 --- a/agent/testmain_test.go +++ b/agent/testmain_test.go @@ -13,6 +13,10 @@ import ( // agent test suite so cross-package DELETE cleanup races with db/server // packages are avoided when running `go test ./...`. func TestMain(m *testing.M) { + // Own database per package (#80): with separate databases, packages stop + // contending on shared tables and the advisory lock below stops + // serializing go test ./... across packages. + db.SuffixTestDatabase("agent") dsn, _, err := db.DSN() if err != nil { os.Exit(m.Run()) diff --git a/db/db.go b/db/db.go index 92a8393..7f01a08 100644 --- a/db/db.go +++ b/db/db.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "net/url" + "os" "strings" "time" @@ -96,6 +97,26 @@ func DSN() (dsn, source string, err error) { return config.PostgresDSN() } +// SuffixTestDatabase rewrites the ARTEX_PG_DSN environment variable so its +// database name carries the given suffix (artex -> artex_agent). Called from +// package TestMain functions so every go test package runs against its own +// database: parallel package processes then cannot contend on the same tables +// (the under-load deadlock family, #80). No-op when ARTEX_PG_DSN is unset — +// config-file and production paths are untouched. Open creates the suffixed +// database automatically (ensureDatabase). +func SuffixTestDatabase(suffix string) { + v := strings.TrimSpace(os.Getenv("ARTEX_PG_DSN")) + if v == "" { + return + } + u, err := url.Parse(v) + if err != nil || u.Path == "" || u.Path == "/" { + return + } + u.Path = "/" + strings.TrimPrefix(u.Path, "/") + "_" + suffix + _ = os.Setenv("ARTEX_PG_DSN", u.String()) +} + // DB wraps the shared *sql.DB. PG handles its own connection pool + concurrency // (MVCC), so unlike the old SQLite store there is no process-wide write mutex. type DB struct{ *sql.DB } diff --git a/db/testmain_test.go b/db/testmain_test.go index f54ac41..588ce2a 100644 --- a/db/testmain_test.go +++ b/db/testmain_test.go @@ -14,6 +14,10 @@ import ( // avoid cross-package cleanup races (e.g. DELETE FROM assets WHERE id > X // from one package deleting assets created by another). func TestMain(m *testing.M) { + // Own database per package (#80): with separate databases, packages stop + // contending on shared tables and the advisory lock below stops + // serializing go test ./... across packages. + SuffixTestDatabase("db") dsn, _, err := DSN() if err != nil { // No DB configured — tests that need PG will skip themselves. diff --git a/evidence/testmain_test.go b/evidence/testmain_test.go index 9b3bf30..fab5e39 100644 --- a/evidence/testmain_test.go +++ b/evidence/testmain_test.go @@ -11,6 +11,9 @@ import ( // Initialize an explicitly configured fresh database before taking the same // suite lock as db, agent and server. Hold it on one pinned connection. func TestMain(m *testing.M) { + // Own database per package (#80): stop contending on the shared tables of + // a single test database during parallel ./... runs. + db.SuffixTestDatabase("evidence") if os.Getenv("ARTEX_PG_DSN") == "" { os.Exit(m.Run()) } diff --git a/llmrec/testmain_test.go b/llmrec/testmain_test.go new file mode 100644 index 0000000..ac04132 --- /dev/null +++ b/llmrec/testmain_test.go @@ -0,0 +1,16 @@ +package llmrec + +import ( + "os" + "testing" + + "github.com/Autumn-27/artex/db" +) + +// Run against this package's own database (db.SuffixTestDatabase, #80): the +// recorder tests require an isolated ARTEX_PG_DSN, and the suffix gives them +// one even inside a full parallel ./... run. +func TestMain(m *testing.M) { + db.SuffixTestDatabase("llmrec") + os.Exit(m.Run()) +} diff --git a/server/testmain_test.go b/server/testmain_test.go index 83483cd..46d8bdc 100644 --- a/server/testmain_test.go +++ b/server/testmain_test.go @@ -13,6 +13,10 @@ import ( // server test suite so cross-package DELETE cleanup races with db/agent // packages are avoided when running `go test ./...`. func TestMain(m *testing.M) { + // Own database per package (#80): with separate databases, packages stop + // contending on shared tables and the advisory lock below stops + // serializing go test ./... across packages. + db.SuffixTestDatabase("server") dsn, _, err := db.DSN() if err != nil { os.Exit(m.Run())