@openclaw/synology-chat 2026.2.22

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/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3
+ import { createSynologyChatPlugin } from "./src/channel.js";
4
+ import { setSynologyRuntime } from "./src/runtime.js";
5
+
6
+ const plugin = {
7
+ id: "synology-chat",
8
+ name: "Synology Chat",
9
+ description: "Native Synology Chat channel plugin for OpenClaw",
10
+ configSchema: emptyPluginConfigSchema(),
11
+ register(api: OpenClawPluginApi) {
12
+ setSynologyRuntime(api.runtime);
13
+ api.registerChannel({ plugin: createSynologyChatPlugin() });
14
+ },
15
+ };
16
+
17
+ export default plugin;
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "synology-chat",
3
+ "channels": ["synology-chat"],
4
+ "configSchema": {
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {}
8
+ }
9
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@openclaw/synology-chat",
3
+ "version": "2026.2.22",
4
+ "description": "Synology Chat channel plugin for OpenClaw",
5
+ "type": "module",
6
+ "dependencies": {
7
+ "zod": "^4.3.6"
8
+ },
9
+ "devDependencies": {
10
+ "openclaw": "workspace:*"
11
+ },
12
+ "openclaw": {
13
+ "extensions": [
14
+ "./index.ts"
15
+ ],
16
+ "channel": {
17
+ "id": "synology-chat",
18
+ "label": "Synology Chat",
19
+ "selectionLabel": "Synology Chat (Webhook)",
20
+ "docsPath": "/channels/synology-chat",
21
+ "docsLabel": "synology-chat",
22
+ "blurb": "Connect your Synology NAS Chat to OpenClaw with full agent capabilities.",
23
+ "order": 90
24
+ },
25
+ "install": {
26
+ "npmSpec": "@openclaw/synology-chat",
27
+ "localPath": "extensions/synology-chat",
28
+ "defaultChoice": "npm"
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,133 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { listAccountIds, resolveAccount } from "./accounts.js";
3
+
4
+ // Save and restore env vars
5
+ const originalEnv = { ...process.env };
6
+
7
+ beforeEach(() => {
8
+ // Clean synology-related env vars before each test
9
+ delete process.env.SYNOLOGY_CHAT_TOKEN;
10
+ delete process.env.SYNOLOGY_CHAT_INCOMING_URL;
11
+ delete process.env.SYNOLOGY_NAS_HOST;
12
+ delete process.env.SYNOLOGY_ALLOWED_USER_IDS;
13
+ delete process.env.SYNOLOGY_RATE_LIMIT;
14
+ delete process.env.OPENCLAW_BOT_NAME;
15
+ });
16
+
17
+ describe("listAccountIds", () => {
18
+ it("returns empty array when no channel config", () => {
19
+ expect(listAccountIds({})).toEqual([]);
20
+ expect(listAccountIds({ channels: {} })).toEqual([]);
21
+ });
22
+
23
+ it("returns ['default'] when base config has token", () => {
24
+ const cfg = { channels: { "synology-chat": { token: "abc" } } };
25
+ expect(listAccountIds(cfg)).toEqual(["default"]);
26
+ });
27
+
28
+ it("returns ['default'] when env var has token", () => {
29
+ process.env.SYNOLOGY_CHAT_TOKEN = "env-token";
30
+ const cfg = { channels: { "synology-chat": {} } };
31
+ expect(listAccountIds(cfg)).toEqual(["default"]);
32
+ });
33
+
34
+ it("returns named accounts", () => {
35
+ const cfg = {
36
+ channels: {
37
+ "synology-chat": {
38
+ accounts: { work: { token: "t1" }, home: { token: "t2" } },
39
+ },
40
+ },
41
+ };
42
+ const ids = listAccountIds(cfg);
43
+ expect(ids).toContain("work");
44
+ expect(ids).toContain("home");
45
+ });
46
+
47
+ it("returns default + named accounts", () => {
48
+ const cfg = {
49
+ channels: {
50
+ "synology-chat": {
51
+ token: "base-token",
52
+ accounts: { work: { token: "t1" } },
53
+ },
54
+ },
55
+ };
56
+ const ids = listAccountIds(cfg);
57
+ expect(ids).toContain("default");
58
+ expect(ids).toContain("work");
59
+ });
60
+ });
61
+
62
+ describe("resolveAccount", () => {
63
+ it("returns full defaults for empty config", () => {
64
+ const cfg = { channels: { "synology-chat": {} } };
65
+ const account = resolveAccount(cfg, "default");
66
+ expect(account.accountId).toBe("default");
67
+ expect(account.enabled).toBe(true);
68
+ expect(account.webhookPath).toBe("/webhook/synology");
69
+ expect(account.dmPolicy).toBe("allowlist");
70
+ expect(account.rateLimitPerMinute).toBe(30);
71
+ expect(account.botName).toBe("OpenClaw");
72
+ });
73
+
74
+ it("uses env var fallbacks", () => {
75
+ process.env.SYNOLOGY_CHAT_TOKEN = "env-tok";
76
+ process.env.SYNOLOGY_CHAT_INCOMING_URL = "https://nas/incoming";
77
+ process.env.SYNOLOGY_NAS_HOST = "192.0.2.1";
78
+ process.env.OPENCLAW_BOT_NAME = "TestBot";
79
+
80
+ const cfg = { channels: { "synology-chat": {} } };
81
+ const account = resolveAccount(cfg);
82
+ expect(account.token).toBe("env-tok");
83
+ expect(account.incomingUrl).toBe("https://nas/incoming");
84
+ expect(account.nasHost).toBe("192.0.2.1");
85
+ expect(account.botName).toBe("TestBot");
86
+ });
87
+
88
+ it("config overrides env vars", () => {
89
+ process.env.SYNOLOGY_CHAT_TOKEN = "env-tok";
90
+ const cfg = {
91
+ channels: { "synology-chat": { token: "config-tok" } },
92
+ };
93
+ const account = resolveAccount(cfg);
94
+ expect(account.token).toBe("config-tok");
95
+ });
96
+
97
+ it("account override takes priority over base config", () => {
98
+ const cfg = {
99
+ channels: {
100
+ "synology-chat": {
101
+ token: "base-tok",
102
+ botName: "BaseName",
103
+ accounts: {
104
+ work: { token: "work-tok", botName: "WorkBot" },
105
+ },
106
+ },
107
+ },
108
+ };
109
+ const account = resolveAccount(cfg, "work");
110
+ expect(account.token).toBe("work-tok");
111
+ expect(account.botName).toBe("WorkBot");
112
+ });
113
+
114
+ it("parses comma-separated allowedUserIds string", () => {
115
+ const cfg = {
116
+ channels: {
117
+ "synology-chat": { allowedUserIds: "user1, user2, user3" },
118
+ },
119
+ };
120
+ const account = resolveAccount(cfg);
121
+ expect(account.allowedUserIds).toEqual(["user1", "user2", "user3"]);
122
+ });
123
+
124
+ it("handles allowedUserIds as array", () => {
125
+ const cfg = {
126
+ channels: {
127
+ "synology-chat": { allowedUserIds: ["u1", "u2"] },
128
+ },
129
+ };
130
+ const account = resolveAccount(cfg);
131
+ expect(account.allowedUserIds).toEqual(["u1", "u2"]);
132
+ });
133
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Account resolution: reads config from channels.synology-chat,
3
+ * merges per-account overrides, falls back to environment variables.
4
+ */
5
+
6
+ import type { SynologyChatChannelConfig, ResolvedSynologyChatAccount } from "./types.js";
7
+
8
+ /** Extract the channel config from the full OpenClaw config object. */
9
+ function getChannelConfig(cfg: any): SynologyChatChannelConfig | undefined {
10
+ return cfg?.channels?.["synology-chat"];
11
+ }
12
+
13
+ /** Parse allowedUserIds from string or array to string[]. */
14
+ function parseAllowedUserIds(raw: string | string[] | undefined): string[] {
15
+ if (!raw) return [];
16
+ if (Array.isArray(raw)) return raw.filter(Boolean);
17
+ return raw
18
+ .split(",")
19
+ .map((s) => s.trim())
20
+ .filter(Boolean);
21
+ }
22
+
23
+ /**
24
+ * List all configured account IDs for this channel.
25
+ * Returns ["default"] if there's a base config, plus any named accounts.
26
+ */
27
+ export function listAccountIds(cfg: any): string[] {
28
+ const channelCfg = getChannelConfig(cfg);
29
+ if (!channelCfg) return [];
30
+
31
+ const ids = new Set<string>();
32
+
33
+ // If base config has a token, there's a "default" account
34
+ const hasBaseToken = channelCfg.token || process.env.SYNOLOGY_CHAT_TOKEN;
35
+ if (hasBaseToken) {
36
+ ids.add("default");
37
+ }
38
+
39
+ // Named accounts
40
+ if (channelCfg.accounts) {
41
+ for (const id of Object.keys(channelCfg.accounts)) {
42
+ ids.add(id);
43
+ }
44
+ }
45
+
46
+ return Array.from(ids);
47
+ }
48
+
49
+ /**
50
+ * Resolve a specific account by ID with full defaults applied.
51
+ * Falls back to env vars for the "default" account.
52
+ */
53
+ export function resolveAccount(cfg: any, accountId?: string | null): ResolvedSynologyChatAccount {
54
+ const channelCfg = getChannelConfig(cfg) ?? {};
55
+ const id = accountId || "default";
56
+
57
+ // Account-specific overrides (if named account exists)
58
+ const accountOverride = channelCfg.accounts?.[id] ?? {};
59
+
60
+ // Env var fallbacks (primarily for the "default" account)
61
+ const envToken = process.env.SYNOLOGY_CHAT_TOKEN ?? "";
62
+ const envIncomingUrl = process.env.SYNOLOGY_CHAT_INCOMING_URL ?? "";
63
+ const envNasHost = process.env.SYNOLOGY_NAS_HOST ?? "localhost";
64
+ const envAllowedUserIds = process.env.SYNOLOGY_ALLOWED_USER_IDS ?? "";
65
+ const envRateLimit = process.env.SYNOLOGY_RATE_LIMIT;
66
+ const envBotName = process.env.OPENCLAW_BOT_NAME ?? "OpenClaw";
67
+
68
+ // Merge: account override > base channel config > env var
69
+ return {
70
+ accountId: id,
71
+ enabled: accountOverride.enabled ?? channelCfg.enabled ?? true,
72
+ token: accountOverride.token ?? channelCfg.token ?? envToken,
73
+ incomingUrl: accountOverride.incomingUrl ?? channelCfg.incomingUrl ?? envIncomingUrl,
74
+ nasHost: accountOverride.nasHost ?? channelCfg.nasHost ?? envNasHost,
75
+ webhookPath: accountOverride.webhookPath ?? channelCfg.webhookPath ?? "/webhook/synology",
76
+ dmPolicy: accountOverride.dmPolicy ?? channelCfg.dmPolicy ?? "allowlist",
77
+ allowedUserIds: parseAllowedUserIds(
78
+ accountOverride.allowedUserIds ?? channelCfg.allowedUserIds ?? envAllowedUserIds,
79
+ ),
80
+ rateLimitPerMinute:
81
+ accountOverride.rateLimitPerMinute ??
82
+ channelCfg.rateLimitPerMinute ??
83
+ (envRateLimit ? parseInt(envRateLimit, 10) || 30 : 30),
84
+ botName: accountOverride.botName ?? channelCfg.botName ?? envBotName,
85
+ allowInsecureSsl: accountOverride.allowInsecureSsl ?? channelCfg.allowInsecureSsl ?? false,
86
+ };
87
+ }
@@ -0,0 +1,340 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ // Mock external dependencies
4
+ vi.mock("openclaw/plugin-sdk", () => ({
5
+ DEFAULT_ACCOUNT_ID: "default",
6
+ setAccountEnabledInConfigSection: vi.fn((_opts: any) => ({})),
7
+ registerPluginHttpRoute: vi.fn(() => vi.fn()),
8
+ buildChannelConfigSchema: vi.fn((schema: any) => ({ schema })),
9
+ }));
10
+
11
+ vi.mock("./client.js", () => ({
12
+ sendMessage: vi.fn().mockResolvedValue(true),
13
+ sendFileUrl: vi.fn().mockResolvedValue(true),
14
+ }));
15
+
16
+ vi.mock("./webhook-handler.js", () => ({
17
+ createWebhookHandler: vi.fn(() => vi.fn()),
18
+ }));
19
+
20
+ vi.mock("./runtime.js", () => ({
21
+ getSynologyRuntime: vi.fn(() => ({
22
+ config: { loadConfig: vi.fn().mockResolvedValue({}) },
23
+ channel: {
24
+ reply: {
25
+ dispatchReplyWithBufferedBlockDispatcher: vi.fn().mockResolvedValue({
26
+ counts: {},
27
+ }),
28
+ },
29
+ },
30
+ })),
31
+ }));
32
+
33
+ vi.mock("zod", () => ({
34
+ z: {
35
+ object: vi.fn(() => ({
36
+ passthrough: vi.fn(() => ({ _type: "zod-schema" })),
37
+ })),
38
+ },
39
+ }));
40
+
41
+ const { createSynologyChatPlugin } = await import("./channel.js");
42
+
43
+ describe("createSynologyChatPlugin", () => {
44
+ it("returns a plugin object with all required sections", () => {
45
+ const plugin = createSynologyChatPlugin();
46
+ expect(plugin.id).toBe("synology-chat");
47
+ expect(plugin.meta).toBeDefined();
48
+ expect(plugin.capabilities).toBeDefined();
49
+ expect(plugin.config).toBeDefined();
50
+ expect(plugin.security).toBeDefined();
51
+ expect(plugin.outbound).toBeDefined();
52
+ expect(plugin.gateway).toBeDefined();
53
+ });
54
+
55
+ describe("meta", () => {
56
+ it("has correct id and label", () => {
57
+ const plugin = createSynologyChatPlugin();
58
+ expect(plugin.meta.id).toBe("synology-chat");
59
+ expect(plugin.meta.label).toBe("Synology Chat");
60
+ expect(plugin.meta.docsPath).toBe("/channels/synology-chat");
61
+ });
62
+ });
63
+
64
+ describe("capabilities", () => {
65
+ it("supports direct chat with media", () => {
66
+ const plugin = createSynologyChatPlugin();
67
+ expect(plugin.capabilities.chatTypes).toEqual(["direct"]);
68
+ expect(plugin.capabilities.media).toBe(true);
69
+ expect(plugin.capabilities.threads).toBe(false);
70
+ });
71
+ });
72
+
73
+ describe("config", () => {
74
+ it("listAccountIds delegates to accounts module", () => {
75
+ const plugin = createSynologyChatPlugin();
76
+ const result = plugin.config.listAccountIds({});
77
+ expect(Array.isArray(result)).toBe(true);
78
+ });
79
+
80
+ it("resolveAccount returns account config", () => {
81
+ const cfg = { channels: { "synology-chat": { token: "t1" } } };
82
+ const plugin = createSynologyChatPlugin();
83
+ const account = plugin.config.resolveAccount(cfg, "default");
84
+ expect(account.accountId).toBe("default");
85
+ });
86
+
87
+ it("defaultAccountId returns 'default'", () => {
88
+ const plugin = createSynologyChatPlugin();
89
+ expect(plugin.config.defaultAccountId({})).toBe("default");
90
+ });
91
+ });
92
+
93
+ describe("security", () => {
94
+ it("resolveDmPolicy returns policy, allowFrom, normalizeEntry", () => {
95
+ const plugin = createSynologyChatPlugin();
96
+ const account = {
97
+ accountId: "default",
98
+ enabled: true,
99
+ token: "t",
100
+ incomingUrl: "u",
101
+ nasHost: "h",
102
+ webhookPath: "/w",
103
+ dmPolicy: "allowlist" as const,
104
+ allowedUserIds: ["user1"],
105
+ rateLimitPerMinute: 30,
106
+ botName: "Bot",
107
+ allowInsecureSsl: true,
108
+ };
109
+ const result = plugin.security.resolveDmPolicy({ cfg: {}, account });
110
+ expect(result.policy).toBe("allowlist");
111
+ expect(result.allowFrom).toEqual(["user1"]);
112
+ expect(typeof result.normalizeEntry).toBe("function");
113
+ expect(result.normalizeEntry(" USER1 ")).toBe("user1");
114
+ });
115
+ });
116
+
117
+ describe("pairing", () => {
118
+ it("has notifyApproval and normalizeAllowEntry", () => {
119
+ const plugin = createSynologyChatPlugin();
120
+ expect(plugin.pairing.idLabel).toBe("synologyChatUserId");
121
+ expect(typeof plugin.pairing.normalizeAllowEntry).toBe("function");
122
+ expect(plugin.pairing.normalizeAllowEntry(" USER1 ")).toBe("user1");
123
+ expect(typeof plugin.pairing.notifyApproval).toBe("function");
124
+ });
125
+ });
126
+
127
+ describe("security.collectWarnings", () => {
128
+ it("warns when token is missing", () => {
129
+ const plugin = createSynologyChatPlugin();
130
+ const account = {
131
+ accountId: "default",
132
+ enabled: true,
133
+ token: "",
134
+ incomingUrl: "https://nas/incoming",
135
+ nasHost: "h",
136
+ webhookPath: "/w",
137
+ dmPolicy: "allowlist" as const,
138
+ allowedUserIds: [],
139
+ rateLimitPerMinute: 30,
140
+ botName: "Bot",
141
+ allowInsecureSsl: false,
142
+ };
143
+ const warnings = plugin.security.collectWarnings({ account });
144
+ expect(warnings.some((w: string) => w.includes("token"))).toBe(true);
145
+ });
146
+
147
+ it("warns when allowInsecureSsl is true", () => {
148
+ const plugin = createSynologyChatPlugin();
149
+ const account = {
150
+ accountId: "default",
151
+ enabled: true,
152
+ token: "t",
153
+ incomingUrl: "https://nas/incoming",
154
+ nasHost: "h",
155
+ webhookPath: "/w",
156
+ dmPolicy: "allowlist" as const,
157
+ allowedUserIds: [],
158
+ rateLimitPerMinute: 30,
159
+ botName: "Bot",
160
+ allowInsecureSsl: true,
161
+ };
162
+ const warnings = plugin.security.collectWarnings({ account });
163
+ expect(warnings.some((w: string) => w.includes("SSL"))).toBe(true);
164
+ });
165
+
166
+ it("warns when dmPolicy is open", () => {
167
+ const plugin = createSynologyChatPlugin();
168
+ const account = {
169
+ accountId: "default",
170
+ enabled: true,
171
+ token: "t",
172
+ incomingUrl: "https://nas/incoming",
173
+ nasHost: "h",
174
+ webhookPath: "/w",
175
+ dmPolicy: "open" as const,
176
+ allowedUserIds: [],
177
+ rateLimitPerMinute: 30,
178
+ botName: "Bot",
179
+ allowInsecureSsl: false,
180
+ };
181
+ const warnings = plugin.security.collectWarnings({ account });
182
+ expect(warnings.some((w: string) => w.includes("open"))).toBe(true);
183
+ });
184
+
185
+ it("returns no warnings for fully configured account", () => {
186
+ const plugin = createSynologyChatPlugin();
187
+ const account = {
188
+ accountId: "default",
189
+ enabled: true,
190
+ token: "t",
191
+ incomingUrl: "https://nas/incoming",
192
+ nasHost: "h",
193
+ webhookPath: "/w",
194
+ dmPolicy: "allowlist" as const,
195
+ allowedUserIds: ["user1"],
196
+ rateLimitPerMinute: 30,
197
+ botName: "Bot",
198
+ allowInsecureSsl: false,
199
+ };
200
+ const warnings = plugin.security.collectWarnings({ account });
201
+ expect(warnings).toHaveLength(0);
202
+ });
203
+ });
204
+
205
+ describe("messaging", () => {
206
+ it("normalizeTarget strips prefix and trims", () => {
207
+ const plugin = createSynologyChatPlugin();
208
+ expect(plugin.messaging.normalizeTarget("synology-chat:123")).toBe("123");
209
+ expect(plugin.messaging.normalizeTarget(" 456 ")).toBe("456");
210
+ expect(plugin.messaging.normalizeTarget("")).toBeUndefined();
211
+ });
212
+
213
+ it("targetResolver.looksLikeId matches numeric IDs", () => {
214
+ const plugin = createSynologyChatPlugin();
215
+ expect(plugin.messaging.targetResolver.looksLikeId("12345")).toBe(true);
216
+ expect(plugin.messaging.targetResolver.looksLikeId("synology-chat:99")).toBe(true);
217
+ expect(plugin.messaging.targetResolver.looksLikeId("notanumber")).toBe(false);
218
+ expect(plugin.messaging.targetResolver.looksLikeId("")).toBe(false);
219
+ });
220
+ });
221
+
222
+ describe("directory", () => {
223
+ it("returns empty stubs", async () => {
224
+ const plugin = createSynologyChatPlugin();
225
+ expect(await plugin.directory.self()).toBeNull();
226
+ expect(await plugin.directory.listPeers()).toEqual([]);
227
+ expect(await plugin.directory.listGroups()).toEqual([]);
228
+ });
229
+ });
230
+
231
+ describe("agentPrompt", () => {
232
+ it("returns formatting hints", () => {
233
+ const plugin = createSynologyChatPlugin();
234
+ const hints = plugin.agentPrompt.messageToolHints();
235
+ expect(Array.isArray(hints)).toBe(true);
236
+ expect(hints.length).toBeGreaterThan(5);
237
+ expect(hints.some((h: string) => h.includes("<URL|display text>"))).toBe(true);
238
+ });
239
+ });
240
+
241
+ describe("outbound", () => {
242
+ it("sendText throws when no incomingUrl", async () => {
243
+ const plugin = createSynologyChatPlugin();
244
+ await expect(
245
+ plugin.outbound.sendText({
246
+ account: {
247
+ accountId: "default",
248
+ enabled: true,
249
+ token: "t",
250
+ incomingUrl: "",
251
+ nasHost: "h",
252
+ webhookPath: "/w",
253
+ dmPolicy: "open",
254
+ allowedUserIds: [],
255
+ rateLimitPerMinute: 30,
256
+ botName: "Bot",
257
+ allowInsecureSsl: true,
258
+ },
259
+ text: "hello",
260
+ to: "user1",
261
+ }),
262
+ ).rejects.toThrow("not configured");
263
+ });
264
+
265
+ it("sendText returns OutboundDeliveryResult on success", async () => {
266
+ const plugin = createSynologyChatPlugin();
267
+ const result = await plugin.outbound.sendText({
268
+ account: {
269
+ accountId: "default",
270
+ enabled: true,
271
+ token: "t",
272
+ incomingUrl: "https://nas/incoming",
273
+ nasHost: "h",
274
+ webhookPath: "/w",
275
+ dmPolicy: "open",
276
+ allowedUserIds: [],
277
+ rateLimitPerMinute: 30,
278
+ botName: "Bot",
279
+ allowInsecureSsl: true,
280
+ },
281
+ text: "hello",
282
+ to: "user1",
283
+ });
284
+ expect(result.channel).toBe("synology-chat");
285
+ expect(result.messageId).toBeDefined();
286
+ expect(result.chatId).toBe("user1");
287
+ });
288
+
289
+ it("sendMedia throws when missing incomingUrl", async () => {
290
+ const plugin = createSynologyChatPlugin();
291
+ await expect(
292
+ plugin.outbound.sendMedia({
293
+ account: {
294
+ accountId: "default",
295
+ enabled: true,
296
+ token: "t",
297
+ incomingUrl: "",
298
+ nasHost: "h",
299
+ webhookPath: "/w",
300
+ dmPolicy: "open",
301
+ allowedUserIds: [],
302
+ rateLimitPerMinute: 30,
303
+ botName: "Bot",
304
+ allowInsecureSsl: true,
305
+ },
306
+ mediaUrl: "https://example.com/img.png",
307
+ to: "user1",
308
+ }),
309
+ ).rejects.toThrow("not configured");
310
+ });
311
+ });
312
+
313
+ describe("gateway", () => {
314
+ it("startAccount returns stop function for disabled account", async () => {
315
+ const plugin = createSynologyChatPlugin();
316
+ const ctx = {
317
+ cfg: {
318
+ channels: { "synology-chat": { enabled: false } },
319
+ },
320
+ accountId: "default",
321
+ log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
322
+ };
323
+ const result = await plugin.gateway.startAccount(ctx);
324
+ expect(typeof result.stop).toBe("function");
325
+ });
326
+
327
+ it("startAccount returns stop function for account without token", async () => {
328
+ const plugin = createSynologyChatPlugin();
329
+ const ctx = {
330
+ cfg: {
331
+ channels: { "synology-chat": { enabled: true } },
332
+ },
333
+ accountId: "default",
334
+ log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
335
+ };
336
+ const result = await plugin.gateway.startAccount(ctx);
337
+ expect(typeof result.stop).toBe("function");
338
+ });
339
+ });
340
+ });