Files
Agent-System/internal/bizdev/squad_commit_actor_test.go
T
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

179 lines
6.3 KiB
Go

package bizdev
import (
"context"
"errors"
"strings"
"testing"
"time"
"chat-agent-system/internal/app/docflow"
"chat-agent-system/internal/app/uow"
"chat-agent-system/internal/config"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/docrevision"
)
type recordingMaterializer struct {
inner AgentOutputMaterializer
got docrevision.AgentMaterializeRequest
err error
}
func (r *recordingMaterializer) MaterializeAgentOutput(ctx context.Context, req docrevision.AgentMaterializeRequest) (*docrevision.AgentMaterializeResult, error) {
r.got = req
if r.err != nil {
return nil, r.err
}
if r.inner == nil {
return nil, errors.New("recording materializer has no inner")
}
return r.inner.MaterializeAgentOutput(ctx, req)
}
func TestSquadCommitActor_UsesAuthorWhenTriggerDiffers(t *testing.T) {
author, behalf := squadCommitActor(74, &docgov.DocumentRecord{AuthorID: 4})
if author != 4 {
t.Fatalf("commit actor want 4 got %d", author)
}
if behalf == nil || *behalf != 74 {
t.Fatalf("on behalf want 74 got %v", behalf)
}
same, behalfSame := squadCommitActor(4, &docgov.DocumentRecord{AuthorID: 4})
if same != 4 || behalfSame != nil {
t.Fatalf("author trigger must not proxy: actor=%d behalf=%v", same, behalfSame)
}
missing, behalfMissing := squadCommitActor(74, &docgov.DocumentRecord{})
if missing != 74 || behalfMissing != nil {
t.Fatalf("unset author must keep trigger: actor=%d behalf=%v", missing, behalfMissing)
}
}
func TestExecuteTask_NonAuthorTriggerMaterializesAsAuthor(t *testing.T) {
svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})
ctx := context.Background()
const (
projectID = int64(6022)
authorID = int64(4)
triggerID = int64(74)
docID = "doc_proj6022_bd_intake"
)
if _, err := svc.docGov.CreateOrGetDocument(ctx, docID, projectID, "bd_intake", "用户初始需求收集表", "V0.1_triage", "", authorID, "admin", 0, "", authorID, "admin"); err != nil {
t.Fatal(err)
}
rec := &recordingMaterializer{inner: revMaterializer{rev: svc.docRev}}
svc.SetAgentMaterializer(rec)
res, err := svc.ExecuteTask(ctx, TaskRunRequest{
TaskKey: "intake", ProjectID: projectID, ProjectName: "测试项目",
CustomerName: "C", DeviceType: "医疗器械", Backend: "stub",
Timeout: 10 * time.Second, DocID: docID, UserID: triggerID,
})
if err != nil {
t.Fatalf("ExecuteTask: %v reason=%v", err, res)
}
if res == nil || !res.Success {
t.Fatalf("non-author trigger must succeed, got %#v", res)
}
if rec.got.AutoCommitUserID != authorID {
t.Fatalf("AutoCommitUserID want %d got %d", authorID, rec.got.AutoCommitUserID)
}
if rec.got.OnBehalfOf == nil || *rec.got.OnBehalfOf != triggerID {
t.Fatalf("OnBehalfOf want %d got %v", triggerID, rec.got.OnBehalfOf)
}
}
func TestExecuteTask_AuthorTriggerDoesNotSetOnBehalfOf(t *testing.T) {
svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})
ctx := context.Background()
const (
projectID = int64(6023)
authorID = int64(4)
docID = "doc_proj6023_bd_intake"
)
if _, err := svc.docGov.CreateOrGetDocument(ctx, docID, projectID, "bd_intake", "用户初始需求收集表", "V0.1_triage", "", authorID, "admin", 0, "", 0, ""); err != nil {
t.Fatal(err)
}
rec := &recordingMaterializer{inner: revMaterializer{rev: svc.docRev}}
svc.SetAgentMaterializer(rec)
res, err := svc.ExecuteTask(ctx, TaskRunRequest{
TaskKey: "intake", ProjectID: projectID, ProjectName: "P",
CustomerName: "C", DeviceType: "医疗器械", Backend: "stub",
Timeout: 10 * time.Second, DocID: docID, UserID: authorID,
})
if err != nil || res == nil || !res.Success {
t.Fatalf("author trigger failed: err=%v res=%#v", err, res)
}
if rec.got.AutoCommitUserID != authorID || rec.got.OnBehalfOf != nil {
t.Fatalf("author path changed: user=%d behalf=%v", rec.got.AutoCommitUserID, rec.got.OnBehalfOf)
}
}
func TestExecuteTask_NonAuthorPassesDutyAuthorizer(t *testing.T) {
svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})
ctx := context.Background()
const (
projectID = int64(6024)
authorID = int64(4)
triggerID = int64(74)
docID = "doc_proj6024_bd_intake"
)
if _, err := svc.docGov.CreateOrGetDocument(ctx, docID, projectID, "bd_intake", "用户初始需求收集表", "V0.1_triage", "", authorID, "admin", 0, "", 0, ""); err != nil {
t.Fatal(err)
}
svc.SetAgentMaterializer(&docflow.Commands{Gov: svc.docGov, Rev: svc.docRev, Auth: docflow.DutyAuthorizer()})
res, err := svc.ExecuteTask(ctx, TaskRunRequest{
TaskKey: "intake", ProjectID: projectID, ProjectName: "P",
CustomerName: "C", DeviceType: "医疗器械", Backend: "stub",
Timeout: 10 * time.Second, DocID: docID, UserID: triggerID,
})
if err != nil || res == nil || !res.Success {
t.Fatalf("duty authorizer must accept author-proxy commit: err=%v reason=%v", err, errorReason(res))
}
if strings.Contains(errorReason(res), "permission denied") {
t.Fatalf("permission denied leaked: %s", res.ErrorReason)
}
}
func TestExecuteTask_PermissionDeniedNamesNonAuthor(t *testing.T) {
svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})
ctx := context.Background()
const docID = "doc_perm_denied"
if _, err := svc.docGov.CreateOrGetDocument(ctx, docID, 77, "research", "调研", "V0.5", "", 4, "admin", 0, "", 0, ""); err != nil {
t.Fatal(err)
}
svc.SetAgentMaterializer(&recordingMaterializer{err: uow.ErrPermissionDenied})
res, err := svc.ExecuteTask(ctx, TaskRunRequest{
TaskKey: "research", ProjectID: 77, ProjectName: "P",
CustomerName: "C", DeviceType: "医疗器械", Backend: "stub",
Timeout: 10 * time.Second, DocID: docID, UserID: 74,
})
if err == nil || res == nil || res.Success {
t.Fatalf("want visible permission failure, err=%v res=%#v", err, res)
}
if !strings.Contains(res.ErrorReason, "触发用户非文档编制人") {
t.Fatalf("reason must name non-author denial, got %q", res.ErrorReason)
}
if !errors.Is(err, uow.ErrPermissionDenied) {
t.Fatalf("sentinel must stay errors.Is compatible, got %v", err)
}
}
type revMaterializer struct {
rev *docrevision.Service
}
func (r revMaterializer) MaterializeAgentOutput(ctx context.Context, req docrevision.AgentMaterializeRequest) (*docrevision.AgentMaterializeResult, error) {
return r.rev.MaterializeAgentOutput(ctx, req)
}
func errorReason(res *TaskRunResult) string {
if res == nil {
return ""
}
return res.ErrorReason
}