Files
AutoZero/agent/utf8_test.go
T
Sakurasan d5dac5a6fc feat: AutoZero 通用 Agent 框架(从 AutoGo 五阶段拆分完成)
llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、
builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、
编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。

产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt)
留在 AutoGo,宿主接入方式见 MIGRATION.md。
2026-10-26 00:54:46 +08:00

81 lines
3.6 KiB
Go
Raw 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 agent
import (
"strings"
"testing"
"unicode/utf8"
"git.gopher.ink/mirrors2/AutoZero/llm"
)
// 截断类代码的共性坑:`s[:n]` 的 n 是**字节**下标,而这里的文本大量是中文(每字 3 字节)
// 与 emoji(4 字节)。切在中间不会报错,只会留下一个非法字节序列——发给模型是乱码,
// 某些网关直接 400,写进会话 JSONL 后连回读都不干净。
// 这几条用例的共同断言是 utf8.ValidString:它们在修复前全部会失败(预算恰好落在半个字上)。
const cjkLine = "把中文标题与 emoji 😀 都留下,别劈开" // 每轮 3 字节为主,含一个 4 字节 emoji
func TestPreviewKeepsUTF8(t *testing.T) {
// 逐个预算长度试:只要有一个切点落在字符中间,preview 就会产出非法序列。
long := strings.Repeat(cjkLine, 30)
for n := 1; n <= utf8.RuneCountInString(long); n++ {
// preview 的预算固定 60 字,所以这里改的是内容里"第 60 个字符落在哪"。
got := preview(strings.Repeat("中", n) + " " + long)
if !utf8.ValidString(got) {
t.Fatalf("preview 在偏移 %d 处劈开字符:%q", n, got)
}
}
}
func TestCompactKeepsUTF8(t *testing.T) {
// 工具参数里的中文路径/内容是常态(write 的 content、bash 的 echo)。
input := `{"path":"` + strings.Repeat("中文路径", 60) + `","content":"` + strings.Repeat("内容", 200) + `"}`
if got := compact(input); !utf8.ValidString(got) {
t.Fatalf("compact 产出非法 UTF-8:%q", got)
}
// 非 JSON 入参走原样截断那条路,同样不能切坏。
raw := strings.Repeat("查询", 300)
if got := compact(raw); !utf8.ValidString(got) {
t.Fatalf("compact 对非 JSON 入参产出非法 UTF-8:%q", got)
}
}
// TestSerializeForSummaryKeepsUTF8 是最贵的一处:这条串就是发给模型的摘要提示词,
// 而 perMsgCap 是字节预算,中文消息下几乎必然切在半个字上。
func TestSerializeForSummaryKeepsUTF8(t *testing.T) {
msgs := []llm.Message{
{Role: llm.RoleUser, Content: strings.Repeat("用户诉求", 200)},
{Role: llm.RoleAssistant, Content: strings.Repeat("回答内容", 200)},
{Role: llm.RoleAssistant, Content: "先看一下", ToolCalls: []llm.ToolCall{
{ID: "c1", Name: "bash", Input: `{"command":"echo ` + strings.Repeat("输出中文", 500) + `"}`},
}},
{Role: llm.RoleTool, ToolCallID: "c1", Content: strings.Repeat("工具结果", 400)},
}
for _, cap := range []int{1, 5, 7, 64, 200, 1999, 2001} {
got := serializeForSummary(msgs, cap, 0)
if !utf8.ValidString(got) {
t.Errorf("perMsgCap=%d 时摘要输入含非法字符", cap)
}
}
}
// TestTruncateToolResultsStillAligns 是既有正确实现的护栏(这两处本来就退到字符边界):
// 它们不该被后来的"统一截断"重构换掉语义——头尾都要能独立成为一个合法串。
func TestTruncateToolResultsStillAligns(t *testing.T) {
content := strings.Repeat("中文输出", 5000) + "😀"
out := truncateToolResults([]llm.Message{{Role: llm.RoleTool, ToolCallID: "1", Content: content}}, 1000)
if !utf8.ValidString(out[0].Content) {
t.Errorf("截断后的工具结果非法:%q", out[0].Content[len(out[0].Content)-40:])
}
// 预算放不下一个字符时要给空串,而不是半个;放得下时就给整字。
if got := cutUTF8Head(content, 2); got != "" {
t.Errorf("2 字节预算放不下一个中文,应得空串,得到 %q", got)
}
if got := cutUTF8Head(content, 4); got != "中" {
t.Errorf("4 字节预算应正好装下第一个中文,得到 %q", got)
}
if !utf8.ValidString(cutUTF8Tail(content, 5)) {
t.Error("cutUTF8Tail 切坏了")
}
}