@openclaw/zalo 2026.3.13 → 2026.5.1-beta.2
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/README.md +1 -1
- package/api.ts +9 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +5 -0
- package/index.test.ts +15 -0
- package/index.ts +16 -13
- package/openclaw.plugin.json +514 -1
- package/package.json +31 -5
- package/runtime-api.test.ts +17 -0
- package/runtime-api.ts +75 -0
- package/secret-contract-api.ts +5 -0
- package/setup-api.ts +34 -0
- package/setup-entry.ts +13 -0
- package/src/accounts.test.ts +70 -0
- package/src/accounts.ts +19 -19
- package/src/actions.runtime.ts +5 -0
- package/src/actions.test.ts +32 -0
- package/src/actions.ts +20 -14
- package/src/api.test.ts +93 -2
- package/src/api.ts +29 -2
- package/src/approval-auth.test.ts +17 -0
- package/src/approval-auth.ts +25 -0
- package/src/channel.directory.test.ts +19 -6
- package/src/channel.runtime.ts +93 -0
- package/src/channel.startup.test.ts +26 -19
- package/src/channel.ts +228 -336
- package/src/config-schema.ts +3 -3
- package/src/group-access.ts +4 -3
- package/src/monitor.group-policy.test.ts +0 -12
- package/src/monitor.image.polling.test.ts +110 -0
- package/src/monitor.lifecycle.test.ts +41 -22
- package/src/monitor.pairing.lifecycle.test.ts +141 -0
- package/src/monitor.polling.media-reply.test.ts +425 -0
- package/src/monitor.reply-once.lifecycle.test.ts +171 -0
- package/src/monitor.ts +460 -206
- package/src/monitor.types.ts +4 -0
- package/src/monitor.webhook.test.ts +392 -62
- package/src/monitor.webhook.ts +73 -36
- package/src/outbound-media.test.ts +182 -0
- package/src/outbound-media.ts +241 -0
- package/src/outbound-payload.contract.test.ts +45 -0
- package/src/probe.ts +1 -1
- package/src/proxy.ts +1 -1
- package/src/runtime-api.ts +75 -0
- package/src/runtime-support.ts +91 -0
- package/src/runtime.ts +6 -3
- package/src/secret-contract.ts +109 -0
- package/src/secret-input.ts +1 -9
- package/src/send.test.ts +120 -0
- package/src/send.ts +15 -13
- package/src/session-route.ts +32 -0
- package/src/setup-allow-from.ts +94 -0
- package/src/setup-core.ts +149 -0
- package/src/{onboarding.status.test.ts → setup-status.test.ts} +13 -4
- package/src/setup-surface.test.ts +175 -0
- package/src/{onboarding.ts → setup-surface.ts} +59 -177
- package/src/status-issues.test.ts +2 -14
- package/src/status-issues.ts +8 -2
- package/src/test-support/lifecycle-test-support.ts +413 -0
- package/src/test-support/monitor-mocks-test-support.ts +209 -0
- package/src/token.test.ts +15 -0
- package/src/token.ts +8 -17
- package/src/types.ts +2 -2
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -101
- package/src/channel.sendpayload.test.ts +0 -44
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createRuntimeEnv } from "openclaw/plugin-sdk/plugin-test-runtime";
|
|
2
|
+
import { afterAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
createImageLifecycleCore,
|
|
5
|
+
createImageUpdate,
|
|
6
|
+
createLifecycleMonitorSetup,
|
|
7
|
+
expectImageLifecycleDelivery,
|
|
8
|
+
settleAsyncWork,
|
|
9
|
+
} from "./test-support/lifecycle-test-support.js";
|
|
10
|
+
import {
|
|
11
|
+
getUpdatesMock,
|
|
12
|
+
getZaloRuntimeMock,
|
|
13
|
+
loadCachedLifecycleMonitorModule,
|
|
14
|
+
resetLifecycleTestState,
|
|
15
|
+
sendMessageMock,
|
|
16
|
+
} from "./test-support/monitor-mocks-test-support.js";
|
|
17
|
+
|
|
18
|
+
describe("Zalo polling image handling", () => {
|
|
19
|
+
const {
|
|
20
|
+
core,
|
|
21
|
+
finalizeInboundContextMock,
|
|
22
|
+
recordInboundSessionMock,
|
|
23
|
+
fetchRemoteMediaMock,
|
|
24
|
+
saveMediaBufferMock,
|
|
25
|
+
} = createImageLifecycleCore();
|
|
26
|
+
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
await resetLifecycleTestState();
|
|
29
|
+
getZaloRuntimeMock.mockReturnValue(core);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterAll(async () => {
|
|
33
|
+
await resetLifecycleTestState();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("downloads inbound image media from photo_url and preserves display_name", async () => {
|
|
37
|
+
getUpdatesMock
|
|
38
|
+
.mockResolvedValueOnce({
|
|
39
|
+
ok: true,
|
|
40
|
+
result: createImageUpdate({ date: 1774084566880 }),
|
|
41
|
+
})
|
|
42
|
+
.mockImplementation(() => new Promise(() => {}));
|
|
43
|
+
|
|
44
|
+
const { monitorZaloProvider } = await loadCachedLifecycleMonitorModule("zalo-image-polling");
|
|
45
|
+
const abort = new AbortController();
|
|
46
|
+
const runtime = createRuntimeEnv();
|
|
47
|
+
const { account, config } = createLifecycleMonitorSetup({
|
|
48
|
+
accountId: "default",
|
|
49
|
+
dmPolicy: "open",
|
|
50
|
+
});
|
|
51
|
+
const run = monitorZaloProvider({
|
|
52
|
+
token: "zalo-token", // pragma: allowlist secret
|
|
53
|
+
account,
|
|
54
|
+
config,
|
|
55
|
+
runtime,
|
|
56
|
+
abortSignal: abort.signal,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await settleAsyncWork();
|
|
60
|
+
expect(fetchRemoteMediaMock).toHaveBeenCalledTimes(1);
|
|
61
|
+
expectImageLifecycleDelivery({
|
|
62
|
+
fetchRemoteMediaMock,
|
|
63
|
+
saveMediaBufferMock,
|
|
64
|
+
finalizeInboundContextMock,
|
|
65
|
+
recordInboundSessionMock,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
abort.abort();
|
|
69
|
+
await run;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("rejects unauthorized DM images before downloading media", async () => {
|
|
73
|
+
getUpdatesMock
|
|
74
|
+
.mockResolvedValueOnce({
|
|
75
|
+
ok: true,
|
|
76
|
+
result: createImageUpdate({
|
|
77
|
+
messageId: "msg-unauthorized-1",
|
|
78
|
+
userId: "user-unauthorized-1",
|
|
79
|
+
chatId: "chat-unauthorized-1",
|
|
80
|
+
}),
|
|
81
|
+
})
|
|
82
|
+
.mockImplementation(() => new Promise(() => {}));
|
|
83
|
+
|
|
84
|
+
const { monitorZaloProvider } = await loadCachedLifecycleMonitorModule("zalo-image-polling");
|
|
85
|
+
const abort = new AbortController();
|
|
86
|
+
const runtime = createRuntimeEnv();
|
|
87
|
+
const { account, config } = createLifecycleMonitorSetup({
|
|
88
|
+
accountId: "default",
|
|
89
|
+
dmPolicy: "pairing",
|
|
90
|
+
allowFrom: ["allowed-user"],
|
|
91
|
+
});
|
|
92
|
+
const run = monitorZaloProvider({
|
|
93
|
+
token: "zalo-token", // pragma: allowlist secret
|
|
94
|
+
account,
|
|
95
|
+
config,
|
|
96
|
+
runtime,
|
|
97
|
+
abortSignal: abort.signal,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await settleAsyncWork();
|
|
101
|
+
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
|
102
|
+
expect(fetchRemoteMediaMock).not.toHaveBeenCalled();
|
|
103
|
+
expect(saveMediaBufferMock).not.toHaveBeenCalled();
|
|
104
|
+
expect(finalizeInboundContextMock).not.toHaveBeenCalled();
|
|
105
|
+
expect(recordInboundSessionMock).not.toHaveBeenCalled();
|
|
106
|
+
|
|
107
|
+
abort.abort();
|
|
108
|
+
await run;
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
createEmptyPluginRegistry,
|
|
3
|
+
createRuntimeEnv,
|
|
4
|
+
setActivePluginRegistry,
|
|
5
|
+
} from "openclaw/plugin-sdk/plugin-test-runtime";
|
|
2
6
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import {
|
|
4
|
-
import { setActivePluginRegistry } from "../../../src/plugins/runtime.js";
|
|
7
|
+
import type { OpenClawConfig } from "../runtime-api.js";
|
|
5
8
|
import type { ResolvedZaloAccount } from "./accounts.js";
|
|
6
9
|
|
|
7
10
|
const getWebhookInfoMock = vi.fn(async () => ({ ok: true, result: { url: "" } }));
|
|
@@ -9,8 +12,8 @@ const deleteWebhookMock = vi.fn(async () => ({ ok: true, result: { url: "" } }))
|
|
|
9
12
|
const getUpdatesMock = vi.fn(() => new Promise(() => {}));
|
|
10
13
|
const setWebhookMock = vi.fn(async () => ({ ok: true, result: { url: "" } }));
|
|
11
14
|
|
|
12
|
-
vi.mock("./api.js", async (
|
|
13
|
-
const actual = await
|
|
15
|
+
vi.mock("./api.js", async () => {
|
|
16
|
+
const actual = await vi.importActual<typeof import("./api.js")>("./api.js");
|
|
14
17
|
return {
|
|
15
18
|
...actual,
|
|
16
19
|
deleteWebhook: deleteWebhookMock,
|
|
@@ -28,10 +31,6 @@ vi.mock("./runtime.js", () => ({
|
|
|
28
31
|
}),
|
|
29
32
|
}));
|
|
30
33
|
|
|
31
|
-
async function waitForPollingLoopStart(): Promise<void> {
|
|
32
|
-
await vi.waitFor(() => expect(getUpdatesMock).toHaveBeenCalledTimes(1));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
34
|
const TEST_ACCOUNT = {
|
|
36
35
|
accountId: "default",
|
|
37
36
|
config: {},
|
|
@@ -39,11 +38,11 @@ const TEST_ACCOUNT = {
|
|
|
39
38
|
|
|
40
39
|
const TEST_CONFIG = {} as OpenClawConfig;
|
|
41
40
|
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
41
|
+
async function settleLifecycleWork(): Promise<void> {
|
|
42
|
+
for (let i = 0; i < 6; i += 1) {
|
|
43
|
+
await Promise.resolve();
|
|
44
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
45
|
+
}
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
async function startLifecycleMonitor(
|
|
@@ -55,7 +54,7 @@ async function startLifecycleMonitor(
|
|
|
55
54
|
) {
|
|
56
55
|
const { monitorZaloProvider } = await import("./monitor.js");
|
|
57
56
|
const abort = new AbortController();
|
|
58
|
-
const runtime =
|
|
57
|
+
const runtime = createRuntimeEnv();
|
|
59
58
|
const run = monitorZaloProvider({
|
|
60
59
|
token: "test-token",
|
|
61
60
|
account: TEST_ACCOUNT,
|
|
@@ -80,7 +79,8 @@ describe("monitorZaloProvider lifecycle", () => {
|
|
|
80
79
|
settled = true;
|
|
81
80
|
});
|
|
82
81
|
|
|
83
|
-
await
|
|
82
|
+
await settleLifecycleWork();
|
|
83
|
+
expect(getUpdatesMock).toHaveBeenCalledTimes(1);
|
|
84
84
|
|
|
85
85
|
expect(getWebhookInfoMock).toHaveBeenCalledTimes(1);
|
|
86
86
|
expect(deleteWebhookMock).not.toHaveBeenCalled();
|
|
@@ -104,7 +104,8 @@ describe("monitorZaloProvider lifecycle", () => {
|
|
|
104
104
|
|
|
105
105
|
const { abort, runtime, run } = await startLifecycleMonitor();
|
|
106
106
|
|
|
107
|
-
await
|
|
107
|
+
await settleLifecycleWork();
|
|
108
|
+
expect(getUpdatesMock).toHaveBeenCalledTimes(1);
|
|
108
109
|
|
|
109
110
|
expect(getWebhookInfoMock).toHaveBeenCalledTimes(1);
|
|
110
111
|
expect(deleteWebhookMock).toHaveBeenCalledTimes(1);
|
|
@@ -122,7 +123,8 @@ describe("monitorZaloProvider lifecycle", () => {
|
|
|
122
123
|
|
|
123
124
|
const { abort, runtime, run } = await startLifecycleMonitor();
|
|
124
125
|
|
|
125
|
-
await
|
|
126
|
+
await settleLifecycleWork();
|
|
127
|
+
expect(getUpdatesMock).toHaveBeenCalledTimes(1);
|
|
126
128
|
|
|
127
129
|
expect(getWebhookInfoMock).toHaveBeenCalledTimes(1);
|
|
128
130
|
expect(deleteWebhookMock).not.toHaveBeenCalled();
|
|
@@ -139,10 +141,24 @@ describe("monitorZaloProvider lifecycle", () => {
|
|
|
139
141
|
const registry = createEmptyPluginRegistry();
|
|
140
142
|
setActivePluginRegistry(registry);
|
|
141
143
|
|
|
144
|
+
let resolveSetWebhookCalled: (() => void) | undefined;
|
|
145
|
+
const setWebhookCalled = new Promise<void>((resolve) => {
|
|
146
|
+
resolveSetWebhookCalled = resolve;
|
|
147
|
+
});
|
|
148
|
+
setWebhookMock.mockImplementationOnce(async () => {
|
|
149
|
+
resolveSetWebhookCalled?.();
|
|
150
|
+
return { ok: true, result: { url: "" } };
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
let resolveDeleteWebhookCalled: (() => void) | undefined;
|
|
154
|
+
const deleteWebhookCalled = new Promise<void>((resolve) => {
|
|
155
|
+
resolveDeleteWebhookCalled = resolve;
|
|
156
|
+
});
|
|
142
157
|
let resolveDeleteWebhook: (() => void) | undefined;
|
|
143
158
|
deleteWebhookMock.mockImplementationOnce(
|
|
144
159
|
() =>
|
|
145
160
|
new Promise((resolve) => {
|
|
161
|
+
resolveDeleteWebhookCalled?.();
|
|
146
162
|
resolveDeleteWebhook = () => resolve({ ok: true, result: { url: "" } });
|
|
147
163
|
}),
|
|
148
164
|
);
|
|
@@ -157,15 +173,18 @@ describe("monitorZaloProvider lifecycle", () => {
|
|
|
157
173
|
settled = true;
|
|
158
174
|
});
|
|
159
175
|
|
|
160
|
-
await
|
|
161
|
-
|
|
176
|
+
await setWebhookCalled;
|
|
177
|
+
await settleLifecycleWork();
|
|
178
|
+
expect(setWebhookMock).toHaveBeenCalledTimes(1);
|
|
179
|
+
expect(registry.httpRoutes).toHaveLength(2);
|
|
162
180
|
|
|
163
181
|
abort.abort();
|
|
164
182
|
|
|
165
|
-
await
|
|
183
|
+
await deleteWebhookCalled;
|
|
184
|
+
expect(deleteWebhookMock).toHaveBeenCalledTimes(1);
|
|
166
185
|
expect(deleteWebhookMock).toHaveBeenCalledWith("test-token", undefined, 5000);
|
|
167
186
|
expect(settled).toBe(false);
|
|
168
|
-
expect(registry.httpRoutes).toHaveLength(
|
|
187
|
+
expect(registry.httpRoutes).toHaveLength(2);
|
|
169
188
|
|
|
170
189
|
resolveDeleteWebhook?.();
|
|
171
190
|
await monitoredRun;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { withServer } from "openclaw/plugin-sdk/test-env";
|
|
2
|
+
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
createLifecycleMonitorSetup,
|
|
5
|
+
createTextUpdate,
|
|
6
|
+
postWebhookReplay,
|
|
7
|
+
settleAsyncWork,
|
|
8
|
+
} from "./test-support/lifecycle-test-support.js";
|
|
9
|
+
import {
|
|
10
|
+
resetLifecycleTestState,
|
|
11
|
+
sendMessageMock,
|
|
12
|
+
setLifecycleRuntimeCore,
|
|
13
|
+
startWebhookLifecycleMonitor,
|
|
14
|
+
} from "./test-support/monitor-mocks-test-support.js";
|
|
15
|
+
|
|
16
|
+
describe("Zalo pairing lifecycle", () => {
|
|
17
|
+
const readAllowFromStoreMock = vi.fn(async () => [] as string[]);
|
|
18
|
+
const upsertPairingRequestMock = vi.fn(async () => ({ code: "PAIRCODE", created: true }));
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
await resetLifecycleTestState();
|
|
22
|
+
setLifecycleRuntimeCore({
|
|
23
|
+
pairing: {
|
|
24
|
+
readAllowFromStore: readAllowFromStoreMock,
|
|
25
|
+
upsertPairingRequest: upsertPairingRequestMock,
|
|
26
|
+
},
|
|
27
|
+
commands: {
|
|
28
|
+
shouldComputeCommandAuthorized: vi.fn(() => false),
|
|
29
|
+
resolveCommandAuthorizedFromAuthorizers: vi.fn(() => false),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterAll(async () => {
|
|
35
|
+
await resetLifecycleTestState();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function createPairingMonitorSetup() {
|
|
39
|
+
return createLifecycleMonitorSetup({
|
|
40
|
+
accountId: "acct-zalo-pairing",
|
|
41
|
+
dmPolicy: "pairing",
|
|
42
|
+
allowFrom: [],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
it("emits one pairing reply across duplicate webhook replay and scopes reads and writes to accountId", async () => {
|
|
47
|
+
const monitor = await startWebhookLifecycleMonitor({
|
|
48
|
+
...createPairingMonitorSetup(),
|
|
49
|
+
cacheKey: "zalo-pairing-lifecycle",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await withServer(
|
|
54
|
+
(req, res) => monitor.route.handler(req, res),
|
|
55
|
+
async (baseUrl) => {
|
|
56
|
+
const { first, replay } = await postWebhookReplay({
|
|
57
|
+
baseUrl,
|
|
58
|
+
path: "/hooks/zalo",
|
|
59
|
+
secret: "supersecret",
|
|
60
|
+
payload: createTextUpdate({
|
|
61
|
+
messageId: `zalo-pairing-${Date.now()}`,
|
|
62
|
+
userId: "user-unauthorized",
|
|
63
|
+
userName: "Unauthorized User",
|
|
64
|
+
chatId: "dm-pairing-1",
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(first.status).toBe(200);
|
|
69
|
+
expect(replay.status).toBe(200);
|
|
70
|
+
await settleAsyncWork();
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(readAllowFromStoreMock).toHaveBeenCalledTimes(1);
|
|
75
|
+
expect(readAllowFromStoreMock).toHaveBeenCalledWith(
|
|
76
|
+
expect.objectContaining({
|
|
77
|
+
channel: "zalo",
|
|
78
|
+
accountId: "acct-zalo-pairing",
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
expect(upsertPairingRequestMock).toHaveBeenCalledTimes(1);
|
|
82
|
+
expect(upsertPairingRequestMock).toHaveBeenCalledWith(
|
|
83
|
+
expect.objectContaining({
|
|
84
|
+
channel: "zalo",
|
|
85
|
+
accountId: "acct-zalo-pairing",
|
|
86
|
+
id: "user-unauthorized",
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
|
90
|
+
expect(sendMessageMock).toHaveBeenCalledWith(
|
|
91
|
+
"zalo-token",
|
|
92
|
+
expect.objectContaining({
|
|
93
|
+
chat_id: "dm-pairing-1",
|
|
94
|
+
text: expect.stringContaining("PAIRCODE"),
|
|
95
|
+
}),
|
|
96
|
+
undefined,
|
|
97
|
+
);
|
|
98
|
+
} finally {
|
|
99
|
+
await monitor.stop();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("does not emit a second pairing reply when replay arrives after the first send fails", async () => {
|
|
104
|
+
sendMessageMock.mockRejectedValueOnce(new Error("pairing send failed"));
|
|
105
|
+
|
|
106
|
+
const monitor = await startWebhookLifecycleMonitor({
|
|
107
|
+
...createPairingMonitorSetup(),
|
|
108
|
+
cacheKey: "zalo-pairing-lifecycle",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await withServer(
|
|
113
|
+
(req, res) => monitor.route.handler(req, res),
|
|
114
|
+
async (baseUrl) => {
|
|
115
|
+
const { first, replay } = await postWebhookReplay({
|
|
116
|
+
baseUrl,
|
|
117
|
+
path: "/hooks/zalo",
|
|
118
|
+
secret: "supersecret",
|
|
119
|
+
payload: createTextUpdate({
|
|
120
|
+
messageId: `zalo-pairing-retry-${Date.now()}`,
|
|
121
|
+
userId: "user-unauthorized",
|
|
122
|
+
userName: "Unauthorized User",
|
|
123
|
+
chatId: "dm-pairing-1",
|
|
124
|
+
}),
|
|
125
|
+
settleBeforeReplay: true,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(first.status).toBe(200);
|
|
129
|
+
expect(replay.status).toBe(200);
|
|
130
|
+
await settleAsyncWork();
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(upsertPairingRequestMock).toHaveBeenCalledTimes(1);
|
|
135
|
+
expect(sendMessageMock).toHaveBeenCalledTimes(1);
|
|
136
|
+
expect(monitor.runtime.error).not.toHaveBeenCalled();
|
|
137
|
+
} finally {
|
|
138
|
+
await monitor.stop();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|