Files
Agent-System/internal/docrevision/document_commit_concurrency_pg_test.go
T
weichenandCursor f34bec40f5 test(docgov): replay same-digest across two pools and four sessions
Cross-pool idempotency is same key plus same digest; a stale in-memory
head from the seeder hid the one-winner four-session lock.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 14:34:30 +00:00

249 lines
8.3 KiB
Go

package docrevision_test
import (
"context"
"fmt"
"strings"
"sync"
"testing"
"time"
"chat-agent-system/internal/app/docflow"
"chat-agent-system/internal/app/uow"
"chat-agent-system/internal/db"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/docrevision"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestDocumentCommitConcurrencyPG(t *testing.T) {
if testing.Short() {
t.Skip("postgresql concurrency is not part of -short")
}
dsn := db.SkipIfNoTestDatabaseURL(t)
if strings.Contains(dsn, "5433") || strings.Contains(dsn, "agent_system_dev") ||
strings.Contains(dsn, "5434") && strings.Contains(dsn, "agent_system_test") ||
strings.Contains(dsn, "5435") {
t.Fatal("TEST_DATABASE_URL points at a shared env port; refuse agent_system_dev/test/main")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatal(err)
}
t.Cleanup(pool.Close)
if err := db.RunMigrationsOnceForTest(ctx, pool, dsn); err != nil {
t.Fatalf("migrate: %v", err)
}
suffix := fmt.Sprintf("%d", time.Now().UnixNano())
var projectID int64
if err := pool.QueryRow(ctx, `INSERT INTO projects (code, name, status) VALUES ($1,$2,'active') RETURNING id`,
"docflow-"+suffix, "docflow").Scan(&projectID); err != nil {
t.Fatalf("project: %v", err)
}
t.Cleanup(func() {
_, _ = pool.Exec(context.Background(), `DELETE FROM document_change_events WHERE doc_id LIKE $1`, "docflow_"+suffix+"%")
_, _ = pool.Exec(context.Background(), `DELETE FROM document_versions WHERE doc_id LIKE $1`, "docflow_"+suffix+"%")
_, _ = pool.Exec(context.Background(), `DELETE FROM document_snapshots WHERE doc_id LIKE $1`, "docflow_"+suffix+"%")
_, _ = pool.Exec(context.Background(), `DELETE FROM document_records WHERE project_id=$1`, projectID)
_, _ = pool.Exec(context.Background(), `DELETE FROM projects WHERE id=$1`, projectID)
})
docID := "docflow_" + suffix
rev := docrevision.NewService(pool)
gov := docgov.NewGovernanceEngine(pool)
if _, err := gov.CreateOrGetDocument(ctx, docID, projectID, "intake", "t", "V0.5_feasibility", "ws/"+docID, 1, "A", 2, "R", 3, "P"); err != nil {
t.Fatal(err)
}
st, err := rev.OpenTemplateVersion(ctx, docID, "# base\n", docrevision.ActorSnapshot{}, projectID, "ws/"+docID)
if err != nil {
t.Fatal(err)
}
cmds := &docflow.Commands{UoW: uow.NewPG(pool), Gov: gov, Rev: rev}
var wg sync.WaitGroup
wins := 0
var mu sync.Mutex
for i := 0; i < 2; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
_, err := cmds.HumanEdit(ctx, docflow.Principal{UserID: 1}, docflow.HumanEditCommand{
DocID: docID,
Request: docrevision.HumanRevisionRequest{
DocID: docID, Markdown: fmt.Sprintf("# base\n\nuser %d\n", i),
ExpectedHeadSnapshot: &st.HeadSnapshotID, ExpectedRevisionNo: &st.HeadRevisionNo,
IdempotencyKey: fmt.Sprintf("pg-%s-%d", suffix, i), ActorUserID: 1,
LifecycleState: "drafting", ProjectID: projectID, WorkDir: "ws/" + docID,
},
})
if err == nil {
mu.Lock()
wins++
mu.Unlock()
}
}(i)
}
wg.Wait()
if wins != 1 {
t.Fatalf("same etag concurrent edits: wins=%d", wins)
}
got, err := rev.GetRevisionState(ctx, docID)
if err != nil {
t.Fatal(err)
}
if got.HeadRevisionNo != 1 {
t.Fatalf("head revision=%d", got.HeadRevisionNo)
}
}
func refuseSharedDSN(t *testing.T, dsn string) {
t.Helper()
if strings.Contains(dsn, "5433") || strings.Contains(dsn, "agent_system_dev") ||
(strings.Contains(dsn, "5434") && strings.Contains(dsn, "agent_system_test")) ||
strings.Contains(dsn, "5435") {
t.Fatal("TEST_DATABASE_URL points at a shared env port; refuse agent_system_dev/test/main")
}
}
func TestDocumentCommitTwoPoolsAndFourSessionsPG(t *testing.T) {
if testing.Short() {
t.Skip("postgresql concurrency is not part of -short")
}
dsn := db.SkipIfNoTestDatabaseURL(t)
refuseSharedDSN(t, dsn)
ctx := context.Background()
poolA, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatal(err)
}
t.Cleanup(poolA.Close)
poolB, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatal(err)
}
t.Cleanup(poolB.Close)
if err := db.RunMigrationsOnceForTest(ctx, poolA, dsn); err != nil {
t.Fatalf("migrate: %v", err)
}
suffix := fmt.Sprintf("%d", time.Now().UnixNano())
var projectID int64
if err := poolA.QueryRow(ctx, `INSERT INTO projects (code, name, status) VALUES ($1,$2,'active') RETURNING id`,
"docflow2p-"+suffix, "docflow2p").Scan(&projectID); err != nil {
t.Fatalf("project: %v", err)
}
t.Cleanup(func() {
_, _ = poolA.Exec(context.Background(), `DELETE FROM document_change_events WHERE doc_id LIKE $1`, "docflow2p_"+suffix+"%")
_, _ = poolA.Exec(context.Background(), `DELETE FROM document_versions WHERE doc_id LIKE $1`, "docflow2p_"+suffix+"%")
_, _ = poolA.Exec(context.Background(), `DELETE FROM document_snapshots WHERE doc_id LIKE $1`, "docflow2p_"+suffix+"%")
_, _ = poolA.Exec(context.Background(), `DELETE FROM document_records WHERE project_id=$1`, projectID)
_, _ = poolA.Exec(context.Background(), `DELETE FROM projects WHERE id=$1`, projectID)
})
docID := "docflow2p_" + suffix
seed := docrevision.NewService(poolA)
gov := docgov.NewGovernanceEngine(poolA)
if _, err := gov.CreateOrGetDocument(ctx, docID, projectID, "intake", "t", "V0.5_feasibility", "ws/"+docID, 1, "A", 2, "R", 3, "P"); err != nil {
t.Fatal(err)
}
st, err := seed.OpenTemplateVersion(ctx, docID, "# base\n", docrevision.ActorSnapshot{}, projectID, "ws/"+docID)
if err != nil {
t.Fatal(err)
}
a := &docflow.Commands{UoW: uow.NewPG(poolA), Gov: docgov.NewGovernanceEngine(poolA), Rev: docrevision.NewService(poolA)}
b := &docflow.Commands{UoW: uow.NewPG(poolB), Gov: docgov.NewGovernanceEngine(poolB), Rev: docrevision.NewService(poolB)}
edit := func(cmds *docflow.Commands, i int) error {
_, err := cmds.HumanEdit(ctx, docflow.Principal{UserID: 1}, docflow.HumanEditCommand{
DocID: docID,
Request: docrevision.HumanRevisionRequest{
DocID: docID, Markdown: fmt.Sprintf("# base\n\npool %d\n", i),
ExpectedHeadSnapshot: &st.HeadSnapshotID, ExpectedRevisionNo: &st.HeadRevisionNo,
IdempotencyKey: fmt.Sprintf("2p-%s-%d", suffix, i), ActorUserID: 1,
LifecycleState: "drafting", ProjectID: projectID, WorkDir: "ws/" + docID,
},
})
return err
}
var wg sync.WaitGroup
errs := make([]error, 2)
wg.Add(2)
go func() { defer wg.Done(); errs[0] = edit(a, 0) }()
go func() { defer wg.Done(); errs[1] = edit(b, 1) }()
wg.Wait()
wins := 0
for _, err := range errs {
if err == nil {
wins++
}
}
if wins != 1 {
t.Fatalf("two pools same etag: wins=%d errs=%v", wins, errs)
}
// Same idempotency key + same digest must replay across pools.
// A different digest with the same key is ErrIdempotencyConflict (covered in unit tests).
winnerIdx := 1
if errs[0] == nil {
winnerIdx = 0
}
winnerKey := fmt.Sprintf("2p-%s-%d", suffix, winnerIdx)
replay := docrevision.HumanRevisionRequest{
DocID: docID, Markdown: fmt.Sprintf("# base\n\npool %d\n", winnerIdx),
IdempotencyKey: winnerKey, ActorUserID: 1, LifecycleState: "drafting",
ProjectID: projectID, WorkDir: "ws/" + docID,
}
first, err := a.HumanEdit(ctx, docflow.Principal{UserID: 1}, docflow.HumanEditCommand{DocID: docID, Request: replay})
if err != nil {
t.Fatal(err)
}
second, err := b.HumanEdit(ctx, docflow.Principal{UserID: 1}, docflow.HumanEditCommand{DocID: docID, Request: replay})
if err != nil {
t.Fatal(err)
}
if first.State.HeadSnapshotID != second.State.HeadSnapshotID {
t.Fatalf("idempotent replay across pools diverged")
}
var extra sync.WaitGroup
extraWins := 0
var extraMu sync.Mutex
fresh := docrevision.NewService(poolA)
st2, err := fresh.GetRevisionState(ctx, docID)
if err != nil {
t.Fatal(err)
}
for i := 0; i < 4; i++ {
extra.Add(1)
go func(i int) {
defer extra.Done()
cmds := a
if i%2 == 1 {
cmds = b
}
_, err := cmds.HumanEdit(ctx, docflow.Principal{UserID: 1}, docflow.HumanEditCommand{
DocID: docID,
Request: docrevision.HumanRevisionRequest{
DocID: docID, Markdown: fmt.Sprintf("# base\n\nfour %d\n", i),
ExpectedHeadSnapshot: &st2.HeadSnapshotID, ExpectedRevisionNo: &st2.HeadRevisionNo,
IdempotencyKey: fmt.Sprintf("4s-%s-%d", suffix, i), ActorUserID: 1,
LifecycleState: "drafting", ProjectID: projectID, WorkDir: "ws/" + docID,
},
})
if err == nil {
extraMu.Lock()
extraWins++
extraMu.Unlock()
}
}(i)
}
extra.Wait()
if extraWins != 1 {
t.Fatalf("four sessions same etag: wins=%d", extraWins)
}
}