//go:build integration // Package minimax integration test - 真实 HTTP 请求验证 ModeAnthropic 端到端. // // 默认 `go test ./...` 不会跑本文件(build tag 隔离),避免 CI 误消耗 token 配额. // // 运行方式: // // go test -tags integration -run TestIntegration_ModeAnthropic_TokenPlan \ // ./pkg/providers/minimax/ -v // // 前置条件: // - core/.env 中有 MINIMAX_TOKEN_PLAN_KEY= // - 网络能访问 https://api.minimaxi.com/anthropic/v1/messages // // 背景:RFC PR1/PR2 data-driven-capabilities 落地后,flysafe SDK 消费者需要从 // anthropic.New BaseURL hack 迁移到 minimax.New(Mode: ModeAnthropic).本测试 // 验证迁移目标的端到端可行性,覆盖: // - HTTP endpoint 拼接(baseURL + "/anthropic" + "/v1/messages") // - Bearer auth // - SSE 流解析(wire.ParseAnthropicStream 对 minimax anthropic 兼容端点的响应) // - Anthropic 协议兼容性 // - 零误报 WarningEvent(走 minimax 包静态表正确识别 M2.7-highspeed 能力) package minimax import ( "bufio" "context" "os" "path/filepath" "strings" "testing" "time" "git.flytoex.net/yuanwei/flyto-agent/pkg/flyto" ) // TestIntegration_ModeAnthropic_TokenPlan 验证 minimax.New(Mode: ModeAnthropic) // 能正确用 Token Plan Key 打通 minimax Anthropic 兼容端点. func TestIntegration_ModeAnthropic_TokenPlan(t *testing.T) { key := loadTokenPlanKey(t) if key == "" { return // loadTokenPlanKey 已调用 t.Skip } p := New(Config{ APIKey: key, Region: RegionChina, Mode: ModeAnthropic, }) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() ch, err := p.Stream(ctx, &flyto.Request{ Model: "MiniMax-M2.7-highspeed", Messages: []flyto.Message{flyto.UserText("Say ONE word: OK")}, MaxTokens: 200, }) if err != nil { t.Fatalf("Stream() 返回错误: %v", err) } // 收集所有 event,统计类型分布. // // 注意:provider 层 channel **不包含** DoneEvent - 后者是 engine.go 主循环在 // provider.Stream() 返回之后追加的(参见 engine/engine.go:2563/3371). // provider 层的"流终止"信号是 UsageEvent(wire/anthropic.go:299-306 的 message_stop case). var ( textContent strings.Builder sawUsage bool warnings []*flyto.WarningEvent eventCounts = map[string]int{} ) for evt := range ch { eventCounts[evt.EventType()]++ switch e := evt.(type) { case *flyto.TextEvent: textContent.WriteString(e.Text) case *flyto.TextDeltaEvent: textContent.WriteString(e.Text) case *flyto.UsageEvent: sawUsage = true _ = e // 仅用于标记,具体字段不校验 case *flyto.WarningEvent: warnings = append(warnings, e) case *flyto.ErrorEvent: t.Fatalf("收到 ErrorEvent: %v (code=%s)", e.Err, e.Code) } } // --- 断言 1: provider 层流终止信号(UsageEvent)存在 --- // minimax anthropic 兼容端点发 message_stop,wire 解析器把它转成 UsageEvent. // 没收到说明 SSE 流被截断或协议不兼容. if !sawUsage { t.Fatalf("未收到 UsageEvent(provider 层流终止信号)。event 分布: %v", eventCounts) } // --- 断言 2: 响应文本非空且包含 "OK" --- text := strings.ToUpper(strings.TrimSpace(textContent.String())) if text == "" { t.Fatalf("响应文本为空。event 分布: %v", eventCounts) } if !strings.Contains(text, "OK") { t.Errorf("响应应含 OK,实际: %q(event 分布: %v)", text, eventCounts) } // --- 断言 3: 零 feature_unsupported warning --- // 本请求未设 ThinkingBudget / NeedsThinking / EnableCaching,不应触发 want×can. // 若走 anthropic provider 的 BaseURL hack(反面例子),会因为 anthropicModels 静态表 // 查不到 "MiniMax-M2.7-highspeed" 而产生误报--minimax provider 走自己的静态表, // M2.7-highspeed 能被正确识别,不应有此 warning. for _, w := range warnings { if w.Code == "feature_unsupported" { t.Errorf("意外的 feature_unsupported warning: code=%s msg=%s detail=%s", w.Code, w.Message, w.Detail) } } t.Logf("✓ 端到端通过: model=MiniMax-M2.7-highspeed mode=Anthropic text=%q events=%v", text, eventCounts) } // loadTokenPlanKey 从 core/.env 读取 MINIMAX_TOKEN_PLAN_KEY. // // 搜索路径:当前目录 → 上溯 4 层(容忍 `go test ./...` 或 `go test ./pkg/providers/minimax/` // 两种工作目录).找不到则 t.Skip,不让 CI 误失败. func loadTokenPlanKey(t *testing.T) string { t.Helper() var envPath string candidates := []string{ ".env", "../.env", "../../.env", "../../../.env", "../../../../.env", } for _, c := range candidates { abs, _ := filepath.Abs(c) if _, err := os.Stat(abs); err == nil { envPath = abs break } } if envPath == "" { t.Skipf("找不到 core/.env (tried: %v),跳过 integration test", candidates) return "" } f, err := os.Open(envPath) if err != nil { t.Skipf("打开 %s 失败: %v", envPath, err) return "" } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if strings.HasPrefix(line, "MINIMAX_TOKEN_PLAN_KEY=") { key := strings.TrimPrefix(line, "MINIMAX_TOKEN_PLAN_KEY=") if key == "" { t.Skip("core/.env 中 MINIMAX_TOKEN_PLAN_KEY 值为空") return "" } return key } } t.Skipf("%s 中未找到 MINIMAX_TOKEN_PLAN_KEY", envPath) return "" }