聊天入口只把项目 ID 放在授权字段,协调器仍读空的 UserCtx.Project.ID,受控任务因此以 project_id=0 跳过账本投影,回复却称正文已保存。
296 lines
8.8 KiB
Go
296 lines
8.8 KiB
Go
package bizdev
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"chat-agent-system/internal/config"
|
||
"chat-agent-system/internal/docgov"
|
||
)
|
||
|
||
func TestNormalizeDocID_LegacyPatterns(t *testing.T) {
|
||
tests := []struct {
|
||
in string
|
||
project int64
|
||
taskKey string
|
||
expected string
|
||
}{
|
||
{"doc-p56-research", 56, "research", "doc_proj56_research"},
|
||
{"doc-proj56-research", 56, "research", "doc_proj56_research"},
|
||
{"doc_proj56_research", 56, "research", "doc_proj56_research"},
|
||
{"", 56, "research", "doc_proj56_research"},
|
||
{"", 0, "research", ""},
|
||
}
|
||
for _, tc := range tests {
|
||
got := normalizeDocID(tc.in, tc.project, tc.taskKey)
|
||
if got != tc.expected {
|
||
t.Fatalf("normalizeDocID(%q) = %q, want %q", tc.in, got, tc.expected)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestResolveAndValidateDocID_LegacyFallbackWhenNotRegistered(t *testing.T) {
|
||
svc := MustNewService(t, &config.Config{DataDir: t.TempDir()})
|
||
ctx := context.Background()
|
||
|
||
got, err := svc.ResolveAndValidateDocID(ctx, TaskRunRequest{
|
||
DocID: "doc-p56-research",
|
||
ProjectID: 56,
|
||
TaskKey: "research",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
if got != "doc_proj56_research" {
|
||
t.Fatalf("got %q, want doc_proj56_research", got)
|
||
}
|
||
}
|
||
|
||
func TestResolveAndValidateDocID_ExistingDocMismatchRejected(t *testing.T) {
|
||
svc := MustNewService(t, &config.Config{DataDir: t.TempDir()})
|
||
ctx := context.Background()
|
||
|
||
docID := "doc_proj56_research"
|
||
_, err := svc.docGov.CreateOrGetDocument(
|
||
ctx, docID, 56, "research", "调研报告", "V0.5", "",
|
||
1, "张工", 2, "李主管", 3, "王总监",
|
||
)
|
||
if err != nil {
|
||
t.Fatalf("CreateOrGetDocument: %v", err)
|
||
}
|
||
|
||
_, err = svc.ResolveAndValidateDocID(ctx, TaskRunRequest{
|
||
DocID: docID,
|
||
ProjectID: 99,
|
||
TaskKey: "research",
|
||
})
|
||
if err == nil || !strings.Contains(err.Error(), "does not belong to project") {
|
||
t.Fatalf("expected project mismatch error, got %v", err)
|
||
}
|
||
|
||
_, err = svc.ResolveAndValidateDocID(ctx, TaskRunRequest{
|
||
DocID: docID,
|
||
ProjectID: 56,
|
||
TaskKey: "intake",
|
||
})
|
||
if err == nil || !strings.Contains(err.Error(), "task_key mismatch") {
|
||
t.Fatalf("expected task_key mismatch error, got %v", err)
|
||
}
|
||
}
|
||
|
||
// 客户端不得用 doc_id 改写 task_key:UI-05 误路由若仍带 intake 文档 + feasibility_reg,必须拒绝。
|
||
func TestResolveAndValidateDocID_ClientDocIDCannotOverrideTaskKey(t *testing.T) {
|
||
svc := MustNewService(t, &config.Config{DataDir: t.TempDir()})
|
||
ctx := context.Background()
|
||
|
||
docID := "doc_proj6109_intake"
|
||
_, err := svc.docGov.CreateOrGetDocument(
|
||
ctx, docID, 6109, "intake", "用户初始需求收集表", "V0.1", "",
|
||
1, "张工", 2, "李主管", 3, "王总监",
|
||
)
|
||
if err != nil {
|
||
t.Fatalf("CreateOrGetDocument: %v", err)
|
||
}
|
||
|
||
_, err = svc.ResolveAndValidateDocID(ctx, TaskRunRequest{
|
||
DocID: docID,
|
||
ProjectID: 6109,
|
||
TaskKey: "feasibility_reg",
|
||
})
|
||
if err == nil || !strings.Contains(err.Error(), "task_key mismatch") {
|
||
t.Fatalf("expected task_key mismatch, got %v", err)
|
||
}
|
||
|
||
got, err := svc.ResolveAndValidateDocID(ctx, TaskRunRequest{
|
||
DocID: docID,
|
||
ProjectID: 6109,
|
||
TaskKey: "intake",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("matching task_key must be accepted: %v", err)
|
||
}
|
||
if got != docID {
|
||
t.Fatalf("got %q, want %q", got, docID)
|
||
}
|
||
}
|
||
|
||
func TestResolveAndValidateDocID_ExistingDocAccepted(t *testing.T) {
|
||
svc := MustNewService(t, &config.Config{DataDir: t.TempDir()})
|
||
ctx := context.Background()
|
||
|
||
docID := "doc_proj56_research"
|
||
_, err := svc.docGov.CreateOrGetDocument(
|
||
ctx, docID, 56, "research", "调研报告", "V0.5", "",
|
||
1, "张工", 2, "李主管", 3, "王总监",
|
||
)
|
||
if err != nil {
|
||
t.Fatalf("CreateOrGetDocument: %v", err)
|
||
}
|
||
|
||
got, err := svc.ResolveAndValidateDocID(ctx, TaskRunRequest{
|
||
DocID: "doc-p56-research",
|
||
ProjectID: 56,
|
||
TaskKey: "research",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
if got != docID {
|
||
t.Fatalf("got %q, want %q", got, docID)
|
||
}
|
||
}
|
||
|
||
func TestCreateOrGetDocument_ConcurrentSameDocID(t *testing.T) {
|
||
svc := MustNewService(t, &config.Config{DataDir: t.TempDir()})
|
||
ctx := context.Background()
|
||
docID := "doc_proj77_research"
|
||
|
||
type outcome struct {
|
||
id string
|
||
err error
|
||
}
|
||
ch := make(chan outcome, 2)
|
||
create := func() {
|
||
doc, err := svc.docGov.CreateOrGetDocument(
|
||
ctx, docID, 77, "research", "调研报告", "V0.5", "",
|
||
1, "张工", 2, "李主管", 3, "王总监",
|
||
)
|
||
id := ""
|
||
if doc != nil {
|
||
id = doc.DocID
|
||
}
|
||
ch <- outcome{id: id, err: err}
|
||
}
|
||
go create()
|
||
go create()
|
||
first := <-ch
|
||
second := <-ch
|
||
if first.err != nil {
|
||
t.Fatalf("first create failed: %v", first.err)
|
||
}
|
||
if second.err != nil {
|
||
t.Fatalf("second create failed: %v", second.err)
|
||
}
|
||
if first.id != docID || second.id != docID {
|
||
t.Fatalf("doc ids mismatch: first=%q second=%q want %q", first.id, second.id, docID)
|
||
}
|
||
doc, err := svc.docGov.GetDocument(ctx, docID)
|
||
if err != nil || doc == nil {
|
||
t.Fatalf("expected single persisted doc %s, got err=%v doc=%v", docID, err, doc)
|
||
}
|
||
}
|
||
|
||
func TestExecuteTask_LegacyDocIDDoesNotFailValidation(t *testing.T) {
|
||
svc := MustNewService(t, &config.Config{DataDir: t.TempDir()})
|
||
ctx := context.Background()
|
||
|
||
res, err := svc.ExecuteTask(ctx, TaskRunRequest{
|
||
TaskKey: "research",
|
||
ProjectID: 56,
|
||
ProjectName: "智能体演示项目",
|
||
CustomerName: "北京水木东方",
|
||
DeviceType: "医疗器械",
|
||
Backend: "stub",
|
||
Timeout: 10 * time.Second,
|
||
DocID: "doc-p56-research",
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("ExecuteTask with legacy doc_id failed: %v", err)
|
||
}
|
||
if !res.Success {
|
||
if strings.Contains(res.ErrorReason, "doc_id not found") {
|
||
t.Fatalf("legacy doc_id still rejected: %s", res.ErrorReason)
|
||
}
|
||
t.Fatalf("task failed for other reason: %s", res.ErrorReason)
|
||
}
|
||
if res.DocRecord == nil || res.DocRecord.DocID != "doc_proj56_research" {
|
||
t.Fatalf("expected doc_proj56_research record, got %#v", res.DocRecord)
|
||
}
|
||
}
|
||
|
||
func TestExecuteTask_CreateDocumentFailureIsNotSuccess(t *testing.T) {
|
||
svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})
|
||
svc.createDocumentHook = func(context.Context, string, int64, string, string, string, string, int64, string, int64, string, int64, string) (*docgov.DocumentRecord, error) {
|
||
return nil, errors.New("ledger unavailable")
|
||
}
|
||
res, err := svc.ExecuteTask(context.Background(), TaskRunRequest{
|
||
TaskKey: "research",
|
||
ProjectID: 56,
|
||
ProjectName: "智能体演示项目",
|
||
CustomerName: "北京水木东方",
|
||
DeviceType: "医疗器械",
|
||
Backend: "stub",
|
||
Timeout: 10 * time.Second,
|
||
})
|
||
if err == nil || !strings.Contains(err.Error(), "ledger unavailable") {
|
||
t.Fatalf("want ledger error, got %v", err)
|
||
}
|
||
if res == nil || res.Success {
|
||
t.Fatalf("no ledger must not report success: %+v", res)
|
||
}
|
||
if !strings.Contains(res.ErrorReason, "create document ledger failed") {
|
||
t.Fatalf("ErrorReason=%q", res.ErrorReason)
|
||
}
|
||
if res.DocRecord != nil {
|
||
t.Fatalf("failed create must not return a ledger, got %#v", res.DocRecord)
|
||
}
|
||
}
|
||
|
||
func TestExecuteTask_ControlledKeyWithoutProjectRejected(t *testing.T) {
|
||
svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})
|
||
ctx := context.Background()
|
||
|
||
res, err := svc.ExecuteTask(ctx, TaskRunRequest{
|
||
TaskKey: "intake",
|
||
ProjectID: 0,
|
||
DocID: "",
|
||
ProjectName: "掌上便携超声",
|
||
CustomerName: "水木清研",
|
||
DeviceType: "二类掌上超声",
|
||
Backend: "stub",
|
||
Timeout: 10 * time.Second,
|
||
})
|
||
if !errors.Is(err, ErrUnboundControlledProject) {
|
||
t.Fatalf("controlled task without project must fail closed, err=%v", err)
|
||
}
|
||
if res == nil || res.Success || res.DocRecord != nil {
|
||
t.Fatalf("unbound controlled task must not create a ledger or report success: %#v", res)
|
||
}
|
||
if !strings.Contains(res.ErrorReason, "会话未绑定项目") {
|
||
t.Fatalf("ErrorReason=%q", res.ErrorReason)
|
||
}
|
||
}
|
||
|
||
func TestExecuteTask_MaterializeFailureSurfacesInTaskRunResult(t *testing.T) {
|
||
svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})
|
||
ctx := context.Background()
|
||
svc.docRev.AgentCommit = false // force MaterializeAgentOutput to fail after squad success
|
||
|
||
res, err := svc.ExecuteTask(ctx, TaskRunRequest{
|
||
TaskKey: "research",
|
||
ProjectID: 56,
|
||
ProjectName: "智能体演示项目",
|
||
CustomerName: "北京水木东方",
|
||
DeviceType: "医疗器械",
|
||
Backend: "stub",
|
||
Timeout: 10 * time.Second,
|
||
DocID: "doc-p56-research",
|
||
UserID: 1,
|
||
})
|
||
if err == nil {
|
||
t.Fatal("expected Materialize error to be returned")
|
||
}
|
||
if res == nil {
|
||
t.Fatal("TaskRunResult must be non-nil with visible failure")
|
||
}
|
||
if res.Success {
|
||
t.Fatal("TaskRunResult.Success must be false when ledger/report materialize fails")
|
||
}
|
||
if !strings.Contains(res.ErrorReason, "文档版本账本/变更报告落盘失败") {
|
||
t.Fatalf("ErrorReason must be visible to callers, got %q", res.ErrorReason)
|
||
}
|
||
}
|