返回 DeepSeek-Reasonix
golden_baseline_test.go
根目录 / internal / boot / golden_baseline_test.go
1 package boot
2
3 import (
4 "context"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "runtime"
9 "slices"
10 "strconv"
11 "strings"
12 "testing"
13
14 "reasonix/internal/agent"
15 "reasonix/internal/config"
16 "reasonix/internal/provider"
17 "reasonix/internal/tool"
18 )
19
20 // Golden baseline for the cache-stable provider contract. Without a deliberate
21 // migration, the system prompt, tool schemas, provider request, and prefix hash
22 // remain byte-identical. Session-context tests cover dynamic data separately.
23 //
24 // The stable-prefix/session-context migration intentionally updates the system
25 // and prefix goldens once while leaving tool_schemas.json byte-identical. Future
26 // reviewed provider-visible changes must regenerate with:
27 //
28 // REASONIX_UPDATE_GOLDEN=1 go test ./internal/boot -run TestGoldenBaseline -count=1
29 // REASONIX_GOLDEN_SHELL=powershell REASONIX_UPDATE_GOLDEN=1 go test ./internal/boot -run TestGoldenBaseline -count=1
30 //
31 // and call the cache impact out in the commit message.
32 //
33 // Machine dependence is designed out of the golden: the environment probe
34 // section is disabled in the fixture config (it embeds GOOS/GOARCH, shell
35 // labels, and probe output; internal/environment covers it with its own
36 // snapshot tests), and the workspace root path is normalized to <ROOT>.
37 const goldenBaselineDir = "testdata/golden"
38
39 // goldenBaseline is one deterministic capture of the provider-visible
40 // runtime surface with zero extensions installed.
41 type goldenBaseline struct {
42 SystemPrompt string
43 ToolSchemas []byte
44 ProviderReq []byte
45 PrefixShape agent.PrefixShape
46 }
47
48 func captureGoldenBaseline(t *testing.T) goldenBaseline {
49 t.Helper()
50 isolateConfigHome(t)
51 dir := robustTempDir(t)
52 t.Chdir(dir)
53
54 fixture := `
55 default_model = "test-model"
56
57 [agent]
58 system_prompt = "BASE SYSTEM PROMPT"
59
60 [environment]
61 enabled = false
62
63 [tools.shell]
64 # This golden records the Bash contract; Windows auto selects PowerShell.
65 # Pin the dialect just as the search engine below is pinned.
66 prefer = "bash"
67
68 [tools.search]
69 # Pin the grep engine: on "auto" the tool's description (and with it the tool
70 # schemas, provider request, and cache prefix) changes depending on whether rg
71 # happens to be on PATH, which would make this golden machine-dependent.
72 engine = "native"
73
74 [[providers]]
75 name = "test-model"
76 kind = "openai"
77 base_url = "https://example.invalid"
78 model = "x"
79 api_key_env = "REASONIX_TEST_KEY_UNSET"
80 `
81 forcePowerShell := runtime.GOOS == "windows" || os.Getenv("REASONIX_GOLDEN_SHELL") == "powershell"
82 if forcePowerShell {
83 // Pin 5.1 independently of whether PowerShell 7 is installed.
84 fixture = strings.Replace(fixture, `prefer = "bash"`, `prefer = "powershell"`, 1)
85 if runtime.GOOS != "windows" {
86 // A configured PowerShell path is enough to compose the Windows tool
87 // contract; Build does not execute it. This lets POSIX CI guard the
88 // Windows provider snapshot without Wine.
89 fake := filepath.Join(t.TempDir(), "powershell")
90 if err := os.WriteFile(fake, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
91 t.Fatalf("write fake PowerShell: %v", err)
92 }
93 // The user's own shell choice: a checkout may not name one where
94 // sandboxed commands write.
95 userConfig := config.UserConfigPath()
96 writeFile(t, filepath.Dir(userConfig), filepath.Base(userConfig), "[tools.shell]\nprefer = \"powershell\"\npath = "+strconv.Quote(fake)+"\n")
97 }
98 }
99 writeFile(t, dir, "reasonix.toml", fixture)
100 approveWorkspace(t, dir)
101
102 ctrl, err := Build(context.Background(), Options{})
103 if err != nil {
104 t.Fatalf("Build: %v", err)
105 }
106 defer ctrl.Close()
107
108 // 1. System prompt, with the machine-specific workspace root normalized.
109 prompt := systemMessage(ctrl.History())
110 if strings.TrimSpace(prompt) == "" {
111 t.Fatal("Build composed an empty system prompt")
112 }
113 actualDir, err := os.Getwd()
114 if err != nil {
115 t.Fatalf("resolve fixture working directory: %v", err)
116 }
117 prompt = normalizeGoldenRoot(prompt, dir, actualDir)
118
119 // 2. Provider-visible tool contract, through the same canonical schema
120 // path the runtime registry uses.
121 entries := ctrl.ToolContractEntries()
122 if len(entries) == 0 {
123 t.Fatal("Build registered no tools")
124 }
125 toolJSON, err := json.MarshalIndent(entries, "", " ")
126 if err != nil {
127 t.Fatalf("marshal tool contract: %v", err)
128 }
129
130 // 3. Provider request serialization: a fixed synthetic conversation plus
131 // the live tool schemas, assembled the same way the agent loop assembles
132 // requests (ModelMessages + CreatedAt stripped before send).
133 schemas := make([]provider.ToolSchema, 0, len(entries))
134 for _, e := range entries {
135 schemas = append(schemas, provider.ToolSchema{
136 Name: e.Name,
137 Description: e.Description,
138 Parameters: e.Schema,
139 })
140 }
141 temp := 0.7
142 req := provider.Request{
143 Messages: provider.ModelMessages([]provider.Message{
144 {Role: provider.RoleSystem, Content: prompt},
145 {Role: provider.RoleUser, Content: "USER PROMPT", RawContent: "USER PROMPT", CreatedAt: 1700000000000},
146 {Role: provider.RoleAssistant, Content: "ASSISTANT REPLY", ReasoningContent: "THINKING", ReasoningSignature: "sig", ToolCalls: []provider.ToolCall{{ID: "call_1", Name: "read", Arguments: `{"path":"a.txt"}`}}, CreatedAt: 1700000001000},
147 {Role: provider.RoleTool, Content: "TOOL RESULT", ToolCallID: "call_1", Name: "read", CreatedAt: 1700000002000},
148 {Role: provider.RoleUser, Content: "LOCAL ONLY", LocalOnly: true},
149 }),
150 Tools: schemas,
151 Temperature: &temp,
152 MaxTokens: 1024,
153 }
154 for i := range req.Messages {
155 req.Messages[i].CreatedAt = 0
156 }
157 reqJSON, err := json.MarshalIndent(req, "", " ")
158 if err != nil {
159 t.Fatalf("marshal provider request: %v", err)
160 }
161
162 return goldenBaseline{
163 SystemPrompt: prompt,
164 ToolSchemas: append(toolJSON, '\n'),
165 ProviderReq: append(reqJSON, '\n'),
166 PrefixShape: agent.CaptureShape(prompt, schemas, 0),
167 }
168 }
169
170 // normalizeGoldenRoot replaces every known spelling of the workspace root
171 // (including the actual cwd and symlink-evaluated forms) with a stable
172 // placeholder. Windows may report the cwd using an 8.3 short-path alias even
173 // when TempDir returned the long form, so both values are required.
174 func normalizeGoldenRoot(prompt string, roots ...string) string {
175 out := prompt
176 seen := make(map[string]struct{}, len(roots)*2)
177 replace := func(root string) {
178 if root == "" {
179 return
180 }
181 if _, ok := seen[root]; ok {
182 return
183 }
184 // System-prompt workspace paths are Go-quoted. On Windows that doubles
185 // backslashes, so replace the quoted spelling before the raw alias.
186 out = strings.ReplaceAll(out, strconv.Quote(root), strconv.Quote("<ROOT>"))
187 out = strings.ReplaceAll(out, root, "<ROOT>")
188 seen[root] = struct{}{}
189 }
190 for _, root := range roots {
191 replace(root)
192 if real, err := filepath.EvalSymlinks(root); err == nil && real != root {
193 replace(real)
194 }
195 }
196 return out
197 }
198
199 func TestNormalizeGoldenRootReplacesQuotedWindowsPath(t *testing.T) {
200 root := `C:\Users\RUNNER~1\AppData\Local\Temp\reasonix-test-123`
201 prompt := "Current workspace: " + strconv.Quote(root)
202 if got, want := normalizeGoldenRoot(prompt, root), `Current workspace: "<ROOT>"`; got != want {
203 t.Fatalf("normalizeGoldenRoot = %q, want %q", got, want)
204 }
205 }
206
207 func TestNormalizeGoldenRootReplacesEveryAlias(t *testing.T) {
208 prompt := "long=/tmp/reasonix-long short=/tmp/reasonix-short"
209 got := normalizeGoldenRoot(prompt, "/tmp/reasonix-long", "/tmp/reasonix-short")
210 if got != "long=<ROOT> short=<ROOT>" {
211 t.Fatalf("normalizeGoldenRoot = %q", got)
212 }
213 }
214
215 func TestGoldenBaselineNoExtensions(t *testing.T) {
216 // Resolve the golden directory before the fixture chdirs into a temp
217 // workspace, or reads/writes would land inside the fixture.
218 goldenDir, err := filepath.Abs(goldenBaselineDir)
219 if err != nil {
220 t.Fatalf("resolve golden dir: %v", err)
221 }
222 if runtime.GOOS == "windows" || os.Getenv("REASONIX_GOLDEN_SHELL") == "powershell" {
223 goldenDir = filepath.Join(goldenDir, "windows-powershell")
224 }
225
226 first := captureGoldenBaseline(t)
227
228 // In-run determinism: an identical second Build must capture the exact
229 // same surface before we bother comparing against the committed golden.
230 second := captureGoldenBaseline(t)
231 if first.SystemPrompt != second.SystemPrompt {
232 t.Fatalf("system prompt is not deterministic across identical Builds, first diff: %q", firstDivergence(first.SystemPrompt, second.SystemPrompt))
233 }
234 if string(first.ToolSchemas) != string(second.ToolSchemas) {
235 t.Fatal("tool schemas are not deterministic across identical Builds")
236 }
237 if string(first.ProviderReq) != string(second.ProviderReq) {
238 t.Fatal("provider request serialization is not deterministic across identical Builds")
239 }
240
241 shapeJSON, err := json.MarshalIndent(first.PrefixShape, "", " ")
242 if err != nil {
243 t.Fatalf("marshal prefix shape: %v", err)
244 }
245 shapeJSON = append(shapeJSON, '\n')
246
247 artifacts := map[string][]byte{
248 "system_prompt.txt": []byte(first.SystemPrompt),
249 "tool_schemas.json": first.ToolSchemas,
250 "provider_request.json": first.ProviderReq,
251 "prefix_shape.json": shapeJSON,
252 }
253
254 if os.Getenv("REASONIX_UPDATE_GOLDEN") == "1" {
255 if err := os.MkdirAll(goldenDir, 0o755); err != nil {
256 t.Fatalf("mkdir golden dir: %v", err)
257 }
258 for name, data := range artifacts {
259 if err := os.WriteFile(filepath.Join(goldenDir, name), data, 0o644); err != nil {
260 t.Fatalf("update golden %s: %v", name, err)
261 }
262 t.Logf("updated golden %s (%d bytes)", name, len(data))
263 }
264 return
265 }
266
267 for name, want := range artifacts {
268 got, err := os.ReadFile(filepath.Join(goldenDir, name))
269 if err != nil {
270 t.Fatalf("read golden %s: %v (record it with REASONIX_UPDATE_GOLDEN=1)", name, err)
271 }
272 if string(got) != string(want) {
273 t.Fatalf("golden %s drifted from the committed cache-contract baseline (%d bytes golden, %d bytes actual); first diff: %q\n"+
274 "If this drift is deliberate, regenerate with REASONIX_UPDATE_GOLDEN=1 and document the cache impact.",
275 name, len(got), len(want), firstDivergence(string(got), string(want)))
276 }
277 }
278 }
279
280 func TestWindowsProviderSurfaceUsesPwshAndFormalJobsOnly(t *testing.T) {
281 if runtime.GOOS != "windows" {
282 t.Setenv("REASONIX_GOLDEN_SHELL", "powershell")
283 }
284 baseline := captureGoldenBaseline(t)
285 var entries []tool.ContractEntry
286 if err := json.Unmarshal(baseline.ToolSchemas, &entries); err != nil {
287 t.Fatal(err)
288 }
289 byName := make(map[string]tool.ContractEntry, len(entries))
290 for _, entry := range entries {
291 byName[entry.Name] = entry
292 }
293 for _, want := range []string{"pwsh", "job_output", "job_kill"} {
294 if _, ok := byName[want]; !ok {
295 t.Fatalf("Windows provider surface missing %q: %v", want, byName)
296 }
297 }
298 for _, hidden := range []string{"bash", "Bash", "PowerShell", "powershell", "bash_output", "wait", "kill_shell"} {
299 if _, ok := byName[hidden]; ok {
300 t.Fatalf("compatibility alias %q leaked into Windows provider schema", hidden)
301 }
302 }
303 var schema struct {
304 Required []string `json:"required"`
305 Properties map[string]json.RawMessage `json:"properties"`
306 }
307 if err := json.Unmarshal(byName["pwsh"].Schema, &schema); err != nil {
308 t.Fatal(err)
309 }
310 if !slices.Contains(schema.Required, "command") || !slices.Contains(schema.Required, "description") {
311 t.Fatalf("pwsh required fields = %v", schema.Required)
312 }
313 if _, ok := schema.Properties["preserve_background_processes"]; ok {
314 t.Fatal("pwsh schema exposed preserve_background_processes")
315 }
316 }
317
318 // TestGoldenBaselineContractSanity cross-checks the golden tool contract
319 // against the compile-time builtin contract so the boot-level golden cannot
320 // silently drift away from the tool package's own committed contract.
321 func TestGoldenBaselineContractSanity(t *testing.T) {
322 builtins := tool.BuiltinContractEntries()
323 if len(builtins) == 0 {
324 t.Fatal("no builtin contract entries")
325 }
326 seen := make(map[string]bool, len(builtins))
327 for _, e := range builtins {
328 if seen[e.Name] {
329 t.Fatalf("duplicate builtin contract entry %q", e.Name)
330 }
331 seen[e.Name] = true
332 if len(e.Schema) == 0 {
333 t.Fatalf("builtin contract entry %q has an empty canonical schema", e.Name)
334 }
335 }
336 }
337
337 lines GO