Lock the document row and re-authorize duty before edit, agent commit, approve, or reject share a single transaction. Characterization tests pin ETag races, one legal approve/reject terminal, and same-key digest conflicts. Co-authored-by: Cursor <cursoragent@cursor.com>
172 lines
5.2 KiB
Go
172 lines
5.2 KiB
Go
package docflow
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
|
|
"chat-agent-system/internal/app/uow"
|
|
"chat-agent-system/internal/docgov"
|
|
"chat-agent-system/internal/docrevision"
|
|
)
|
|
|
|
func TestHumanEditCharacterizationSameETagOneWinner(t *testing.T) {
|
|
ctx := context.Background()
|
|
rev := docrevision.NewService(nil)
|
|
st, err := rev.OpenTemplateVersion(ctx, "doc_flow_etag", "# base\n", docrevision.ActorSnapshot{}, 1, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cmds := &Commands{Rev: rev}
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex
|
|
wins, losses := 0, 0
|
|
for i := 0; i < 8; i++ {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
_, err := cmds.HumanEdit(ctx, Principal{UserID: int64(i + 1)}, HumanEditCommand{
|
|
DocID: "doc_flow_etag",
|
|
Request: docrevision.HumanRevisionRequest{
|
|
DocID: "doc_flow_etag", Markdown: fmt.Sprintf("# base\n\nedit %d\n", i),
|
|
ExpectedHeadSnapshot: &st.HeadSnapshotID, ExpectedRevisionNo: &st.HeadRevisionNo,
|
|
IdempotencyKey: fmt.Sprintf("flow-%d", i), ActorUserID: int64(i + 1), LifecycleState: "drafting",
|
|
},
|
|
})
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if err == nil {
|
|
wins++
|
|
} else if err == docrevision.ErrHeadConflict {
|
|
losses++
|
|
} else {
|
|
t.Errorf("unexpected %v", err)
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
if wins != 1 || losses != 7 {
|
|
t.Fatalf("wins=%d losses=%d", wins, losses)
|
|
}
|
|
got, err := rev.GetRevisionState(ctx, "doc_flow_etag")
|
|
if err != nil || got.HeadRevisionNo != 1 {
|
|
t.Fatalf("head=%+v err=%v", got, err)
|
|
}
|
|
}
|
|
|
|
func TestApproveRejectCharacterizationOneTerminal(t *testing.T) {
|
|
ctx := context.Background()
|
|
gov := docgov.NewGovernanceEngine(nil)
|
|
doc, err := gov.CreateOrGetDocument(ctx, "doc_flow_appr", 1, "intake", "t", "V0.5_feasibility", "", 1, "A", 2, "R", 3, "P")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := gov.SubmitForReview(ctx, doc.DocID, 1, docgov.RoleAuthor, "A"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := gov.ReviewApprove(ctx, doc.DocID, 2, docgov.RoleReviewer, "R", "ok"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cmds := &Commands{Gov: gov}
|
|
var wg sync.WaitGroup
|
|
results := make(chan string, 2)
|
|
wg.Add(2)
|
|
go func() {
|
|
defer wg.Done()
|
|
doc, err := cmds.Approve(ctx, Principal{UserID: 3, Role: docgov.RoleApprover, Name: "P"}, DecisionCommand{DocID: doc.DocID})
|
|
if err == nil && doc != nil {
|
|
results <- string(doc.CurrentState)
|
|
} else {
|
|
results <- "err"
|
|
}
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
doc, err := cmds.Reject(ctx, Principal{UserID: 3, Role: docgov.RoleApprover, Name: "P"}, DecisionCommand{
|
|
DocID: doc.DocID, RejectReason: "no", RollbackTo: docgov.StateDrafting,
|
|
})
|
|
if err == nil && doc != nil {
|
|
results <- string(doc.CurrentState)
|
|
} else {
|
|
results <- "err"
|
|
}
|
|
}()
|
|
wg.Wait()
|
|
close(results)
|
|
var states []string
|
|
for s := range results {
|
|
states = append(states, s)
|
|
}
|
|
if len(states) != 2 {
|
|
t.Fatalf("%v", states)
|
|
}
|
|
approved, rejected := 0, 0
|
|
for _, s := range states {
|
|
switch s {
|
|
case string(docgov.StateApproved):
|
|
approved++
|
|
case string(docgov.StateDrafting), string(docgov.StateRejected):
|
|
rejected++
|
|
}
|
|
}
|
|
if approved+rejected != 1 {
|
|
t.Fatalf("expected one legal terminal, got %v", states)
|
|
}
|
|
}
|
|
|
|
func TestIdempotentHumanEditSameDigest(t *testing.T) {
|
|
ctx := context.Background()
|
|
rev := docrevision.NewService(nil)
|
|
st, err := rev.OpenTemplateVersion(ctx, "doc_flow_idemp", "# base\n", docrevision.ActorSnapshot{}, 1, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cmds := &Commands{Rev: rev}
|
|
req := docrevision.HumanRevisionRequest{
|
|
DocID: "doc_flow_idemp", Markdown: "# base\n\nedit\n",
|
|
ExpectedHeadSnapshot: &st.HeadSnapshotID, ExpectedRevisionNo: &st.HeadRevisionNo,
|
|
IdempotencyKey: "same-key", ActorUserID: 1, LifecycleState: "drafting",
|
|
}
|
|
first, err := cmds.HumanEdit(ctx, Principal{UserID: 1}, HumanEditCommand{DocID: req.DocID, Request: req})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := cmds.HumanEdit(ctx, Principal{UserID: 1}, HumanEditCommand{DocID: req.DocID, Request: req})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.State.HeadSnapshotID != second.State.HeadSnapshotID {
|
|
t.Fatal("same key+digest must replay")
|
|
}
|
|
req.Markdown = "# base\n\nother digest\n"
|
|
if _, err := cmds.HumanEdit(ctx, Principal{UserID: 1}, HumanEditCommand{DocID: req.DocID, Request: req}); err != docrevision.ErrIdempotencyConflict {
|
|
t.Fatalf("different digest: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReauthDeniesWrongAuthor(t *testing.T) {
|
|
ctx := context.Background()
|
|
gov := docgov.NewGovernanceEngine(nil)
|
|
if _, err := gov.CreateOrGetDocument(ctx, "doc_flow_authz", 1, "intake", "t", "V0.5_feasibility", "", 1, "A", 2, "R", 3, "P"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rev := docrevision.NewService(nil)
|
|
st, err := rev.OpenTemplateVersion(ctx, "doc_flow_authz", "# base\n", docrevision.ActorSnapshot{}, 1, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cmds := &Commands{Gov: gov, Rev: rev}
|
|
_, err = cmds.HumanEdit(ctx, Principal{UserID: 99}, HumanEditCommand{
|
|
DocID: "doc_flow_authz",
|
|
Request: docrevision.HumanRevisionRequest{
|
|
DocID: "doc_flow_authz", Markdown: "# base\n\nnope\n",
|
|
ExpectedHeadSnapshot: &st.HeadSnapshotID, ExpectedRevisionNo: &st.HeadRevisionNo,
|
|
IdempotencyKey: "authz-deny", ActorUserID: 99, LifecycleState: "drafting",
|
|
},
|
|
})
|
|
if err != uow.ErrPermissionDenied {
|
|
t.Fatalf("want permission denied, got %v", err)
|
|
}
|
|
}
|