Verify shared settings transactions on isolated PostgreSQL (P8)
Why: SQLite coverage cannot prove PostgreSQL CAS and multi-row migration rollback. Evidence: SELF /tmp/project-settings-postgres-business-20261014-973d52c1.json ran two real PostgreSQL business tests in a fresh local cluster and stopped that cluster afterward. Impact: Add opt-in isolated database checks for two-connection CAS, private settings boundaries, default restoration, injected migration rollback and idempotent replay. These tests do not select deployment credentials. Refs: P8, docs/records/2026-10-14-rdm-settings-completion-contract.md
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"agent-system-backend/internal/store"
|
||||
)
|
||||
|
||||
// These checks require a disposable database. They never select a deployment DSN.
|
||||
func isolatedSettingsPostgres(t *testing.T) *store.Store {
|
||||
t.Helper()
|
||||
dsn := os.Getenv("AGENT_SETTINGS_PG_TEST_DSN")
|
||||
if dsn == "" {
|
||||
t.Skip("NOT_RUN: AGENT_SETTINGS_PG_TEST_DSN requires an isolated PostgreSQL fixture")
|
||||
}
|
||||
if os.Getenv("AGENT_SETTINGS_PG_ISOLATED") != "1" {
|
||||
t.Fatal("refusing PostgreSQL settings tests without an isolated fixture marker")
|
||||
}
|
||||
probe, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
t.Fatal("cannot open isolated PostgreSQL fixture")
|
||||
}
|
||||
var database string
|
||||
err = probe.QueryRow("SELECT current_database()").Scan(&database)
|
||||
probe.Close()
|
||||
if err != nil || !strings.HasPrefix(database, "settings_") {
|
||||
t.Fatal("PostgreSQL settings fixture database must have the settings_ prefix")
|
||||
}
|
||||
st, err := store.OpenPostgres(dsn, t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal("cannot initialize isolated PostgreSQL fixture")
|
||||
}
|
||||
t.Cleanup(func() { st.Close() })
|
||||
return st
|
||||
}
|
||||
|
||||
func TestPostgresSharedSettingsConcurrentCASAndPersonalBoundary(t *testing.T) {
|
||||
first, second := isolatedSettingsPostgres(t), isolatedSettingsPostgres(t)
|
||||
a, b := NewService(first), NewService(second)
|
||||
project := fmt.Sprintf("pg-cas-%d", time.Now().UnixNano())
|
||||
start := make(chan struct{})
|
||||
results := make(chan error, 2)
|
||||
var writers sync.WaitGroup
|
||||
for i, service := range []*Service{a, b} {
|
||||
writers.Add(1)
|
||||
go func(service *Service, style string) {
|
||||
defer writers.Done()
|
||||
<-start
|
||||
_, err := service.MergeSharedProject(project, map[string]any{"deliverables.style": style}, 0)
|
||||
results <- err
|
||||
}(service, []string{"formal", "minimal"}[i])
|
||||
}
|
||||
close(start)
|
||||
writers.Wait()
|
||||
close(results)
|
||||
passed, conflicts := 0, 0
|
||||
for err := range results {
|
||||
switch {
|
||||
case err == nil:
|
||||
passed++
|
||||
case errors.Is(err, ErrSettingsConflict):
|
||||
conflicts++
|
||||
default:
|
||||
t.Fatalf("unexpected concurrent write result: %v", err)
|
||||
}
|
||||
}
|
||||
if passed != 1 || conflicts != 1 {
|
||||
t.Fatalf("CAS admitted %d writers with %d conflicts", passed, conflicts)
|
||||
}
|
||||
values, revision, err := b.SharedProjectScope(project)
|
||||
if err != nil || revision != 1 {
|
||||
t.Fatalf("second connection sees revision %d: %v", revision, err)
|
||||
}
|
||||
if err := first.MergeSettingsScope("alice", ScopeProjectPrefix+project, map[string]any{"context.userInstructions": "private", "models.chat.model": "legacy"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b.Effective("bob", project).GetString("context.userInstructions", "") != "" || b.Effective("alice", project).GetString("models.chat.model", "") != "" {
|
||||
t.Fatal("legacy model override or another member's preferences became effective")
|
||||
}
|
||||
if got := b.Effective("bob", project).GetString("deliverables.style", ""); got != a.Effective("alice", project).GetString("deliverables.style", "") || len(values) == 0 {
|
||||
t.Fatal("shared project configuration diverged across actors/connections")
|
||||
}
|
||||
if _, err := a.MergeSharedProject(project, map[string]any{}, revision, []string{"deliverables.style"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := b.Effective("bob", project).GetString("deliverables.style", ""); got != "modern" {
|
||||
t.Fatalf("removing the override did not restore the default: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgresProjectMigrationRollbackAndReplay(t *testing.T) {
|
||||
st := isolatedSettingsPostgres(t)
|
||||
s := NewService(st)
|
||||
project := fmt.Sprintf("pg-migration-%d", time.Now().UnixNano())
|
||||
if err := st.MergeSettingsScope("alice", ScopeProjectPrefix+project, map[string]any{"deliverables.style": "formal", "context.userInstructions": "private"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
preview, err := s.PreviewProjectMigration("alice", project, []MigrationChoice{{Path: "deliverables.style", SourceOwner: "alice"}}, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
selected, _, err := s.MigrationSelection("alice", project, preview.Observation, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Abort after the observation has been updated, proving that both rows roll back.
|
||||
function := fmt.Sprintf("settings_reject_%d", time.Now().UnixNano())
|
||||
trigger := function + "_trigger"
|
||||
ddl := fmt.Sprintf(`CREATE FUNCTION %s() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.scope='%s' THEN RAISE EXCEPTION 'fixture migration target failure'; END IF; RETURN NEW; END $$`, function, ScopeSharedProjectPrefix+project)
|
||||
if _, err := st.DB().Exec(ddl); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
st.DB().Exec("DROP TRIGGER IF EXISTS " + trigger + " ON settings")
|
||||
st.DB().Exec("DROP FUNCTION IF EXISTS " + function + "()")
|
||||
})
|
||||
if _, err := st.DB().Exec(fmt.Sprintf("CREATE TRIGGER %s BEFORE INSERT OR UPDATE ON settings FOR EACH ROW EXECUTE FUNCTION %s()", trigger, function)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.ApplyProjectMigration("alice", project, preview.Observation, 0, selected); err == nil {
|
||||
t.Fatal("injected PostgreSQL target failure succeeded")
|
||||
}
|
||||
observation, err := st.GetSettingsScope(OwnerSystemRow, observationPrefix+preview.Observation)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, done := observation["appliedRevision"]; done {
|
||||
t.Fatal("rollback retained a completed observation")
|
||||
}
|
||||
if _, revision, err := s.SharedProjectScope(project); err != nil || revision != 0 {
|
||||
t.Fatal("failed migration changed the project target")
|
||||
}
|
||||
if _, err := st.DB().Exec("DROP TRIGGER " + trigger + " ON settings"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
out, err := s.ApplyProjectMigration("alice", project, preview.Observation, 0, selected)
|
||||
if err != nil || out.ProjectRevision != 1 || out.Replayed != (attempt == 1) {
|
||||
t.Fatalf("apply/replay attempt %d: %+v, %v", attempt, out, err)
|
||||
}
|
||||
}
|
||||
old, err := st.GetSettingsScope("alice", ScopeProjectPrefix+project)
|
||||
if err != nil || old["context.userInstructions"] != "private" || old["deliverables.style"] != "formal" {
|
||||
t.Fatal("migration changed a legacy or private source row")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user