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) }