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,186 @@
|
|
|
1
|
+
package client
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"net/http"
|
|
6
|
+
"net/http/httptest"
|
|
7
|
+
"os"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"strings"
|
|
10
|
+
"testing"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// 增量续读端点 `GET /desk/session/{id}/events?after=<seq>` 的边界。
|
|
14
|
+
//
|
|
15
|
+
// 这条路由与 `/messages` 共用 `/desk/session/` 这个**子树前缀** —— 所以这一组同时
|
|
16
|
+
// 钉住「分派没串线」:`/messages` 仍全量、`/events` 才增量、未知子路径**明说** 404
|
|
17
|
+
// (而不是默默回一份看起来正常的全量历史 —— 那会把「增量没生效」伪装成「增量没实现」)。
|
|
18
|
+
func TestHandleSessionEvents(t *testing.T) {
|
|
19
|
+
home := t.TempDir()
|
|
20
|
+
dir := filepath.Join(home, ".magic-agent", "sessions")
|
|
21
|
+
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
22
|
+
t.Fatal(err)
|
|
23
|
+
}
|
|
24
|
+
t.Setenv("HOME", home)
|
|
25
|
+
id := "sess-ev"
|
|
26
|
+
lines := []string{
|
|
27
|
+
`{"seq":1,"kind":"user","text":"问题","session_id":"` + id + `"}`,
|
|
28
|
+
`{"seq":2,"kind":"thinking","text":"思考","session_id":"` + id + `"}`,
|
|
29
|
+
`{"seq":3,"kind":"text","text":"正文","session_id":"` + id + `"}`,
|
|
30
|
+
`{"seq":4,"kind":"turn_end","text":"完成","session_id":"` + id + `"}`,
|
|
31
|
+
}
|
|
32
|
+
if err := os.WriteFile(filepath.Join(dir, id+".jsonl"), []byte(strings.Join(lines, "\n")+"\n"), 0o600); err != nil {
|
|
33
|
+
t.Fatal(err)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
svc := newTestService(t, fakeCore(t, goodContract))
|
|
37
|
+
srv := httptest.NewServer(svc.Handler())
|
|
38
|
+
defer srv.Close()
|
|
39
|
+
|
|
40
|
+
type eventsBody struct {
|
|
41
|
+
Session string `json:"session"`
|
|
42
|
+
Events []map[string]any `json:"events"`
|
|
43
|
+
Count int `json:"count"`
|
|
44
|
+
After uint64 `json:"after"`
|
|
45
|
+
LastSeq uint64 `json:"lastSeq"`
|
|
46
|
+
SnapshotRequired bool `json:"snapshotRequired"`
|
|
47
|
+
}
|
|
48
|
+
get := func(t *testing.T, url string) (int, eventsBody) {
|
|
49
|
+
t.Helper()
|
|
50
|
+
resp := doGET(t, url, true)
|
|
51
|
+
var b eventsBody
|
|
52
|
+
if resp.statusCode == http.StatusOK {
|
|
53
|
+
if err := json.Unmarshal(resp.body, &b); err != nil {
|
|
54
|
+
t.Fatalf("解析响应失败:%v(body=%s)", err, resp.body)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return resp.statusCode, b
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
t.Run("省略 after = 全量", func(t *testing.T) {
|
|
61
|
+
code, b := get(t, srv.URL+"/desk/session/"+id+"/events?token=tok")
|
|
62
|
+
if code != http.StatusOK {
|
|
63
|
+
t.Fatalf("期望 200,实得 %d", code)
|
|
64
|
+
}
|
|
65
|
+
if len(b.Events) != 4 || b.Count != 4 {
|
|
66
|
+
t.Fatalf("期望 4 条,实得 %d(count=%d)", len(b.Events), b.Count)
|
|
67
|
+
}
|
|
68
|
+
if b.LastSeq != 4 {
|
|
69
|
+
t.Fatalf("lastSeq 期望 4,实得 %d", b.LastSeq)
|
|
70
|
+
}
|
|
71
|
+
if b.SnapshotRequired {
|
|
72
|
+
t.Fatal("全量读不该要求快照重建")
|
|
73
|
+
}
|
|
74
|
+
// 首条必须是 user —— 这条同时验证 core 落盘的 kind:"user" 能被读回来。
|
|
75
|
+
if b.Events[0]["kind"] != "user" {
|
|
76
|
+
t.Fatalf("首条 kind 期望 user,实得 %v", b.Events[0]["kind"])
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
t.Run("after 排他", func(t *testing.T) {
|
|
81
|
+
code, b := get(t, srv.URL+"/desk/session/"+id+"/events?after=2&token=tok")
|
|
82
|
+
if code != http.StatusOK {
|
|
83
|
+
t.Fatalf("期望 200,实得 %d", code)
|
|
84
|
+
}
|
|
85
|
+
if len(b.Events) != 2 {
|
|
86
|
+
t.Fatalf("after=2 期望 2 条(3、4),实得 %d", len(b.Events))
|
|
87
|
+
}
|
|
88
|
+
// 排他:不能把 seq=2 自己再回一遍。
|
|
89
|
+
if seq, _ := b.Events[0]["seq"].(float64); seq != 3 {
|
|
90
|
+
t.Fatalf("首条期望 seq=3,实得 %v", b.Events[0]["seq"])
|
|
91
|
+
}
|
|
92
|
+
if b.After != 2 || b.LastSeq != 4 || b.SnapshotRequired {
|
|
93
|
+
t.Fatalf("元信息不对:after=%d lastSeq=%d snapshot=%v", b.After, b.LastSeq, b.SnapshotRequired)
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
t.Run("追平 = 空增量不要求快照", func(t *testing.T) {
|
|
98
|
+
code, b := get(t, srv.URL+"/desk/session/"+id+"/events?after=4&token=tok")
|
|
99
|
+
if code != http.StatusOK {
|
|
100
|
+
t.Fatalf("期望 200,实得 %d", code)
|
|
101
|
+
}
|
|
102
|
+
if len(b.Events) != 0 {
|
|
103
|
+
t.Fatalf("已追平期望空数组,实得 %d 条", len(b.Events))
|
|
104
|
+
}
|
|
105
|
+
if b.SnapshotRequired {
|
|
106
|
+
t.Fatal("追平不是超前,不该要求快照重建")
|
|
107
|
+
}
|
|
108
|
+
// 空数组而非 null:客户端是「往画布上追加」,null 得处处判空。
|
|
109
|
+
if !strings.Contains(string(doGET(t, srv.URL+"/desk/session/"+id+"/events?after=4&token=tok", true).body), `"events":[]`) {
|
|
110
|
+
t.Fatal("空增量应序列化成 [],不是 null")
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
t.Run("游标超前 = 要求快照重建", func(t *testing.T) {
|
|
115
|
+
code, b := get(t, srv.URL+"/desk/session/"+id+"/events?after=99&token=tok")
|
|
116
|
+
if code != http.StatusOK {
|
|
117
|
+
t.Fatalf("期望 200,实得 %d", code)
|
|
118
|
+
}
|
|
119
|
+
if !b.SnapshotRequired {
|
|
120
|
+
t.Fatal("after 超前于磁盘 maxSeq 时应要求快照重建")
|
|
121
|
+
}
|
|
122
|
+
if b.LastSeq != 4 {
|
|
123
|
+
t.Fatalf("超前时 lastSeq 仍须是文件的 4,实得 %d", b.LastSeq)
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
t.Run("不存在会话 = 空数组不是 null", func(t *testing.T) {
|
|
128
|
+
code, b := get(t, srv.URL+"/desk/session/no-such/events?token=tok")
|
|
129
|
+
if code != http.StatusOK {
|
|
130
|
+
t.Fatalf("期望 200,实得 %d", code)
|
|
131
|
+
}
|
|
132
|
+
if b.Events == nil || len(b.Events) != 0 {
|
|
133
|
+
t.Fatalf("期望空数组,实得 %#v", b.Events)
|
|
134
|
+
}
|
|
135
|
+
body := string(doGET(t, srv.URL+"/desk/session/no-such/events?token=tok", true).body)
|
|
136
|
+
if !strings.Contains(body, `"events":[]`) {
|
|
137
|
+
t.Fatalf("期望 \"events\":[],实得 %s", body)
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
t.Run("after 非法 = 400", func(t *testing.T) {
|
|
142
|
+
// ⚠️ 不能静默当 0:那样客户端以为拿到了增量、实际每次收全量,
|
|
143
|
+
// 性能问题会被伪装成「没问题」。
|
|
144
|
+
for _, bad := range []string{"abc", "-1", "1.5", "99999999999999999999999"} {
|
|
145
|
+
code, _ := get(t, srv.URL+"/desk/session/"+id+"/events?after="+bad+"&token=tok")
|
|
146
|
+
if code != http.StatusBadRequest {
|
|
147
|
+
t.Fatalf("after=%q 期望 400,实得 %d", bad, code)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
t.Run("未知子路径 = 404", func(t *testing.T) {
|
|
153
|
+
code, _ := get(t, srv.URL+"/desk/session/"+id+"/bogus?token=tok")
|
|
154
|
+
if code != http.StatusNotFound {
|
|
155
|
+
t.Fatalf("未知子路径期望 404,实得 %d", code)
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
t.Run("缺令牌 = 401", func(t *testing.T) {
|
|
160
|
+
resp := doGET(t, srv.URL+"/desk/session/"+id+"/events?after=1", false)
|
|
161
|
+
if resp.statusCode != http.StatusUnauthorized {
|
|
162
|
+
t.Fatalf("期望 401,实得 %d", resp.statusCode)
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
t.Run("路径注入 = 400", func(t *testing.T) {
|
|
167
|
+
resp := doGET(t, srv.URL+"/desk/session/..%2F..%2Fetc%2Fpasswd/events?token=tok", true)
|
|
168
|
+
if resp.statusCode != http.StatusBadRequest {
|
|
169
|
+
t.Fatalf("路径注入期望 400,实得 %d", resp.statusCode)
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// 分派没串线:/messages 必须仍是全量(否则增量改动会悄悄改掉既有行为)。
|
|
174
|
+
t.Run("messages 仍全量", func(t *testing.T) {
|
|
175
|
+
resp := doGET(t, srv.URL+"/desk/session/"+id+"/messages?token=tok", true)
|
|
176
|
+
var b struct {
|
|
177
|
+
Events []map[string]any `json:"events"`
|
|
178
|
+
}
|
|
179
|
+
if err := json.Unmarshal(resp.body, &b); err != nil {
|
|
180
|
+
t.Fatal(err)
|
|
181
|
+
}
|
|
182
|
+
if len(b.Events) != 4 {
|
|
183
|
+
t.Fatalf("/messages 期望仍返回全部 4 条,实得 %d", len(b.Events))
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
package client
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"io"
|
|
6
|
+
"net/http"
|
|
7
|
+
"net/http/httptest"
|
|
8
|
+
"net/url"
|
|
9
|
+
"os"
|
|
10
|
+
"path/filepath"
|
|
11
|
+
"strings"
|
|
12
|
+
"testing"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// TestHandleSessionMessagesReadAndValidate 验「看历史」路由的边界:
|
|
16
|
+
//
|
|
17
|
+
// - 正常:写出 jsonl 后能读回(保证 plugin 这条线通)
|
|
18
|
+
// - 不存在的会话 → 200 + 空 events,不是 404(空会话是合法状态)
|
|
19
|
+
// - 缺令牌 → 401
|
|
20
|
+
// - POST → 405
|
|
21
|
+
// - 路径注入 `../../etc/passwd` → 400(拒绝 `.` 与 `..` 整段 id)
|
|
22
|
+
// - 空 id(HTTP 客户端归一化后落到 "/desk/session/messages")→ 200 + events=null:
|
|
23
|
+
// 此时 "messages" 成了 id —— 路由不挡。像普通会话一样查,不存在就 200+空 events。
|
|
24
|
+
// 这是合理的:路由没定义「纯 /messages」这条,前缀归一化后当成 id 处理不越权。
|
|
25
|
+
func TestHandleSessionMessagesReadAndValidate(t *testing.T) {
|
|
26
|
+
// DefaultLogDir 用 HOME 解析成 `~/.magic-agent/sessions`。
|
|
27
|
+
// 临时把 HOME 指到 t.TempDir(),fixture 写到 `<temp>/.magic-agent/sessions/<id>.jsonl`,
|
|
28
|
+
// 不然 UserHomeDir 解析出来的目录不是 fixture 所在的地方。
|
|
29
|
+
home := t.TempDir()
|
|
30
|
+
dir := filepath.Join(home, ".magic-agent", "sessions")
|
|
31
|
+
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|
32
|
+
t.Fatal(err)
|
|
33
|
+
}
|
|
34
|
+
t.Setenv("HOME", home)
|
|
35
|
+
id := "sess-good"
|
|
36
|
+
path := filepath.Join(dir, id+".jsonl")
|
|
37
|
+
want := []string{
|
|
38
|
+
`{"seq":1,"kind":"thinking","text":"思考","session_id":"` + id + `","at":"2026-09-27T20:00:00Z"}`,
|
|
39
|
+
`{"seq":2,"kind":"text","text":"正文","session_id":"` + id + `","at":"2026-09-27T20:00:01Z"}`,
|
|
40
|
+
`{"seq":3,"kind":"turn_end","text":"完成","session_id":"` + id + `","at":"2026-09-27T20:00:02Z"}`,
|
|
41
|
+
}
|
|
42
|
+
if err := os.WriteFile(path, []byte(strings.Join(want, "\n")+"\n"), 0o600); err != nil {
|
|
43
|
+
t.Fatal(err)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
svc := newTestService(t, fakeCore(t, goodContract))
|
|
47
|
+
srv := httptest.NewServer(svc.Handler())
|
|
48
|
+
defer srv.Close()
|
|
49
|
+
|
|
50
|
+
// --- 正常:3 条 events,按 seq 升序
|
|
51
|
+
resp := doGET(t, srv.URL+"/desk/session/"+id+"/messages?token=tok", true)
|
|
52
|
+
if resp.statusCode != http.StatusOK {
|
|
53
|
+
t.Fatalf("正常请求期望 200,实得 %d", resp.statusCode)
|
|
54
|
+
}
|
|
55
|
+
var body struct {
|
|
56
|
+
Session string `json:"session"`
|
|
57
|
+
Events []map[string]any `json:"events"`
|
|
58
|
+
}
|
|
59
|
+
if err := json.Unmarshal(resp.body, &body); err != nil {
|
|
60
|
+
t.Fatal(err)
|
|
61
|
+
}
|
|
62
|
+
if body.Session != id {
|
|
63
|
+
t.Fatalf("session=%q 实得 %q", id, body.Session)
|
|
64
|
+
}
|
|
65
|
+
if len(body.Events) != 3 {
|
|
66
|
+
t.Fatalf("期望 3 条 events,实得 %d:%v", len(body.Events), body.Events)
|
|
67
|
+
}
|
|
68
|
+
for i, e := range body.Events {
|
|
69
|
+
// want 行是 jsonl 字串原样;Events 经 json.Unmarshal 后是 map —— 这里只验 seq 与 kind。
|
|
70
|
+
var w map[string]any
|
|
71
|
+
_ = json.Unmarshal([]byte(want[i]), &w)
|
|
72
|
+
if e["seq"] != w["seq"] {
|
|
73
|
+
t.Fatalf("event %d seq=%v 期望 %v", i, e["seq"], w["seq"])
|
|
74
|
+
}
|
|
75
|
+
if e["kind"] != w["kind"] {
|
|
76
|
+
t.Fatalf("event %d kind=%v 期望 %v", i, e["kind"], w["kind"])
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- 不存在:200 + events=null
|
|
81
|
+
resp = doGET(t, srv.URL+"/desk/session/no-such/messages?token=tok", true)
|
|
82
|
+
if resp.statusCode != http.StatusOK {
|
|
83
|
+
t.Fatalf("不存在期望 200,实得 %d", resp.statusCode)
|
|
84
|
+
}
|
|
85
|
+
var body2 struct {
|
|
86
|
+
Events []map[string]any `json:"events"`
|
|
87
|
+
}
|
|
88
|
+
_ = json.Unmarshal(resp.body, &body2)
|
|
89
|
+
if body2.Events != nil {
|
|
90
|
+
t.Fatalf("不存在期望 events=null,实得 %v", body2.Events)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// --- 缺令牌 → 401
|
|
94
|
+
resp = doGET(t, srv.URL+"/desk/session/"+id+"/messages", false)
|
|
95
|
+
if resp.statusCode != http.StatusUnauthorized {
|
|
96
|
+
t.Fatalf("缺令牌期望 401,实得 %d", resp.statusCode)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- POST → 405
|
|
100
|
+
req, _ := http.NewRequest(http.MethodPost, srv.URL+"/desk/session/"+id+"/messages?token=tok", nil)
|
|
101
|
+
req.Header.Set("Origin", "https://localhost")
|
|
102
|
+
res, err := http.DefaultClient.Do(req)
|
|
103
|
+
if err != nil {
|
|
104
|
+
t.Fatal(err)
|
|
105
|
+
}
|
|
106
|
+
res.Body.Close()
|
|
107
|
+
if res.StatusCode != http.StatusMethodNotAllowed {
|
|
108
|
+
t.Fatalf("POST 期望 405,实得 %d", res.StatusCode)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// --- 路径注入 `../../etc/passwd` → 400(拒绝 `.` 与 `..` 整段 id)
|
|
112
|
+
resp = doGET(t, srv.URL+"/desk/session/..%2F..%2Fetc%2Fpasswd/messages?token=tok", true)
|
|
113
|
+
if resp.statusCode != http.StatusBadRequest {
|
|
114
|
+
t.Fatalf("路径注入期望 400,实得 %d", resp.statusCode)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- 空 id(仅 `/messages`) → 实际变 /desk/session/messages(HTTP 客户端会归一化 `//`),
|
|
118
|
+
// 此时 id 落到 "messages" —— 像普通会话一样查(不存在 → 200+空 events)。
|
|
119
|
+
// 这是合理的:路由没有定义"纯 /messages"这条,前缀归一化后当成 id 处理不会越权。
|
|
120
|
+
resp = doGET(t, srv.URL+"/desk/session//messages?token=tok", true)
|
|
121
|
+
if resp.statusCode != http.StatusOK {
|
|
122
|
+
t.Fatalf("前缀归一化后空 id 期望 200,实得 %d", resp.statusCode)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// doGET 简单 GET 一把:带 origin、断言状态码、读 body 全文。
|
|
127
|
+
func doGET(t *testing.T, rawURL string, withOrigin bool) (resp struct {
|
|
128
|
+
statusCode int
|
|
129
|
+
body []byte
|
|
130
|
+
}) {
|
|
131
|
+
t.Helper()
|
|
132
|
+
u, err := url.Parse(rawURL)
|
|
133
|
+
if err != nil {
|
|
134
|
+
t.Fatal(err)
|
|
135
|
+
}
|
|
136
|
+
req, _ := http.NewRequest(http.MethodGet, u.String(), nil)
|
|
137
|
+
if withOrigin {
|
|
138
|
+
req.Header.Set("Origin", "https://localhost")
|
|
139
|
+
}
|
|
140
|
+
res, err := http.DefaultClient.Do(req)
|
|
141
|
+
if err != nil {
|
|
142
|
+
t.Fatal(err)
|
|
143
|
+
}
|
|
144
|
+
defer res.Body.Close()
|
|
145
|
+
// 全量读取 body(流式响应可能被自动关流)。
|
|
146
|
+
buf, _ := io.ReadAll(res.Body)
|
|
147
|
+
resp.statusCode = res.StatusCode
|
|
148
|
+
resp.body = buf
|
|
149
|
+
return
|
|
150
|
+
}
|