Files
weichen ae42c9402d
ci / vet-and-test (pull_request) Successful in 52s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Spill cleared tool results to disk and pin the cache-stability contract (N2 WP1)
Why: the completion doc's N2 requires old tool results to become file
paths (ZCode compact pattern) and cache-friendly prefix rules; today the
micro-compactor only left a transcript pointer and nothing pinned prefix
stability. alignToolTail had no direct edge tests.
Evidence: GOWORK=off go vet + go test -count=1 ./... green; new
TestClearOldToolResultsSpillsToDisk / TestSpillResultSanitizesCallID /
TestAlignToolTailEdges / TestSystemTextDeterministicAcrossIdenticalBuilds;
legacy placeholder bytes unchanged when ResultsDir is empty
(TestClearOldToolResultsEmptyDirKeepsLegacyPlaceholder).
Impact: opt-in via Config.ResultsDir; the native adapter wires it to
<workDir>/.agentx/results. No behavior change for other hosts.
Refs: 2026-10-07-agent-stack-n2-plan WP1
2026-10-07 12:59:05 +00:00

137 lines
4.1 KiB
Go

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)
}
}