@symerian/symi 3.0.20 → 3.0.21

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.
Files changed (52) hide show
  1. package/dist/{audio-preflight-BaCdNfrk.js → audio-preflight-D7BVT-ls.js} +4 -4
  2. package/dist/build-info.json +3 -3
  3. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  4. package/dist/{chrome-UfmVM0xR.js → chrome-B5CO2vB5.js} +7 -7
  5. package/dist/{deliver-BqXdac6W.js → deliver-CrwjsDwv.js} +1 -1
  6. package/dist/extensionAPI.js +7 -7
  7. package/dist/{image-DIWsXYcW.js → image-Csu7WcLW.js} +1 -1
  8. package/dist/{manager-DW3SxcPr.js → manager-BkkVjTO8.js} +1 -1
  9. package/dist/{pi-embedded-BNch0U5F.js → pi-embedded-Dhp64z5l.js} +16 -16
  10. package/dist/{pi-embedded-helpers-IkHl02JF.js → pi-embedded-helpers-840E4hop.js} +4 -4
  11. package/dist/{pw-ai-nMkA-oDJ.js → pw-ai-CBgJf_RR.js} +1 -1
  12. package/dist/{runner-DNEC58JI.js → runner-BbFKo1ne.js} +1 -1
  13. package/dist/{synthesis-BWAr0sZ9.js → synthesis-DoEM0E8_.js} +7 -7
  14. package/dist/{web-7a-m_UxL.js → web-BYXJn-Ps.js} +7 -7
  15. package/package.json +1 -1
  16. package/extensions/imessage/index.ts +0 -17
  17. package/extensions/imessage/node_modules/.bin/symi +0 -21
  18. package/extensions/imessage/package.json +0 -15
  19. package/extensions/imessage/src/channel.outbound.test.ts +0 -66
  20. package/extensions/imessage/src/channel.ts +0 -298
  21. package/extensions/imessage/src/runtime.ts +0 -14
  22. package/extensions/imessage/symi.plugin.json +0 -9
  23. package/extensions/line/index.ts +0 -19
  24. package/extensions/line/node_modules/.bin/symi +0 -21
  25. package/extensions/line/package.json +0 -30
  26. package/extensions/line/src/card-command.ts +0 -344
  27. package/extensions/line/src/channel.logout.test.ts +0 -133
  28. package/extensions/line/src/channel.sendPayload.test.ts +0 -312
  29. package/extensions/line/src/channel.startup.test.ts +0 -133
  30. package/extensions/line/src/channel.ts +0 -801
  31. package/extensions/line/src/runtime.ts +0 -14
  32. package/extensions/line/symi.plugin.json +0 -9
  33. package/extensions/signal/index.ts +0 -17
  34. package/extensions/signal/node_modules/.bin/symi +0 -21
  35. package/extensions/signal/package.json +0 -15
  36. package/extensions/signal/src/channel.ts +0 -302
  37. package/extensions/signal/src/runtime.ts +0 -14
  38. package/extensions/signal/symi.plugin.json +0 -9
  39. package/extensions/telegram/index.ts +0 -17
  40. package/extensions/telegram/node_modules/.bin/symi +0 -21
  41. package/extensions/telegram/package.json +0 -15
  42. package/extensions/telegram/src/channel.test.ts +0 -125
  43. package/extensions/telegram/src/channel.ts +0 -560
  44. package/extensions/telegram/src/runtime.ts +0 -14
  45. package/extensions/telegram/symi.plugin.json +0 -9
  46. package/extensions/whatsapp/index.ts +0 -17
  47. package/extensions/whatsapp/node_modules/.bin/symi +0 -21
  48. package/extensions/whatsapp/package.json +0 -15
  49. package/extensions/whatsapp/src/channel.ts +0 -465
  50. package/extensions/whatsapp/src/resolve-target.test.ts +0 -170
  51. package/extensions/whatsapp/src/runtime.ts +0 -14
  52. package/extensions/whatsapp/symi.plugin.json +0 -9
@@ -1,14 +0,0 @@
1
- import type { PluginRuntime } from "symi/plugin-sdk";
2
-
3
- let runtime: PluginRuntime | null = null;
4
-
5
- export function setLineRuntime(r: PluginRuntime): void {
6
- runtime = r;
7
- }
8
-
9
- export function getLineRuntime(): PluginRuntime {
10
- if (!runtime) {
11
- throw new Error("LINE runtime not initialized - plugin not registered");
12
- }
13
- return runtime;
14
- }
@@ -1,9 +0,0 @@
1
- {
2
- "id": "line",
3
- "channels": ["line"],
4
- "configSchema": {
5
- "type": "object",
6
- "additionalProperties": false,
7
- "properties": {}
8
- }
9
- }
@@ -1,17 +0,0 @@
1
- import type { SymiPluginApi } from "symi/plugin-sdk";
2
- import { emptyPluginConfigSchema } from "symi/plugin-sdk";
3
- import { signalPlugin } from "./src/channel.js";
4
- import { setSignalRuntime } from "./src/runtime.js";
5
-
6
- const plugin = {
7
- id: "signal",
8
- name: "Signal",
9
- description: "Signal channel plugin",
10
- configSchema: emptyPluginConfigSchema(),
11
- register(api: SymiPluginApi) {
12
- setSignalRuntime(api.runtime);
13
- api.registerChannel({ plugin: signalPlugin });
14
- },
15
- };
16
-
17
- export default plugin;
@@ -1,21 +0,0 @@
1
- #!/bin/sh
2
- basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
-
4
- case `uname` in
5
- *CYGWIN*|*MINGW*|*MSYS*)
6
- if command -v cygpath > /dev/null 2>&1; then
7
- basedir=`cygpath -w "$basedir"`
8
- fi
9
- ;;
10
- esac
11
-
12
- if [ -z "$NODE_PATH" ]; then
13
- export NODE_PATH="/home/symi/projects/symi/node_modules:/home/symi/projects/node_modules:/home/symi/node_modules:/home/node_modules:/node_modules:/home/symi/projects/symi/node_modules/.pnpm/node_modules"
14
- else
15
- export NODE_PATH="/home/symi/projects/symi/node_modules:/home/symi/projects/node_modules:/home/symi/node_modules:/home/node_modules:/node_modules:/home/symi/projects/symi/node_modules/.pnpm/node_modules:$NODE_PATH"
16
- fi
17
- if [ -x "$basedir/node" ]; then
18
- exec "$basedir/node" "$basedir/../@symerian/symi/symi.mjs" "$@"
19
- else
20
- exec node "$basedir/../@symerian/symi/symi.mjs" "$@"
21
- fi
@@ -1,15 +0,0 @@
1
- {
2
- "name": "@symi/signal",
3
- "version": "3.0.9",
4
- "private": true,
5
- "description": "Symi Signal channel plugin",
6
- "type": "module",
7
- "devDependencies": {
8
- "@symerian/symi": "workspace:*"
9
- },
10
- "symi": {
11
- "extensions": [
12
- "./index.ts"
13
- ]
14
- }
15
- }
@@ -1,302 +0,0 @@
1
- import {
2
- applyAccountNameToChannelSection,
3
- buildBaseChannelStatusSummary,
4
- buildChannelConfigSchema,
5
- collectStatusIssuesFromLastError,
6
- createDefaultChannelRuntimeState,
7
- DEFAULT_ACCOUNT_ID,
8
- deleteAccountFromConfigSection,
9
- formatPairingApproveHint,
10
- getChatChannelMeta,
11
- listSignalAccountIds,
12
- looksLikeSignalTargetId,
13
- migrateBaseNameToDefaultAccount,
14
- normalizeAccountId,
15
- normalizeE164,
16
- normalizeSignalMessagingTarget,
17
- PAIRING_APPROVED_MESSAGE,
18
- resolveChannelMediaMaxBytes,
19
- resolveDefaultSignalAccountId,
20
- resolveSignalAccount,
21
- setAccountEnabledInConfigSection,
22
- signalOnboardingAdapter,
23
- SignalConfigSchema,
24
- type ChannelMessageActionAdapter,
25
- type ChannelPlugin,
26
- type ResolvedSignalAccount,
27
- } from "symi/plugin-sdk";
28
- import { getSignalRuntime } from "./runtime.js";
29
-
30
- const signalMessageActions: ChannelMessageActionAdapter = {
31
- listActions: (ctx) => getSignalRuntime().channel.signal.messageActions?.listActions?.(ctx) ?? [],
32
- supportsAction: (ctx) =>
33
- getSignalRuntime().channel.signal.messageActions?.supportsAction?.(ctx) ?? false,
34
- handleAction: async (ctx) => {
35
- const ma = getSignalRuntime().channel.signal.messageActions;
36
- if (!ma?.handleAction) {
37
- throw new Error("Signal message actions not available");
38
- }
39
- return ma.handleAction(ctx);
40
- },
41
- };
42
-
43
- const meta = getChatChannelMeta("signal");
44
-
45
- export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
46
- id: "signal",
47
- meta: {
48
- ...meta,
49
- },
50
- onboarding: signalOnboardingAdapter,
51
- pairing: {
52
- idLabel: "signalNumber",
53
- normalizeAllowEntry: (entry) => entry.replace(/^signal:/i, ""),
54
- notifyApproval: async ({ id }) => {
55
- await getSignalRuntime().channel.signal.sendMessageSignal(id, PAIRING_APPROVED_MESSAGE);
56
- },
57
- },
58
- capabilities: {
59
- chatTypes: ["direct", "group"],
60
- media: true,
61
- reactions: true,
62
- },
63
- actions: signalMessageActions,
64
- streaming: {
65
- blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
66
- },
67
- reload: { configPrefixes: ["channels.signal"] },
68
- configSchema: buildChannelConfigSchema(SignalConfigSchema),
69
- config: {
70
- listAccountIds: (cfg) => listSignalAccountIds(cfg),
71
- resolveAccount: (cfg, accountId) => resolveSignalAccount({ cfg, accountId }),
72
- defaultAccountId: (cfg) => resolveDefaultSignalAccountId(cfg),
73
- setAccountEnabled: ({ cfg, accountId, enabled }) =>
74
- setAccountEnabledInConfigSection({
75
- cfg,
76
- sectionKey: "signal",
77
- accountId,
78
- enabled,
79
- allowTopLevel: true,
80
- }),
81
- deleteAccount: ({ cfg, accountId }) =>
82
- deleteAccountFromConfigSection({
83
- cfg,
84
- sectionKey: "signal",
85
- accountId,
86
- clearBaseFields: ["account", "httpUrl", "httpHost", "httpPort", "cliPath", "name"],
87
- }),
88
- isConfigured: (account) => account.configured,
89
- describeAccount: (account) => ({
90
- accountId: account.accountId,
91
- name: account.name,
92
- enabled: account.enabled,
93
- configured: account.configured,
94
- baseUrl: account.baseUrl,
95
- }),
96
- resolveAllowFrom: ({ cfg, accountId }) =>
97
- (resolveSignalAccount({ cfg, accountId }).config.allowFrom ?? []).map((entry) =>
98
- String(entry),
99
- ),
100
- formatAllowFrom: ({ allowFrom }) =>
101
- allowFrom
102
- .map((entry) => String(entry).trim())
103
- .filter(Boolean)
104
- .map((entry) => (entry === "*" ? "*" : normalizeE164(entry.replace(/^signal:/i, ""))))
105
- .filter(Boolean),
106
- resolveDefaultTo: ({ cfg, accountId }) =>
107
- resolveSignalAccount({ cfg, accountId }).config.defaultTo?.trim() || undefined,
108
- },
109
- security: {
110
- resolveDmPolicy: ({ cfg, accountId, account }) => {
111
- const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
112
- const useAccountPath = Boolean(cfg.channels?.signal?.accounts?.[resolvedAccountId]);
113
- const basePath = useAccountPath
114
- ? `channels.signal.accounts.${resolvedAccountId}.`
115
- : "channels.signal.";
116
- return {
117
- policy: account.config.dmPolicy ?? "pairing",
118
- allowFrom: account.config.allowFrom ?? [],
119
- policyPath: `${basePath}dmPolicy`,
120
- allowFromPath: basePath,
121
- approveHint: formatPairingApproveHint("signal"),
122
- normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()),
123
- };
124
- },
125
- collectWarnings: ({ account, cfg }) => {
126
- const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
127
- const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
128
- if (groupPolicy !== "open") {
129
- return [];
130
- }
131
- return [
132
- `- Signal groups: groupPolicy="open" allows any member to trigger the bot. Set channels.signal.groupPolicy="allowlist" + channels.signal.groupAllowFrom to restrict senders.`,
133
- ];
134
- },
135
- },
136
- messaging: {
137
- normalizeTarget: normalizeSignalMessagingTarget,
138
- targetResolver: {
139
- looksLikeId: looksLikeSignalTargetId,
140
- hint: "<E.164|uuid:ID|group:ID|signal:group:ID|signal:+E.164>",
141
- },
142
- },
143
- setup: {
144
- resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
145
- applyAccountName: ({ cfg, accountId, name }) =>
146
- applyAccountNameToChannelSection({
147
- cfg,
148
- channelKey: "signal",
149
- accountId,
150
- name,
151
- }),
152
- validateInput: ({ input }) => {
153
- if (
154
- !input.signalNumber &&
155
- !input.httpUrl &&
156
- !input.httpHost &&
157
- !input.httpPort &&
158
- !input.cliPath
159
- ) {
160
- return "Signal requires --signal-number or --http-url/--http-host/--http-port/--cli-path.";
161
- }
162
- return null;
163
- },
164
- applyAccountConfig: ({ cfg, accountId, input }) => {
165
- const namedConfig = applyAccountNameToChannelSection({
166
- cfg,
167
- channelKey: "signal",
168
- accountId,
169
- name: input.name,
170
- });
171
- const next =
172
- accountId !== DEFAULT_ACCOUNT_ID
173
- ? migrateBaseNameToDefaultAccount({
174
- cfg: namedConfig,
175
- channelKey: "signal",
176
- })
177
- : namedConfig;
178
- if (accountId === DEFAULT_ACCOUNT_ID) {
179
- return {
180
- ...next,
181
- channels: {
182
- ...next.channels,
183
- signal: {
184
- ...next.channels?.signal,
185
- enabled: true,
186
- ...(input.signalNumber ? { account: input.signalNumber } : {}),
187
- ...(input.cliPath ? { cliPath: input.cliPath } : {}),
188
- ...(input.httpUrl ? { httpUrl: input.httpUrl } : {}),
189
- ...(input.httpHost ? { httpHost: input.httpHost } : {}),
190
- ...(input.httpPort ? { httpPort: Number(input.httpPort) } : {}),
191
- },
192
- },
193
- };
194
- }
195
- return {
196
- ...next,
197
- channels: {
198
- ...next.channels,
199
- signal: {
200
- ...next.channels?.signal,
201
- enabled: true,
202
- accounts: {
203
- ...next.channels?.signal?.accounts,
204
- [accountId]: {
205
- ...next.channels?.signal?.accounts?.[accountId],
206
- enabled: true,
207
- ...(input.signalNumber ? { account: input.signalNumber } : {}),
208
- ...(input.cliPath ? { cliPath: input.cliPath } : {}),
209
- ...(input.httpUrl ? { httpUrl: input.httpUrl } : {}),
210
- ...(input.httpHost ? { httpHost: input.httpHost } : {}),
211
- ...(input.httpPort ? { httpPort: Number(input.httpPort) } : {}),
212
- },
213
- },
214
- },
215
- },
216
- };
217
- },
218
- },
219
- outbound: {
220
- deliveryMode: "direct",
221
- chunker: (text, limit) => getSignalRuntime().channel.text.chunkText(text, limit),
222
- chunkerMode: "text",
223
- textChunkLimit: 4000,
224
- sendText: async ({ cfg, to, text, accountId, deps }) => {
225
- const send = deps?.sendSignal ?? getSignalRuntime().channel.signal.sendMessageSignal;
226
- const maxBytes = resolveChannelMediaMaxBytes({
227
- cfg,
228
- resolveChannelLimitMb: ({ cfg, accountId }) =>
229
- cfg.channels?.signal?.accounts?.[accountId]?.mediaMaxMb ??
230
- cfg.channels?.signal?.mediaMaxMb,
231
- accountId,
232
- });
233
- const result = await send(to, text, {
234
- maxBytes,
235
- accountId: accountId ?? undefined,
236
- });
237
- return { channel: "signal", ...result };
238
- },
239
- sendMedia: async ({ cfg, to, text, mediaUrl, accountId, deps }) => {
240
- const send = deps?.sendSignal ?? getSignalRuntime().channel.signal.sendMessageSignal;
241
- const maxBytes = resolveChannelMediaMaxBytes({
242
- cfg,
243
- resolveChannelLimitMb: ({ cfg, accountId }) =>
244
- cfg.channels?.signal?.accounts?.[accountId]?.mediaMaxMb ??
245
- cfg.channels?.signal?.mediaMaxMb,
246
- accountId,
247
- });
248
- const result = await send(to, text, {
249
- mediaUrl,
250
- maxBytes,
251
- accountId: accountId ?? undefined,
252
- });
253
- return { channel: "signal", ...result };
254
- },
255
- },
256
- status: {
257
- defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
258
- collectStatusIssues: (accounts) => collectStatusIssuesFromLastError("signal", accounts),
259
- buildChannelSummary: ({ snapshot }) => ({
260
- ...buildBaseChannelStatusSummary(snapshot),
261
- baseUrl: snapshot.baseUrl ?? null,
262
- probe: snapshot.probe,
263
- lastProbeAt: snapshot.lastProbeAt ?? null,
264
- }),
265
- probeAccount: async ({ account, timeoutMs }) => {
266
- const baseUrl = account.baseUrl;
267
- return await getSignalRuntime().channel.signal.probeSignal(baseUrl, timeoutMs);
268
- },
269
- buildAccountSnapshot: ({ account, runtime, probe }) => ({
270
- accountId: account.accountId,
271
- name: account.name,
272
- enabled: account.enabled,
273
- configured: account.configured,
274
- baseUrl: account.baseUrl,
275
- running: runtime?.running ?? false,
276
- lastStartAt: runtime?.lastStartAt ?? null,
277
- lastStopAt: runtime?.lastStopAt ?? null,
278
- lastError: runtime?.lastError ?? null,
279
- probe,
280
- lastInboundAt: runtime?.lastInboundAt ?? null,
281
- lastOutboundAt: runtime?.lastOutboundAt ?? null,
282
- }),
283
- },
284
- gateway: {
285
- startAccount: async (ctx) => {
286
- const account = ctx.account;
287
- ctx.setStatus({
288
- accountId: account.accountId,
289
- baseUrl: account.baseUrl,
290
- });
291
- ctx.log?.info(`[${account.accountId}] starting provider (${account.baseUrl})`);
292
- // Lazy import: the monitor pulls the reply pipeline; avoid ESM init cycles.
293
- return getSignalRuntime().channel.signal.monitorSignalProvider({
294
- accountId: account.accountId,
295
- config: ctx.cfg,
296
- runtime: ctx.runtime,
297
- abortSignal: ctx.abortSignal,
298
- mediaMaxMb: account.config.mediaMaxMb,
299
- });
300
- },
301
- },
302
- };
@@ -1,14 +0,0 @@
1
- import type { PluginRuntime } from "symi/plugin-sdk";
2
-
3
- let runtime: PluginRuntime | null = null;
4
-
5
- export function setSignalRuntime(next: PluginRuntime) {
6
- runtime = next;
7
- }
8
-
9
- export function getSignalRuntime(): PluginRuntime {
10
- if (!runtime) {
11
- throw new Error("Signal runtime not initialized");
12
- }
13
- return runtime;
14
- }
@@ -1,9 +0,0 @@
1
- {
2
- "id": "signal",
3
- "channels": ["signal"],
4
- "configSchema": {
5
- "type": "object",
6
- "additionalProperties": false,
7
- "properties": {}
8
- }
9
- }
@@ -1,17 +0,0 @@
1
- import type { ChannelPlugin, SymiPluginApi } from "symi/plugin-sdk";
2
- import { emptyPluginConfigSchema } from "symi/plugin-sdk";
3
- import { telegramPlugin } from "./src/channel.js";
4
- import { setTelegramRuntime } from "./src/runtime.js";
5
-
6
- const plugin = {
7
- id: "telegram",
8
- name: "Telegram",
9
- description: "Telegram channel plugin",
10
- configSchema: emptyPluginConfigSchema(),
11
- register(api: SymiPluginApi) {
12
- setTelegramRuntime(api.runtime);
13
- api.registerChannel({ plugin: telegramPlugin as ChannelPlugin });
14
- },
15
- };
16
-
17
- export default plugin;
@@ -1,21 +0,0 @@
1
- #!/bin/sh
2
- basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
-
4
- case `uname` in
5
- *CYGWIN*|*MINGW*|*MSYS*)
6
- if command -v cygpath > /dev/null 2>&1; then
7
- basedir=`cygpath -w "$basedir"`
8
- fi
9
- ;;
10
- esac
11
-
12
- if [ -z "$NODE_PATH" ]; then
13
- export NODE_PATH="/home/symi/projects/symi/node_modules:/home/symi/projects/node_modules:/home/symi/node_modules:/home/node_modules:/node_modules:/home/symi/projects/symi/node_modules/.pnpm/node_modules"
14
- else
15
- export NODE_PATH="/home/symi/projects/symi/node_modules:/home/symi/projects/node_modules:/home/symi/node_modules:/home/node_modules:/node_modules:/home/symi/projects/symi/node_modules/.pnpm/node_modules:$NODE_PATH"
16
- fi
17
- if [ -x "$basedir/node" ]; then
18
- exec "$basedir/node" "$basedir/../@symerian/symi/symi.mjs" "$@"
19
- else
20
- exec node "$basedir/../@symerian/symi/symi.mjs" "$@"
21
- fi
@@ -1,15 +0,0 @@
1
- {
2
- "name": "@symi/telegram",
3
- "version": "3.0.9",
4
- "private": true,
5
- "description": "Symi Telegram channel plugin",
6
- "type": "module",
7
- "devDependencies": {
8
- "@symerian/symi": "workspace:*"
9
- },
10
- "symi": {
11
- "extensions": [
12
- "./index.ts"
13
- ]
14
- }
15
- }
@@ -1,125 +0,0 @@
1
- import type {
2
- ChannelAccountSnapshot,
3
- ChannelGatewayContext,
4
- SymiConfig,
5
- PluginRuntime,
6
- ResolvedTelegramAccount,
7
- RuntimeEnv,
8
- } from "symi/plugin-sdk";
9
- import { describe, expect, it, vi } from "vitest";
10
- import { telegramPlugin } from "./channel.js";
11
- import { setTelegramRuntime } from "./runtime.js";
12
-
13
- function createCfg(): SymiConfig {
14
- return {
15
- channels: {
16
- telegram: {
17
- enabled: true,
18
- accounts: {
19
- alerts: { botToken: "token-shared" },
20
- work: { botToken: "token-shared" },
21
- ops: { botToken: "token-ops" },
22
- },
23
- },
24
- },
25
- } as SymiConfig;
26
- }
27
-
28
- function createRuntimeEnv(): RuntimeEnv {
29
- return {
30
- log: vi.fn(),
31
- error: vi.fn(),
32
- exit: vi.fn((code: number): never => {
33
- throw new Error(`exit ${code}`);
34
- }),
35
- };
36
- }
37
-
38
- function createStartAccountCtx(params: {
39
- cfg: SymiConfig;
40
- accountId: string;
41
- runtime: RuntimeEnv;
42
- }): ChannelGatewayContext<ResolvedTelegramAccount> {
43
- const account = telegramPlugin.config.resolveAccount(
44
- params.cfg,
45
- params.accountId,
46
- ) as ResolvedTelegramAccount;
47
- const snapshot: ChannelAccountSnapshot = {
48
- accountId: params.accountId,
49
- configured: true,
50
- enabled: true,
51
- running: false,
52
- };
53
- return {
54
- accountId: params.accountId,
55
- account,
56
- cfg: params.cfg,
57
- runtime: params.runtime,
58
- abortSignal: new AbortController().signal,
59
- log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
60
- getStatus: () => snapshot,
61
- setStatus: vi.fn(),
62
- };
63
- }
64
-
65
- describe("telegramPlugin duplicate token guard", () => {
66
- it("marks secondary account as not configured when token is shared", async () => {
67
- const cfg = createCfg();
68
- const alertsAccount = telegramPlugin.config.resolveAccount(cfg, "alerts");
69
- const workAccount = telegramPlugin.config.resolveAccount(cfg, "work");
70
- const opsAccount = telegramPlugin.config.resolveAccount(cfg, "ops");
71
-
72
- expect(await telegramPlugin.config.isConfigured!(alertsAccount, cfg)).toBe(true);
73
- expect(await telegramPlugin.config.isConfigured!(workAccount, cfg)).toBe(false);
74
- expect(await telegramPlugin.config.isConfigured!(opsAccount, cfg)).toBe(true);
75
-
76
- expect(telegramPlugin.config.unconfiguredReason?.(workAccount, cfg)).toContain(
77
- 'account "alerts"',
78
- );
79
- });
80
-
81
- it("surfaces duplicate-token reason in status snapshot", async () => {
82
- const cfg = createCfg();
83
- const workAccount = telegramPlugin.config.resolveAccount(cfg, "work");
84
- const snapshot = await telegramPlugin.status!.buildAccountSnapshot!({
85
- account: workAccount,
86
- cfg,
87
- runtime: undefined,
88
- probe: undefined,
89
- audit: undefined,
90
- });
91
-
92
- expect(snapshot.configured).toBe(false);
93
- expect(snapshot.lastError).toContain('account "alerts"');
94
- });
95
-
96
- it("blocks startup for duplicate token accounts before polling starts", async () => {
97
- const monitorTelegramProvider = vi.fn(async () => undefined);
98
- const probeTelegram = vi.fn(async () => ({ ok: true, bot: { username: "bot" } }));
99
- const runtime = {
100
- channel: {
101
- telegram: {
102
- monitorTelegramProvider,
103
- probeTelegram,
104
- },
105
- },
106
- logging: {
107
- shouldLogVerbose: () => false,
108
- },
109
- } as unknown as PluginRuntime;
110
- setTelegramRuntime(runtime);
111
-
112
- await expect(
113
- telegramPlugin.gateway!.startAccount!(
114
- createStartAccountCtx({
115
- cfg: createCfg(),
116
- accountId: "work",
117
- runtime: createRuntimeEnv(),
118
- }),
119
- ),
120
- ).rejects.toThrow("Duplicate Telegram bot token");
121
-
122
- expect(probeTelegram).not.toHaveBeenCalled();
123
- expect(monitorTelegramProvider).not.toHaveBeenCalled();
124
- });
125
- });