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 }