Files
weichen 143ee934b8 feat(collab): free-lane chat, claim-on-view, lease ownership, notify inbox
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.
2026-09-24 11:49:43 +00:00

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