Files
weichen c713834954 fix(bizdev): commit squad ledger as the document author
Non-author triggers were denied by dutyAuthorizer because AutoCommitUserID was the triggering user. Record that user on OnBehalfOf and surface the duty mismatch in the task error.
2026-09-28 01:24:25 +00:00

186 lines
5.9 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 TestDutyAuthorizerAllowsDocumentAuthorCommit(t *testing.T) {
rec := &docgov.DocumentRecord{DocID: "doc_flow_author_commit", AuthorID: 4, ApproverID: 9}
auth := DutyAuthorizer()
if err := auth.Authorize(context.Background(), Principal{UserID: 4}, rec, ActionCommit); err != nil {
t.Fatalf("document author must pass agent commit: %v", err)
}
if err := auth.Authorize(context.Background(), Principal{UserID: 74}, rec, ActionCommit); err != uow.ErrPermissionDenied {
t.Fatalf("non-author commit must be denied, got %v", err)
}
if err := auth.Authorize(context.Background(), Principal{UserID: 4}, rec, ActionEdit); err != nil {
t.Fatalf("document author must still pass edit: %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)
}
}