返回 DeepSeek-Reasonix
connection_regression_test.go
根目录 / internal / cli / connection_regression_test.go
1 package cli
2
3 import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 tea "charm.land/bubbletea/v2"
11 "reasonix/internal/config"
12 "reasonix/internal/control"
13 )
14
15 func TestAuditSetupEditsEffectiveProviderSource(t *testing.T) {
16 t.Setenv("REASONIX_HOME", t.TempDir())
17 root := t.TempDir()
18 global := config.Default()
19 global.Providers = []config.ProviderEntry{{Name: "relay", Kind: "openai", BaseURL: "https://global.invalid/v1", Model: "chat", APIKeyEnv: "GLOBAL_KEY"}}
20 if err := global.SaveTo(config.UserConfigPath()); err != nil {
21 t.Fatal(err)
22 }
23 project := filepath.Join(root, "reasonix.toml")
24 if err := os.WriteFile(project, []byte("[[providers]]\nname = \"relay\"\nkind = \"openai\"\nbase_url = \"https://project.invalid/v1\"\nmodel = \"chat\"\napi_key_env = \"PROJECT_KEY\"\n"), 0600); err != nil {
25 t.Fatal(err)
26 }
27 effective, err := config.LoadForRootReadOnly(root)
28 if err != nil {
29 t.Fatal(err)
30 }
31 entry, ok := effective.Provider("relay")
32 if !ok || entry.BaseURL != "https://global.invalid/v1" {
33 t.Fatal("fixture did not resolve global provider")
34 }
35 ctrl := control.New(control.Options{WorkspaceRoot: root})
36 t.Cleanup(ctrl.Close)
37 m := newTestChatTUI()
38 m.ctrl = ctrl
39 m.beginConnectionKeyEdit("relay")
40 if m.setup.configPath != config.UserConfigPath() {
41 t.Fatalf("setup edits shadowed project config %s instead of active global config", m.setup.configPath)
42 }
43 }
44
45 func TestAuditSetupPasteDoesNotEnterChat(t *testing.T) {
46 for _, native := range []bool{false, true} {
47 t.Run(fmt.Sprint(native), func(t *testing.T) {
48 m := newTestChatTUI()
49 m.setup = &connectionSetup{providerName: "relay"}
50 m.input.SetValue("user draft")
51 var msg tea.Msg = tea.PasteMsg{Content: "sk-audit-secret-value"}
52 if native {
53 msg = clipboardTextPasteMsg{text: "sk-audit-secret-value"}
54 }
55 updated, _ := m.Update(msg)
56 m = updated.(chatTUI)
57 if strings.Contains(m.input.Value(), "sk-audit-secret-value") {
58 t.Fatal("pasting into the credential editor inserted the secret into the chat composer")
59 }
60 if m.setup.key != "sk-audit-secret-value" {
61 t.Fatal("paste did not reach masked credential field")
62 }
63 })
64 }
65 }
66
67 func TestAuditShellSetupKeyOnlyEditPersists(t *testing.T) {
68 t.Setenv("REASONIX_HOME", t.TempDir())
69 c := config.Default()
70 c.Providers = []config.ProviderEntry{{Name: "relay", Kind: "openai", BaseURL: "https://relay.invalid/v1", Model: "chat", APIKeyEnv: "OLD_KEY"}}
71 path := config.UserConfigPath()
72 if err := c.SaveTo(path); err != nil {
73 t.Fatal(err)
74 }
75 c, err := config.LoadForEditReadOnlyStrict(path)
76 if err != nil {
77 t.Fatal(err)
78 }
79 s := newProviderSetupSessionForPath(c, path)
80 if err := s.setCredentialForProviders([]string{"relay"}, "OLD_KEY", "updated-secret"); err != nil {
81 t.Fatal(err)
82 }
83 written, err := commitProviderSetupSession(s, path)
84 if err != nil {
85 t.Fatal(err)
86 }
87 if !written {
88 t.Fatal("updating only an existing key silently skipped the commit")
89 }
90 saved, err := config.LoadForEditReadOnlyStrict(path)
91 if err != nil {
92 t.Fatal(err)
93 }
94 entry, _ := saved.Provider("relay")
95 if entry.APIKeyEnv == "OLD_KEY" || !config.CredentialStored(entry.APIKeyEnv) {
96 t.Fatal("new credential was not published")
97 }
98 }
99
100 func TestAuditShellSetupDistinctConnectionKeysStaySeparate(t *testing.T) {
101 t.Setenv("REASONIX_HOME", t.TempDir())
102 c := config.Default()
103 c.Providers = []config.ProviderEntry{{Name: "a", Kind: "openai", BaseURL: "https://relay.invalid/v1", Model: "chat", APIKeyEnv: "SHARED_KEY"}, {Name: "b", Kind: "openai", BaseURL: "https://relay.invalid/v1", Model: "chat", APIKeyEnv: "SHARED_KEY"}}
104 c.DefaultModel = "a/chat"
105 path := config.UserConfigPath()
106 if err := c.SaveTo(path); err != nil {
107 t.Fatal(err)
108 }
109 c, err := config.LoadForEditReadOnlyStrict(path)
110 if err != nil {
111 t.Fatal(err)
112 }
113 s := newProviderSetupSessionForPath(c, path)
114 // Make another legitimate edit so this exercises the commit rather than the
115 // independent key-only no-op defect.
116 if err := s.setDefaultModel("b/chat"); err != nil {
117 t.Fatal(err)
118 }
119 if err := s.setCredentialForProviders([]string{"a"}, "SHARED_KEY", "key-for-a"); err != nil {
120 t.Fatal(err)
121 }
122 if err := s.setCredentialForProviders([]string{"b"}, "SHARED_KEY", "key-for-b"); err != nil {
123 t.Fatal(err)
124 }
125 if _, err := commitProviderSetupSession(s, path); err != nil {
126 t.Fatal(err)
127 }
128 saved, err := config.LoadForEditReadOnlyStrict(path)
129 if err != nil {
130 t.Fatal(err)
131 }
132 a, _ := saved.Provider("a")
133 b, _ := saved.Provider("b")
134 if a.APIKeyEnv == b.APIKeyEnv {
135 t.Fatal("separate edits for A and B were coalesced into B's credential slot")
136 }
137 }
138
139 func TestShellCredentialOnlyEditRejectsConcurrentRotation(t *testing.T) {
140 t.Setenv("REASONIX_HOME", t.TempDir())
141 path := config.UserConfigPath()
142 c := config.Default()
143 c.Providers = []config.ProviderEntry{{Name: "relay", Kind: "openai", Model: "chat", BaseURL: "https://relay.invalid/v1", APIKeyEnv: "OLD_KEY"}}
144 if err := c.SaveTo(path); err != nil {
145 t.Fatal(err)
146 }
147 c, err := config.LoadForEditReadOnlyStrict(path)
148 if err != nil {
149 t.Fatal(err)
150 }
151 s := newProviderSetupSessionForPath(c, path)
152 if err := s.setCredentialForProviders([]string{"relay"}, "OLD_KEY", "stale-draft"); err != nil {
153 t.Fatal(err)
154 }
155 result, err := config.CommitConnectionCredential(config.ConnectionCredentialRequest{RequestID: "other-writer", ConfigPath: path, ProviderNames: []string{"relay"}, Key: "concurrent-key"})
156 if err != nil || !result.Persisted {
157 t.Fatalf("concurrent save: %+v, %v", result, err)
158 }
159 if _, err := commitProviderSetupSession(s, path); err == nil {
160 t.Fatal("stale credential draft overwrote concurrent rotation")
161 }
162 current, err := config.LoadForEditReadOnlyStrict(path)
163 if err != nil {
164 t.Fatal(err)
165 }
166 entry, _ := current.Provider("relay")
167 if entry.APIKeyEnv != result.Slot {
168 t.Fatal("concurrent slot reference changed")
169 }
170 }
171
171 lines GO