154 lines
4.7 KiB
Go
154 lines
4.7 KiB
Go
package app
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"infinite-canvas/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")
|
||
}
|
||
}
|
||
}
|