magic-agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +392 -0
- package/cmd/magic-agent/main.go +12 -0
- package/cmd/magic-client/main.go +144 -0
- package/go.mod +11 -0
- package/go.sum +12 -0
- package/internal/agent/arkclaw.go +587 -0
- package/internal/agent/arkclaw_stream.go +312 -0
- package/internal/agent/arkclaw_stream_test.go +428 -0
- package/internal/agent/arkclaw_test.go +827 -0
- package/internal/agent/ask.go +694 -0
- package/internal/agent/ask_test.go +587 -0
- package/internal/agent/attachments.go +200 -0
- package/internal/agent/attachments_test.go +481 -0
- package/internal/agent/capability.go +154 -0
- package/internal/agent/capability_test.go +190 -0
- package/internal/agent/claude.go +406 -0
- package/internal/agent/codebuddy.go +1044 -0
- package/internal/agent/codebuddy_account_test.go +128 -0
- package/internal/agent/codebuddy_gateway.go +479 -0
- package/internal/agent/codebuddy_gateway_stream.go +358 -0
- package/internal/agent/codebuddy_gateway_test.go +673 -0
- package/internal/agent/codex.go +599 -0
- package/internal/agent/codex_home.go +206 -0
- package/internal/agent/codex_home_test.go +130 -0
- package/internal/agent/codex_stream_test.go +97 -0
- package/internal/agent/codex_test.go +280 -0
- package/internal/agent/custom_models.go +197 -0
- package/internal/agent/custom_models_test.go +283 -0
- package/internal/agent/dsh.go +754 -0
- package/internal/agent/dsh_sdk.go +679 -0
- package/internal/agent/dsh_sdk_test.go +726 -0
- package/internal/agent/dsh_test.go +759 -0
- package/internal/agent/engine.go +469 -0
- package/internal/agent/engine_base.go +497 -0
- package/internal/agent/engine_base_test.go +308 -0
- package/internal/agent/engine_test.go +858 -0
- package/internal/agent/env.go +88 -0
- package/internal/agent/env_test.go +106 -0
- package/internal/agent/jsonextract.go +128 -0
- package/internal/agent/jsonextract_test.go +110 -0
- package/internal/agent/keepalive_test.go +216 -0
- package/internal/agent/llmengine.go +470 -0
- package/internal/agent/llmengine_test.go +551 -0
- package/internal/agent/llmhttp.go +437 -0
- package/internal/agent/llmhttp_test.go +304 -0
- package/internal/agent/llmmodels.go +205 -0
- package/internal/agent/login.go +27 -0
- package/internal/agent/main_test.go +23 -0
- package/internal/agent/models.go +686 -0
- package/internal/agent/models_test.go +877 -0
- package/internal/agent/openclaw.go +543 -0
- package/internal/agent/openclaw_acp.go +949 -0
- package/internal/agent/openclaw_acp_test.go +406 -0
- package/internal/agent/openclaw_test.go +451 -0
- package/internal/agent/output.go +191 -0
- package/internal/agent/permission.go +362 -0
- package/internal/agent/permission_test.go +526 -0
- package/internal/agent/prompt.go +65 -0
- package/internal/agent/runcmd.go +259 -0
- package/internal/agent/runcmd_test.go +81 -0
- package/internal/agent/runcmd_unix.go +56 -0
- package/internal/agent/runcmd_windows.go +66 -0
- package/internal/agent/runner.go +180 -0
- package/internal/agent/runner_test.go +263 -0
- package/internal/agent/spawnhook_test.go +96 -0
- package/internal/agent/stream.go +882 -0
- package/internal/agent/stream_test.go +565 -0
- package/internal/agent/streamjson.go +185 -0
- package/internal/agent/tags.go +52 -0
- package/internal/agent/toolkind.go +96 -0
- package/internal/agent/toolkind_test.go +70 -0
- package/internal/agent/tools_suffix_test.go +261 -0
- package/internal/agent/trae.go +342 -0
- package/internal/agent/workspace_test.go +166 -0
- package/internal/boundary/boundary_test.go +165 -0
- package/internal/cli/ask.go +1113 -0
- package/internal/cli/ask_test.go +1088 -0
- package/internal/cli/attach_test.go +239 -0
- package/internal/cli/contract_test.go +157 -0
- package/internal/cli/control.go +149 -0
- package/internal/cli/control_test.go +192 -0
- package/internal/cli/engines_test.go +205 -0
- package/internal/cli/events.go +198 -0
- package/internal/cli/events_test.go +166 -0
- package/internal/cli/keepalive_test.go +391 -0
- package/internal/cli/login.go +78 -0
- package/internal/cli/permission_test.go +295 -0
- package/internal/cli/prompt_source_test.go +222 -0
- package/internal/cli/root.go +752 -0
- package/internal/cli/session.go +486 -0
- package/internal/cli/session_test.go +202 -0
- package/internal/cli/sessionlog.go +325 -0
- package/internal/cli/sessionlog_command_test.go +204 -0
- package/internal/cli/sessionlog_test.go +262 -0
- package/internal/cli/sessionlog_user_test.go +129 -0
- package/internal/cli/toolkind_wire_test.go +84 -0
- package/internal/cli/workspace_test.go +188 -0
- package/internal/client/cors_test.go +136 -0
- package/internal/client/lan.go +68 -0
- package/internal/client/lan_test.go +53 -0
- package/internal/client/pair.go +163 -0
- package/internal/client/pair_test.go +109 -0
- package/internal/client/profile.go +184 -0
- package/internal/client/service.go +888 -0
- package/internal/client/service_test.go +654 -0
- package/internal/client/session_events_test.go +186 -0
- package/internal/client/session_messages_test.go +150 -0
- package/internal/config/config.go +439 -0
- package/internal/config/config_test.go +448 -0
- package/internal/session/session.go +447 -0
- package/internal/session/session_append.go +241 -0
- package/internal/session/session_append_test.go +161 -0
- package/internal/session/session_kill_unix_test.go +90 -0
- package/internal/session/session_mark_test.go +95 -0
- package/internal/session/session_test.go +237 -0
- package/internal/session/sessionlog.go +128 -0
- package/internal/session/sessionlog_read_test.go +157 -0
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/install.js +121 -0
- package/npm/lib/platform.js +78 -0
- package/package.json +36 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
package config
|
|
2
|
+
|
|
3
|
+
// config_test.go - 配置文件加载器单元测试。
|
|
4
|
+
//
|
|
5
|
+
// 覆盖:路径解析优先级(MAGIC_AGENT_CONFIG / XDG_CONFIG_HOME / ~/.config)、
|
|
6
|
+
// 文件缺失与空文件视为零值、语法错必须上报、宽松键名兼容、环境变量覆盖
|
|
7
|
+
// 文件值、Ready/Missing 判定。
|
|
8
|
+
//
|
|
9
|
+
// 全部用 t.TempDir(),不触碰用户真实配置。
|
|
10
|
+
|
|
11
|
+
import (
|
|
12
|
+
"os"
|
|
13
|
+
"path/filepath"
|
|
14
|
+
"strings"
|
|
15
|
+
"testing"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// clearEnv 清空本包关心的全部环境变量,保证测试不受外部环境影响。
|
|
19
|
+
func clearEnv(t *testing.T) {
|
|
20
|
+
t.Helper()
|
|
21
|
+
for _, k := range []string{EnvPath, "XDG_CONFIG_HOME", EnvArkClawURL, EnvArkClawKey, EnvArkClawClawID, EnvSystemPrompt} {
|
|
22
|
+
t.Setenv(k, "")
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// writeConfig 把内容落到临时文件,返回路径。
|
|
27
|
+
func writeConfig(t *testing.T, content string) string {
|
|
28
|
+
t.Helper()
|
|
29
|
+
p := filepath.Join(t.TempDir(), "config.json")
|
|
30
|
+
if err := os.WriteFile(p, []byte(content), 0o600); err != nil {
|
|
31
|
+
t.Fatal(err)
|
|
32
|
+
}
|
|
33
|
+
return p
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func TestLoadFromFile(t *testing.T) {
|
|
37
|
+
clearEnv(t)
|
|
38
|
+
p := writeConfig(t, `{"arkclaw":{"url":"https://h/a2a/jsonrpc","key":"k1","claw_id":"ci-1"}}`)
|
|
39
|
+
|
|
40
|
+
c, err := LoadFrom(p)
|
|
41
|
+
if err != nil {
|
|
42
|
+
t.Fatalf("LoadFrom: %v", err)
|
|
43
|
+
}
|
|
44
|
+
if c.ArkClaw.URL != "https://h/a2a/jsonrpc" || c.ArkClaw.Key != "k1" || c.ArkClaw.ClawID != "ci-1" {
|
|
45
|
+
t.Errorf("ArkClaw = %+v", c.ArkClaw)
|
|
46
|
+
}
|
|
47
|
+
if !c.ArkClaw.Ready() {
|
|
48
|
+
t.Errorf("Ready() = false, Missing = %v", c.ArkClaw.Missing())
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func TestLoadFromAcceptsLooseKeys(t *testing.T) {
|
|
53
|
+
clearEnv(t)
|
|
54
|
+
// 等价写法:endpoint / apikey / clawId。
|
|
55
|
+
p := writeConfig(t, `{"arkclaw":{"endpoint":"https://h/a2a/jsonrpc","apikey":"k2","clawId":"ci-2"}}`)
|
|
56
|
+
|
|
57
|
+
c, err := LoadFrom(p)
|
|
58
|
+
if err != nil {
|
|
59
|
+
t.Fatalf("LoadFrom: %v", err)
|
|
60
|
+
}
|
|
61
|
+
if c.ArkClaw.URL != "https://h/a2a/jsonrpc" || c.ArkClaw.Key != "k2" || c.ArkClaw.ClawID != "ci-2" {
|
|
62
|
+
t.Errorf("宽松键名未生效: %+v", c.ArkClaw)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// api_key 也应被接受。
|
|
66
|
+
p2 := writeConfig(t, `{"arkclaw":{"url":"u","api_key":"k3","clawID":"ci-3"}}`)
|
|
67
|
+
c2, err := LoadFrom(p2)
|
|
68
|
+
if err != nil {
|
|
69
|
+
t.Fatalf("LoadFrom: %v", err)
|
|
70
|
+
}
|
|
71
|
+
if c2.ArkClaw.Key != "k3" || c2.ArkClaw.ClawID != "ci-3" {
|
|
72
|
+
t.Errorf("api_key / clawID 未生效: %+v", c2.ArkClaw)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func TestLoadFromMissingFileIsNotAnError(t *testing.T) {
|
|
77
|
+
clearEnv(t)
|
|
78
|
+
c, err := LoadFrom(filepath.Join(t.TempDir(), "absent.json"))
|
|
79
|
+
if err != nil {
|
|
80
|
+
t.Fatalf("文件不存在不应报错: %v", err)
|
|
81
|
+
}
|
|
82
|
+
if c.ArkClaw.URL != "" || c.ArkClaw.Key != "" || c.ArkClaw.ClawID != "" {
|
|
83
|
+
t.Errorf("期望零值配置, got %+v", c.ArkClaw)
|
|
84
|
+
}
|
|
85
|
+
if c.ArkClaw.Ready() {
|
|
86
|
+
t.Error("零值配置 Ready() 应为 false")
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func TestLoadFromEmptyPathIsNotAnError(t *testing.T) {
|
|
91
|
+
clearEnv(t)
|
|
92
|
+
if _, err := LoadFrom(""); err != nil {
|
|
93
|
+
t.Fatalf("空路径不应报错: %v", err)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func TestLoadFromBlankFileIsNotAnError(t *testing.T) {
|
|
98
|
+
clearEnv(t)
|
|
99
|
+
p := writeConfig(t, " \n\t\n")
|
|
100
|
+
c, err := LoadFrom(p)
|
|
101
|
+
if err != nil {
|
|
102
|
+
t.Fatalf("空白文件不应报错: %v", err)
|
|
103
|
+
}
|
|
104
|
+
if c.ArkClaw.URL != "" {
|
|
105
|
+
t.Errorf("期望零值, got %+v", c.ArkClaw)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func TestLoadFromMalformedReportsError(t *testing.T) {
|
|
110
|
+
clearEnv(t)
|
|
111
|
+
p := writeConfig(t, `{"arkclaw":{"url":`)
|
|
112
|
+
_, err := LoadFrom(p)
|
|
113
|
+
if err == nil {
|
|
114
|
+
t.Fatal("语法错必须上报,不能静默吞掉")
|
|
115
|
+
}
|
|
116
|
+
if !strings.Contains(err.Error(), "parse config") {
|
|
117
|
+
t.Errorf("err = %v", err)
|
|
118
|
+
}
|
|
119
|
+
if !strings.Contains(err.Error(), p) {
|
|
120
|
+
t.Errorf("错误信息应含路径 %q: %v", p, err)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func TestEnvOverridesFile(t *testing.T) {
|
|
125
|
+
clearEnv(t)
|
|
126
|
+
p := writeConfig(t, `{"arkclaw":{"url":"https://file/a2a/jsonrpc","key":"file-key","claw_id":"file-claw"}}`)
|
|
127
|
+
t.Setenv(EnvArkClawURL, "https://env/a2a/jsonrpc")
|
|
128
|
+
t.Setenv(EnvArkClawKey, "env-key")
|
|
129
|
+
|
|
130
|
+
c, err := LoadFrom(p)
|
|
131
|
+
if err != nil {
|
|
132
|
+
t.Fatalf("LoadFrom: %v", err)
|
|
133
|
+
}
|
|
134
|
+
if c.ArkClaw.URL != "https://env/a2a/jsonrpc" {
|
|
135
|
+
t.Errorf("URL = %q want env 值", c.ArkClaw.URL)
|
|
136
|
+
}
|
|
137
|
+
if c.ArkClaw.Key != "env-key" {
|
|
138
|
+
t.Errorf("Key = %q want env 值", c.ArkClaw.Key)
|
|
139
|
+
}
|
|
140
|
+
// 未被环境变量覆盖的字段应保留文件值。
|
|
141
|
+
if c.ArkClaw.ClawID != "file-claw" {
|
|
142
|
+
t.Errorf("ClawID = %q want 文件值", c.ArkClaw.ClawID)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
func TestEnvOnlyWithoutFile(t *testing.T) {
|
|
147
|
+
clearEnv(t)
|
|
148
|
+
t.Setenv(EnvArkClawURL, "https://env/a2a/jsonrpc")
|
|
149
|
+
t.Setenv(EnvArkClawKey, "env-key")
|
|
150
|
+
t.Setenv(EnvArkClawClawID, "env-claw")
|
|
151
|
+
|
|
152
|
+
c, err := LoadFrom(filepath.Join(t.TempDir(), "absent.json"))
|
|
153
|
+
if err != nil {
|
|
154
|
+
t.Fatalf("LoadFrom: %v", err)
|
|
155
|
+
}
|
|
156
|
+
if !c.ArkClaw.Ready() {
|
|
157
|
+
t.Errorf("纯环境变量应可用: %+v", c.ArkClaw)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── 默认系统提示词(systemPrompt)─────────────────────────────
|
|
162
|
+
|
|
163
|
+
// 顶层 systemPrompt 读取,且与 arkclaw 一节互不影响。
|
|
164
|
+
func TestLoadSystemPrompt(t *testing.T) {
|
|
165
|
+
clearEnv(t)
|
|
166
|
+
p := writeConfig(t, `{
|
|
167
|
+
"systemPrompt": "你是一个中文助手,始终用中文回答所有问题。",
|
|
168
|
+
"arkclaw": {"url":"u","key":"k","claw_id":"c"}
|
|
169
|
+
}`)
|
|
170
|
+
|
|
171
|
+
c, err := LoadFrom(p)
|
|
172
|
+
if err != nil {
|
|
173
|
+
t.Fatalf("LoadFrom: %v", err)
|
|
174
|
+
}
|
|
175
|
+
if c.SystemPrompt != "你是一个中文助手,始终用中文回答所有问题。" {
|
|
176
|
+
t.Errorf("SystemPrompt = %q", c.SystemPrompt)
|
|
177
|
+
}
|
|
178
|
+
if !c.ArkClaw.Ready() {
|
|
179
|
+
t.Errorf("arkclaw 一节应同时生效: %+v", c.ArkClaw)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 键名宽松兼容:system_prompt / system 任选其一。
|
|
184
|
+
func TestLoadSystemPromptLooseKeys(t *testing.T) {
|
|
185
|
+
clearEnv(t)
|
|
186
|
+
for _, tc := range []struct{ name, body string }{
|
|
187
|
+
{"system_prompt", `{"system_prompt":"用中文回答"}`},
|
|
188
|
+
{"system", `{"system":"用中文回答"}`},
|
|
189
|
+
{"camelCase", `{"systemPrompt":"用中文回答"}`},
|
|
190
|
+
} {
|
|
191
|
+
c, err := LoadFrom(writeConfig(t, tc.body))
|
|
192
|
+
if err != nil {
|
|
193
|
+
t.Fatalf("%s: LoadFrom: %v", tc.name, err)
|
|
194
|
+
}
|
|
195
|
+
if c.SystemPrompt != "用中文回答" {
|
|
196
|
+
t.Errorf("%s: SystemPrompt = %q want 用中文回答", tc.name, c.SystemPrompt)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 未配置 / 空白 → 空串(= 不注入,保持原行为)。
|
|
202
|
+
func TestSystemPromptAbsentIsEmpty(t *testing.T) {
|
|
203
|
+
clearEnv(t)
|
|
204
|
+
c, err := LoadFrom(filepath.Join(t.TempDir(), "absent.json"))
|
|
205
|
+
if err != nil {
|
|
206
|
+
t.Fatalf("LoadFrom: %v", err)
|
|
207
|
+
}
|
|
208
|
+
if c.SystemPrompt != "" {
|
|
209
|
+
t.Errorf("未配置时应为空串, got %q", c.SystemPrompt)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
c2, err := LoadFrom(writeConfig(t, `{"systemPrompt":" "}`))
|
|
213
|
+
if err != nil {
|
|
214
|
+
t.Fatalf("LoadFrom: %v", err)
|
|
215
|
+
}
|
|
216
|
+
if c2.SystemPrompt != "" {
|
|
217
|
+
t.Errorf("纯空白应视为未配置, got %q", c2.SystemPrompt)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 环境变量覆盖文件值(文件里有别的值也不生效)。
|
|
222
|
+
func TestSystemPromptEnvOverridesFile(t *testing.T) {
|
|
223
|
+
clearEnv(t)
|
|
224
|
+
p := writeConfig(t, `{"systemPrompt":"文件里的"}`)
|
|
225
|
+
t.Setenv(EnvSystemPrompt, "环境变量里的")
|
|
226
|
+
|
|
227
|
+
c, err := LoadFrom(p)
|
|
228
|
+
if err != nil {
|
|
229
|
+
t.Fatalf("LoadFrom: %v", err)
|
|
230
|
+
}
|
|
231
|
+
if c.SystemPrompt != "环境变量里的" {
|
|
232
|
+
t.Errorf("SystemPrompt = %q want 环境变量值", c.SystemPrompt)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 纯环境变量、无配置文件同样可用。
|
|
237
|
+
func TestSystemPromptEnvOnlyWithoutFile(t *testing.T) {
|
|
238
|
+
clearEnv(t)
|
|
239
|
+
t.Setenv(EnvSystemPrompt, "只靠环境变量")
|
|
240
|
+
|
|
241
|
+
c, err := LoadFrom(filepath.Join(t.TempDir(), "absent.json"))
|
|
242
|
+
if err != nil {
|
|
243
|
+
t.Fatalf("LoadFrom: %v", err)
|
|
244
|
+
}
|
|
245
|
+
if c.SystemPrompt != "只靠环境变量" {
|
|
246
|
+
t.Errorf("SystemPrompt = %q", c.SystemPrompt)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
func TestPathPrefersConfigEnv(t *testing.T) {
|
|
251
|
+
clearEnv(t)
|
|
252
|
+
t.Setenv(EnvPath, "/tmp/magic-agent-explicit.json")
|
|
253
|
+
if got := Path(); got != "/tmp/magic-agent-explicit.json" {
|
|
254
|
+
t.Errorf("Path() = %q", got)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
func TestPathExpandsTilde(t *testing.T) {
|
|
259
|
+
clearEnv(t)
|
|
260
|
+
home, err := os.UserHomeDir()
|
|
261
|
+
if err != nil || home == "" {
|
|
262
|
+
t.Skip("无法取得 home 目录")
|
|
263
|
+
}
|
|
264
|
+
t.Setenv(EnvPath, "~/custom/config.json")
|
|
265
|
+
if got, want := Path(), filepath.Join(home, "custom", "config.json"); got != want {
|
|
266
|
+
t.Errorf("Path() = %q want %q", got, want)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
func TestPathUsesXDGConfigHome(t *testing.T) {
|
|
271
|
+
clearEnv(t)
|
|
272
|
+
// ⚠️ 必须把 HOME 指到空临时目录:探测链第 2 档是 `~/.magic-agent/config.json`,
|
|
273
|
+
// 而**开发机上这个文件是真实存在的**(用户 2026-09-23 定稿的新位置)——
|
|
274
|
+
// 不隔离 HOME,本用例就会拿真机文件当输入,在别人的机器上假红/假绿。
|
|
275
|
+
t.Setenv("HOME", t.TempDir())
|
|
276
|
+
t.Setenv("XDG_CONFIG_HOME", "/tmp/xdg")
|
|
277
|
+
if got, want := Path(), filepath.Join("/tmp/xdg", "magic-agent", "config.json"); got != want {
|
|
278
|
+
t.Errorf("Path() = %q want %q", got, want)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
func TestPathFallsBackToHomeConfig(t *testing.T) {
|
|
283
|
+
clearEnv(t)
|
|
284
|
+
home := t.TempDir()
|
|
285
|
+
t.Setenv("HOME", home)
|
|
286
|
+
if got, want := Path(), filepath.Join(home, ".config", "magic-agent", "config.json"); got != want {
|
|
287
|
+
t.Errorf("Path() = %q want %q", got, want)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* ~/.magic-agent/config.json 的优先级(2026-09-23 加,用户:「应该放在 ~/.magic-agent/ 下」)。
|
|
292
|
+
* 两条一起钉才有意义:**存在时用它**、**不存在时不顶掉老位置** ——
|
|
293
|
+
* 只钉前者会把「老配置被静默忽略」这个最坏的回归放过去。 */
|
|
294
|
+
func TestPathPrefersMagicAgentHomeConfig(t *testing.T) {
|
|
295
|
+
clearEnv(t)
|
|
296
|
+
home := t.TempDir()
|
|
297
|
+
t.Setenv("HOME", home)
|
|
298
|
+
newPath := filepath.Join(home, ".magic-agent", "config.json")
|
|
299
|
+
if err := os.MkdirAll(filepath.Dir(newPath), 0o755); err != nil {
|
|
300
|
+
t.Fatal(err)
|
|
301
|
+
}
|
|
302
|
+
if err := os.WriteFile(newPath, []byte(`{"systemPrompt":"x"}`), 0o600); err != nil {
|
|
303
|
+
t.Fatal(err)
|
|
304
|
+
}
|
|
305
|
+
// 老位置**同时存在**:新位置必须赢(否则「文件放对了却不生效」)。
|
|
306
|
+
oldDir := filepath.Join(home, ".config", "magic-agent")
|
|
307
|
+
if err := os.MkdirAll(oldDir, 0o755); err != nil {
|
|
308
|
+
t.Fatal(err)
|
|
309
|
+
}
|
|
310
|
+
if err := os.WriteFile(filepath.Join(oldDir, "config.json"), []byte(`{"systemPrompt":"old"}`), 0o600); err != nil {
|
|
311
|
+
t.Fatal(err)
|
|
312
|
+
}
|
|
313
|
+
if got := Path(); got != newPath {
|
|
314
|
+
t.Errorf("Path() = %q want %q(~/.magic-agent/config.json 存在时应优先)", got, newPath)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
func TestPathKeepsOldLocationWhenNewMissing(t *testing.T) {
|
|
319
|
+
clearEnv(t)
|
|
320
|
+
home := t.TempDir()
|
|
321
|
+
t.Setenv("HOME", home)
|
|
322
|
+
old := filepath.Join(home, ".config", "magic-agent", "config.json")
|
|
323
|
+
if err := os.MkdirAll(filepath.Dir(old), 0o755); err != nil {
|
|
324
|
+
t.Fatal(err)
|
|
325
|
+
}
|
|
326
|
+
if err := os.WriteFile(old, []byte(`{"systemPrompt":"old"}`), 0o600); err != nil {
|
|
327
|
+
t.Fatal(err)
|
|
328
|
+
}
|
|
329
|
+
// 新位置**不存在** → 老位置必须照旧生效(不能因为「新位置优先」就把它顶掉)。
|
|
330
|
+
if got := Path(); got != old {
|
|
331
|
+
t.Errorf("Path() = %q want %q(新位置不存在时不许顶掉老位置)", got, old)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* agents 数组的解析(2026-09-23):两种写法都要认 —— 凭据内嵌在 URL 里 / 分开写字段;
|
|
336
|
+
* 键名别名与 arkclaw 节同一套(url/endpoint、key/apikey/api_key、claw_id/clawId/clawID)。 */
|
|
337
|
+
func TestAgentsParsedWithBothCredentialStyles(t *testing.T) {
|
|
338
|
+
p := writeConfig(t, `{
|
|
339
|
+
"agents": [
|
|
340
|
+
{ "name": "MagicAI", "url": "https://h/a2a/jsonrpc?apikey=K&clawId=C" },
|
|
341
|
+
{ "name": "Second", "endpoint": "https://h2/a2a/jsonrpc", "apikey": "K2", "clawId": "C2" }
|
|
342
|
+
]
|
|
343
|
+
}`)
|
|
344
|
+
c, err := LoadFrom(p)
|
|
345
|
+
if err != nil {
|
|
346
|
+
t.Fatalf("LoadFrom: %v", err)
|
|
347
|
+
}
|
|
348
|
+
if len(c.Agents) != 2 {
|
|
349
|
+
t.Fatalf("Agents = %d 条,want 2", len(c.Agents))
|
|
350
|
+
}
|
|
351
|
+
a := c.Agents[0]
|
|
352
|
+
if a.Name != "MagicAI" || a.URL != "https://h/a2a/jsonrpc?apikey=K&clawId=C" {
|
|
353
|
+
t.Errorf("第 1 条 = %+v(URL 里内嵌凭据的写法)", a)
|
|
354
|
+
}
|
|
355
|
+
b := c.Agents[1]
|
|
356
|
+
if b.Name != "Second" || b.URL != "https://h2/a2a/jsonrpc" || b.Key != "K2" || b.ClawID != "C2" {
|
|
357
|
+
t.Errorf("第 2 条 = %+v(字段分开写 + 别名 endpoint/apikey/clawId)", b)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
func TestAgentsAbsentIsNotAnError(t *testing.T) {
|
|
362
|
+
// 老配置(没有 agents 节)必须照旧可用 —— 新增字段不许把存量配置打成「解析失败」。
|
|
363
|
+
p := writeConfig(t, `{"arkclaw":{"url":"u","key":"k","claw_id":"c"}}`)
|
|
364
|
+
c, err := LoadFrom(p)
|
|
365
|
+
if err != nil {
|
|
366
|
+
t.Fatalf("LoadFrom: %v", err)
|
|
367
|
+
}
|
|
368
|
+
if len(c.Agents) != 0 {
|
|
369
|
+
t.Errorf("Agents = %v,want 空", c.Agents)
|
|
370
|
+
}
|
|
371
|
+
if !c.ArkClaw.Ready() {
|
|
372
|
+
t.Error("arkclaw 节应照旧解析出来")
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
func TestMissingListsFieldNames(t *testing.T) {
|
|
377
|
+
got := ArkClawConfig{URL: "u"}.Missing()
|
|
378
|
+
if len(got) != 2 || got[0] != "key" || got[1] != "claw_id" {
|
|
379
|
+
t.Errorf("Missing() = %v want [key claw_id]", got)
|
|
380
|
+
}
|
|
381
|
+
if (ArkClawConfig{}).Ready() {
|
|
382
|
+
t.Error("空配置 Ready() 应为 false")
|
|
383
|
+
}
|
|
384
|
+
if !(ArkClawConfig{URL: "u", Key: "k", ClawID: "c"}).Ready() {
|
|
385
|
+
t.Error("三项齐备 Ready() 应为 true")
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
func TestWhitespaceOnlyValuesTreatedAsMissing(t *testing.T) {
|
|
390
|
+
// 只有空白的值等同于未配置,避免把 " " 当成合法 apikey 发出去。
|
|
391
|
+
c := ArkClawConfig{URL: " ", Key: "k", ClawID: "c"}
|
|
392
|
+
if c.Ready() {
|
|
393
|
+
t.Error("纯空白 URL 应视为缺失")
|
|
394
|
+
}
|
|
395
|
+
if got := c.Missing(); len(got) != 1 || got[0] != "url" {
|
|
396
|
+
t.Errorf("Missing() = %v", got)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
func TestLoadUsesPath(t *testing.T) {
|
|
401
|
+
clearEnv(t)
|
|
402
|
+
dir := t.TempDir()
|
|
403
|
+
p := filepath.Join(dir, "config.json")
|
|
404
|
+
if err := os.WriteFile(p, []byte(`{"arkclaw":{"url":"u","key":"k","claw_id":"c"}}`), 0o600); err != nil {
|
|
405
|
+
t.Fatal(err)
|
|
406
|
+
}
|
|
407
|
+
t.Setenv(EnvPath, p)
|
|
408
|
+
|
|
409
|
+
c, err := Load()
|
|
410
|
+
if err != nil {
|
|
411
|
+
t.Fatalf("Load: %v", err)
|
|
412
|
+
}
|
|
413
|
+
if c.ArkClaw.ClawID != "c" {
|
|
414
|
+
t.Errorf("Load() 未走 EnvPath: %+v", c.ArkClaw)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
func TestExpandHome(t *testing.T) {
|
|
419
|
+
home, err := os.UserHomeDir()
|
|
420
|
+
if err != nil || home == "" {
|
|
421
|
+
t.Skip("无法取得 home 目录")
|
|
422
|
+
}
|
|
423
|
+
cases := map[string]string{
|
|
424
|
+
"": "",
|
|
425
|
+
"/abs/path": "/abs/path",
|
|
426
|
+
"rel/path": "rel/path",
|
|
427
|
+
"~": home,
|
|
428
|
+
"~/a/b": filepath.Join(home, "a", "b"),
|
|
429
|
+
"~user/x": "~user/x", // 不支持 ~user 形式,原样返回
|
|
430
|
+
}
|
|
431
|
+
for in, want := range cases {
|
|
432
|
+
if got := expandHome(in); got != want {
|
|
433
|
+
t.Errorf("expandHome(%q) = %q want %q", in, got, want)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
func TestFirstNonEmpty(t *testing.T) {
|
|
439
|
+
if got := firstNonEmpty("", " ", "b", "c"); got != "b" {
|
|
440
|
+
t.Errorf("firstNonEmpty = %q want b", got)
|
|
441
|
+
}
|
|
442
|
+
if got := firstNonEmpty("", " "); got != "" {
|
|
443
|
+
t.Errorf("firstNonEmpty = %q want empty", got)
|
|
444
|
+
}
|
|
445
|
+
if got := firstNonEmpty(" a "); got != "a" {
|
|
446
|
+
t.Errorf("firstNonEmpty 应去空白, got %q", got)
|
|
447
|
+
}
|
|
448
|
+
}
|