Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_pi_truncated_args_test.go

154 lines
4.7 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 app
import (
"context"
"encoding/json"
"strings"
"testing"
"time"
"yingce/backend/internal/model"
)
func TestCloudAgentPiThinkingEnabled(t *testing.T) {
tests := []struct {
name string
level string
want bool
}{
{name: "omitted", level: "", want: false},
{name: "off", level: "off", want: false},
{name: "enabled", level: "medium", want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := cloudAgentPiThinkingEnabled(tt.level); got != tt.want {
t.Fatalf("cloudAgentPiThinkingEnabled(%q) = %v, want %v", tt.level, got, tt.want)
}
})
}
}
// 线上复现(诊断号 ag5865c256d9fb9bcbf2f1719a4ec1023c):模型一次写了很长的工具参数,
// 流式拼出来的 JSON 多了一个括号,任务报"Agent 工具参数不是完整 JSON"。Go 执行循环
// 会带 truncated_tool_arguments 纠偏重做,但 Pi 模型桥接把它当成不可重试的失败,整轮
// 以"Agent 执行中断"结束。修复后桥接必须带纠偏上下文重做同一步。
func TestCloudAgentPiModelRetriesTruncatedToolArguments(t *testing.T) {
s, db, _ := agentMediaFixture(t)
s.disablePiRuntime = true
req := agentTestRequest()
req.PermissionMode = "auto"
root, err := s.CreateCloudAgentRun("user", req, "")
if err != nil {
t.Fatal(err)
}
runID := root.ID
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
payload := map[string]json.RawMessage{"messages": json.RawMessage(`[{"role":"user","content":"继续"}]`), "thinkingLevel": json.RawMessage(`"off"`)}
type outcome struct {
result any
err error
}
done := make(chan outcome, 1)
go func() {
result, stepErr := s.cloudAgentPiModel(ctx, "user", runID, payload)
done <- outcome{result, stepErr}
}()
// 等待模型步骤入队,返回它的任务 ID(跳过已处理过的任务)。
nextStep := func(skip string) string {
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
stored, readErr := s.repo.CloudAgent("user", runID)
if readErr != nil {
t.Fatal(readErr)
}
state, decodeErr := cloudAgentDecode(stored)
if decodeErr != nil {
t.Fatal(decodeErr)
}
if state.ActiveTaskID != "" && state.ActiveTaskID != skip {
return state.ActiveTaskID
}
select {
case got := <-done:
t.Fatalf("model bridge returned before retry: result=%v err=%v", got.result, got.err)
case <-time.After(50 * time.Millisecond):
}
}
t.Fatal("model step was never scheduled")
return ""
}
first := nextStep("")
var firstTask model.Task
if err := db.First(&firstTask, "id = ?", first).Error; err != nil {
t.Fatal(err)
}
var firstInput map[string]any
if err := json.Unmarshal([]byte(firstTask.InputJSON), &firstInput); err != nil {
t.Fatalf("decode first model task input: %v", err)
}
textOptions, ok := firstInput["textOptions"].(map[string]any)
if !ok {
t.Fatalf("first model task textOptions = %#v", firstInput["textOptions"])
}
if thinking, ok := textOptions["thinking"].(bool); !ok || thinking {
t.Fatalf("disabled thinking should be false, got %#v", textOptions["thinking"])
}
if err := db.Model(&model.Task{}).Where("id = ?", first).Updates(map[string]any{
"status": model.TaskStatusFailed,
"error": "Agent 工具参数不是完整 JSON:invalid character '}' after array element",
}).Error; err != nil {
t.Fatal(err)
}
second := nextStep(first)
var retried model.Task
if err := db.First(&retried, "id = ?", second).Error; err != nil {
t.Fatal(err)
}
if !strings.Contains(retried.InputJSON, string(cloudAgentContextTruncatedArguments)) {
t.Fatalf("retried step lacks the truncated_tool_arguments correction: %s", retried.InputJSON)
}
if err := db.Model(&model.Task{}).Where("id = ?", second).Updates(map[string]any{
"status": model.TaskStatusSucceeded,
"result_json": `{"text":"已拆分写入","toolCalls":[]}`,
}).Error; err != nil {
t.Fatal(err)
}
select {
case got := <-done:
if got.err != nil {
t.Fatalf("model bridge failed after correction: %v", got.err)
}
result, _ := got.result.(map[string]any)
if result["text"] != "已拆分写入" {
t.Fatalf("unexpected result: %#v", got.result)
}
case <-ctx.Done():
t.Fatal("model bridge did not return")
}
stored, err := s.repo.CloudAgent("user", runID)
if err != nil {
t.Fatal(err)
}
if stored.Status == "failed" {
t.Fatalf("run failed: %q", stored.FailureMessage)
}
state, err := cloudAgentDecode(stored)
if err != nil {
t.Fatal(err)
}
// 纠偏上下文只用于那一次请求,不能写回运行历史。
for _, message := range state.Canonical.Messages {
if strings.Contains(stringField(message, "content"), string(cloudAgentContextTruncatedArguments)) {
t.Fatal("correction context leaked into persisted canonical history")
}
}
}