返回 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 approveWorkspace(t, dir)
88
89 loaderCalls := 0
90 ctrl, err := Build(context.Background(), Options{
91 Sink: event.Discard,
92 PinnedContextLoader: func(context.Context, string) (agent.PinnedContextSnapshot, error) {
93 loaderCalls++
94 return agent.PinnedContextSnapshot{Files: []agent.PinnedContextFile{{Path: "a.md", Content: "A"}}}, nil
95 },
96 })
97 if err != nil {
98 t.Fatalf("Build: %v", err)
99 }
100 defer ctrl.Close()
101 ctrl.EnsureSessionPath()
102 if err := ctrl.Run(context.Background(), "first"); err != nil {
103 t.Fatalf("first run: %v", err)
104 }
105 if err := ctrl.Run(context.Background(), "second"); err != nil {
106 t.Fatalf("second run: %v", err)
107 }
108
109 recorder.mu.Lock()
110 requests := append([]provider.Request(nil), recorder.requests...)
111 recorder.mu.Unlock()
112 if len(requests) != 2 {
113 t.Fatalf("provider requests = %d, want 2", len(requests))
114 }
115 if len(requests[0].Messages) == 0 {
116 t.Fatal("first provider request has no messages")
117 }
118 if first := requests[0].Messages[0].Content; !strings.HasPrefix(first, "BASE") || strings.Contains(first, "<pinned_context_revision") {
119 t.Fatalf("leading system was rewritten with pinned context: %q", first)
120 }
121 if loaderCalls != 2 {
122 t.Fatalf("loader calls = %d", loaderCalls)
123 }
124 if len(requests[1].Messages) < len(requests[0].Messages) ||
125 !reflect.DeepEqual(requests[1].Messages[:len(requests[0].Messages)], requests[0].Messages) {
126 t.Fatal("second provider request did not preserve the first request as an exact prefix")
127 }
128 revisions := 0
129 for _, message := range ctrl.History() {
130 if agent.IsPinnedContextRevision(message) {
131 revisions++
132 }
133 }
134 if revisions != 1 {
135 t.Fatalf("revision messages = %d, want 1", revisions)
136 }
137 }
138
138 lines GO