返回 DeepSeek-Reasonix
compaction_retention_effect_test.go
根目录 / internal / boot / compaction_retention_effect_test.go
1 package boot
2
3 import (
4 "context"
5 "strings"
6 "sync"
7 "testing"
8
9 "reasonix/internal/event"
10 "reasonix/internal/provider"
11 )
12
13 // compactionEffectProvider answers ordinary turns with enough text to drive the
14 // window past the compaction trigger, and summarizer turns with a digest that
15 // deliberately records nothing. This proves old user turns are summary-owned
16 // rather than silently pinned verbatim by the host.
17 type compactionEffectProvider struct {
18 mu sync.Mutex
19 reqs []provider.Request
20 bulk string
21 }
22
23 func (p *compactionEffectProvider) Name() string { return "boot-compaction-effect" }
24
25 func (p *compactionEffectProvider) Stream(_ context.Context, req provider.Request) (<-chan provider.Chunk, error) {
26 p.mu.Lock()
27 p.reqs = append(p.reqs, req)
28 p.mu.Unlock()
29 text := p.bulk
30 if len(req.Messages) > 0 && strings.HasPrefix(req.Messages[len(req.Messages)-1].Content, "Compact the preceding conversation prefix") {
31 text = "## Standing facts\n- none recorded"
32 }
33 chunks := []provider.Chunk{
34 {Type: provider.ChunkText, Text: text},
35 {Type: provider.ChunkDone},
36 }
37 ch := make(chan provider.Chunk, len(chunks))
38 for _, chunk := range chunks {
39 ch <- chunk
40 }
41 close(ch)
42 return ch, nil
43 }
44
45 func (p *compactionEffectProvider) requests() []provider.Request {
46 p.mu.Lock()
47 defer p.mu.Unlock()
48 return append([]provider.Request(nil), p.reqs...)
49 }
50
51 // TestOldConstraintIsSummaryOwnedAfterCompactionThroughRealBuild proves an old
52 // user constraint enters the summary region and is not separately preserved.
53 // A useful summarizer should retain it; this deliberately lossy fixture makes
54 // accidental host-side verbatim protection observable.
55 func TestOldConstraintIsSummaryOwnedAfterCompactionThroughRealBuild(t *testing.T) {
56 isolateConfigHome(t)
57 dir := robustTempDir(t)
58 t.Chdir(dir)
59
60 rec := &compactionEffectProvider{bulk: strings.Repeat("work output line with detail. ", 400)}
61 provider.Register("boot-compaction-effect", func(provider.Config) (provider.Provider, error) {
62 return rec, nil
63 })
64 // 32000 leaves enough history outside the fixed 16% retained tail to fold.
65 writeFile(t, dir, "reasonix.toml", `
66 default_model = "test-model"
67
68 [agent]
69 system_prompt = "BASE"
70 compact_ratio = 0.5
71 recent_keep = 2
72
73 [[providers]]
74 name = "test-model"
75 kind = "boot-compaction-effect"
76 model = "x"
77 context_window = 32000
78 `)
79
80 ctrl, err := Build(context.Background(), Options{Sink: event.Discard})
81 if err != nil {
82 t.Fatalf("Build: %v", err)
83 }
84 defer ctrl.Close()
85
86 // The constraint is the second user turn so it exercises the old-history
87 // fold region rather than the system prefix.
88 const constraint = "standing constraint: never change the public API"
89 for _, prompt := range []string{"start the task", constraint,
90 "keep going", "keep going", "keep going", "keep going", "keep going"} {
91 if err := ctrl.Run(context.Background(), prompt); err != nil {
92 t.Fatalf("Run(%q): %v", prompt, err)
93 }
94 }
95
96 reqs := rec.requests()
97 compacted := -1
98 for i, req := range reqs {
99 for _, m := range req.Messages {
100 if strings.Contains(m.Content, "<compaction-summary>") {
101 compacted = i
102 }
103 }
104 }
105 if compacted < 0 {
106 t.Fatalf("no request carried a digest; the fixture never compacted (%d requests)", len(reqs))
107 }
108
109 var found bool
110 for _, m := range reqs[compacted].Messages {
111 if strings.Contains(m.Content, constraint) {
112 found = true
113 }
114 }
115 if found {
116 t.Fatalf("old user constraint was preserved verbatim outside the deliberately lossy summary.\nmessages=%s",
117 messageDigest(reqs[compacted].Messages))
118 }
119 }
120
121 func messageDigest(msgs []provider.Message) string {
122 var b strings.Builder
123 for _, m := range msgs {
124 content := m.Content
125 if len(content) > 120 {
126 content = content[:120] + "..."
127 }
128 b.WriteString("\n " + string(m.Role) + ": " + content)
129 }
130 return b.String()
131 }
132
132 lines GO