Open member free-lane for chat initiation; auto-claim on author view; harden lease persist/ownership with readable DOC_BUSY; add notification inbox and supplement-impact HTTP decision APIs wired to composition root.
189 lines
6.1 KiB
Go
189 lines
6.1 KiB
Go
package docgov_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"chat-agent-system/internal/docgov"
|
|
)
|
|
|
|
func TestEditLeaseMutexAndCommentGate(t *testing.T) {
|
|
engine := docgov.NewGovernanceEngine(nil)
|
|
ctx := context.Background()
|
|
docID := "doc_lease_test_001"
|
|
|
|
_, err := engine.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底纪要", "V0.1", "",
|
|
1, "作者", 2, "审核", 3, "批准")
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
|
|
doc, err := engine.AcquireLease(ctx, docgov.LeaseAcquireRequest{
|
|
DocID: docID,
|
|
HolderType: docgov.LeaseHolderSquad,
|
|
HolderID: "run_1",
|
|
TTL: 5 * time.Minute,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("acquire: %v", err)
|
|
}
|
|
if !doc.HasActiveLease(time.Now()) {
|
|
t.Fatal("expected active lease")
|
|
}
|
|
|
|
_, err = engine.AcquireLease(ctx, docgov.LeaseAcquireRequest{
|
|
DocID: docID,
|
|
HolderType: docgov.LeaseHolderHuman,
|
|
HolderID: "42",
|
|
TTL: 5 * time.Minute,
|
|
})
|
|
if !errors.Is(err, docgov.ErrDocumentBusy) {
|
|
t.Fatalf("expected ErrDocumentBusy, got %v", err)
|
|
}
|
|
|
|
_, err = engine.AddComment(ctx, docID, 2, "审核人", docgov.RoleReviewer, "## 结论", docgov.CommentGeneral, "请补充")
|
|
if !errors.Is(err, docgov.ErrDocumentBusy) {
|
|
t.Fatalf("comment should be blocked while leased, got %v", err)
|
|
}
|
|
|
|
_, err = engine.CheckModificationPermission(ctx, docID, docgov.RoleSystem)
|
|
if !errors.Is(err, docgov.ErrDocumentBusy) {
|
|
t.Fatalf("modification check should be busy, got %v", err)
|
|
}
|
|
|
|
if _, err := engine.ReleaseLease(ctx, docID, string(docgov.LeaseHolderSquad), "run_1"); err != nil {
|
|
t.Fatalf("release: %v", err)
|
|
}
|
|
|
|
_, err = engine.AddComment(ctx, docID, 2, "审核人", docgov.RoleReviewer, "## 结论", docgov.CommentGeneral, "请补充")
|
|
if err != nil {
|
|
t.Fatalf("comment after release should succeed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSameHolderRenewLease(t *testing.T) {
|
|
engine := docgov.NewGovernanceEngine(nil)
|
|
ctx := context.Background()
|
|
docID := "doc_lease_renew_001"
|
|
_, _ = engine.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", "", 1, "a", 2, "b", 3, "c")
|
|
|
|
_, err := engine.AcquireLease(ctx, docgov.LeaseAcquireRequest{
|
|
DocID: docID, HolderType: docgov.LeaseHolderHuman, HolderID: "1", TTL: time.Minute,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
doc, err := engine.AcquireLease(ctx, docgov.LeaseAcquireRequest{
|
|
DocID: docID, HolderType: docgov.LeaseHolderHuman, HolderID: "1", TTL: 10 * time.Minute,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("renew should succeed: %v", err)
|
|
}
|
|
if doc.LeaseExpiresAt == nil || time.Until(*doc.LeaseExpiresAt) < 5*time.Minute {
|
|
t.Fatalf("expected renewed TTL, expires=%v", doc.LeaseExpiresAt)
|
|
}
|
|
}
|
|
|
|
func TestStaleFencingRejectedAfterRenew(t *testing.T) {
|
|
engine := docgov.NewGovernanceEngine(nil)
|
|
ctx := context.Background()
|
|
docID := "doc_fence_001"
|
|
_, _ = engine.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", "", 1, "a", 2, "b", 3, "c")
|
|
first, err := engine.AcquireLease(ctx, docgov.LeaseAcquireRequest{
|
|
DocID: docID, HolderType: docgov.LeaseHolderHuman, HolderID: "1", TTL: time.Minute,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
old := first.LeaseFencingToken
|
|
if old == "" {
|
|
t.Fatal("expected fencing token")
|
|
}
|
|
second, err := engine.AcquireLease(ctx, docgov.LeaseAcquireRequest{
|
|
DocID: docID, HolderType: docgov.LeaseHolderHuman, HolderID: "1", TTL: time.Minute,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if second.LeaseFencingToken == old {
|
|
t.Fatal("renew must rotate fencing token")
|
|
}
|
|
if err := engine.AssertLeaseFencing(ctx, docID, old); err != docgov.ErrStaleFencing {
|
|
t.Fatalf("old writer: %v", err)
|
|
}
|
|
if err := engine.AssertLeaseFencing(ctx, docID, second.LeaseFencingToken); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestAssertHumanCanEdit_ChecksDutyBeforeLease(t *testing.T) {
|
|
engine := docgov.NewGovernanceEngine(nil)
|
|
ctx := context.Background()
|
|
docID := "doc_assert_order_001"
|
|
_, err := engine.CreateOrGetDocument(ctx, docID, 1, "intake", "摸底", "V0.1", "",
|
|
1, "作者", 2, "审核", 3, "批准")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Non-author without member flag must fail without becoming lease holder.
|
|
if err := engine.AssertHumanCanEdit(ctx, docID, 99, docgov.RoleAuthor, false); err == nil {
|
|
t.Fatal("non-author AssertHumanCanEdit must fail")
|
|
}
|
|
doc, err := engine.GetDocument(ctx, docID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if doc.HasActiveLease(time.Now()) && doc.LeaseHolderID == "99" {
|
|
t.Fatal("unauthorized actor must not hold human lease")
|
|
}
|
|
|
|
// Authorized member (non-author) may edit under deny_only_submit.
|
|
if err := engine.AssertHumanCanEdit(ctx, docID, 99, docgov.RoleAuthor, true); err != nil {
|
|
t.Fatalf("member AssertHumanCanEdit: %v", err)
|
|
}
|
|
_, _ = engine.ReleaseLease(ctx, docID, string(docgov.LeaseHolderHuman), "99")
|
|
|
|
if err := engine.AssertHumanCanEdit(ctx, docID, 1, docgov.RoleAuthor, false); err != nil {
|
|
t.Fatalf("author AssertHumanCanEdit: %v", err)
|
|
}
|
|
doc, err = engine.GetDocument(ctx, docID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !doc.HasActiveLease(time.Now()) || doc.LeaseHolderType != docgov.LeaseHolderHuman || doc.LeaseHolderID != "1" {
|
|
t.Fatalf("author should hold human lease after assert, got type=%s id=%s", doc.LeaseHolderType, doc.LeaseHolderID)
|
|
}
|
|
}
|
|
|
|
func TestAcquireLeaseClampsHumanTTL(t *testing.T) {
|
|
engine := docgov.NewGovernanceEngine(nil)
|
|
ctx := context.Background()
|
|
docID := "doc_ttl_clamp"
|
|
if _, err := engine.CreateOrGetDocument(ctx, docID, 1, "intake", "ttl", "V0.1", "",
|
|
1, "作者", 2, "审核", 3, "批准"); err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
before := time.Now()
|
|
doc, err := engine.AcquireLease(ctx, docgov.LeaseAcquireRequest{
|
|
DocID: docID,
|
|
HolderType: docgov.LeaseHolderHuman,
|
|
HolderID: "1",
|
|
TTL: 1000 * 24 * time.Hour,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if doc.LeaseExpiresAt == nil || doc.LeaseExpiresAt.After(before.Add(docgov.DefaultHumanLeaseTTL+time.Second)) {
|
|
t.Fatalf("ttl not clamped: expires=%v", doc.LeaseExpiresAt)
|
|
}
|
|
if err := engine.CheckHumanCanEdit(ctx, docID, 99, docgov.RoleAuthor, false); err == nil {
|
|
t.Fatal("non-author must not pass edit check")
|
|
}
|
|
if err := engine.CheckHumanCanEdit(ctx, docID, 99, docgov.RoleAuthor, true); err != nil {
|
|
t.Fatalf("member must pass edit check: %v", err)
|
|
}
|
|
}
|