返回 DeepSeek-Reasonix
boot_safe_mode_test.go
根目录 / internal / boot / boot_safe_mode_test.go
1 package boot
2
3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "runtime"
8 "strings"
9 "testing"
10 "time"
11
12 "reasonix/internal/config"
13 "reasonix/internal/event"
14 "reasonix/internal/plugin"
15 )
16
17 // v1.20+ removes product Safe Mode. REASONIX_SAFE_MODE no longer changes boot
18 // behavior; these tests pin that the normal tool/plugin surface stays available.
19
20 func TestBuildIgnoresSafeModeEnvForTools(t *testing.T) {
21 isolateConfigHome(t)
22 dir := robustTempDir(t)
23 t.Chdir(dir)
24 t.Setenv("REASONIX_SAFE_MODE", "1")
25
26 ctrl, err := Build(context.Background(), Options{
27 SessionDir: filepath.Join(t.TempDir(), "sessions"),
28 TokenMode: TokenModeFull,
29 Sink: event.Discard,
30 })
31 if err != nil {
32 t.Fatal(err)
33 }
34 names := map[string]bool{}
35 for _, e := range ctrl.AllToolContractEntries() {
36 names[e.Name] = true
37 }
38 ctrl.Close()
39 for _, name := range []string{"install_source", "run_skill", "read_skill", "use_capability"} {
40 if !names[name] {
41 t.Fatalf("REASONIX_SAFE_MODE must not strip %s from the capability registry", name)
42 }
43 }
44 }
45
46 func TestBuildMemoryMigrationFailureWarnsAndContinues(t *testing.T) {
47 isolateConfigHome(t)
48 project := robustTempDir(t)
49 t.Setenv("REASONIX_SAFE_MODE", "")
50 globalDir := filepath.Join(config.MemoryUserDir(), "memory", "global")
51 if err := os.MkdirAll(filepath.Dir(globalDir), 0o755); err != nil {
52 t.Fatal(err)
53 }
54 if err := os.WriteFile(globalDir, []byte("not a directory"), 0o644); err != nil {
55 t.Fatal(err)
56 }
57 var notices []event.Event
58 ctrl, err := Build(context.Background(), Options{
59 WorkspaceRoot: project,
60 SessionDir: filepath.Join(t.TempDir(), "sessions"),
61 Sink: event.FuncSink(func(e event.Event) {
62 if e.Kind == event.Notice {
63 notices = append(notices, e)
64 }
65 }),
66 })
67 if err != nil {
68 t.Fatalf("Build should continue after memory migration failure: %v", err)
69 }
70 ctrl.Close()
71 for _, notice := range notices {
72 if notice.Level == event.LevelWarn && strings.Contains(notice.Text, "Memory metadata migration") && notice.Detail != "" {
73 return
74 }
75 }
76 t.Fatalf("memory migration warning missing: %+v", notices)
77 }
78
79 func TestBuildNormalModeKeepsSourceConnectorAndSkillTools(t *testing.T) {
80 isolateConfigHome(t)
81 dir := robustTempDir(t)
82 t.Chdir(dir)
83 t.Setenv("REASONIX_SAFE_MODE", "")
84
85 for _, tokenMode := range []string{TokenModeFull, TokenModeDelivery} {
86 ctrl, err := Build(context.Background(), Options{
87 SessionDir: filepath.Join(t.TempDir(), "sessions"),
88 TokenMode: tokenMode,
89 Sink: event.Discard,
90 })
91 if err != nil {
92 t.Fatalf("Build(%q): %v", tokenMode, err)
93 }
94 names := map[string]bool{}
95 for _, e := range ctrl.AllToolContractEntries() {
96 names[e.Name] = true
97 }
98 visible := map[string]bool{}
99 for _, e := range ctrl.ToolContractEntries() {
100 visible[e.Name] = true
101 }
102 ctrl.Close()
103 // Full registry always has skill/install tools + use_capability.
104 for _, name := range []string{"install_source", "run_skill", "read_skill", "use_capability"} {
105 if !names[name] {
106 t.Fatalf("normal mode (%q) must register %s in the capability registry", tokenMode, name)
107 }
108 }
109 // Provider-visible surface is the unified core for every role setting.
110 if !visible["use_capability"] {
111 t.Fatalf("normal mode (%q) must expose use_capability", tokenMode)
112 }
113 if visible["connect_tool_source"] {
114 t.Fatalf("normal mode (%q) must not expose connect_tool_source", tokenMode)
115 }
116 }
117 }
118
119 func TestBuildStillSpawnsExtraPluginsWhenSafeModeEnvSet(t *testing.T) {
120 if runtime.GOOS == "windows" {
121 t.Skip("sentinel spec uses /bin/sh")
122 }
123 isolateConfigHome(t)
124 workspace := robustTempDir(t)
125 t.Chdir(workspace)
126 t.Setenv("REASONIX_SAFE_MODE", "1")
127 marker := filepath.Join(plugin.MCPStateDir(config.ReasonixHomeDir(), workspace, "acp-extra"), "started")
128 ctrl, err := Build(context.Background(), Options{
129 SessionDir: filepath.Join(t.TempDir(), "sessions"),
130 Sink: event.Discard,
131 ExtraPlugins: []plugin.Spec{{
132 Name: "acp-extra",
133 Command: "/bin/sh",
134 Args: []string{"-c", "echo started > '" + marker + "'"},
135 }},
136 })
137 if err != nil {
138 t.Fatalf("Build: %v", err)
139 }
140 ctrl.Close()
141 deadline := time.Now().Add(5 * time.Second)
142 for {
143 if _, err := os.Stat(marker); err == nil {
144 return
145 }
146 if time.Now().After(deadline) {
147 t.Fatal("host-supplied MCP server never spawned; REASONIX_SAFE_MODE must not drop ExtraPlugins")
148 }
149 time.Sleep(10 * time.Millisecond)
150 }
151 }
152
152 lines GO