Files
weichen f8730791b9 fix(coordinator): 回填会话项目绑定,避免受控投影被跳过
聊天入口只把项目 ID 放在授权字段,协调器仍读空的 UserCtx.Project.ID,受控任务因此以 project_id=0 跳过账本投影,回复却称正文已保存。
2026-09-28 12:34:32 +00:00

296 lines
8.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}