magic-agent 2.4.3 → 2.6.3
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/CLIENT-CONTRACT.md +467 -41
- package/README.md +15 -14
- package/client-ui/pc/app.css +74 -0
- package/client-ui/pc/js/04-task-list.js +5 -1
- package/client-ui/pc/js/07-chat-stream-render.js +3 -3
- package/client-ui/pc/js/90-glue.js +40 -2
- package/client-ui/pc/js/92-composer.js +19 -4
- package/client-ui/pc/js/93-rail.js +16 -5
- package/client-ui/shared/conv/02-media-kinds.js +2 -2
- package/client-ui/shared/conv/03-chat-message.js +247 -23
- package/client-ui/shared/conv/06-ask-answer.js +75 -1
- package/client-ui/shared/conv/09-stream-apply.js +72 -7
- package/client-ui/shared/conv/19-attachments.js +157 -6
- package/client-ui/shared/conv/27-conv-ledger.js +5 -2
- package/client-ui/shared/conv/29-msg-variants.js +217 -0
- package/client-ui/shared/conv/conv.css +329 -174
- package/client-ui/shared/conv/dom-map.md +1 -0
- package/client-ui/shared/conversation.css +13 -2
- package/client-ui/shared/conversation.js +373 -50
- package/client-ui/shared/transport-http.js +14 -0
- package/client-ui-mobile/apps.css +16 -0
- package/client-ui-mobile/apps.js +406 -0
- package/client-ui-mobile/index.html +5554 -639
- package/client-ui-mobile/reqs.css +110 -1
- package/client-ui-mobile/reqs.js +533 -74
- package/client-ui-mobile/reqshot.js +505 -0
- package/client-ui-mobile/scripts.css +67 -0
- package/client-ui-mobile/scripts.js +599 -0
- package/client-ui-mobile/vendor/cose-base.js +3214 -0
- package/client-ui-mobile/vendor/cytoscape-fcose.js +1549 -0
- package/client-ui-mobile/vendor/cytoscape.min.js +32 -0
- package/client-ui-mobile/vendor/layout-base.js +5230 -0
- package/cmd/magic-client/browsectl.go +199 -0
- package/cmd/magic-client/clientctl.go +69 -10
- package/cmd/magic-client/main.go +210 -32
- package/internal/agent/ask.go +45 -0
- package/internal/agent/ask_test.go +39 -0
- package/internal/agent/attachments_test.go +6 -2
- package/internal/agent/capability.go +7 -1
- package/internal/agent/claude.go +20 -15
- package/internal/agent/codebuddy.go +125 -34
- package/internal/agent/codebuddy_account_test.go +54 -0
- package/internal/agent/codebuddy_stream_test.go +103 -0
- package/internal/agent/credits.go +713 -0
- package/internal/agent/credits_minimax_test.go +97 -0
- package/internal/agent/credits_test.go +211 -0
- package/internal/agent/custom_models.go +155 -0
- package/internal/agent/custom_models_test.go +125 -0
- package/internal/agent/dsh.go +123 -49
- package/internal/agent/dsh_caps_cache_test.go +184 -0
- package/internal/agent/dsh_resume.go +157 -7
- package/internal/agent/dsh_test.go +108 -6
- package/internal/agent/engine.go +37 -1
- package/internal/agent/engine_base.go +21 -17
- package/internal/agent/engine_base_test.go +2 -2
- package/internal/agent/engine_test.go +47 -86
- package/internal/agent/env.go +173 -1
- package/internal/agent/env_test.go +114 -0
- package/internal/agent/fallback.go +313 -0
- package/internal/agent/fallback_test.go +340 -0
- package/internal/agent/launch.go +43 -1
- package/internal/agent/limitnotice.go +207 -1
- package/internal/agent/limitnotice_test.go +140 -0
- package/internal/agent/llmhttp.go +46 -4
- package/internal/agent/llmhttp_test.go +157 -0
- package/internal/agent/models.go +53 -1
- package/internal/agent/models_test.go +20 -5
- package/internal/agent/pidalive_windows_test.go +40 -0
- package/internal/agent/runcmd.go +26 -0
- package/internal/agent/runcmd_windows.go +36 -5
- package/internal/agent/stream.go +166 -6
- package/internal/agent/stream_test.go +25 -38
- package/internal/agent/stream_timeline_test.go +2 -37
- package/internal/agent/streamjson.go +119 -7
- package/internal/agent/tools_suffix_test.go +6 -3
- package/internal/agent/trae.go +50 -243
- package/internal/agent/trae_exec.go +433 -0
- package/internal/agent/trae_exec_test.go +181 -0
- package/internal/agent/wraplaunch_test.go +113 -0
- package/internal/cli/ask.go +168 -37
- package/internal/cli/ask_test.go +1 -1
- package/internal/cli/checkin.go +77 -0
- package/internal/cli/clientcmd.go +3 -2
- package/internal/cli/control.go +46 -5
- package/internal/cli/control_test.go +65 -0
- package/internal/cli/engines_test.go +103 -0
- package/internal/cli/events.go +15 -0
- package/internal/cli/events_test.go +50 -0
- package/internal/cli/judge.go +104 -0
- package/internal/cli/quotas.go +125 -0
- package/internal/cli/requirements.go +51 -0
- package/internal/cli/root.go +157 -11
- package/internal/cli/session.go +75 -3
- package/internal/cli/session_test.go +82 -3
- package/internal/cli/sessionlog.go +6 -0
- package/internal/cli/sessionlog_test.go +22 -2
- package/internal/cli/suggest.go +7 -5
- package/internal/cli/team.go +37 -9
- package/internal/cli/team_test.go +39 -0
- package/internal/client/ab.go +372 -0
- package/internal/client/ab_test.go +287 -0
- package/internal/client/apk.go +8 -0
- package/internal/client/apk_test.go +59 -0
- package/internal/client/apps.go +1078 -0
- package/internal/client/apps_test.go +516 -0
- package/internal/client/browse.go +84 -36
- package/internal/client/browse_test.go +107 -9
- package/internal/client/browseinput.go +481 -0
- package/internal/client/browseinput_test.go +216 -0
- package/internal/client/corecmd.go +23 -0
- package/internal/client/corecmd_other.go +8 -0
- package/internal/client/corecmd_windows.go +24 -0
- package/internal/client/coreexe.go +196 -0
- package/internal/client/coreexe_test.go +161 -0
- package/internal/client/coreupgrade.go +866 -0
- package/internal/client/coreupgrade_test.go +561 -0
- package/internal/client/daemon.go +297 -33
- package/internal/client/daemon_test.go +33 -0
- package/internal/client/engines_checkin_test.go +57 -0
- package/internal/client/fallback_flags_test.go +64 -0
- package/internal/client/feishu.go +1259 -0
- package/internal/client/feishu_test.go +649 -0
- package/internal/client/health_identity_test.go +84 -0
- package/internal/client/hostenv.go +68 -0
- package/internal/client/hostenv_test.go +37 -0
- package/internal/client/jobs.go +518 -24
- package/internal/client/jobs_auto.go +571 -0
- package/internal/client/jobs_auto_test.go +431 -0
- package/internal/client/jobs_test.go +299 -2
- package/internal/client/knowledge.go +854 -0
- package/internal/client/knowledge_test.go +510 -0
- package/internal/client/lan.go +3 -3
- package/internal/client/plugins.go +5 -1
- package/internal/client/prefs.go +67 -0
- package/internal/client/procstart_linux.go +59 -0
- package/internal/client/publicurl.go +82 -0
- package/internal/client/quotas_merge_test.go +107 -0
- package/internal/client/requirements.go +207 -2
- package/internal/client/requirements_shots_test.go +200 -0
- package/internal/client/requirements_test.go +103 -0
- package/internal/client/restart.go +288 -0
- package/internal/client/restart_handoff_test.go +230 -0
- package/internal/client/restart_test.go +290 -0
- package/internal/client/scripts.go +1047 -0
- package/internal/client/scripts_decode_implausible.go +38 -0
- package/internal/client/scripts_decode_other.go +31 -0
- package/internal/client/scripts_decode_test.go +130 -0
- package/internal/client/scripts_decode_windows.go +182 -0
- package/internal/client/scripts_test.go +739 -0
- package/internal/client/service.go +717 -97
- package/internal/client/service_test.go +217 -1
- package/internal/client/sessions_cache.go +148 -0
- package/internal/client/sessions_cache_test.go +163 -0
- package/internal/client/sessions_fold.go +165 -0
- package/internal/client/sessions_fold_test.go +204 -0
- package/internal/client/singleinstance.go +470 -0
- package/internal/client/singleinstance_test.go +607 -0
- package/internal/client/suggest.go +105 -2
- package/internal/client/suggest_test.go +86 -0
- package/internal/client/team_flags.go +25 -11
- package/internal/client/team_flags_test.go +39 -0
- package/internal/client/testmain_test.go +88 -0
- package/internal/client/workspace_default_test.go +158 -0
- package/internal/client/workspacelockenv_test.go +54 -0
- package/internal/config/config.go +48 -0
- package/internal/judge/judge.go +315 -0
- package/internal/requirements/bootstrap.go +16 -0
- package/internal/requirements/consolidate.go +100 -9
- package/internal/requirements/doc.go +154 -3
- package/internal/requirements/graph.go +138 -0
- package/internal/requirements/memory.go +7 -0
- package/internal/requirements/modules.go +312 -0
- package/internal/requirements/modules_test.go +373 -0
- package/internal/requirements/relations.go +155 -0
- package/internal/requirements/relations_test.go +231 -0
- package/internal/requirements/requirements.go +10 -0
- package/internal/requirements/shots.go +418 -0
- package/internal/requirements/shots_test.go +387 -0
- package/internal/session/multiclient_test.go +368 -0
- package/internal/session/session.go +175 -11
- package/internal/session/session_test.go +7 -0
- package/internal/session/status.go +21 -0
- package/internal/session/status_test.go +28 -0
- package/internal/session/title.go +16 -0
- package/internal/session/title_test.go +26 -0
- package/internal/session/workspace_lock.go +265 -28
- package/internal/suggest/suggest.go +23 -1
- package/internal/team/event.go +3 -1
- package/internal/team/execute.go +21 -6
- package/internal/team/fallback_route_test.go +80 -0
- package/internal/team/planner.go +15 -3
- package/internal/team/route.go +89 -17
- package/internal/team/team.go +18 -4
- package/npm/apk/magic-agent-mobile-release.apk +0 -0
- package/npm/apk/version.json +6 -3
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-arm64/magic-client +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-client +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-client +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-client +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/dist/win32-x64/magic-client.exe +0 -0
- package/npm/install.js +11 -0
- package/package.json +14 -3
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
package client
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
7
|
+
"io"
|
|
8
|
+
"log"
|
|
9
|
+
"net"
|
|
10
|
+
"net/http"
|
|
11
|
+
"net/http/httptest"
|
|
12
|
+
"os"
|
|
13
|
+
"os/exec"
|
|
14
|
+
"path/filepath"
|
|
15
|
+
"strconv"
|
|
16
|
+
"strings"
|
|
17
|
+
"testing"
|
|
18
|
+
"time"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// 2026-10-23 用户:「我的 tab 版本中除了升级 apk,也要触发 npm 升级」。
|
|
22
|
+
// 这里钉住「桌面端升级」这条路:前缀探测、命令拼装、SSE 成功/失败/原地重启。
|
|
23
|
+
|
|
24
|
+
func TestNpmPrefixForPackage(t *testing.T) {
|
|
25
|
+
root := t.TempDir()
|
|
26
|
+
// Homebrew 形状:<prefix>/lib/node_modules/magic-agent —— 前缀是 <prefix>,不是 lib。
|
|
27
|
+
brewPkg := filepath.Join(root, "opt", "lib", "node_modules", "magic-agent")
|
|
28
|
+
if got := npmPrefixForPackage(brewPkg); got != filepath.Join(root, "opt") {
|
|
29
|
+
t.Fatalf("npmPrefixForPackage(<prefix>/lib/node_modules/…) = %q, want %q", got, filepath.Join(root, "opt"))
|
|
30
|
+
}
|
|
31
|
+
// Windows / 普通形状:<prefix>/node_modules/magic-agent —— 前缀是 <prefix>。
|
|
32
|
+
flatPkg := filepath.Join(root, "usr", "node_modules", "magic-agent")
|
|
33
|
+
if got := npmPrefixForPackage(flatPkg); got != filepath.Join(root, "usr") {
|
|
34
|
+
t.Fatalf("npmPrefixForPackage(<prefix>/node_modules/…) = %q, want %q", got, filepath.Join(root, "usr"))
|
|
35
|
+
}
|
|
36
|
+
// 不是 npm 包(自己编译的二进制):没有前缀可装回。
|
|
37
|
+
if got := npmPrefixForPackage(filepath.Join(root, "somewhere", "magic-agent")); got != "" {
|
|
38
|
+
t.Fatalf("非 npm 布局应返回空,got %q", got)
|
|
39
|
+
}
|
|
40
|
+
if got := npmPrefixForPackage(""); got != "" {
|
|
41
|
+
t.Fatalf("空包根应返回空,got %q", got)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func TestCorePackageOf(t *testing.T) {
|
|
46
|
+
root := t.TempDir()
|
|
47
|
+
pkg := filepath.Join(root, "lib", "node_modules", "magic-agent")
|
|
48
|
+
bin := filepath.Join(pkg, "npm", "dist", "darwin-arm64", "magic-agent")
|
|
49
|
+
if err := os.MkdirAll(filepath.Dir(bin), 0o755); err != nil {
|
|
50
|
+
t.Fatal(err)
|
|
51
|
+
}
|
|
52
|
+
if err := os.WriteFile(filepath.Join(pkg, "package.json"), []byte(`{"name":"magic-agent"}`), 0o644); err != nil {
|
|
53
|
+
t.Fatal(err)
|
|
54
|
+
}
|
|
55
|
+
if got := corePackageOf(bin); got != pkg {
|
|
56
|
+
t.Fatalf("corePackageOf = %q, want %q", got, pkg)
|
|
57
|
+
}
|
|
58
|
+
// 别的包(不是 magic-agent):认不出来。
|
|
59
|
+
other := filepath.Join(root, "lib", "node_modules", "other-pkg")
|
|
60
|
+
if err := os.MkdirAll(other, 0o755); err != nil {
|
|
61
|
+
t.Fatal(err)
|
|
62
|
+
}
|
|
63
|
+
if err := os.WriteFile(filepath.Join(other, "package.json"), []byte(`{"name":"other-pkg"}`), 0o644); err != nil {
|
|
64
|
+
t.Fatal(err)
|
|
65
|
+
}
|
|
66
|
+
if got := corePackageOf(filepath.Join(other, "bin", "x")); got != "" {
|
|
67
|
+
t.Fatalf("别的包不该被认成 magic-agent,got %q", got)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func TestResolveCoreUpgradeBuildsFixedCommand(t *testing.T) {
|
|
72
|
+
// npm 必须在 PATH 上(测试机一般有;没有就跳过这条,不假装测过)。
|
|
73
|
+
npmDir := ""
|
|
74
|
+
if p, err := exec.LookPath("npm"); err == nil {
|
|
75
|
+
npmDir = filepath.Dir(p)
|
|
76
|
+
}
|
|
77
|
+
if npmDir == "" {
|
|
78
|
+
t.Skip("PATH 上没有 npm")
|
|
79
|
+
}
|
|
80
|
+
t.Setenv("PATH", npmDir+string(os.PathListSeparator)+os.Getenv("PATH")+string(os.PathListSeparator)+"/usr/bin:/bin")
|
|
81
|
+
|
|
82
|
+
dir, _ := filepath.EvalSymlinks(t.TempDir()) // macOS 的 /var → /private/var:compare 用 realpath
|
|
83
|
+
bin := filepath.Join(dir, "bin", "magic-agent")
|
|
84
|
+
if err := os.MkdirAll(filepath.Dir(bin), 0o755); err != nil {
|
|
85
|
+
t.Fatal(err)
|
|
86
|
+
}
|
|
87
|
+
if err := os.WriteFile(bin, []byte("#!/bin/sh\necho magic-agent 2.4.1\n"), 0o755); err != nil {
|
|
88
|
+
t.Fatal(err)
|
|
89
|
+
}
|
|
90
|
+
t.Setenv("MAGIC_AGENT_BIN", "")
|
|
91
|
+
// findCoreBin 先看 PATH:把它指到临时目录,避开测试机上真装的那份。
|
|
92
|
+
t.Setenv("PATH", filepath.Dir(bin)+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
93
|
+
|
|
94
|
+
got := resolveCoreUpgrade("v2.4.2", bin)
|
|
95
|
+
if !got.CanUpgrade {
|
|
96
|
+
t.Fatalf("应当可以升级,Note=%q", got.Note)
|
|
97
|
+
}
|
|
98
|
+
want := "npm install magic-agent@2.4.2 --no-fund --no-audit -g --prefix " + dir
|
|
99
|
+
if got.Command() != want {
|
|
100
|
+
t.Fatalf("command = %q, want %q", got.Command(), want)
|
|
101
|
+
}
|
|
102
|
+
if got.Installed != "2.4.1" {
|
|
103
|
+
t.Fatalf("Installed = %q, want 2.4.1(盘上那份的版本,升级后拿它判断真的变了)", got.Installed)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 没查到 registry:不升级、给出原因(而不是拿一个猜的版本去装)。
|
|
107
|
+
if q := resolveCoreUpgrade("", bin); q.CanUpgrade || q.Note == "" {
|
|
108
|
+
t.Fatalf("没有 latest 时不应升级:%+v", q)
|
|
109
|
+
}
|
|
110
|
+
// 版本号只取 x.y.z(防注入进 npm 参数)。
|
|
111
|
+
if q := resolveCoreUpgrade("2.4.2; rm -rf /", bin); strings.Contains(q.Command(), "rm") {
|
|
112
|
+
t.Fatalf("版本号没有被清理:%q", q.Command())
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// registry 上的 latest 比本地还旧时,**不能**降级安装(npm i -g 是无条件安装)。
|
|
117
|
+
func TestCoreUpgradeRefusesDowngrade(t *testing.T) {
|
|
118
|
+
s, vf := newUpgradeService(t)
|
|
119
|
+
t.Cleanup(func() {
|
|
120
|
+
if err := os.WriteFile(vf, []byte("2.4.1"), 0o644); err != nil {
|
|
121
|
+
t.Fatal(err)
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
// 本地已经是 2.4.2,registry 上只有 2.4.1。
|
|
125
|
+
if err := os.WriteFile(vf, []byte("2.4.2"), 0o644); err != nil {
|
|
126
|
+
t.Fatal(err)
|
|
127
|
+
}
|
|
128
|
+
if err := os.WriteFile(filepath.Join(filepath.Dir(vf), "latest.txt"), []byte(`"2.4.1"`+"\n"), 0o644); err != nil {
|
|
129
|
+
t.Fatal(err)
|
|
130
|
+
}
|
|
131
|
+
req, _ := http.NewRequest(http.MethodPost, "/desk/core/upgrade", nil)
|
|
132
|
+
req.Header.Set("X-Magic-Token", "test-token-0123456789abcdef")
|
|
133
|
+
rec := httptest.NewRecorder()
|
|
134
|
+
s.Handler().ServeHTTP(rec, req)
|
|
135
|
+
if rec.Code != http.StatusConflict {
|
|
136
|
+
t.Fatalf("降级应被拒(409),got %d: %s", rec.Code, rec.Body.String())
|
|
137
|
+
}
|
|
138
|
+
if !strings.Contains(rec.Body.String(), "不高于本机已装") {
|
|
139
|
+
t.Fatalf("应说清为什么不降级:%s", rec.Body.String())
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// replacedAfterStart:盘上的二进制比本进程新 = npm 刚换上去、还没重启。
|
|
144
|
+
func TestReplacedAfterStart(t *testing.T) {
|
|
145
|
+
dir := t.TempDir()
|
|
146
|
+
bin := filepath.Join(dir, "magic-agent")
|
|
147
|
+
if err := os.WriteFile(bin, []byte("#!/bin/sh\n"), 0o755); err != nil {
|
|
148
|
+
t.Fatal(err)
|
|
149
|
+
}
|
|
150
|
+
st, _ := os.Stat(bin)
|
|
151
|
+
// 二进制在「本进程启动」之后才落盘 ⇒ npm 已经装了新版、只差重启。
|
|
152
|
+
start := fileTime(st).Add(-time.Second)
|
|
153
|
+
s := &Service{}
|
|
154
|
+
s.procStartOverride = func() (time.Time, bool) { return start, true }
|
|
155
|
+
if !s.replacedAfterStart(bin) {
|
|
156
|
+
t.Fatal("本进程之后写上去的二进制应判成「已换新」(否则界面会再喊一次升级)")
|
|
157
|
+
}
|
|
158
|
+
// 二进制比本进程旧:正常情况,不当成「已换新」。
|
|
159
|
+
start = fileTime(st).Add(5 * time.Second)
|
|
160
|
+
s.procStartOverride = func() (time.Time, bool) { return start, true }
|
|
161
|
+
if s.replacedAfterStart(bin) {
|
|
162
|
+
t.Fatal("比本进程旧的二进制不该被判成「已换新」")
|
|
163
|
+
}
|
|
164
|
+
if s.replacedAfterStart(filepath.Join(dir, "不存在")) {
|
|
165
|
+
t.Fatal("不存在的路径不该判成「已换新」")
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func TestParseSemver(t *testing.T) {
|
|
170
|
+
for in, want := range map[string]string{
|
|
171
|
+
"magic-agent v2.4.2": "2.4.2",
|
|
172
|
+
"2.4.2": "2.4.2",
|
|
173
|
+
"v2.4.2\n": "2.4.2",
|
|
174
|
+
`"2.4.2"`: "2.4.2",
|
|
175
|
+
"no digits": "",
|
|
176
|
+
} {
|
|
177
|
+
if got := parseSemver(in); got != want {
|
|
178
|
+
t.Errorf("parseSemver(%q) = %q, want %q", in, got, want)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
func TestCmpSemver(t *testing.T) {
|
|
184
|
+
if cmpSemver("2.4.2", "2.4.1") <= 0 || cmpSemver("2.4.1", "2.4.2") >= 0 || cmpSemver("2.4.2", "2.4.2") != 0 {
|
|
185
|
+
t.Fatal("cmpSemver 比较错误")
|
|
186
|
+
}
|
|
187
|
+
if cmpSemver("2.10.0", "2.9.9") <= 0 {
|
|
188
|
+
t.Fatal("多位数字段必须按数值比较")
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// writeVersionedCore 造一个能 `--version` 的假 core;版本号写进 version.txt,测试里可改写。
|
|
193
|
+
func writeVersionedCore(t *testing.T, dir, version string) (bin, vf string) {
|
|
194
|
+
t.Helper()
|
|
195
|
+
bin = filepath.Join(dir, "magic-agent")
|
|
196
|
+
vf = filepath.Join(dir, "version.txt")
|
|
197
|
+
if err := os.WriteFile(vf, []byte(version), 0o644); err != nil {
|
|
198
|
+
t.Fatal(err)
|
|
199
|
+
}
|
|
200
|
+
script := "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then printf 'magic-agent v'; cat " + vf + "; fi\n"
|
|
201
|
+
if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
|
|
202
|
+
t.Fatal(err)
|
|
203
|
+
}
|
|
204
|
+
return bin, vf
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// fakeNPM 把 npm 换成一个脚本:`view` 回 $FAKE_NPM_LATEST;`install -g` 把盘上假 core 的版本
|
|
208
|
+
// 改成目标版本(模拟真 npm 换掉二进制),其余参数忽略。
|
|
209
|
+
//
|
|
210
|
+
// ⚠️ 用 node 写而不是 sh:真机上 `npm` 本身就是 node 脚本,没装 node 的机器上根本没有 npm
|
|
211
|
+
// (resolveCoreUpgrade 会直接说「PATH 上没有 npm」,走不到这里);而 sh 版一旦被别的测试
|
|
212
|
+
// 改了 PATH、没把假 npm 排在真 npm 前面,`install` 会落到系统命令上 —— 等于在测试里
|
|
213
|
+
// 真的跑了一次 `npm i -g magic-agent`(2026-10-26 实测就这样踩过一次)。node 没有同名的
|
|
214
|
+
// 系统命令,落错了也只会报错退出,不会去动真包。
|
|
215
|
+
func fakeNPM(t *testing.T, dir, vf string) {
|
|
216
|
+
t.Helper()
|
|
217
|
+
node, err := exec.LookPath("node")
|
|
218
|
+
if err != nil {
|
|
219
|
+
t.Skip("PATH 上没有 node:没法把 npm 替换成不会伤到真安装的假实现")
|
|
220
|
+
}
|
|
221
|
+
const js = "const fs=require('fs'),path=require('path'),a=process.argv.slice(2);\n" +
|
|
222
|
+
"const d=path.dirname(process.argv[1]);\n" +
|
|
223
|
+
// 行为覆盖:环境变量(每个子进程各读各的,测试里 t.Setenv 即可)优先,其次 npm 目录里的
|
|
224
|
+
// behavior.json(给「同一个假 npm 服务整个用例」的场景用)。
|
|
225
|
+
"try{if(process.env.FAKE_NPM_BEHAVIOR)Object.assign(process.env,JSON.parse(process.env.FAKE_NPM_BEHAVIOR));}catch(e){}\n" +
|
|
226
|
+
"try{Object.assign(process.env,JSON.parse(fs.readFileSync(path.join(d,'behavior.json'),'utf8')));}catch(e){}\n" +
|
|
227
|
+
"if(a[0]==='view'){process.stdout.write(fs.readFileSync(path.join(d,'latest.txt'),'utf8'));}\n" +
|
|
228
|
+
"else if(a[0]==='install'){const m=/^magic-agent@(.+)$/.exec(a.find(x=>/^magic-agent@/.test(x))||'');\n" +
|
|
229
|
+
" if(process.env.FAKE_NPM_VERFILE)fs.writeFileSync(process.env.FAKE_NPM_VERFILE,m?m[1]:'bad-args');}\n"
|
|
230
|
+
// sh 垫片只做一件事:exec 到 node 并把 $0 换成 npm.js —— 假 npm 按自己所在目录找 latest.txt。
|
|
231
|
+
script := "#!/bin/sh\nexec " + node + " " + filepath.Join(dir, "npm.js") + " \"$@\"\n"
|
|
232
|
+
if err := os.WriteFile(filepath.Join(dir, "npm.js"), []byte(js), 0o644); err != nil {
|
|
233
|
+
t.Fatal(err)
|
|
234
|
+
}
|
|
235
|
+
if err := os.WriteFile(filepath.Join(dir, "npm"), []byte(script), 0o755); err != nil {
|
|
236
|
+
t.Fatal(err)
|
|
237
|
+
}
|
|
238
|
+
if err := os.WriteFile(filepath.Join(dir, "latest.txt"), []byte(`"2.4.2"`+"\n"), 0o644); err != nil {
|
|
239
|
+
t.Fatal(err)
|
|
240
|
+
}
|
|
241
|
+
t.Setenv("FAKE_NPM_VERFILE", vf)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
func newUpgradeService(t *testing.T) (*Service, string) {
|
|
245
|
+
t.Helper()
|
|
246
|
+
work := t.TempDir()
|
|
247
|
+
binDir := filepath.Join(work, "prefix", "bin")
|
|
248
|
+
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
|
249
|
+
t.Fatal(err)
|
|
250
|
+
}
|
|
251
|
+
core, vf := writeVersionedCore(t, binDir, "2.4.1")
|
|
252
|
+
// 假 core 是测试**刚写出来**的:把「本进程启动时刻」钉在它**之后**(等它写完再钉),
|
|
253
|
+
// 它就不会被 replacedAfterStart 当成「装好了没重启」。
|
|
254
|
+
coreMtime, err := os.Stat(core)
|
|
255
|
+
if err != nil {
|
|
256
|
+
t.Fatal(err)
|
|
257
|
+
}
|
|
258
|
+
afterCore := fileTime(coreMtime).Add(time.Hour)
|
|
259
|
+
npmDir := filepath.Join(work, "npmbin")
|
|
260
|
+
_ = os.MkdirAll(npmDir, 0o755)
|
|
261
|
+
fakeNPM(t, npmDir, vf)
|
|
262
|
+
// ⚠️ 路径顺序:系统 bin 必须在最后垫底 —— npmDir 里只有假 npm,若把 /bin 之类放在
|
|
263
|
+
// 前面,测试机上(macOS)真系统里的 `install` 会先被找到,测的就不是 npm 了。
|
|
264
|
+
sysPath := "/usr/local/bin:/usr/bin:/bin"
|
|
265
|
+
t.Setenv("PATH", npmDir+string(os.PathListSeparator)+binDir+string(os.PathListSeparator)+sysPath)
|
|
266
|
+
t.Setenv("MAGIC_AGENT_BIN", "")
|
|
267
|
+
|
|
268
|
+
s := &Service{opts: Options{MagicAgent: core}, log: testLogger()}
|
|
269
|
+
// ⚠️ 基准时刻必须在这里**算一次存下来**:每次调用都现算的话,越晚算越靠后,
|
|
270
|
+
// 「二进制比本进程新」就永远不成立(曾经因此让「只差重启」那条用例悄悄测不到)。
|
|
271
|
+
s.procStartOverride = func() (time.Time, bool) { return afterCore, true }
|
|
272
|
+
s.contract = Contract{ContractVersion: RequiredContractVersion, Version: "2.4.1", Features: requiredFeatures}
|
|
273
|
+
// 「问 registry」那一问换成不联网的假体(coreStatus 与升级接口共用)。
|
|
274
|
+
s.latestFn = func(context.Context) string { return "" }
|
|
275
|
+
// 一律按「假 core」解析(Hint 固定),绝不去找本进程(`go test` 跑的就是刚编出来的
|
|
276
|
+
// magic-agent,--version 报的是真仓库的版本)。
|
|
277
|
+
s.coreTarget = func(context.Context) coreUpgradeTarget {
|
|
278
|
+
return s.resolveCoreUpgrade("2.4.2", core)
|
|
279
|
+
}
|
|
280
|
+
return s, vf
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
func TestCoreStatusReportsUpgradable(t *testing.T) {
|
|
284
|
+
s, vf := newUpgradeService(t)
|
|
285
|
+
info := s.coreStatus(t.Context())
|
|
286
|
+
if !info.Upgradable || !info.CanUpgrade || info.Latest != "2.4.2" || info.Version != "2.4.1" {
|
|
287
|
+
t.Fatalf("coreStatus = %+v", info)
|
|
288
|
+
}
|
|
289
|
+
if info.Daemon {
|
|
290
|
+
t.Fatal("测试里的进程不是 start 拉起的,不应报告为后台常驻")
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 原地重启那几秒:npm 已经把盘上那份换成 2.4.2(二进制 mtime 在本进程启动之后),
|
|
294
|
+
// 跑着的仍是 2.4.1。`--version` 跑的是**新文件**,所以「正在跑的版本」只能取契约缓存。
|
|
295
|
+
if err := os.WriteFile(vf, []byte("2.4.2"), 0o644); err != nil {
|
|
296
|
+
t.Fatal(err)
|
|
297
|
+
}
|
|
298
|
+
core := filepath.Join(filepath.Dir(vf), "magic-agent")
|
|
299
|
+
coreMtime, err := os.Stat(core)
|
|
300
|
+
if err != nil {
|
|
301
|
+
t.Fatal(err)
|
|
302
|
+
}
|
|
303
|
+
// 进程「启动」早于这次换盘 ⇒ 盘上那份是启动之后才换上去的。
|
|
304
|
+
s.procStartOverride = func() (time.Time, bool) { return fileTime(coreMtime).Add(-time.Hour), true }
|
|
305
|
+
info = s.coreStatus(t.Context())
|
|
306
|
+
if !info.RestartOnly || info.Upgradable || info.Version != "2.4.1" || info.Installed != "2.4.2" {
|
|
307
|
+
t.Fatalf("重启待办没有报出来:%+v", info)
|
|
308
|
+
}
|
|
309
|
+
if !strings.Contains(info.Note, "重启") {
|
|
310
|
+
t.Fatalf("应提示重启后生效:%+v", info)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// readSSE 解析 SSE,返回 (event, data) 序列。
|
|
315
|
+
func readSSE(t *testing.T, body string) [][2]string {
|
|
316
|
+
t.Helper()
|
|
317
|
+
var out [][2]string
|
|
318
|
+
for _, block := range strings.Split(body, "\n\n") {
|
|
319
|
+
var ev, data string
|
|
320
|
+
for _, ln := range strings.Split(block, "\n") {
|
|
321
|
+
if strings.HasPrefix(ln, "event: ") {
|
|
322
|
+
ev = strings.TrimPrefix(ln, "event: ")
|
|
323
|
+
} else if strings.HasPrefix(ln, "data: ") {
|
|
324
|
+
data += strings.TrimPrefix(ln, "data: ")
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if data != "" {
|
|
328
|
+
out = append(out, [2]string{ev, data})
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return out
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
func TestCoreUpgradeStreamsAndReallyInstalls(t *testing.T) {
|
|
335
|
+
s, vf := newUpgradeService(t)
|
|
336
|
+
orgSpawner := restartSpawner
|
|
337
|
+
t.Cleanup(func() { restartSpawner = orgSpawner })
|
|
338
|
+
restartSpawner = func(string, *log.Logger, string) {
|
|
339
|
+
t.Logf("重启助手本该在这里起身(测试里不拉真的 magic-client)")
|
|
340
|
+
}
|
|
341
|
+
rec := httptest.NewRecorder()
|
|
342
|
+
s.handleCoreUpgrade(rec, httptest.NewRequest(http.MethodPost, "/desk/core/upgrade", nil))
|
|
343
|
+
if rec.Code != http.StatusOK {
|
|
344
|
+
t.Fatalf("code = %d, body=%s", rec.Code, rec.Body.String())
|
|
345
|
+
}
|
|
346
|
+
events := readSSE(t, rec.Body.String())
|
|
347
|
+
var names []string
|
|
348
|
+
var end map[string]any
|
|
349
|
+
for _, e := range events {
|
|
350
|
+
names = append(names, e[0])
|
|
351
|
+
if e[0] == "end" {
|
|
352
|
+
_ = json.Unmarshal([]byte(e[1]), &end)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if len(names) < 3 || names[0] != "open" || names[1] != "step" || names[len(names)-1] != "end" {
|
|
356
|
+
t.Fatalf("事件序列 = %v(应为 open → step → … → end)", names)
|
|
357
|
+
}
|
|
358
|
+
if end == nil || end["ok"] != true {
|
|
359
|
+
t.Fatalf("end = %v,全量:%s", end, rec.Body.String())
|
|
360
|
+
}
|
|
361
|
+
// 前台 serve(没有 daemon 状态):不重启,只说重启后生效 —— 不假称已生效。
|
|
362
|
+
if end["restarting"] != false || !strings.Contains(end["note"].(string), "重启") {
|
|
363
|
+
t.Fatalf("前台 serve 的 note 不对:%v", end)
|
|
364
|
+
}
|
|
365
|
+
// 新二进制**真的换上了**(npm 被调用、盘上版本变了)。
|
|
366
|
+
if b, _ := os.ReadFile(vf); string(b) != "2.4.2" {
|
|
367
|
+
t.Fatalf("npm install 没把盘上版本换成 2.4.2:%q", string(b))
|
|
368
|
+
}
|
|
369
|
+
// 而且装的是目标版本(2026-10-26:`-g` 曾插在 install 与包名之间,假 npm 读不到 install,
|
|
370
|
+
// 这条测试就变成永远不失败 —— 现在假 npm 按包名找参数,参数顺序怎么排都认得出)。
|
|
371
|
+
if b, _ := os.ReadFile(vf); strings.Contains(string(b), "bad-args") {
|
|
372
|
+
t.Fatalf("npm 收到的参数里没有 magic-agent@<版本>:%q", string(b))
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
func TestCoreUpgradeFailsWhenBinaryUnchanged(t *testing.T) {
|
|
377
|
+
// npm 退出 0 但什么都没装(装到别的前缀 / 源还没同步):必须报失败,不能谎报成功。
|
|
378
|
+
//
|
|
379
|
+
// 假 npm 只让 `install` 成功、什么都不做(`view` 照旧回新版本)。开关走假 npm 自己的
|
|
380
|
+
// behavior.json,不碰 PATH 里的任何东西。
|
|
381
|
+
// ⚠️ 必须在 newUpgradeService **之前**写:假 npm 每个进程只在启动时读一次 behavior.json,
|
|
382
|
+
// Helper 之后再改文件对已经起来的假 npm 不生效(曾因此让这条用例悄悄测成「真装好了」)。
|
|
383
|
+
t.Setenv("FAKE_NPM_BEHAVIOR", `{"FAKE_NPM_VERFILE":""}`)
|
|
384
|
+
s, _ := newUpgradeService(t)
|
|
385
|
+
rec := httptest.NewRecorder()
|
|
386
|
+
s.handleCoreUpgrade(rec, httptest.NewRequest(http.MethodPost, "/desk/core/upgrade", nil))
|
|
387
|
+
if rec.Code != http.StatusOK {
|
|
388
|
+
t.Fatalf("code = %d, body=%s", rec.Code, rec.Body.String())
|
|
389
|
+
}
|
|
390
|
+
events := readSSE(t, rec.Body.String())
|
|
391
|
+
last := events[len(events)-1]
|
|
392
|
+
var end map[string]any
|
|
393
|
+
_ = json.Unmarshal([]byte(last[1]), &end)
|
|
394
|
+
if last[0] != "end" || end["ok"] != false || !strings.Contains(end["error"].(string), "版本没有变化") {
|
|
395
|
+
t.Fatalf("应报「版本没有变化」,实际:%s", rec.Body.String())
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// TestRestartDaemonReal 端到端:真起一个后台 serve(假 core + 假 npm),升级后
|
|
400
|
+
// 自动用**原来的端口与参数**重启到新二进制上 —— 前端不需要换地址也能重连。
|
|
401
|
+
func TestRestartDaemonReal(t *testing.T) {
|
|
402
|
+
if testing.Short() {
|
|
403
|
+
t.Skip("-short")
|
|
404
|
+
}
|
|
405
|
+
self, err := os.Executable()
|
|
406
|
+
if err != nil {
|
|
407
|
+
t.Fatal(err)
|
|
408
|
+
}
|
|
409
|
+
dir := t.TempDir()
|
|
410
|
+
core, _ := writeVersionedCore(t, dir, "2.4.1")
|
|
411
|
+
/* 假 core 是**测试进程跑起来之后**才写出来的:不拨回过去,子进程会把它判成
|
|
412
|
+
「本进程启动后盘上才换上去的」(replacedAfterStart),升级接口直接回 409「只差重启」。
|
|
413
|
+
真实场景里 core 都是先前装好的,这里把 mtime 拨到一小时前。 */
|
|
414
|
+
past := time.Now().Add(-time.Hour)
|
|
415
|
+
if err := os.Chtimes(core, past, past); err != nil {
|
|
416
|
+
t.Fatal(err)
|
|
417
|
+
}
|
|
418
|
+
npmDir := filepath.Join(dir, "npmbin")
|
|
419
|
+
_ = os.MkdirAll(npmDir, 0o755)
|
|
420
|
+
fakeNPM(t, npmDir, filepath.Join(dir, "version.txt"))
|
|
421
|
+
|
|
422
|
+
fakeServe := filepath.Join(dir, "magic-client")
|
|
423
|
+
if err := os.Symlink(self, fakeServe); err != nil {
|
|
424
|
+
t.Skipf("无法建符号链接:%v", err)
|
|
425
|
+
}
|
|
426
|
+
statePath := filepath.Join(dir, "client.json")
|
|
427
|
+
srv := httptest.NewServer(http.NotFoundHandler())
|
|
428
|
+
port := srv.Listener.Addr().(*net.TCPAddr).Port
|
|
429
|
+
srv.Close()
|
|
430
|
+
_ = os.WriteFile(statePath+".port", []byte(strconv.Itoa(port)), 0o644)
|
|
431
|
+
|
|
432
|
+
cmd := exec.Command(fakeServe)
|
|
433
|
+
cmd.Env = append(os.Environ(),
|
|
434
|
+
"RUN_AS_SERVE=1",
|
|
435
|
+
"MAGIC_TEST_STATE="+statePath,
|
|
436
|
+
"MAGIC_AGENT_BIN="+core,
|
|
437
|
+
"PATH="+npmDir+string(os.PathListSeparator)+filepath.Dir(core)+string(os.PathListSeparator)+"/usr/bin:/bin",
|
|
438
|
+
)
|
|
439
|
+
cmd.Dir = dir
|
|
440
|
+
logFile, _ := os.Create(filepath.Join(dir, "serve.log"))
|
|
441
|
+
defer logFile.Close()
|
|
442
|
+
cmd.Stdout, cmd.Stderr = logFile, logFile
|
|
443
|
+
if err := cmd.Start(); err != nil {
|
|
444
|
+
t.Fatal(err)
|
|
445
|
+
}
|
|
446
|
+
defer func() { _ = cmd.Process.Kill() }()
|
|
447
|
+
|
|
448
|
+
// 子进程监听后会**重写** client.json.port;真实端口以它为准(父进程预挑的只是第一个尝试)。
|
|
449
|
+
portFile := statePath + ".port"
|
|
450
|
+
for i := 0; i < 100; i++ {
|
|
451
|
+
b, err := os.ReadFile(portFile)
|
|
452
|
+
if err == nil {
|
|
453
|
+
var p int
|
|
454
|
+
if _, e := fmt.Sscanf(string(b), "%d", &p); e == nil && p > 0 {
|
|
455
|
+
port = p
|
|
456
|
+
break
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
time.Sleep(50 * time.Millisecond)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
base := "http://127.0.0.1:" + strconv.Itoa(port)
|
|
463
|
+
if !waitHTTP(base + "/desk/health") {
|
|
464
|
+
logFile.Sync()
|
|
465
|
+
b, _ := os.ReadFile(filepath.Join(dir, "serve.log"))
|
|
466
|
+
t.Fatalf("假 serve 没起来(日志:%s)", b)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
req, _ := http.NewRequest(http.MethodPost, base+"/desk/core/upgrade", nil)
|
|
470
|
+
req.Header.Set("X-Magic-Token", "test-token-0123456789abcdef")
|
|
471
|
+
resp, err := http.DefaultClient.Do(req)
|
|
472
|
+
if err != nil {
|
|
473
|
+
t.Fatal(err)
|
|
474
|
+
}
|
|
475
|
+
body, _ := io.ReadAll(resp.Body)
|
|
476
|
+
resp.Body.Close()
|
|
477
|
+
events := readSSE(t, string(body))
|
|
478
|
+
if len(events) == 0 {
|
|
479
|
+
t.Fatalf("SSE 为空:%s", body)
|
|
480
|
+
}
|
|
481
|
+
var end map[string]any
|
|
482
|
+
_ = json.Unmarshal([]byte(events[len(events)-1][1]), &end)
|
|
483
|
+
if end["ok"] != true || end["restarting"] != true {
|
|
484
|
+
b, _ := os.ReadFile(filepath.Join(dir, "serve.log"))
|
|
485
|
+
t.Fatalf("升级没有触发重启:%s\nserve.log:\n%s", body, b)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// 新实例起来后,/desk/version 报的是新版本(假 serve 每次启动读一遍 core --version)。
|
|
489
|
+
deadline := time.Now().Add(10 * time.Second)
|
|
490
|
+
for time.Now().Before(deadline) {
|
|
491
|
+
if r, err := http.Get(base + "/desk/version"); err == nil {
|
|
492
|
+
b, _ := io.ReadAll(r.Body)
|
|
493
|
+
r.Body.Close()
|
|
494
|
+
if strings.Contains(string(b), "2.4.2") {
|
|
495
|
+
// 端口不能不换(手机里存的是这个地址)。
|
|
496
|
+
return
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
time.Sleep(100 * time.Millisecond)
|
|
500
|
+
}
|
|
501
|
+
t.Fatal("重启后新版本没有生效(端口应保持 " + strconv.Itoa(port) + ")")
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
func waitHTTP(u string) bool {
|
|
505
|
+
for i := 0; i < 100; i++ {
|
|
506
|
+
if r, err := http.Get(u); err == nil {
|
|
507
|
+
r.Body.Close()
|
|
508
|
+
if r.StatusCode == http.StatusOK {
|
|
509
|
+
return true
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
time.Sleep(50 * time.Millisecond)
|
|
513
|
+
}
|
|
514
|
+
return false
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// TestNpmEnvStripsDeadLoopbackProxy 客户端里跑 npm 时不能再继承「指向死端口」的代理。
|
|
518
|
+
// 现场(2026-10-27):宿主 IDE 塞了 HTTP_PROXY=127.0.0.1:<已失效端口>,客户端里的
|
|
519
|
+
// `npm view` 静默失败 → 手机看到「桌面端没有可升级版本」,而 registry 上明明有。
|
|
520
|
+
func TestNpmEnvStripsDeadLoopbackProxy(t *testing.T) {
|
|
521
|
+
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
522
|
+
if err != nil {
|
|
523
|
+
t.Skipf("拿不到回环端口:%v", err)
|
|
524
|
+
}
|
|
525
|
+
addr := l.Addr().String()
|
|
526
|
+
l.Close() // 关掉 ⇒ 这个端口上没人监听
|
|
527
|
+
|
|
528
|
+
live, err := net.Listen("tcp", "127.0.0.1:0")
|
|
529
|
+
if err != nil {
|
|
530
|
+
t.Skipf("拿不到回环端口:%v", err)
|
|
531
|
+
}
|
|
532
|
+
defer live.Close()
|
|
533
|
+
|
|
534
|
+
t.Setenv("HTTP_PROXY", "http://"+addr)
|
|
535
|
+
t.Setenv("https_proxy", "http://"+addr)
|
|
536
|
+
t.Setenv("HTTPS_PROXY", "http://"+live.Addr().String())
|
|
537
|
+
t.Setenv("ALL_PROXY", "socks5://proxy.corp.example:1080") // 非回环:不许动
|
|
538
|
+
|
|
539
|
+
env := npmEnv()
|
|
540
|
+
if v, ok := envValue(env, "HTTP_PROXY"); ok {
|
|
541
|
+
t.Fatalf("死回环代理应被剔除,实际留下 %q", v)
|
|
542
|
+
}
|
|
543
|
+
if v, ok := envValue(env, "https_proxy"); ok {
|
|
544
|
+
t.Fatalf("死回环代理(小写)应被剔除,实际留下 %q", v)
|
|
545
|
+
}
|
|
546
|
+
if v, ok := envValue(env, "HTTPS_PROXY"); !ok || !strings.Contains(v, "127.0.0.1") {
|
|
547
|
+
t.Fatalf("活着的回环代理不该被剔除,实际 %q ok=%v", v, ok)
|
|
548
|
+
}
|
|
549
|
+
if v, ok := envValue(env, "ALL_PROXY"); !ok || !strings.Contains(v, "proxy.corp.example") {
|
|
550
|
+
t.Fatalf("非回环代理一律不碰,实际 %q ok=%v", v, ok)
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
func envValue(env []string, name string) (string, bool) {
|
|
555
|
+
for _, kv := range env {
|
|
556
|
+
if k, v, ok := strings.Cut(kv, "="); ok && k == name {
|
|
557
|
+
return v, true
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return "", false
|
|
561
|
+
}
|