返回 DeepSeek-Reasonix
dingtalk_save_test.go
根目录 / desktop / dingtalk_save_test.go
1 package main
2
3 import (
4 "testing"
5
6 "reasonix/internal/config"
7 )
8
9 // TestSetBotSettingsDingtalkRoundTrip 验证钉钉配置经 SetBotSettings 后能正确落盘并被读回。
10 // 这是排查「钉钉配置界面几个字段写进去无法保存」的回归测试。
11 func TestSetBotSettingsDingtalkRoundTrip(t *testing.T) {
12 isolateDesktopUserDirs(t)
13 cfg := config.Default()
14 cfg.Bot.Dingtalk = config.DingtalkBotConfig{
15 Enabled: true,
16 ClientID: "appkey-initial",
17 SecretEnv: "DINGTALK_CLIENT_SECRET",
18 BotName: "InitialBot",
19 RequireMention: true,
20 SessionMappings: []config.BotConnectionSessionMapping{{
21 RemoteID: "cid-preserved",
22 Scope: "global",
23 SessionID: "path:/sessions/preserved.jsonl",
24 SessionSource: "auto",
25 }},
26 }
27 if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
28 t.Fatalf("save initial: %v", err)
29 }
30
31 app := NewApp()
32 view := botSettingsView(cfg.Bot)
33 view.Dingtalk.ClientID = "appkey-edited"
34 view.Dingtalk.BotName = "EditedBot"
35 view.Dingtalk.RequireMention = false
36 view.Dingtalk.ClientSecretEnv = "DINGTALK_CLIENT_SECRET"
37 if err := app.SetBotSettings(view); err != nil {
38 t.Fatalf("SetBotSettings: %v", err)
39 }
40
41 got := config.LoadForEditWithoutCredentials(config.UserConfigPath())
42 if got.Bot.Dingtalk.ClientID != "appkey-edited" {
43 t.Fatalf("clientId = %q, want appkey-edited", got.Bot.Dingtalk.ClientID)
44 }
45 if got.Bot.Dingtalk.BotName != "EditedBot" {
46 t.Fatalf("botName = %q, want EditedBot", got.Bot.Dingtalk.BotName)
47 }
48 if got.Bot.Dingtalk.RequireMention {
49 t.Fatalf("requireMention = true, want false")
50 }
51 if got.Bot.Dingtalk.SecretEnv != "DINGTALK_CLIENT_SECRET" {
52 t.Fatalf("secretEnv = %q, want DINGTALK_CLIENT_SECRET", got.Bot.Dingtalk.SecretEnv)
53 }
54 if mappings := got.Bot.Dingtalk.SessionMappings; len(mappings) != 1 || mappings[0].RemoteID != "cid-preserved" || mappings[0].SessionID != "path:/sessions/preserved.jsonl" {
55 t.Fatalf("session mappings = %+v, want hidden legacy mapping preserved", mappings)
56 }
57 }
58
59 // TestDesktopBotConfigConfiguredDetectsDingtalk 防止回归:用户只配置钉钉(其他平台
60 // 均未配置)时,desktopBotConfigConfigured 必须返回 true,否则 legacy 合并路径会
61 // 用 legacy config 整体覆盖 userCfg.Bot,导致钉钉 ClientID 等配置被丢弃
62 // (表现为「输入 ClientID 后消失」)。
63 func TestDesktopBotConfigConfiguredDetectsDingtalk(t *testing.T) {
64 cfg := config.Default()
65 cfg.Bot.Dingtalk.ClientID = "dingappkey123"
66 if !desktopBotConfigConfigured(cfg.Bot) {
67 t.Fatalf("dingtalk-only bot config must count as configured")
68 }
69 // 仅 SecretEnv 配了也应算已配置(环境变量形式的密钥)。
70 cfg = config.Default()
71 cfg.Bot.Dingtalk.SecretEnv = "DINGTALK_CLIENT_SECRET"
72 if !desktopBotConfigConfigured(cfg.Bot) {
73 t.Fatalf("dingtalk secret-env-only config must count as configured")
74 }
75 }
76
77 // TestDingtalkRuntimeConnectionID 验证测试发送时解析到真实运行的 connection id
78 // (自定义 id 如 dingtalk-test 必须命中),而不是硬编码退回 "dingtalk"。
79 func TestDingtalkRuntimeConnectionID(t *testing.T) {
80 // 自定义 id:优先命中启用的钉钉连接。
81 got := dingtalkRuntimeConnectionID([]config.BotConnectionConfig{
82 {ID: "feishu-feishu", Provider: "feishu", Enabled: true},
83 {ID: "dingtalk-test", Provider: "dingtalk", Enabled: true},
84 })
85 if got != "dingtalk-test" {
86 t.Fatalf("runtime connection id = %q, want dingtalk-test", got)
87 }
88 // 未启用:跳过。
89 got = dingtalkRuntimeConnectionID([]config.BotConnectionConfig{
90 {ID: "dingtalk-disabled", Provider: "dingtalk", Enabled: false},
91 })
92 if got != "dingtalk" {
93 t.Fatalf("disabled connection must be skipped, got %q", got)
94 }
95 // 无 id 的钉钉连接:退回 provider 名。
96 got = dingtalkRuntimeConnectionID([]config.BotConnectionConfig{
97 {Provider: "dingtalk", Enabled: true},
98 })
99 if got != "dingtalk" {
100 t.Fatalf("id-less connection should fall back to %q, got %q", "dingtalk", got)
101 }
102 // 无任何钉钉连接:退回 "dingtalk"(legacy [bot.dingtalk] 路径)。
103 got = dingtalkRuntimeConnectionID(nil)
104 if got != "dingtalk" {
105 t.Fatalf("no connection should fall back to %q, got %q", "dingtalk", got)
106 }
107 }
108
109 // TestDingtalkRuntimeConnectionResolvesCredentials 验证测试发送凭据从所选
110 // connection 解析(而不是 legacy [bot.dingtalk] 块)——否则 connection 驱动的
111 // 运行时在 legacy 块为空时误报 dingtalk_secret_missing。
112 func TestDingtalkRuntimeConnectionResolvesCredentials(t *testing.T) {
113 conns := []config.BotConnectionConfig{
114 {ID: "feishu-feishu", Provider: "feishu", Enabled: true},
115 {ID: "dingtalk-main", Provider: "dingtalk", Enabled: true,
116 Credential: config.BotConnectionCredential{AppID: "app-from-conn", AppSecretEnv: "DINGTALK_CONN_SECRET"}},
117 }
118 conn, id, ok := dingtalkRuntimeConnection(conns)
119 if !ok {
120 t.Fatal("enabled dingtalk connection not found")
121 }
122 if id != "dingtalk-main" {
123 t.Fatalf("runtime id = %q, want dingtalk-main", id)
124 }
125 if conn.Credential.AppID != "app-from-conn" || conn.Credential.AppSecretEnv != "DINGTALK_CONN_SECRET" {
126 t.Fatalf("resolved connection credentials = %q/%q, want app-from-conn/DINGTALK_CONN_SECRET",
127 conn.Credential.AppID, conn.Credential.AppSecretEnv)
128 }
129 // 未启用的钉钉连接不应命中。
130 if _, _, ok := dingtalkRuntimeConnection([]config.BotConnectionConfig{
131 {ID: "dingtalk-off", Provider: "dingtalk", Enabled: false},
132 }); ok {
133 t.Fatal("disabled dingtalk connection must not be selected")
134 }
135 }
136
137 // TestSetBotSettingsDingtalkRuntimeOptionsRoundTrip 验证 legacy [bot.dingtalk]
138 // 的模型/工具审批模式/工作目录经 SetBotSettings 落盘并被读回。这是排查
139 // 「钉钉面板模型、权限切换无法保存」的回归测试:config 结构体与 settings
140 // view 都支持这些字段,但 render.go 漏写导致 SaveTo 丢弃(已被修复)。
141 func TestSetBotSettingsDingtalkRuntimeOptionsRoundTrip(t *testing.T) {
142 isolateDesktopUserDirs(t)
143 cfg := config.Default()
144 cfg.Bot.Dingtalk = config.DingtalkBotConfig{
145 Enabled: true,
146 ClientID: "ding-appkey",
147 ClientSecret: "secret",
148 SecretEnv: "DINGTALK_CLIENT_SECRET",
149 BotName: "安博特",
150 RequireMention: true,
151 Model: "deepseek-flash/deepseek-v4-flash",
152 ToolApprovalMode: "danger-full-access",
153 WorkspaceRoot: "/tmp/probe-ws",
154 }
155 if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
156 t.Fatalf("save initial: %v", err)
157 }
158
159 app := NewApp()
160 view := botSettingsView(cfg.Bot)
161 if view.Dingtalk.Model != "deepseek-flash/deepseek-v4-flash" {
162 t.Fatalf("view model = %q, want deepseek-flash", view.Dingtalk.Model)
163 }
164 if view.Dingtalk.ToolApprovalMode != "danger-full-access" {
165 t.Fatalf("view permission preset = %q, want danger-full-access", view.Dingtalk.ToolApprovalMode)
166 }
167 view.Dingtalk.Model = "deepseek-flash/deepseek-v4-flash"
168 view.Dingtalk.ToolApprovalMode = "danger-full-access"
169 view.Dingtalk.WorkspaceRoot = "/tmp/probe-ws"
170 if err := app.SetBotSettings(view); err != nil {
171 t.Fatalf("SetBotSettings: %v", err)
172 }
173
174 got := config.LoadForEditWithoutCredentials(config.UserConfigPath())
175 if got.Bot.Dingtalk.Model != "deepseek-flash/deepseek-v4-flash" {
176 t.Fatalf("model = %q, want deepseek-flash (render must persist)", got.Bot.Dingtalk.Model)
177 }
178 if got.Bot.Dingtalk.ToolApprovalMode != "danger-full-access" {
179 t.Fatalf("toolApprovalMode = %q, want danger-full-access (render must persist)", got.Bot.Dingtalk.ToolApprovalMode)
180 }
181 if got.Bot.Dingtalk.WorkspaceRoot != "/tmp/probe-ws" {
182 t.Fatalf("workspaceRoot = %q, want /tmp/probe-ws (render must persist)", got.Bot.Dingtalk.WorkspaceRoot)
183 }
184 // 原有字段不能丢。
185 if got.Bot.Dingtalk.ClientSecret != "secret" {
186 t.Fatalf("clientSecret lost: %q", got.Bot.Dingtalk.ClientSecret)
187 }
188 if got.Bot.Dingtalk.BotName != "安博特" {
189 t.Fatalf("botName lost: %q", got.Bot.Dingtalk.BotName)
190 }
191 }
192
192 lines GO