Files
Agent-Native/runtime/session/r2_regression_test.go
T
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

224 lines
6.2 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 session
import (
"fmt"
"path/filepath"
"strings"
"testing"
"agent-native/runtime/llm"
)
func TestR2CompactKeepsToolPair(t *testing.T) {
s, err := Open(filepath.Join(t.TempDir(), "s.jsonl"))
if err != nil {
t.Fatal(err)
}
msgs := []llm.Message{
{Role: llm.RoleUser, Content: "ask"},
{Role: llm.RoleAssistant, Content: "", ToolCalls: []llm.ToolCall{{ID: "c", Name: "read"}}},
{Role: llm.RoleTool, ToolCallID: "c", Content: "file-body"},
}
for _, m := range msgs {
if err := s.Append(m); err != nil {
t.Fatal(err)
}
}
ok, err := s.Compact(1, func(messages []llm.Message) (string, error) {
return "summary", nil
})
if err != nil || !ok {
t.Fatalf("compact: %v %v", ok, err)
}
loaded, err := s.Load()
if err != nil {
t.Fatal(err)
}
var sawTool bool
for _, m := range loaded {
if m.Role == llm.RoleTool && m.Content == "file-body" {
sawTool = true
}
}
if !sawTool {
t.Fatalf("tool result dropped: %+v", loaded)
}
}
func TestR2CompactRejectsAppendDuringSummary(t *testing.T) {
s, err := Open(filepath.Join(t.TempDir(), "s.jsonl"))
if err != nil {
t.Fatal(err)
}
for _, text := range []string{"a", "b", "c"} {
if err := s.Append(llm.Message{Role: llm.RoleUser, Content: text}); err != nil {
t.Fatal(err)
}
}
_, err = s.Compact(1, func(messages []llm.Message) (string, error) {
return "summary", s.Append(llm.Message{Role: llm.RoleUser, Content: "raced"})
})
if err == nil || !strings.Contains(err.Error(), "compaction conflict") {
t.Fatalf("want conflict, got %v", err)
}
}
func TestCompactRetriesThenCommits(t *testing.T) {
s, err := Open(filepath.Join(t.TempDir(), "s.jsonl"))
if err != nil {
t.Fatal(err)
}
for _, text := range []string{"a", "b", "c"} {
if err := s.Append(llm.Message{Role: llm.RoleUser, Content: text}); err != nil {
t.Fatal(err)
}
}
raced := false
ok, err := s.Compact(1, func(messages []llm.Message) (string, error) {
if !raced {
raced = true
if err := s.Append(llm.Message{Role: llm.RoleUser, Content: "raced"}); err != nil {
return "", err
}
return "", fmt.Errorf("compaction conflict: session changed during summary")
}
return "summary", nil
})
if err != nil || !ok {
t.Fatalf("retry compact: %v %v", ok, err)
}
loaded, err := s.Load()
if err != nil {
t.Fatal(err)
}
var saw bool
for _, m := range loaded {
if m.Content == "raced" || m.Content == "summary" {
saw = true
}
}
if !saw {
t.Fatalf("retried summary missing: %+v", loaded)
}
}
func TestCompactLeavesNoOrphanToolCall(t *testing.T) {
s, err := Open(filepath.Join(t.TempDir(), "s.jsonl"))
if err != nil {
t.Fatal(err)
}
msgs := []llm.Message{
{Role: llm.RoleUser, Content: "先看"},
{Role: llm.RoleAssistant, ToolCalls: []llm.ToolCall{{ID: "old", Name: "read_file"}}},
{Role: llm.RoleTool, ToolCallID: "old", Name: "read_file", Content: "旧"},
{Role: llm.RoleUser, Content: "再改"},
{Role: llm.RoleAssistant, ToolCalls: []llm.ToolCall{{ID: "a", Name: "read_file"}, {ID: "b", Name: "grep"}}},
{Role: llm.RoleTool, ToolCallID: "a", Name: "read_file", Content: "甲"},
{Role: llm.RoleTool, ToolCallID: "b", Name: "grep", Content: "乙"},
}
for _, m := range msgs {
if err := s.Append(m); err != nil {
t.Fatal(err)
}
}
ok, err := s.Compact(2, func(messages []llm.Message) (string, error) {
return "摘要", nil
})
if err != nil || !ok {
t.Fatalf("compact: %v %v", ok, err)
}
loaded, err := s.Load()
if err != nil {
t.Fatal(err)
}
assertToolPairs(t, loaded)
}
func assertToolPairs(t *testing.T, msgs []llm.Message) {
t.Helper()
calls := map[string]int{}
for _, m := range msgs {
if m.Role != llm.RoleAssistant {
continue
}
for _, c := range m.ToolCalls {
calls[c.ID]++
}
}
for _, m := range msgs {
if m.Role != llm.RoleTool {
continue
}
if calls[m.ToolCallID] == 0 {
t.Fatalf("orphan tool result %s in %+v", m.ToolCallID, msgs)
}
calls[m.ToolCallID]--
}
for id, n := range calls {
if n != 0 {
t.Fatalf("orphan tool call %s in %+v", id, msgs)
}
}
}
func TestR2AppendVisibleAfterReopen(t *testing.T) {
path := filepath.Join(t.TempDir(), "s.jsonl")
s, err := Open(path)
if err != nil {
t.Fatal(err)
}
if err := s.Append(llm.Message{Role: llm.RoleUser, Content: "durable"}); err != nil {
t.Fatal(err)
}
again, err := Open(path)
if err != nil {
t.Fatal(err)
}
msgs, err := again.Load()
if err != nil {
t.Fatal(err)
}
if len(msgs) != 1 || msgs[0].Content != "durable" {
t.Fatalf("reopen: %+v", msgs)
}
}
// ---- N2 WP1:alignToolTail 边界直接单测(原仅经 Store.Compact 间接覆盖)----
func tailMsgs() []llm.Message {
call := func(id string) llm.ToolCall { return llm.ToolCall{ID: id, Name: "read", Arguments: "{}"} }
return []llm.Message{
{Role: llm.RoleUser, Content: "u1"}, // 0
{Role: llm.RoleAssistant, ToolCalls: []llm.ToolCall{call("a"), call("b")}}, // 1 多调用 assistant
{Role: llm.RoleTool, ToolCallID: "a", Content: "ra"}, // 2
{Role: llm.RoleTool, ToolCallID: "b", Content: "rb"}, // 3
{Role: llm.RoleUser, Content: "u2"}, // 4
{Role: llm.RoleAssistant, ToolCalls: []llm.ToolCall{call("c")}}, // 5
{Role: llm.RoleTool, ToolCallID: "c", Content: "rc"}, // 6
}
}
func TestAlignToolTailEdges(t *testing.T) {
msgs := tailMsgs()
// tail 恰落在多调用 assistant 的结果组内 → 回退到该 assistant。
if got := alignToolTail(msgs, 3); got != 1 {
t.Fatalf("落在多调用结果组内:got %d want 1", got)
}
// tail 落在结果组边界外(user)→ 不动。
if got := alignToolTail(msgs, 4); got != 4 {
t.Fatalf("落在 user 上:got %d want 4", got)
}
// 结尾整组是 tool 角色 → 回退到其 assistant。
if got := alignToolTail(msgs, 6); got != 5 {
t.Fatalf("落在末尾结果上:got %d want 5", got)
}
// 从头起全是 tool 角色(退化形态)→ 回退到 0,不越界。
allTool := []llm.Message{
{Role: llm.RoleTool, ToolCallID: "x", Content: "1"},
{Role: llm.RoleTool, ToolCallID: "y", Content: "2"},
}
if got := alignToolTail(allTool, 1); got != 0 {
t.Fatalf("全 tool:got %d want 0", got)
}
}