返回 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 sessionDir := filepath.Join(root, "must-not-exist")
63 disabled := "disabled"
64 ctrl, err := Build(context.Background(), Options{WorkspaceRoot: root, SessionDir: sessionDir, EffortOverride: &disabled, Sink: event.Discard})
65 var role *RoleReasoningError
66 if ctrl != nil || !errors.As(err, &role) || role.Role != "planner" {
67 t.Fatalf("%v %v", ctrl, err)
68 }
69 if _, err := os.Stat(sessionDir); !os.IsNotExist(err) {
70 t.Fatalf("session resources created before preflight: %v", err)
71 }
72 }
73
74 func TestBuildRetainsTheSelectedRoleConfigurationSnapshot(t *testing.T) {
75 isolateConfigHome(t)
76 root := robustTempDir(t)
77 cfg := config.Default()
78 cfg.DefaultModel = "go/deepseek-v4-pro"
79 cfg.Agent.PlannerModel = cfg.DefaultModel
80 cfg.Providers = []config.ProviderEntry{{Name: "go", Kind: "openai", BaseURL: "https://opencode.ai/zen/go/v1", Model: "deepseek-v4-pro", Effort: "max"}}
81 disabled := "disabled"
82 if err := ValidateReasoningSnapshot(cfg, Options{EffortOverride: &disabled}); err != nil {
83 t.Fatal(err)
84 }
85 // Another settings transaction wins on disk after selection was validated.
86 writeFile(t, root, "reasonix.toml", `default_model="broken/model"
87 [[providers]]
88 name="broken"
89 kind="anthropic"
90 base_url="https://custom.example"
91 model="model"
92 thinking="enabled"
93 effort="max"
94 `)
95 ctrl, err := Build(context.Background(), Options{WorkspaceRoot: root, ConfigSnapshot: cfg, EffortOverride: &disabled, Sink: event.Discard})
96 if err != nil {
97 t.Fatalf("assembly reloaded a different role snapshot: %v", err)
98 }
99 defer ctrl.Close()
100 if cfg.Providers[0].Effort != "max" {
101 t.Fatal("execution override mutated the planner snapshot")
102 }
103 }
104
105 func TestRoleReasoningPreflightKeepsRuntimeSelectedVision(t *testing.T) {
106 isolateConfigHome(t)
107 root := robustTempDir(t)
108 t.Setenv("CUSTOM_KEY", "sk-test")
109 writeFile(t, root, "reasonix.toml", `default_model="custom/text"
110 [agent]
111 vision_model="auto"
112 [[providers]]
113 name="custom"
114 kind="openai"
115 base_url="https://example.invalid/v1"
116 api_key_env="CUSTOM_KEY"
117 models=["text","vision-pro"]
118 vision_models=["vision-pro"]
119 `)
120 ctrl, err := Build(context.Background(), Options{WorkspaceRoot: root, Sink: event.Discard})
121 if err != nil {
122 t.Fatalf("vision_model=auto must not fail assembly: %v", err)
123 }
124 ctrl.Close()
125 // A removed explicit vision reference keeps the executor usable, exactly as
126 // the lazy image-input path did; an unsupported effort on a resolvable
127 // vision reference is still rejected before any session exists.
128 cfg := config.Default()
129 cfg.DefaultModel = "custom/text"
130 cfg.Agent.VisionModel = "gone/vision"
131 cfg.Providers = []config.ProviderEntry{{Name: "custom", Kind: "openai", BaseURL: "https://example.invalid/v1", Models: []string{"text"}}}
132 if err := preflightRoleReasoning(cfg, Options{}, nil, false); err != nil {
133 t.Fatalf("removed vision reference blocked assembly: %v", err)
134 }
135 cfg.Agent.VisionModel = "vision/deepseek-v4-flash-vision-exp"
136 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"})
137 var role *RoleReasoningError
138 if err := preflightRoleReasoning(cfg, Options{}, nil, false); !errors.As(err, &role) || role.Role != "vision" {
139 t.Fatalf("invalid vision effort not reported: %v", err)
140 }
141 }
142
143 func TestBuildFreezesCallerConfigurationSnapshot(t *testing.T) {
144 isolateConfigHome(t)
145 root := robustTempDir(t)
146 if _, err := config.SetCredential("SNAPSHOT_KEY", "sk-first"); err != nil {
147 t.Fatal(err)
148 }
149 cfg := config.Default()
150 cfg.DefaultModel = "custom/text"
151 cfg.Providers = []config.ProviderEntry{{Name: "custom", Kind: "openai", BaseURL: "https://example.invalid/v1", APIKeyEnv: "SNAPSHOT_KEY", Model: "text"}}
152 ctrl, err := Build(context.Background(), Options{WorkspaceRoot: root, ConfigSnapshot: cfg, Sink: event.Discard})
153 if err != nil {
154 t.Fatal(err)
155 }
156 defer ctrl.Close()
157 // A credential rotation after assembly must not reach the running runtime.
158 if _, err := config.SetCredential("SNAPSHOT_KEY", "sk-rotated"); err != nil {
159 t.Fatal(err)
160 }
161 if got := cfg.Providers[0].APIKey(); got != "sk-first" {
162 t.Fatalf("caller snapshot rereads credentials: %q", got)
163 }
164 }
165
165 lines GO