返回 DeepSeek-Reasonix
compact_user_turns_test.go
根目录 / internal / agent / compact_user_turns_test.go
1 package agent
2
3 import (
4 "context"
5 "strings"
6 "testing"
7
8 "reasonix/internal/event"
9 "reasonix/internal/provider"
10 "reasonix/internal/tool"
11 )
12
13 // retentionSession puts one user turn of the given size in the fold region,
14 // behind enough assistant work that the recent tail cannot reach it.
15 func retentionSession(midTurn string) *Session {
16 big := strings.Repeat("work output line with detail. ", 250)
17 return &Session{Messages: []provider.Message{
18 {Role: provider.RoleSystem, Content: "sys"},
19 {Role: provider.RoleUser, Content: "first task"},
20 {Role: provider.RoleAssistant, Content: big},
21 {Role: provider.RoleTool, ToolCallID: "1", Name: "read_file", Content: big},
22 {Role: provider.RoleUser, Content: midTurn},
23 {Role: provider.RoleAssistant, Content: big},
24 {Role: provider.RoleTool, ToolCallID: "2", Name: "read_file", Content: big},
25 {Role: provider.RoleUser, Content: "next"},
26 {Role: provider.RoleAssistant, Content: "ok"},
27 }}
28 }
29
30 func compactWithSink(t *testing.T, sess *Session) []event.Event {
31 t.Helper()
32 var got []event.Event
33 sink := event.FuncSink(func(e event.Event) { got = append(got, e) })
34 a := New(&fakeProvider{reply: "digest"}, tool.NewRegistry(), sess,
35 Options{ContextWindow: 8_000, CompactRatio: 0.85, RecentKeep: 2}, sink)
36 if err := a.compact(context.Background(), "manual", "", true); err != nil {
37 t.Fatalf("compact: %v", err)
38 }
39 return got
40 }
41
42 func noticeMentioning(events []event.Event, substr string) (event.Event, bool) {
43 for _, e := range events {
44 if e.Kind == event.Notice && strings.Contains(e.Text+e.Detail, substr) {
45 return e, true
46 }
47 }
48 return event.Event{}, false
49 }
50
51 // A turn past the budget is the one case where compaction still hands a user's
52 // own words to the summarizer. That has to be visible: the projection reads as
53 // complete either way, so silence here is indistinguishable from success.
54 func TestCompactionFoldsAllOldUserTurnsWithoutKeepNotice(t *testing.T) {
55 oversize := strings.Repeat("constraint detail. ", 500) // ~2375 tokens, past the per-turn ceiling
56 events := compactWithSink(t, retentionSession(oversize))
57
58 if _, ok := noticeMentioning(events, "[[keep]]"); ok {
59 t.Fatalf("deprecated keep notice was emitted; events=%+v", noticeTexts(events))
60 }
61 tele, ok := noticeMentioning(events, "user_dropped=")
62 if !ok {
63 t.Fatal("compaction telemetry carries no user-turn retention counts")
64 }
65 if !strings.Contains(tele.Detail, "user_dropped=2") {
66 t.Errorf("telemetry detail = %q, want user_dropped=2", tele.Detail)
67 }
68 }
69
70 // The notice must stay rare enough to mean something: a fold that kept every
71 // user turn has nothing to warn about.
72 func TestCompactionSilentWhenEveryUserTurnKept(t *testing.T) {
73 events := compactWithSink(t, retentionSession("by the way, always use pnpm not npm"))
74
75 if _, ok := noticeMentioning(events, "[[keep]]"); ok {
76 t.Errorf("warned about dropped turns when none were dropped; events=%+v", noticeTexts(events))
77 }
78 tele, ok := noticeMentioning(events, "user_kept=")
79 if !ok {
80 t.Fatal("compaction telemetry carries no user-turn retention counts")
81 }
82 if !strings.Contains(tele.Detail, "user_dropped=2") {
83 t.Errorf("telemetry detail = %q, want user_dropped=2", tele.Detail)
84 }
85 }
86
87 func noticeTexts(events []event.Event) []string {
88 var out []string
89 for _, e := range events {
90 if e.Kind == event.Notice {
91 out = append(out, e.Text)
92 }
93 }
94 return out
95 }
96
96 lines GO