Files
Agent-System-Backend/internal/settings/postgres_settings_test.go
T
weichen f4ac28b18e 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
2026-10-14 02:38:37 +00:00

154 lines
5.9 KiB
Go

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")
}
}