package session import ( "os" "path/filepath" "strings" "testing" "agent-native/runtime/llm" ) func TestClearOldToolResultsKeepsLastFive(t *testing.T) { var msgs []llm.Message for i := 0; i < 8; i++ { id := string(rune('a' + i)) msgs = append(msgs, llm.Message{Role: llm.RoleAssistant, ToolCalls: []llm.ToolCall{{ID: id, Name: "read_file"}}}, llm.Message{Role: llm.RoleTool, Name: "read_file", ToolCallID: id, Content: "正文-" + id}, ) } out := ClearOldToolResults(msgs, 5) if len(out) != len(msgs) { t.Fatalf("len %d, want %d", len(out), len(msgs)) } cleared := 0 for i, m := range out { if m.Role != llm.RoleTool { if m.Role != msgs[i].Role || len(m.ToolCalls) != len(msgs[i].ToolCalls) { t.Fatalf("pairing changed at %d", i) } continue } if m.ToolCallID != msgs[i].ToolCallID { t.Fatalf("tool id changed at %d", i) } if strings.Contains(m.Content, "旧工具结果已清理") { cleared++ if !strings.Contains(m.Content, m.ToolCallID) { t.Fatalf("placeholder missing call id: %s", m.Content) } } else if m.Content != msgs[i].Content { t.Fatalf("recent result changed: %s", m.Content) } } if cleared != 3 { t.Fatalf("cleared %d, want 3", cleared) } for _, m := range out[len(out)-10:] { if m.Role == llm.RoleTool && strings.Contains(m.Content, "旧工具结果已清理") { t.Fatalf("last five were cleared: %s", m.ToolCallID) } } } func TestNeedsClearUsesConservativeWindow(t *testing.T) { if NeedsClear(10, 0, 0, 0) { t.Fatal("small estimate must fit the default window") } if !NeedsClear(DefaultContextWindow, 0, DefaultOutputReserve, DefaultContextBuffer) { t.Fatal("estimate at the default window must clear") } } // ---- N2 WP1:旧工具结果落盘(路径占位)---- func spillFixture() []llm.Message { msgs := []llm.Message{{Role: llm.RoleUser, Content: "u"}} for i := 0; i < 7; i++ { id := "call-" + string(rune('a'+i)) msgs = append(msgs, llm.Message{Role: llm.RoleAssistant, ToolCalls: []llm.ToolCall{{ID: id, Name: "read", Arguments: "{}"}}}, llm.Message{Role: llm.RoleTool, ToolCallID: id, Name: "read", Content: "完整结果内容-" + id}) } return msgs } func TestClearOldToolResultsSpillsToDisk(t *testing.T) { dir := t.TempDir() out := ClearOldToolResultsTo(spillFixture(), 5, dir) if len(out) != len(spillFixture()) { t.Fatal("消息条数不得变") } // 最旧两条被落盘,占位带路径。 for i, id := range []string{"call-a", "call-b"} { path := filepath.Join(dir, id+".txt") b, err := os.ReadFile(path) if err != nil { t.Fatalf("落盘文件缺失:%v", err) } if string(b) != "完整结果内容-"+id { t.Fatalf("落盘内容不符:%q", b) } placeholder := out[2+i*2].Content if !strings.Contains(placeholder, path) { t.Fatalf("占位未带路径:%q", placeholder) } } // 保留的最近 5 条正文不动。 if out[len(out)-1].Content != "完整结果内容-call-g" { t.Fatalf("最近结果被误改:%q", out[len(out)-1].Content) } } func TestClearOldToolResultsEmptyDirKeepsLegacyPlaceholder(t *testing.T) { out := ClearOldToolResultsTo(spillFixture(), 5, "") legacy := ClearOldToolResults(spillFixture(), 5) for i := range out { if out[i].Content != legacy[i].Content { t.Fatalf("空 dir 应与旧行为逐字节一致:msg %d", i) } } } func TestClearOldToolResultsSpillFailureFallsBack(t *testing.T) { // 不可写目录:占位回落为转录指针,不阻断。 blocked := filepath.Join(t.TempDir(), "file") if err := os.WriteFile(blocked, []byte("x"), 0o644); err != nil { t.Fatal(err) } out := ClearOldToolResultsTo(spillFixture(), 5, filepath.Join(blocked, "sub")) if !strings.Contains(out[2].Content, "会话记录中的调用 call-a") { t.Fatalf("写盘失败应回落转录指针:%q", out[2].Content) } } func TestSpillResultSanitizesCallID(t *testing.T) { dir := t.TempDir() path, ok := spillResult(dir, "call/../evil\x00id", "x") if !ok { t.Fatal("spill 失败") } base := filepath.Base(path) if strings.Contains(base, "/") || strings.ContainsRune(base, 0) || filepath.Dir(path) != dir { t.Fatalf("文件名未消毒或越出目录:%q", path) } }