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

168 lines
4.8 KiB
Go

package bizdev_test
import (
"bytes"
"context"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"github.com/xuri/excelize/v2"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/config"
"chat-agent-system/internal/quote"
)
func TestExecuteTaskIndependent(t *testing.T) {
tmpDir := t.TempDir()
cfg := &config.Config{
DataDir: tmpDir,
AgentBackend: "stub",
AgentTimeout: 5 * time.Second,
}
svc := bizdev.MustNewService(t, cfg)
// 测试 1: 单独执行 intake (不依赖任何前序)
intakeRes, err := svc.ExecuteTask(context.Background(), bizdev.TaskRunRequest{
TaskKey: "intake",
ProjectID: 1,
ProjectName: "微创吻合器研发",
CustomerName: "北京天工",
DeviceType: "腔镜微创器械",
IntendedUse: "微创手术软组织缝合切除",
TargetMarket: "中国 (NMPA)",
})
if err != nil {
t.Fatalf("execute intake failed: %v", err)
}
if !strings.Contains(intakeRes.MarkdownBody, "用户初始需求收集表") {
t.Errorf("intake response missing title")
}
if intakeRes.AdvisorBrief == nil {
t.Fatalf("intake response must include AdvisorBrief")
}
if intakeRes.AdvisorBrief.MaturityLevel != "V0.1" {
t.Errorf("expected V0.1 maturity, got %s", intakeRes.AdvisorBrief.MaturityLevel)
}
if len(intakeRes.AdvisorBrief.BaselineAssumptions) == 0 {
t.Errorf("expected non-empty baseline assumptions in AdvisorBrief")
}
if !strings.Contains(intakeRes.AdvisorBrief.CommunicationKit, "北京天工") {
t.Errorf("expected customer name inside communication kit")
}
// 测试 2: 单独执行 cdo_quote (直接输出 Excel 附件)
quoteRes, err := svc.ExecuteTask(context.Background(), bizdev.TaskRunRequest{
TaskKey: "cdo_quote",
ProjectID: 1,
ProjectName: "微创吻合器研发",
CustomerName: "北京天工",
})
if err != nil {
t.Fatalf("execute cdo_quote failed: %v", err)
}
if len(quoteRes.Attachments) == 0 {
t.Fatalf("cdo_quote must output Excel attachment")
}
if quoteRes.Attachments[0].Filename != "CDO报价核算-V01.xlsx" {
t.Errorf("unexpected attachment filename: %s", quoteRes.Attachments[0].Filename)
}
in := quote.Input{
CustomerName: "北京天工",
ProductName: "微创吻合器研发",
Hours: map[string]float64{
"结构工程师": 320,
"硬件工程师": 320,
"软件工程师": 480,
"项目经理": 160,
},
}
summary, err := quote.CalculateAndFill(in, "templates/xlsx/cdo_quote.xlsx", filepath.Join(tmpDir, "ref.xlsx"))
if err != nil {
t.Fatalf("reference quote calculation failed: %v", err)
}
expectedTotal := bizdev.FormatMoney(summary.TotalPrice)
if !strings.Contains(quoteRes.MarkdownBody, expectedTotal) {
t.Errorf("markdown missing formatted total price %s matching Excel summary", expectedTotal)
}
if !strings.Contains(quoteRes.MarkdownBody, "ID2") || !strings.Contains(quoteRes.MarkdownBody, "ID6") {
t.Errorf("markdown missing backfilled quote table lines")
}
if !strings.Contains(quoteRes.MarkdownBody, "微创吻合器研发") {
t.Errorf("markdown missing product_name")
}
xlsxPrice, err := readQuoteSheetTotal(quoteRes.Attachments[0].Data)
if err != nil {
t.Fatalf("read Excel J18 from attachment: %v", err)
}
if diff := xlsxPrice - summary.TotalPrice; diff < -0.01 || diff > 0.01 {
t.Errorf("Excel J18 total %v != summary total %v", xlsxPrice, summary.TotalPrice)
}
}
func TestExecuteTaskWritesMaterials(t *testing.T) {
tmpDir := t.TempDir()
workDir := filepath.Join(tmpDir, "workspace")
cfg := &config.Config{
DataDir: tmpDir,
AgentBackend: "stub",
AgentTimeout: 5 * time.Second,
}
svc := bizdev.MustNewService(t, cfg)
materialContent := []byte("客户提供的补充需求说明")
res, err := svc.ExecuteTask(context.Background(), bizdev.TaskRunRequest{
TaskKey: "intake",
ProjectID: 1,
ProjectName: "测试项目",
CustomerName: "测试客户",
WorkDir: workDir,
Materials: []bizdev.MaterialInput{
{Filename: "requirements.txt", Content: materialContent},
},
})
if err != nil {
t.Fatalf("execute intake with materials failed: %v", err)
}
materialPath := filepath.Join(res.WorkDir, "materials_text", "requirements.txt")
data, err := os.ReadFile(materialPath)
if err != nil {
t.Fatalf("expected material file at %s: %v", materialPath, err)
}
if !bytes.Equal(data, materialContent) {
t.Errorf("material content mismatch: got %q, want %q", data, materialContent)
}
}
func readQuoteSheetTotal(xlsxData []byte) (float64, error) {
f, err := excelize.OpenReader(bytes.NewReader(xlsxData))
if err != nil {
return 0, err
}
defer f.Close()
const sheet = "报价模型"
raw, err := f.GetCellValue(sheet, "J18")
if err != nil {
return 0, err
}
return parseExcelNumber(raw)
}
func parseExcelNumber(raw string) (float64, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return 0, nil
}
raw = strings.ReplaceAll(raw, ",", "")
return strconv.ParseFloat(raw, 64)
}