TaskRunResult now fails visibly when ledger/report materialization errors; invalid or unassessed reports are never auto-committed. Co-authored-by: Cursor <cursoragent@cursor.com>
233 lines
6.7 KiB
Go
233 lines
6.7 KiB
Go
package bizdev
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"chat-agent-system/internal/config"
|
|
)
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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_EmptyDocIDSkipsDocRevision(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 err != nil {
|
|
t.Fatalf("empty docID ExecuteTask panicked or failed: %v", err)
|
|
}
|
|
if res == nil || !res.Success {
|
|
t.Fatalf("CLI-like stage path should succeed without doc ledger, got %#v", res)
|
|
}
|
|
if res.DocRecord != nil {
|
|
t.Fatalf("empty docID must not create a document record, got %#v", res.DocRecord)
|
|
}
|
|
if _, stErr := svc.DocRevision().GetRevisionState(ctx, ""); !strings.Contains(stErr.Error(), "invalid doc_id") {
|
|
t.Fatalf("empty GetRevisionState should be invalid, got %v", stErr)
|
|
}
|
|
}
|
|
|
|
func TestExecuteTask_MaterializeFailureSurfacesInTaskRunResult(t *testing.T) {
|
|
svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})
|
|
ctx := context.Background()
|
|
svc.DocRevision().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)
|
|
}
|
|
}
|