返回 DeepSeek-Reasonix
reasoning_preflight_test.go
根目录 / internal / boot / reasoning_preflight_test.go
1 package boot
2
3 import (
4 "context"
5 "errors"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10
11 "reasonix/internal/config"
12 "reasonix/internal/event"
13 "reasonix/internal/provider"
14 )
15
16 func TestRoleReasoningPreflightKeepsPlannerEffortIndependent(t *testing.T) {
17 for _, model := range []string{"deepseek-v4-flash", "deepseek-v4-pro", "deepseek-v4-flash-vision-exp"} {
18 cfg := config.Default()
19 cfg.DefaultModel, cfg.Agent.PlannerModel = "go/"+model, "go/"+model
20 cfg.Providers = []config.ProviderEntry{{Name: "go", Kind: "anthropic", BaseURL: "https://opencode.ai/zen/go", RequestURL: "https://opencode.ai/zen/go/v1/messages", Model: model, Thinking: "enabled", Effort: "max"}}
21 disabled := "disabled"
22 if err := preflightRoleReasoning(cfg, Options{EffortOverride: &disabled}, nil, false); err != nil {
23 t.Fatal(err)
24 }
25 if cfg.Providers[0].Effort != "max" {
26 t.Fatal("preflight mutated planner effort")
27 }
28 cfg.Agent.PlannerModel = "bad/" + model
29 cfg.Providers = append(cfg.Providers, config.ProviderEntry{Name: "bad", Kind: "anthropic", BaseURL: "https://custom.example", Model: model, Thinking: "enabled", Effort: "max"})
30 err := preflightRoleReasoning(cfg, Options{EffortOverride: &disabled}, nil, false)
31 var role *RoleReasoningError
32 var unsupported *provider.UnsupportedReasoningEffort
33 if !errors.As(err, &role) || !errors.As(err, &unsupported) {
34 t.Fatalf("missing typed role/adapter errors: %v", err)
35 }
36 if role.Role != "planner" || role.Effort != "max" || role.Source != "providers.bad.effort" || role.Kind != "anthropic" || !strings.Contains(err.Error(), "enabled disabled") {
37 t.Fatalf("%+v", role)
38 }
39 }
40 }
41
42 func TestBuildRejectsRoleEffortBeforeCreatingSessionResources(t *testing.T) {
43 isolateConfigHome(t)
44 root := robustTempDir(t)
45 t.Chdir(root)
46 writeFile(t, root, "reasonix.toml", `default_model="exec/deepseek-v4-pro"
47 [agent]
48 planner_model="planner/deepseek-v4-pro"
49 [[providers]]
50 name="exec"
51 kind="openai"
52 base_url="https://opencode.ai/zen/go/v1"
53 model="deepseek-v4-pro"
54 [[providers]]
55 name="planner"
56 kind="anthropic"
57 base_url="https://custom.example"
58 model="deepseek-v4-pro"
59 thinking="enabled"
60 effort="max"
61 `)
62 approveWorkspace(t, root)
63 sessionDir := filepath.Join(root, "must-not-exist")
64 disabled := "disabled"
65 ctrl, err := Build(context.Background(), Options{WorkspaceRoot: root, SessionDir: sessionDir, EffortOverride: &disabled, Sink: event.Discard})
66 var role *RoleReasoningError
67 if ctrl != nil || !errors.As(err, &role) || role.Role != "planner" {
68 t.Fatalf("%v %v", ctrl, err)
69 }
70 if _, err := os.Stat(sessionDir); !os.IsNotExist(err) {
71 t.Fatalf("session resources created before preflight: %v", err)
72 }
73 }
74
75 func TestBuildRetainsTheSelectedRoleConfigurationSnapshot(t *testing.T) {
76 isolateConfigHome(t)
77 root := robustTempDir(t)
78 cfg := config.Default()
79 cfg.DefaultModel = "go/deepseek-v4-pro"
80 cfg.Agent.PlannerModel = cfg.DefaultModel
81 cfg.Providers = []config.ProviderEntry{{Name: "go", Kind: "openai", BaseURL: "https://opencode.ai/zen/go/v1", Model: "deepseek-v4-pro", Effort: "max"}}
82 disabled := "disabled"
83 if err := ValidateReasoningSnapshot(cfg, Options{EffortOverride: &disabled}); err != nil {
84 t.Fatal(err)
85 }
86 // Another settings transaction wins on disk after selection was validated.
87 writeFile(t, root, "reasonix.toml", `default_model="broken/model"
88 [[providers]]
89 name="broken"
90 kind="anthropic"
91 base_url="https://custom.example"
92 model="model"
93 thinking="enabled"
94 effort="max"
95 `)
96 approveWorkspace(t, root)
97 ctrl, err := Build(context.Background(), Options{WorkspaceRoot: root, ConfigSnapshot: cfg, EffortOverride: &disabled, Sink: event.Discard})
98 if err != nil {
99 t.Fatalf("assembly reloaded a different role snapshot: %v", err)
100 }
101 defer ctrl.Close()
102 if cfg.Providers[0].Effort != "max" {
103 t.Fatal("execution override mutated the planner snapshot")
104 }
105 }
106
107 func TestRoleReasoningPreflightKeepsRuntimeSelectedVision(t *testing.T) {
108 isolateConfigHome(t)
109 root := robustTempDir(t)
110 t.Setenv("CUSTOM_KEY", "sk-test")
111 writeFile(t, root, "reasonix.toml", `default_model="custom/text"
112 [agent]
113 vision_model="auto"
114 [[providers]]
115 name="custom"
116 kind="openai"
117 base_url="https://example.invalid/v1"
118 api_key_env="CUSTOM_KEY"
119 models=["text","vision-pro"]
120 vision_models=["vision-pro"]
121 `)
122 approveWorkspace(t, root)
123 ctrl, err := Build(context.Background(), Options{WorkspaceRoot: root, Sink: event.Discard})
124 if err != nil {
125 t.Fatalf("vision_model=auto must not fail assembly: %v", err)
126 }
127 ctrl.Close()
128 // A removed explicit vision reference keeps the executor usable, exactly as
129 // the lazy image-input path did; an unsupported effort on a resolvable
130 // vision reference is still rejected before any session exists.
131 cfg := config.Default()
132 cfg.DefaultModel = "custom/text"
133 cfg.Agent.VisionModel = "gone/vision"
134 cfg.Providers = []config.ProviderEntry{{Name: "custom", Kind: "openai", BaseURL: "https://example.invalid/v1", Models: []string{"text"}}}
135 if err := preflightRoleReasoning(cfg, Options{}, nil, false); err != nil {
136 t.Fatalf("removed vision reference blocked assembly: %v", err)
137 }
138 cfg.Agent.VisionModel = "vision/deepseek-v4-flash-vision-exp"
139 cfg.Providers = append(cfg.Providers, config.ProviderEntry{Name: "vision", Kind: "anthropic", BaseURL: "https://custom.example", Model: "deepseek-v4-flash-vision-exp", Thinking: "enabled", Effort: "max"})
140 var role *RoleReasoningError
141 if err := preflightRoleReasoning(cfg, Options{}, nil, false); !errors.As(err, &role) || role.Role != "vision" {
142 t.Fatalf("invalid vision effort not reported: %v", err)
143 }
144 }
145
146 func TestBuildFreezesCallerConfigurationSnapshot(t *testing.T) {
147 isolateConfigHome(t)
148 root := robustTempDir(t)
149 if _, err := config.SetCredential("SNAPSHOT_KEY", "sk-first"); err != nil {
150 t.Fatal(err)
151 }
152 cfg := config.Default()
153 cfg.DefaultModel = "custom/text"
154 cfg.Providers = []config.ProviderEntry{{Name: "custom", Kind: "openai", BaseURL: "https://example.invalid/v1", APIKeyEnv: "SNAPSHOT_KEY", Model: "text"}}
155 ctrl, err := Build(context.Background(), Options{WorkspaceRoot: root, ConfigSnapshot: cfg, Sink: event.Discard})
156 if err != nil {
157 t.Fatal(err)
158 }
159 defer ctrl.Close()
160 // A credential rotation after assembly must not reach the running runtime.
161 if _, err := config.SetCredential("SNAPSHOT_KEY", "sk-rotated"); err != nil {
162 t.Fatal(err)
163 }
164 if got := cfg.Providers[0].APIKey(); got != "sk-first" {
165 t.Fatalf("caller snapshot rereads credentials: %q", got)
166 }
167 }
168
168 lines GO