返回 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 approveWorkspace(t, dir)
80
81 ctrl, err := Build(context.Background(), Options{Sink: event.Discard})
82 if err != nil {
83 t.Fatalf("Build: %v", err)
84 }
85 defer ctrl.Close()
86
87 // The constraint is the second user turn so it exercises the old-history
88 // fold region rather than the system prefix.
89 const constraint = "standing constraint: never change the public API"
90 for _, prompt := range []string{"start the task", constraint,
91 "keep going", "keep going", "keep going", "keep going", "keep going"} {
92 if err := ctrl.Run(context.Background(), prompt); err != nil {
93 t.Fatalf("Run(%q): %v", prompt, err)
94 }
95 }
96
97 reqs := rec.requests()
98 compacted := -1
99 for i, req := range reqs {
100 for _, m := range req.Messages {
101 if strings.Contains(m.Content, "<compaction-summary>") {
102 compacted = i
103 }
104 }
105 }
106 if compacted < 0 {
107 t.Fatalf("no request carried a digest; the fixture never compacted (%d requests)", len(reqs))
108 }
109
110 var found bool
111 for _, m := range reqs[compacted].Messages {
112 if strings.Contains(m.Content, constraint) {
113 found = true
114 }
115 }
116 if found {
117 t.Fatalf("old user constraint was preserved verbatim outside the deliberately lossy summary.\nmessages=%s",
118 messageDigest(reqs[compacted].Messages))
119 }
120 }
121
122 func messageDigest(msgs []provider.Message) string {
123 var b strings.Builder
124 for _, m := range msgs {
125 content := m.Content
126 if len(content) > 120 {
127 content = content[:120] + "..."
128 }
129 b.WriteString("\n " + string(m.Role) + ": " + content)
130 }
131 return b.String()
132 }
133
133 lines GO