Files
Cyrene_v2/internal/runtime/loop/native_toolcall_test.go
AskaEth 91e8d7a029 docs(runtime): internal/runtime/loop 44 个文件注释改中文(逐行 1:1,代码零字节改动)
主人 2026-10-03 拍板「注释一律中文」;本模块按长期分批协议(一个模块一次派工)执行。
注释行数 1329 → 1329 逐行 1:1(含中文 33 → 1304);行尾注释 5 条一并译。
校验:机械校验(非注释改动行)= 空;独立 Go AST 程序(剥注释复渲)证明 44/44 文件代码全等;
全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。
gofmt:capacity_loop_test.go / flow.go 改动前即未格式化,按铁律保留原样未动。
2026-10-03 22:28:21 +08:00

132 lines
4.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package loop
import (
"context"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
// nativeDatetimeCall 是一个 provider 原生工具调用响应:适配器把
// 调用放在 Response.ToolCalls(而非 Content)中,这正是
// 思考模型在 datetime 工具被广告时对「现在几点」返回的
// 形态。Content 为空,因此忽略 ToolCalls 的循环会产出
// 静默的空回复。
func nativeDatetimeCall() model.Response {
return model.Response{
Model: "m",
Content: "",
ToolCalls: []model.ToolCall{{
ID: "call_native_1",
Name: "datetime",
Args: map[string]any{"timezone": "Asia/Shanghai"},
}},
}
}
// assertNativeFeedBack 检查 req 携带 assistant tool_use 轮以及
// 与 toolCallID 关联的工具结果。
func assertNativeFeedBack(t *testing.T, req model.Request, toolCallID string) {
t.Helper()
for i := 0; i < len(req.Messages)-1; i++ {
m := req.Messages[i]
if m.Role != model.RoleAssistant || len(m.Content) == 0 {
continue
}
if m.Content[0].Type != model.ContentTypeToolCall || m.Content[0].ToolCallID != toolCallID {
continue
}
next := req.Messages[i+1]
if next.Role != model.RoleTool {
t.Fatalf("message after assistant tool_use has role %q, want tool", next.Role)
}
if next.ToolCallID != toolCallID {
t.Fatalf("tool result ToolCallID = %q, want %q", next.ToolCallID, toolCallID)
}
if len(next.Content) == 0 || next.Content[0].Type != model.ContentTypeToolResult {
t.Fatalf("tool result content = %+v, want a tool_result part", next.Content)
}
return
}
t.Fatalf("no native assistant tool_use + tool_result pair for %q in %+v", toolCallID, req.Messages)
}
// TestRun_NativeToolCall_Streaming 覆盖真实聊天路径(设置 OnStream 时
// 循环驱动 ChatStream):Content 为空的 provider 原生工具调用
// 必须被执行并以原生 tool_use + tool_result 回灌,且
// run 必须以模型的后续文本结束,而不是空回复。
func TestRun_NativeToolCall_Streaming(t *testing.T) {
t.Parallel()
ad := &mockAdapter{stream: []mgResp{
{resp: nativeDatetimeCall()},
{resp: model.Response{Model: "m", Content: "现在是 10:34。"}},
}}
td := dispatcherWith(t, &mockTool{name: "datetime", result: map[string]any{"now": "2026-09-08 22:34:00"}})
a := newTestAgent(t, ad, td)
l := New(Options{Agent: a, MaxRounds: 8, OnStream: func(model.StreamEvent) {}})
res, err := l.Run(context.Background(), "sess", []model.Message{model.TextMessage(model.RoleUser, "瞅瞅当前系统时间是多少")})
if err != nil {
t.Fatalf("Run: %v", err)
}
if res.Text != "现在是 10:34。" {
t.Fatalf("Text = %q, want the follow-up answer (empty means the native call was dropped)", res.Text)
}
if res.Steps != 2 {
t.Fatalf("Steps = %d, want 2", res.Steps)
}
if res.ToolCallsExecuted != 1 {
t.Fatalf("ToolCallsExecuted = %d, want 1", res.ToolCallsExecuted)
}
if len(ad.streamReqs) < 2 {
t.Fatalf("stream requests = %d, want >= 2", len(ad.streamReqs))
}
assertNativeFeedBack(t, ad.streamReqs[1], "call_native_1")
}
// TestRun_NativeToolCall_NonStreaming 覆盖静音/内部路径,其中
// 循环委托给 agent.RunStep(adapter.Chat),后者必须解码同一个
// 原生工具调用。
func TestRun_NativeToolCall_NonStreaming(t *testing.T) {
t.Parallel()
ad := &mockAdapter{chat: []mgResp{
{resp: nativeDatetimeCall()},
{resp: model.Response{Model: "m", Content: "现在是 10:34。"}},
}}
td := dispatcherWith(t, &mockTool{name: "datetime", result: map[string]any{"now": "2026-09-08 22:34:00"}})
a := newTestAgent(t, ad, td)
l := New(Options{Agent: a, MaxRounds: 8})
res, err := l.Run(context.Background(), "sess", []model.Message{model.TextMessage(model.RoleUser, "几点了")})
if err != nil {
t.Fatalf("Run: %v", err)
}
if res.Text != "现在是 10:34。" || res.ToolCallsExecuted != 1 {
t.Fatalf("Text = %q, ToolCallsExecuted = %d; want follow-up text + 1 call", res.Text, res.ToolCallsExecuted)
}
if len(ad.reqs) < 2 {
t.Fatalf("chat requests = %d, want >= 2", len(ad.reqs))
}
assertNativeFeedBack(t, ad.reqs[1], "call_native_1")
}
// TestExtractToolCalls_PrefersNative 钉住解码器契约:provider 原生
// ToolCalls 优先于 Content 信封并保留 provider id,使
// 回灌能关联该结果。
func TestExtractToolCalls_PrefersNative(t *testing.T) {
t.Parallel()
resp := nativeDatetimeCall()
resp.Content = `{"tool_calls":[{"name":"legacy","args":{}}]}`
calls, ok := agent.ExtractToolCalls(resp)
if !ok || len(calls) != 1 {
t.Fatalf("ExtractToolCalls = (%+v, %v), want 1 native call", calls, ok)
}
if calls[0].ID != "call_native_1" || calls[0].Name != "datetime" {
t.Fatalf("native call = %+v, want id=call_native_1 name=datetime", calls[0])
}
if calls[0].Args["timezone"] != "Asia/Shanghai" {
t.Fatalf("native args = %+v, want timezone=Asia/Shanghai", calls[0].Args)
}
}