聊天入口只把项目 ID 放在授权字段,协调器仍读空的 UserCtx.Project.ID,受控任务因此以 project_id=0 跳过账本投影,回复却称正文已保存。
168 lines
4.8 KiB
Go
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)
|
|
}
|