@syengup/friday-channel-next 0.1.5 → 0.1.6
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/install.js +2 -2
- package/package.json +1 -1
- package/src/e2e/auto-approve.integration.test.ts +415 -0
package/install.js
CHANGED
|
@@ -96,7 +96,7 @@ let installed = false;
|
|
|
96
96
|
|
|
97
97
|
try {
|
|
98
98
|
const out = execSync(
|
|
99
|
-
`${openclawCmd} plugins install @syengup/friday-channel-next@latest`,
|
|
99
|
+
`${openclawCmd} plugins install @syengup/friday-channel-next@latest --force`,
|
|
100
100
|
{ encoding: "utf8", stdio: "pipe", timeout: 120000 }
|
|
101
101
|
);
|
|
102
102
|
if (out.trim()) console.log(out.trim());
|
|
@@ -124,7 +124,7 @@ if (!installed) {
|
|
|
124
124
|
process.exit(1);
|
|
125
125
|
}
|
|
126
126
|
warn("Manual install complete, but auto-upgrade is NOT available.");
|
|
127
|
-
warn("To enable auto-upgrade later, run: openclaw plugins install @syengup/friday-channel-next");
|
|
127
|
+
warn("To enable auto-upgrade later, run: openclaw plugins install @syengup/friday-channel-next --force");
|
|
128
128
|
// Clean up legacy dir even in fallback to avoid duplicate warnings
|
|
129
129
|
if (existsSync(join(USER_HOME, ".openclaw", "extensions", "friday-channel-next"))) {
|
|
130
130
|
warn("Legacy install detected. Remove it to avoid duplicate warnings:");
|
package/package.json
CHANGED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E test: 模拟 iOS 两步自动批准流程 (device-approve → nodes-approve)
|
|
3
|
+
*
|
|
4
|
+
* iOS 端 connect 成功后会调用 performPostConnectHealthCheckIfNeeded():
|
|
5
|
+
* 1. GET /friday-next/health?deviceId=...&nodeDeviceId=... (selfHeal 默认 true)
|
|
6
|
+
* 2. 如果 devicePairing.status == "pending" → POST /friday-next/device-approve
|
|
7
|
+
* 3. 如果 nodePairing.status == "pending" → POST /friday-next/nodes-approve
|
|
8
|
+
*/
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
|
+
import { createAppSimulator } from "../test-support/app-simulator.js";
|
|
11
|
+
import { createTempHistoryDir, removeTempHistoryDir, setMockRuntime } from "../test-support/mock-runtime.js";
|
|
12
|
+
import { __setMockNodePairingForTests } from "../agent/node-pairing-bridge.js";
|
|
13
|
+
|
|
14
|
+
const FAKE_DEVICE_ID = "a80b8c4b305fb02c5772c409c6dfcbacde691b61557f7779511ad1a5be8fdf06";
|
|
15
|
+
const DEVICE_REQUEST_ID = "12f150e8-b1bc-4688-be23-e3a7fa8b9e51";
|
|
16
|
+
const NODE_REQUEST_ID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
|
|
17
|
+
|
|
18
|
+
const { mockListDevices, mockApproveDevice } = vi.hoisted(() => ({
|
|
19
|
+
mockListDevices: vi.fn(),
|
|
20
|
+
mockApproveDevice: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock("openclaw/plugin-sdk/device-bootstrap", () => ({
|
|
24
|
+
listDevicePairing: mockListDevices,
|
|
25
|
+
approveDevicePairing: mockApproveDevice,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
describe("e2e two-step auto-approval", () => {
|
|
29
|
+
let historyDir = "";
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
historyDir = createTempHistoryDir();
|
|
33
|
+
setMockRuntime({ historyDir, authToken: "test-token" });
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
// 默认:设备和节点都不在 pending/paired 中
|
|
36
|
+
mockListDevices.mockResolvedValue({ pending: [], paired: [] });
|
|
37
|
+
mockApproveDevice.mockResolvedValue({ status: "approved", requestId: DEVICE_REQUEST_ID });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
removeTempHistoryDir(historyDir);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ── Step 1: Device Approve ───────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
it("Step 1: POST /friday-next/device-approve 成功批准设备", async () => {
|
|
47
|
+
mockListDevices.mockResolvedValueOnce({
|
|
48
|
+
pending: [{ requestId: DEVICE_REQUEST_ID, deviceId: FAKE_DEVICE_ID }],
|
|
49
|
+
paired: [],
|
|
50
|
+
});
|
|
51
|
+
mockApproveDevice.mockResolvedValueOnce({
|
|
52
|
+
status: "approved",
|
|
53
|
+
requestId: DEVICE_REQUEST_ID,
|
|
54
|
+
device: { deviceId: FAKE_DEVICE_ID, approvedAtMs: 1700000000000 },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const app = createAppSimulator({ token: "test-token" });
|
|
58
|
+
const res = await app.rawRequest({
|
|
59
|
+
method: "POST",
|
|
60
|
+
path: "/friday-next/device-approve",
|
|
61
|
+
headers: { "content-type": "application/json" },
|
|
62
|
+
body: JSON.stringify({ deviceId: FAKE_DEVICE_ID }),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(res.status).toBe(200);
|
|
66
|
+
const body = JSON.parse(res.body);
|
|
67
|
+
expect(body.ok).toBe(true);
|
|
68
|
+
expect(body.deviceId).toBe(FAKE_DEVICE_ID.toUpperCase());
|
|
69
|
+
expect(body.requestId).toBe(DEVICE_REQUEST_ID);
|
|
70
|
+
expect(mockApproveDevice).toHaveBeenCalledWith(DEVICE_REQUEST_ID);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("Step 1: device 已在 paired 列表中时返回 alreadyApproved", async () => {
|
|
74
|
+
mockListDevices.mockResolvedValueOnce({
|
|
75
|
+
pending: [],
|
|
76
|
+
paired: [{ deviceId: FAKE_DEVICE_ID, approvedAtMs: 1700000000000 }],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const app = createAppSimulator({ token: "test-token" });
|
|
80
|
+
const res = await app.rawRequest({
|
|
81
|
+
method: "POST",
|
|
82
|
+
path: "/friday-next/device-approve",
|
|
83
|
+
headers: { "content-type": "application/json" },
|
|
84
|
+
body: JSON.stringify({ deviceId: FAKE_DEVICE_ID }),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(res.status).toBe(200);
|
|
88
|
+
const body = JSON.parse(res.body);
|
|
89
|
+
expect(body.ok).toBe(true);
|
|
90
|
+
expect(body.alreadyApproved).toBe(true);
|
|
91
|
+
expect(mockApproveDevice).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("Step 1: device 不在 pending/paired 中时返回 404", async () => {
|
|
95
|
+
mockListDevices.mockResolvedValueOnce({ pending: [], paired: [] });
|
|
96
|
+
|
|
97
|
+
const app = createAppSimulator({ token: "test-token" });
|
|
98
|
+
const res = await app.rawRequest({
|
|
99
|
+
method: "POST",
|
|
100
|
+
path: "/friday-next/device-approve",
|
|
101
|
+
headers: { "content-type": "application/json" },
|
|
102
|
+
body: JSON.stringify({ deviceId: FAKE_DEVICE_ID }),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(res.status).toBe(404);
|
|
106
|
+
const body = JSON.parse(res.body);
|
|
107
|
+
expect(body.error).toContain("No pending device found");
|
|
108
|
+
expect(body.deviceId).toBe(FAKE_DEVICE_ID.toUpperCase());
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ── Step 2: Node Approve ─────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
it("Step 2: POST /friday-next/nodes-approve 成功批准节点", async () => {
|
|
114
|
+
const mockListNodePairing = vi.fn().mockResolvedValueOnce({
|
|
115
|
+
pending: [{ requestId: NODE_REQUEST_ID, nodeId: FAKE_DEVICE_ID }],
|
|
116
|
+
paired: [],
|
|
117
|
+
});
|
|
118
|
+
const mockApproveNodePairing = vi.fn().mockResolvedValueOnce({
|
|
119
|
+
requestId: NODE_REQUEST_ID,
|
|
120
|
+
node: { nodeId: FAKE_DEVICE_ID, approvedAtMs: 1700000000000 },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
__setMockNodePairingForTests({
|
|
124
|
+
listNodePairing: mockListNodePairing,
|
|
125
|
+
approveNodePairing: mockApproveNodePairing,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const app = createAppSimulator({ token: "test-token" });
|
|
129
|
+
const res = await app.rawRequest({
|
|
130
|
+
method: "POST",
|
|
131
|
+
path: "/friday-next/nodes-approve",
|
|
132
|
+
headers: { "content-type": "application/json" },
|
|
133
|
+
body: JSON.stringify({ nodeId: FAKE_DEVICE_ID }),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(res.status).toBe(200);
|
|
137
|
+
const body = JSON.parse(res.body);
|
|
138
|
+
expect(body.ok).toBe(true);
|
|
139
|
+
expect(body.nodeId).toBe(FAKE_DEVICE_ID.toUpperCase());
|
|
140
|
+
expect(body.requestId).toBe(NODE_REQUEST_ID);
|
|
141
|
+
expect(mockApproveNodePairing).toHaveBeenCalledWith(NODE_REQUEST_ID, {
|
|
142
|
+
callerScopes: ["operator.admin", "operator.pairing", "operator.read", "operator.write"],
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("Step 2: node 已在 paired 中且有 caps 时返回 alreadyApproved", async () => {
|
|
147
|
+
const mockListNodePairing = vi.fn().mockResolvedValueOnce({
|
|
148
|
+
pending: [],
|
|
149
|
+
paired: [{
|
|
150
|
+
nodeId: FAKE_DEVICE_ID,
|
|
151
|
+
approvedAtMs: 1700000000000,
|
|
152
|
+
caps: ["location", "canvas"],
|
|
153
|
+
commands: ["canvas.present"],
|
|
154
|
+
}],
|
|
155
|
+
});
|
|
156
|
+
const mockApproveNodePairing = vi.fn();
|
|
157
|
+
|
|
158
|
+
__setMockNodePairingForTests({
|
|
159
|
+
listNodePairing: mockListNodePairing,
|
|
160
|
+
approveNodePairing: mockApproveNodePairing,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const app = createAppSimulator({ token: "test-token" });
|
|
164
|
+
const res = await app.rawRequest({
|
|
165
|
+
method: "POST",
|
|
166
|
+
path: "/friday-next/nodes-approve",
|
|
167
|
+
headers: { "content-type": "application/json" },
|
|
168
|
+
body: JSON.stringify({ nodeId: FAKE_DEVICE_ID }),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(res.status).toBe(200);
|
|
172
|
+
const body = JSON.parse(res.body);
|
|
173
|
+
expect(body.ok).toBe(true);
|
|
174
|
+
expect(body.alreadyApproved).toBe(true);
|
|
175
|
+
expect(body.caps).toEqual(["location", "canvas"]);
|
|
176
|
+
expect(mockApproveNodePairing).not.toHaveBeenCalled();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("Step 2: node 不在 pending/paired 中时返回 404", async () => {
|
|
180
|
+
const mockListNodePairing = vi.fn().mockResolvedValueOnce({
|
|
181
|
+
pending: [],
|
|
182
|
+
paired: [],
|
|
183
|
+
});
|
|
184
|
+
const mockApproveNodePairing = vi.fn();
|
|
185
|
+
|
|
186
|
+
__setMockNodePairingForTests({
|
|
187
|
+
listNodePairing: mockListNodePairing,
|
|
188
|
+
approveNodePairing: mockApproveNodePairing,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const app = createAppSimulator({ token: "test-token" });
|
|
192
|
+
const res = await app.rawRequest({
|
|
193
|
+
method: "POST",
|
|
194
|
+
path: "/friday-next/nodes-approve",
|
|
195
|
+
headers: { "content-type": "application/json" },
|
|
196
|
+
body: JSON.stringify({ nodeId: FAKE_DEVICE_ID }),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(res.status).toBe(404);
|
|
200
|
+
const body = JSON.parse(res.body);
|
|
201
|
+
expect(body.error).toContain("No pending node found");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ── Combined: full iOS flow simulation ───────────────────────────
|
|
205
|
+
|
|
206
|
+
it("完整两步流程: device-approve → nodes-approve 串行成功", async () => {
|
|
207
|
+
// Step 1 mock: 设备在 pending 中
|
|
208
|
+
mockListDevices.mockResolvedValueOnce({
|
|
209
|
+
pending: [{ requestId: DEVICE_REQUEST_ID, deviceId: FAKE_DEVICE_ID }],
|
|
210
|
+
paired: [],
|
|
211
|
+
});
|
|
212
|
+
mockApproveDevice.mockResolvedValueOnce({
|
|
213
|
+
status: "approved",
|
|
214
|
+
requestId: DEVICE_REQUEST_ID,
|
|
215
|
+
device: { deviceId: FAKE_DEVICE_ID, approvedAtMs: 1700000000000 },
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Step 2 mock: 节点在 pending 中
|
|
219
|
+
const mockListNodePairing = vi.fn().mockResolvedValueOnce({
|
|
220
|
+
pending: [{ requestId: NODE_REQUEST_ID, nodeId: FAKE_DEVICE_ID }],
|
|
221
|
+
paired: [],
|
|
222
|
+
});
|
|
223
|
+
const mockApproveNodePairing = vi.fn().mockResolvedValueOnce({
|
|
224
|
+
requestId: NODE_REQUEST_ID,
|
|
225
|
+
node: { nodeId: FAKE_DEVICE_ID, approvedAtMs: 1700000000001 },
|
|
226
|
+
});
|
|
227
|
+
__setMockNodePairingForTests({
|
|
228
|
+
listNodePairing: mockListNodePairing,
|
|
229
|
+
approveNodePairing: mockApproveNodePairing,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const app = createAppSimulator({ token: "test-token" });
|
|
233
|
+
|
|
234
|
+
// Step 1: 批准设备
|
|
235
|
+
const step1 = await app.rawRequest({
|
|
236
|
+
method: "POST",
|
|
237
|
+
path: "/friday-next/device-approve",
|
|
238
|
+
headers: { "content-type": "application/json" },
|
|
239
|
+
body: JSON.stringify({ deviceId: FAKE_DEVICE_ID }),
|
|
240
|
+
});
|
|
241
|
+
expect(step1.status).toBe(200);
|
|
242
|
+
expect(JSON.parse(step1.body).ok).toBe(true);
|
|
243
|
+
expect(mockApproveDevice).toHaveBeenCalledTimes(1);
|
|
244
|
+
|
|
245
|
+
// Step 2: 批准节点
|
|
246
|
+
const step2 = await app.rawRequest({
|
|
247
|
+
method: "POST",
|
|
248
|
+
path: "/friday-next/nodes-approve",
|
|
249
|
+
headers: { "content-type": "application/json" },
|
|
250
|
+
body: JSON.stringify({ nodeId: FAKE_DEVICE_ID }),
|
|
251
|
+
});
|
|
252
|
+
expect(step2.status).toBe(200);
|
|
253
|
+
expect(JSON.parse(step2.body).ok).toBe(true);
|
|
254
|
+
expect(mockApproveNodePairing).toHaveBeenCalledTimes(1);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ── Auth tests ───────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
it("device-approve: 正确 token 正常批准", async () => {
|
|
260
|
+
mockListDevices.mockResolvedValueOnce({
|
|
261
|
+
pending: [{ requestId: DEVICE_REQUEST_ID, deviceId: FAKE_DEVICE_ID }],
|
|
262
|
+
paired: [],
|
|
263
|
+
});
|
|
264
|
+
mockApproveDevice.mockResolvedValueOnce({
|
|
265
|
+
status: "approved",
|
|
266
|
+
requestId: DEVICE_REQUEST_ID,
|
|
267
|
+
device: { deviceId: FAKE_DEVICE_ID, approvedAtMs: 1700000000000 },
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const app = createAppSimulator({ token: "test-token" });
|
|
271
|
+
const res = await app.rawRequest({
|
|
272
|
+
method: "POST",
|
|
273
|
+
path: "/friday-next/device-approve",
|
|
274
|
+
headers: { "content-type": "application/json" },
|
|
275
|
+
body: JSON.stringify({ deviceId: FAKE_DEVICE_ID }),
|
|
276
|
+
});
|
|
277
|
+
expect(res.status).toBe(200);
|
|
278
|
+
expect(JSON.parse(res.body).ok).toBe(true);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("device-approve: 错误 token 返回 401", async () => {
|
|
282
|
+
const app = createAppSimulator({ token: "wrong-token" });
|
|
283
|
+
const res = await app.rawRequest({
|
|
284
|
+
method: "POST",
|
|
285
|
+
path: "/friday-next/device-approve",
|
|
286
|
+
headers: { "content-type": "application/json" },
|
|
287
|
+
body: JSON.stringify({ deviceId: FAKE_DEVICE_ID }),
|
|
288
|
+
});
|
|
289
|
+
expect(res.status).toBe(401);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// ── Health endpoint with selfHeal ────────────────────────────────
|
|
293
|
+
|
|
294
|
+
it("GET /friday-next/health?selfHeal=true 自动批准 pending device", async () => {
|
|
295
|
+
mockListDevices.mockResolvedValueOnce({
|
|
296
|
+
pending: [{ requestId: DEVICE_REQUEST_ID, deviceId: FAKE_DEVICE_ID }],
|
|
297
|
+
paired: [],
|
|
298
|
+
});
|
|
299
|
+
mockApproveDevice.mockResolvedValueOnce({
|
|
300
|
+
status: "approved",
|
|
301
|
+
requestId: DEVICE_REQUEST_ID,
|
|
302
|
+
device: { deviceId: FAKE_DEVICE_ID, approvedAtMs: 1700000000000 },
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const app = createAppSimulator({ token: "test-token" });
|
|
306
|
+
const res = await app.rawRequest({
|
|
307
|
+
method: "GET",
|
|
308
|
+
path: `/friday-next/health?deviceId=${FAKE_DEVICE_ID}&selfHeal=true`,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(res.status).toBe(200);
|
|
312
|
+
const body = JSON.parse(res.body);
|
|
313
|
+
expect(body.ok).toBe(true);
|
|
314
|
+
expect(body.devicePairing.status).toBe("ok");
|
|
315
|
+
expect(body.devicePairing.detail).toContain("auto-approved");
|
|
316
|
+
expect(body.repairActions).toHaveLength(1);
|
|
317
|
+
expect(body.repairActions[0].component).toBe("devicePairing");
|
|
318
|
+
expect(body.repairActions[0].result).toBe("ok");
|
|
319
|
+
expect(mockApproveDevice).toHaveBeenCalledWith(DEVICE_REQUEST_ID);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("GET /friday-next/health 不带 selfHeal 时不自动批准 (只返回 pending)", async () => {
|
|
323
|
+
mockListDevices.mockResolvedValueOnce({
|
|
324
|
+
pending: [{ requestId: DEVICE_REQUEST_ID, deviceId: FAKE_DEVICE_ID }],
|
|
325
|
+
paired: [],
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const app = createAppSimulator({ token: "test-token" });
|
|
329
|
+
const res = await app.rawRequest({
|
|
330
|
+
method: "GET",
|
|
331
|
+
path: `/friday-next/health?deviceId=${FAKE_DEVICE_ID}`,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
expect(res.status).toBe(200);
|
|
335
|
+
const body = JSON.parse(res.body);
|
|
336
|
+
expect(body.devicePairing.status).toBe("pending");
|
|
337
|
+
expect(body.devicePairing.detail).toContain("pending approval");
|
|
338
|
+
expect(mockApproveDevice).not.toHaveBeenCalled();
|
|
339
|
+
// ok 仍为 true,因为 pending 也视为 ok
|
|
340
|
+
expect(body.ok).toBe(true);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("GET /friday-next/health?selfHeal=true 自动批准 pending node", async () => {
|
|
344
|
+
mockListDevices.mockResolvedValue({ pending: [], paired: [] }); // device 不相关
|
|
345
|
+
|
|
346
|
+
const mockListNodePairing = vi.fn().mockResolvedValueOnce({
|
|
347
|
+
pending: [{ requestId: NODE_REQUEST_ID, nodeId: FAKE_DEVICE_ID }],
|
|
348
|
+
paired: [],
|
|
349
|
+
});
|
|
350
|
+
const mockApproveNodePairing = vi.fn().mockResolvedValueOnce({
|
|
351
|
+
requestId: NODE_REQUEST_ID,
|
|
352
|
+
node: { nodeId: FAKE_DEVICE_ID },
|
|
353
|
+
});
|
|
354
|
+
__setMockNodePairingForTests({
|
|
355
|
+
listNodePairing: mockListNodePairing,
|
|
356
|
+
approveNodePairing: mockApproveNodePairing,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const app = createAppSimulator({ token: "test-token" });
|
|
360
|
+
const res = await app.rawRequest({
|
|
361
|
+
method: "GET",
|
|
362
|
+
path: `/friday-next/health?nodeDeviceId=${FAKE_DEVICE_ID}&selfHeal=true`,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
expect(res.status).toBe(200);
|
|
366
|
+
const body = JSON.parse(res.body);
|
|
367
|
+
expect(body.nodePairing.status).toBe("ok");
|
|
368
|
+
expect(body.nodePairing.detail).toContain("auto-approved");
|
|
369
|
+
expect(body.repairActions).toHaveLength(1);
|
|
370
|
+
expect(body.repairActions[0].component).toBe("nodePairing");
|
|
371
|
+
expect(body.repairActions[0].result).toBe("ok");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("GET /friday-next/health?selfHeal=true 同时自动批准 device 和 node", async () => {
|
|
375
|
+
// Device is pending
|
|
376
|
+
mockListDevices.mockResolvedValueOnce({
|
|
377
|
+
pending: [{ requestId: DEVICE_REQUEST_ID, deviceId: FAKE_DEVICE_ID }],
|
|
378
|
+
paired: [],
|
|
379
|
+
});
|
|
380
|
+
mockApproveDevice.mockResolvedValueOnce({
|
|
381
|
+
status: "approved",
|
|
382
|
+
requestId: DEVICE_REQUEST_ID,
|
|
383
|
+
device: { deviceId: FAKE_DEVICE_ID, approvedAtMs: 1700000000000 },
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Node is pending
|
|
387
|
+
const mockListNodePairing = vi.fn().mockResolvedValueOnce({
|
|
388
|
+
pending: [{ requestId: NODE_REQUEST_ID, nodeId: FAKE_DEVICE_ID }],
|
|
389
|
+
paired: [],
|
|
390
|
+
});
|
|
391
|
+
const mockApproveNodePairing = vi.fn().mockResolvedValueOnce({
|
|
392
|
+
requestId: NODE_REQUEST_ID,
|
|
393
|
+
node: { nodeId: FAKE_DEVICE_ID },
|
|
394
|
+
});
|
|
395
|
+
__setMockNodePairingForTests({
|
|
396
|
+
listNodePairing: mockListNodePairing,
|
|
397
|
+
approveNodePairing: mockApproveNodePairing,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const app = createAppSimulator({ token: "test-token" });
|
|
401
|
+
const res = await app.rawRequest({
|
|
402
|
+
method: "GET",
|
|
403
|
+
path: `/friday-next/health?deviceId=${FAKE_DEVICE_ID}&nodeDeviceId=${FAKE_DEVICE_ID}&selfHeal=true`,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
expect(res.status).toBe(200);
|
|
407
|
+
const body = JSON.parse(res.body);
|
|
408
|
+
expect(body.ok).toBe(true);
|
|
409
|
+
expect(body.devicePairing.status).toBe("ok");
|
|
410
|
+
expect(body.nodePairing.status).toBe("ok");
|
|
411
|
+
expect(body.repairActions).toHaveLength(2);
|
|
412
|
+
expect(body.repairActions[0].component).toBe("devicePairing");
|
|
413
|
+
expect(body.repairActions[1].component).toBe("nodePairing");
|
|
414
|
+
});
|
|
415
|
+
});
|