test: cover duty lock, heal/revoke skip, and instance duty adapter
Unit evidence for AE5a/AE5b filters; optional PG cases when TEST_DATABASE_URL is set. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"chat-agent-system/internal/projectinstance"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stubAuthorOnlyPolicy struct {
|
||||||
|
allowed bool
|
||||||
|
revision int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubAuthorOnlyPolicy) GetAuthorOnlyApprovalPolicy(_ context.Context) (projectinstance.AuthorOnlyPolicySnapshot, error) {
|
||||||
|
return projectinstance.AuthorOnlyPolicySnapshot{Allowed: s.allowed, Revision: s.revision}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubAuthorOnlyPolicy) LockAuthorOnlyApprovalPolicy(ctx context.Context, _ pgx.Tx) (projectinstance.AuthorOnlyPolicySnapshot, error) {
|
||||||
|
return s.GetAuthorOnlyApprovalPolicy(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
type stubMemberLookup struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
ids map[int64]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubMemberLookup) ActiveMemberIDs(_ context.Context, _ int64) (map[int64]struct{}, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
out := make(map[int64]struct{}, len(s.ids))
|
||||||
|
for id := range s.ids {
|
||||||
|
out[id] = struct{}{}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProjectInstanceDutyAdapter_AuthorOnlyLive(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
store := projectinstance.NewMemoryStore()
|
||||||
|
_ = projectinstance.SeedProjectMeta(store, 501, projectinstance.LifecyclePlanning, "", 1, 0)
|
||||||
|
svc := projectinstance.NewService(store, projectinstance.TestResolver(), nil, nil)
|
||||||
|
svc.SetAuthorOnlyPolicySource(&stubAuthorOnlyPolicy{allowed: true, revision: 1})
|
||||||
|
svc.SetActiveMemberLookup(&stubMemberLookup{ids: map[int64]struct{}{7: {}}})
|
||||||
|
|
||||||
|
plan := projectinstance.ValidTestPlan()
|
||||||
|
plan.Stages[0].DocTasks[0].DutyPolicy = projectinstance.DutyPolicyAuthorOnly
|
||||||
|
plan.Stages[0].DocTasks[0].Duties = projectinstance.DocDuty{AuthorUserID: 7}
|
||||||
|
|
||||||
|
if _, err := svc.SaveWorkflowPlan(ctx, 501, 1, true, projectinstance.SavePlanInput{
|
||||||
|
Plan: plan, ExpectedPlanRevision: 0, ExpectedSelectionVersion: 1,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("save: %v", err)
|
||||||
|
}
|
||||||
|
preview, err := svc.ActivationPreview(ctx, 501, 1, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("preview: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := svc.Activate(ctx, 501, 1, true, projectinstance.ActivateInput{
|
||||||
|
ConfirmationToken: preview.ConfirmationToken,
|
||||||
|
ExpectedProjectVersion: preview.ExpectedProjectVersion,
|
||||||
|
ExpectedPlanRevision: preview.ExpectedPlanRevision,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("activate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter := &projectInstanceDutyAdapter{inst: svc}
|
||||||
|
author, reviewer, approver, policy, governed, err := adapter.ResolveGovernedDuty(ctx, 501, "intake")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve: %v", err)
|
||||||
|
}
|
||||||
|
if !governed || policy != projectinstance.DutyPolicyAuthorOnly {
|
||||||
|
t.Fatalf("governed=%v policy=%q", governed, policy)
|
||||||
|
}
|
||||||
|
if author != 7 || reviewer != 7 || approver != 7 {
|
||||||
|
t.Fatalf("want 7/7/7 got %d/%d/%d", author, reviewer, approver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProjectInstanceDutyAdapter_NoLiveInstance(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
store := projectinstance.NewMemoryStore()
|
||||||
|
_ = projectinstance.SeedProjectMeta(store, 502, projectinstance.LifecyclePlanning, "", 1, 0)
|
||||||
|
svc := projectinstance.NewService(store, projectinstance.TestResolver(), nil, nil)
|
||||||
|
adapter := &projectInstanceDutyAdapter{inst: svc}
|
||||||
|
_, _, _, policy, governed, err := adapter.ResolveGovernedDuty(ctx, 502, "intake")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve: %v", err)
|
||||||
|
}
|
||||||
|
if governed || policy != "" {
|
||||||
|
t.Fatalf("want ungoverned, got governed=%v policy=%q", governed, policy)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,14 +5,28 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"chat-agent-system/internal/docduty"
|
"chat-agent-system/internal/docduty"
|
||||||
|
"chat-agent-system/internal/docgov"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type fakeInstanceDuty struct {
|
||||||
|
author, reviewer, approver int64
|
||||||
|
policy string
|
||||||
|
governed bool
|
||||||
|
err error
|
||||||
|
called bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeInstanceDuty) ResolveGovernedDuty(_ context.Context, _ int64, _ string) (int64, int64, int64, string, bool, error) {
|
||||||
|
f.called = true
|
||||||
|
return f.author, f.reviewer, f.approver, f.policy, f.governed, f.err
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolveTaskDocumentDuties_UsesCreatorNotHardcodedReviewer(t *testing.T) {
|
func TestResolveTaskDocumentDuties_UsesCreatorNotHardcodedReviewer(t *testing.T) {
|
||||||
req := TaskRunRequest{
|
req := TaskRunRequest{
|
||||||
UserID: 170, ProjectID: 6145,
|
UserID: 170, ProjectID: 6145,
|
||||||
AuthorName: "编制人A",
|
AuthorName: "编制人A",
|
||||||
}
|
}
|
||||||
got := resolveTaskDocumentDuties(context.Background(), nil, req)
|
got, _, _ := resolveTaskDocumentDuties(context.Background(), nil, req, nil)
|
||||||
if got.Author.UserID != 170 {
|
if got.Author.UserID != 170 {
|
||||||
t.Fatalf("author want 170 got %+v", got.Author)
|
t.Fatalf("author want 170 got %+v", got.Author)
|
||||||
}
|
}
|
||||||
@@ -28,13 +42,87 @@ func TestResolveTaskDocumentDuties_IgnoresNonMemberRequestReviewer(t *testing.T)
|
|||||||
req := TaskRunRequest{
|
req := TaskRunRequest{
|
||||||
UserID: 170, ReviewerID: 2, ReviewerName: "审核负责人",
|
UserID: 170, ReviewerID: 2, ReviewerName: "审核负责人",
|
||||||
}
|
}
|
||||||
got := resolveTaskDocumentDuties(context.Background(), nil, req)
|
got, _, _ := resolveTaskDocumentDuties(context.Background(), nil, req, nil)
|
||||||
if got.Reviewer.UserID != 0 {
|
if got.Reviewer.UserID != 0 {
|
||||||
t.Fatalf("request reviewer 2 is not a member: %+v", got.Reviewer)
|
t.Fatalf("request reviewer 2 is not a member: %+v", got.Reviewer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveTaskDocumentDuties_GovernedUsesInstance(t *testing.T) {
|
||||||
|
src := &fakeInstanceDuty{
|
||||||
|
author: 9, reviewer: 9, approver: 9,
|
||||||
|
policy: "author_only", governed: true,
|
||||||
|
}
|
||||||
|
req := TaskRunRequest{
|
||||||
|
UserID: 170, ProjectID: 42, TaskKey: "intake",
|
||||||
|
AuthorName: "编制人A", ReviewerID: 2,
|
||||||
|
}
|
||||||
|
got, policy, governed := resolveTaskDocumentDuties(context.Background(), nil, req, src)
|
||||||
|
if !governed || policy != "author_only" {
|
||||||
|
t.Fatalf("governed=%v policy=%q", governed, policy)
|
||||||
|
}
|
||||||
|
if got.Author.UserID != 9 || got.Reviewer.UserID != 9 || got.Approver.UserID != 9 {
|
||||||
|
t.Fatalf("want A/A/A=9, got %+v", got)
|
||||||
|
}
|
||||||
|
if !src.called {
|
||||||
|
t.Fatal("expected ResolveGovernedDuty called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveTaskDocumentDuties_UngovernedFallsBack(t *testing.T) {
|
||||||
|
src := &fakeInstanceDuty{governed: false}
|
||||||
|
req := TaskRunRequest{
|
||||||
|
UserID: 170, ProjectID: 42, TaskKey: "intake",
|
||||||
|
AuthorName: "编制人A",
|
||||||
|
}
|
||||||
|
got, policy, governed := resolveTaskDocumentDuties(context.Background(), nil, req, src)
|
||||||
|
if governed || policy != "" {
|
||||||
|
t.Fatalf("want ungoverned, got governed=%v policy=%q", governed, policy)
|
||||||
|
}
|
||||||
|
if got.Author.UserID != 170 {
|
||||||
|
t.Fatalf("author want 170 got %+v", got.Author)
|
||||||
|
}
|
||||||
|
if got.Reviewer.UserID != 0 {
|
||||||
|
t.Fatalf("no members → reviewer empty, got %+v", got.Reviewer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGovernedSkipsMemoryHealOverwrite(t *testing.T) {
|
||||||
|
doc := &docgov.DocumentRecord{
|
||||||
|
DocID: "doc_x", AuthorID: 9, AuthorName: "A",
|
||||||
|
ReviewerID: 9, ReviewerName: "A",
|
||||||
|
ApproverID: 9, ApproverName: "A",
|
||||||
|
DutyLocked: true, DutyPolicy: "author_only",
|
||||||
|
}
|
||||||
|
assigned := docduty.Binding{
|
||||||
|
Author: docduty.Holder{UserID: 1, Name: "作者"},
|
||||||
|
Reviewer: docduty.Holder{UserID: 2, Name: "审核"},
|
||||||
|
Approver: docduty.Holder{UserID: 3, Name: "批准"},
|
||||||
|
}
|
||||||
|
memberIDs := map[int64]struct{}{1: {}, 2: {}, 3: {}, 9: {}}
|
||||||
|
|
||||||
|
governed := true
|
||||||
|
if !governed {
|
||||||
|
next, _ := docduty.HealBinding(bindingFromDoc(doc), assigned, memberIDs)
|
||||||
|
writeBindingToDoc(doc, next)
|
||||||
|
}
|
||||||
|
if doc.ReviewerID != 9 || doc.ApproverID != 9 {
|
||||||
|
t.Fatalf("governed must keep A/A/A, got R=%d Ap=%d", doc.ReviewerID, doc.ApproverID)
|
||||||
|
}
|
||||||
|
|
||||||
|
doc2 := &docgov.DocumentRecord{
|
||||||
|
DocID: "doc_y", AuthorID: 9, ReviewerID: 9, ApproverID: 9,
|
||||||
|
}
|
||||||
|
next, changed := docduty.HealBinding(bindingFromDoc(doc2), assigned, memberIDs)
|
||||||
|
if !changed {
|
||||||
|
t.Fatal("expected heal change for ungoverned path")
|
||||||
|
}
|
||||||
|
writeBindingToDoc(doc2, next)
|
||||||
|
if doc2.ReviewerID != 2 || doc2.ApproverID != 3 {
|
||||||
|
t.Fatalf("ungoverned heal want 2/3, got R=%d Ap=%d", doc2.ReviewerID, doc2.ApproverID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBindingFromDocRoundTrip(t *testing.T) {
|
func TestBindingFromDocRoundTrip(t *testing.T) {
|
||||||
// compile-time guard that helper stays aligned with Resolve slots
|
|
||||||
_ = docduty.Binding{}
|
_ = docduty.Binding{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package docduty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClearDutySlotSQL_SkipsLockedRows(t *testing.T) {
|
||||||
|
for _, duty := range []string{DutyReviewer, DutyApprover} {
|
||||||
|
sql, ok := clearDutySlotSQL(duty)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("%s: expected sql", duty)
|
||||||
|
}
|
||||||
|
if !strings.Contains(sql, "NOT duty_locked") {
|
||||||
|
t.Fatalf("%s sql missing duty_locked filter:\n%s", duty, sql)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package docduty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHealProjectDocuments_SkipsDutyLocked_PG(t *testing.T) {
|
||||||
|
ctx, _, pool := openDutyTestPool(t)
|
||||||
|
suffix := time.Now().UnixNano()
|
||||||
|
authorID := insertDutyUser(t, ctx, pool, suffix, "lk_author")
|
||||||
|
reviewerID := insertDutyUser(t, ctx, pool, suffix, "lk_reviewer")
|
||||||
|
approverID := insertDutyUser(t, ctx, pool, suffix, "lk_approver")
|
||||||
|
|
||||||
|
var projectID int64
|
||||||
|
if err := pool.QueryRow(ctx, `
|
||||||
|
INSERT INTO projects (code, name, status, creator_user_id)
|
||||||
|
VALUES ($1, $2, 'active', $3)
|
||||||
|
RETURNING id
|
||||||
|
`, fmt.Sprintf("duty-lock-%d", suffix), "locked heal", authorID).Scan(&projectID); err != nil {
|
||||||
|
t.Fatalf("insert project: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_, _ = pool.Exec(context.Background(), `DELETE FROM document_records WHERE project_id = $1`, projectID)
|
||||||
|
_, _ = pool.Exec(context.Background(), `DELETE FROM project_members WHERE project_id = $1`, projectID)
|
||||||
|
_, _ = pool.Exec(context.Background(), `DELETE FROM projects WHERE id = $1`, projectID)
|
||||||
|
_, _ = pool.Exec(context.Background(), `DELETE FROM users WHERE id IN ($1,$2,$3)`, authorID, reviewerID, approverID)
|
||||||
|
})
|
||||||
|
|
||||||
|
if _, err := pool.Exec(ctx, `
|
||||||
|
INSERT INTO project_members (project_id, user_id, project_role, document_duty, status, joined_at, updated_at)
|
||||||
|
VALUES ($1, $2, 'owner', 'author', 'active', now(), now()),
|
||||||
|
($1, $3, 'member', 'reviewer', 'active', now(), now()),
|
||||||
|
($1, $4, 'member', 'approver', 'active', now(), now())
|
||||||
|
`, projectID, authorID, reviewerID, approverID); err != nil {
|
||||||
|
t.Fatalf("insert members: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lockedDoc := fmt.Sprintf("doc_proj%d_intake", projectID)
|
||||||
|
openDoc := fmt.Sprintf("doc_proj%d_research", projectID)
|
||||||
|
// 锁定文档:A/A/A,与项目默认分离职责不同
|
||||||
|
insertDutyDoc(t, ctx, pool, projectID, lockedDoc, "intake", authorID, authorID, authorID)
|
||||||
|
if _, err := pool.Exec(ctx, `
|
||||||
|
UPDATE document_records
|
||||||
|
SET duty_locked = true, duty_policy = 'author_only'
|
||||||
|
WHERE doc_id = $1
|
||||||
|
`, lockedDoc); err != nil {
|
||||||
|
t.Fatalf("lock doc: %v", err)
|
||||||
|
}
|
||||||
|
// 未锁定文档:错误槽位,应被 heal 纠正
|
||||||
|
insertDutyDoc(t, ctx, pool, projectID, openDoc, "research", authorID, 0, 0)
|
||||||
|
|
||||||
|
_, n, err := HealProjectDocuments(ctx, pool, projectID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("heal: %v", err)
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
t.Fatal("expected unlocked doc to be healed")
|
||||||
|
}
|
||||||
|
|
||||||
|
assertDocAuthorID(t, ctx, pool, lockedDoc, authorID)
|
||||||
|
assertDocReviewer(t, ctx, pool, lockedDoc, authorID)
|
||||||
|
assertDocApprover(t, ctx, pool, lockedDoc, authorID)
|
||||||
|
assertDocReviewer(t, ctx, pool, openDoc, reviewerID)
|
||||||
|
assertDocApprover(t, ctx, pool, openDoc, approverID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplySlotRevocations_SkipsDutyLocked_PG(t *testing.T) {
|
||||||
|
ctx, _, pool := openDutyTestPool(t)
|
||||||
|
suffix := time.Now().UnixNano()
|
||||||
|
authorID := insertDutyUser(t, ctx, pool, suffix, "rv_author")
|
||||||
|
reviewerID := insertDutyUser(t, ctx, pool, suffix, "rv_reviewer")
|
||||||
|
|
||||||
|
var projectID int64
|
||||||
|
if err := pool.QueryRow(ctx, `
|
||||||
|
INSERT INTO projects (code, name, status, creator_user_id)
|
||||||
|
VALUES ($1, $2, 'active', $3)
|
||||||
|
RETURNING id
|
||||||
|
`, fmt.Sprintf("duty-revlock-%d", suffix), "locked revoke", authorID).Scan(&projectID); err != nil {
|
||||||
|
t.Fatalf("insert project: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_, _ = pool.Exec(context.Background(), `DELETE FROM document_records WHERE project_id = $1`, projectID)
|
||||||
|
_, _ = pool.Exec(context.Background(), `DELETE FROM project_members WHERE project_id = $1`, projectID)
|
||||||
|
_, _ = pool.Exec(context.Background(), `DELETE FROM projects WHERE id = $1`, projectID)
|
||||||
|
_, _ = pool.Exec(context.Background(), `DELETE FROM users WHERE id IN ($1,$2)`, authorID, reviewerID)
|
||||||
|
})
|
||||||
|
if _, err := pool.Exec(ctx, `
|
||||||
|
INSERT INTO project_members (project_id, user_id, project_role, document_duty, status, joined_at, updated_at)
|
||||||
|
VALUES ($1, $2, 'owner', 'author', 'active', now(), now()),
|
||||||
|
($1, $3, 'member', 'reviewer', 'active', now(), now())
|
||||||
|
`, projectID, authorID, reviewerID); err != nil {
|
||||||
|
t.Fatalf("insert members: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lockedDoc := fmt.Sprintf("doc_proj%d_intake", projectID)
|
||||||
|
openDoc := fmt.Sprintf("doc_proj%d_research", projectID)
|
||||||
|
insertDutyDoc(t, ctx, pool, projectID, lockedDoc, "intake", authorID, reviewerID, 0)
|
||||||
|
insertDutyDoc(t, ctx, pool, projectID, openDoc, "research", authorID, reviewerID, 0)
|
||||||
|
if _, err := pool.Exec(ctx, `
|
||||||
|
UPDATE document_records SET duty_locked = true, duty_policy = 'author_only' WHERE doc_id = $1
|
||||||
|
`, lockedDoc); err != nil {
|
||||||
|
t.Fatalf("lock: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := pool.Begin(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := ApplySlotRevocations(ctx, tx, projectID, PlanSlotRevocations(MemberDutyChange{
|
||||||
|
UserID: reviewerID, PreviousDuty: DutyReviewer, NextDuty: DutyNone,
|
||||||
|
})); err != nil {
|
||||||
|
_ = tx.Rollback(ctx)
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := tx.Commit(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertDocReviewer(t, ctx, pool, lockedDoc, reviewerID)
|
||||||
|
assertDocReviewer(t, ctx, pool, openDoc, 0)
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package docgov
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLockDocumentDuty_Memory(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
engine := NewGovernanceEngine(nil)
|
||||||
|
doc, err := engine.CreateOrGetDocument(
|
||||||
|
ctx, "doc_lock_mem", 9, "intake", "锁定测试",
|
||||||
|
"V0.5", "ws/lock",
|
||||||
|
1, "A", 1, "A", 1, "A",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create: %v", err)
|
||||||
|
}
|
||||||
|
if doc.DutyLocked || doc.DutyPolicy != "" {
|
||||||
|
t.Fatalf("defaults: locked=%v policy=%q", doc.DutyLocked, doc.DutyPolicy)
|
||||||
|
}
|
||||||
|
if err := engine.LockDocumentDuty(ctx, doc.DocID, "author_only"); err != nil {
|
||||||
|
t.Fatalf("lock: %v", err)
|
||||||
|
}
|
||||||
|
got, err := engine.GetDocument(ctx, doc.DocID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get: %v", err)
|
||||||
|
}
|
||||||
|
if !got.DutyLocked || got.DutyPolicy != "author_only" {
|
||||||
|
t.Fatalf("after lock: locked=%v policy=%q", got.DutyLocked, got.DutyPolicy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListWorkflowDocuments_IncludesDutyPolicy_Memory(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
engine := NewGovernanceEngine(nil)
|
||||||
|
doc, err := engine.CreateOrGetDocument(
|
||||||
|
ctx, "doc_proj9_intake", 9, "intake", "投影测试",
|
||||||
|
"V0.5", "ws/proj",
|
||||||
|
7, "编制", 7, "编制", 7, "编制",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create: %v", err)
|
||||||
|
}
|
||||||
|
if err := engine.LockDocumentDuty(ctx, doc.DocID, "author_only"); err != nil {
|
||||||
|
t.Fatalf("lock: %v", err)
|
||||||
|
}
|
||||||
|
list, err := engine.ListWorkflowDocumentsByProject(ctx, 9)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list: %v", err)
|
||||||
|
}
|
||||||
|
if len(list) != 1 || list[0].DutyPolicy != "author_only" {
|
||||||
|
t.Fatalf("want duty_policy=author_only, got %+v", list)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user