返回 DeepSeek-Reasonix
pinned_context_test.go
根目录 / internal / boot / pinned_context_test.go
1 package boot
2
3 import (
4 "context"
5 "errors"
6 "reflect"
7 "strings"
8 "sync"
9 "testing"
10
11 "reasonix/internal/agent"
12 "reasonix/internal/event"
13 "reasonix/internal/provider"
14 )
15
16 const bootPinnedContextProviderKind = "boot-pinned-context-test"
17
18 var (
19 bootPinnedContextProviderOnce sync.Once
20 bootPinnedContextProviderMu sync.Mutex
21 bootPinnedContextProviderLive *bootPinnedContextProvider
22 )
23
24 type bootPinnedContextProvider struct {
25 mu sync.Mutex
26 requests []provider.Request
27 }
28
29 func (p *bootPinnedContextProvider) Name() string { return bootPinnedContextProviderKind }
30
31 func (p *bootPinnedContextProvider) Stream(_ context.Context, req provider.Request) (<-chan provider.Chunk, error) {
32 p.mu.Lock()
33 p.requests = append(p.requests, req)
34 p.mu.Unlock()
35 ch := make(chan provider.Chunk, 2)
36 ch <- provider.Chunk{Type: provider.ChunkText, Text: "ok"}
37 ch <- provider.Chunk{Type: provider.ChunkDone}
38 close(ch)
39 return ch, nil
40 }
41
42 func registerBootPinnedContextProvider() {
43 bootPinnedContextProviderOnce.Do(func() {
44 provider.Register(bootPinnedContextProviderKind, func(provider.Config) (provider.Provider, error) {
45 bootPinnedContextProviderMu.Lock()
46 defer bootPinnedContextProviderMu.Unlock()
47 if bootPinnedContextProviderLive == nil {
48 return nil, errors.New("boot pinned-context provider is not installed")
49 }
50 return bootPinnedContextProviderLive, nil
51 })
52 })
53 }
54
55 func useBootPinnedContextProvider(t *testing.T, p *bootPinnedContextProvider) {
56 t.Helper()
57 bootPinnedContextProviderMu.Lock()
58 bootPinnedContextProviderLive = p
59 bootPinnedContextProviderMu.Unlock()
60 t.Cleanup(func() {
61 bootPinnedContextProviderMu.Lock()
62 if bootPinnedContextProviderLive == p {
63 bootPinnedContextProviderLive = nil
64 }
65 bootPinnedContextProviderMu.Unlock()
66 })
67 }
68
69 func TestBuildInjectsPinnedContextOnceWithStablePrefix(t *testing.T) {
70 isolateConfigHome(t)
71 dir := robustTempDir(t)
72 t.Chdir(dir)
73 registerBootPinnedContextProvider()
74 recorder := &bootPinnedContextProvider{}
75 useBootPinnedContextProvider(t, recorder)
76 writeFile(t, dir, "reasonix.toml", `
77 default_model = "test-model"
78
79 [agent]
80 system_prompt = "BASE"
81
82 [[providers]]
83 name = "test-model"
84 kind = "boot-pinned-context-test"
85 model = "x"
86 `)
87
88 loaderCalls := 0
89 ctrl, err := Build(context.Background(), Options{
90 Sink: event.Discard,
91 PinnedContextLoader: func(context.Context, string) (agent.PinnedContextSnapshot, error) {
92 loaderCalls++
93 return agent.PinnedContextSnapshot{Files: []agent.PinnedContextFile{{Path: "a.md", Content: "A"}}}, nil
94 },
95 })
96 if err != nil {
97 t.Fatalf("Build: %v", err)
98 }
99 defer ctrl.Close()
100 ctrl.EnsureSessionPath()
101 if err := ctrl.Run(context.Background(), "first"); err != nil {
102 t.Fatalf("first run: %v", err)
103 }
104 if err := ctrl.Run(context.Background(), "second"); err != nil {
105 t.Fatalf("second run: %v", err)
106 }
107
108 recorder.mu.Lock()
109 requests := append([]provider.Request(nil), recorder.requests...)
110 recorder.mu.Unlock()
111 if len(requests) != 2 {
112 t.Fatalf("provider requests = %d, want 2", len(requests))
113 }
114 if len(requests[0].Messages) == 0 {
115 t.Fatal("first provider request has no messages")
116 }
117 if first := requests[0].Messages[0].Content; !strings.HasPrefix(first, "BASE") || strings.Contains(first, "<pinned_context_revision") {
118 t.Fatalf("leading system was rewritten with pinned context: %q", first)
119 }
120 if loaderCalls != 2 {
121 t.Fatalf("loader calls = %d", loaderCalls)
122 }
123 if len(requests[1].Messages) < len(requests[0].Messages) ||
124 !reflect.DeepEqual(requests[1].Messages[:len(requests[0].Messages)], requests[0].Messages) {
125 t.Fatal("second provider request did not preserve the first request as an exact prefix")
126 }
127 revisions := 0
128 for _, message := range ctrl.History() {
129 if agent.IsPinnedContextRevision(message) {
130 revisions++
131 }
132 }
133 if revisions != 1 {
134 t.Fatalf("revision messages = %d, want 1", revisions)
135 }
136 }
137
137 lines GO