@open-mercato/channel-discord 0.6.8-develop.7090.1.4dbca7b350
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/.turbo/turbo-build.log +2 -0
- package/build.mjs +7 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-001.spec.js +86 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-001.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-002.spec.js +39 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-002.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-003.spec.js +97 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-003.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-004.spec.js +65 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-004.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-005.spec.js +110 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-005.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-006.spec.js +103 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-006.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-007.spec.js +93 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-007.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-008.spec.js +52 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-008.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/helpers/discordSignature.js +46 -0
- package/dist/modules/channel_discord/__integration__/helpers/discordSignature.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/helpers/eventsQueue.js +16 -0
- package/dist/modules/channel_discord/__integration__/helpers/eventsQueue.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/meta.js +10 -0
- package/dist/modules/channel_discord/__integration__/meta.js.map +7 -0
- package/dist/modules/channel_discord/acl.js +15 -0
- package/dist/modules/channel_discord/acl.js.map +7 -0
- package/dist/modules/channel_discord/ai-agents.js +97 -0
- package/dist/modules/channel_discord/ai-agents.js.map +7 -0
- package/dist/modules/channel_discord/api/ai-auto-reply/channels/route.js +73 -0
- package/dist/modules/channel_discord/api/ai-auto-reply/channels/route.js.map +7 -0
- package/dist/modules/channel_discord/api/channels/[id]/ai-auto-reply/route.js +222 -0
- package/dist/modules/channel_discord/api/channels/[id]/ai-auto-reply/route.js.map +7 -0
- package/dist/modules/channel_discord/api/interactions/route.js +173 -0
- package/dist/modules/channel_discord/api/interactions/route.js.map +7 -0
- package/dist/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.js +196 -0
- package/dist/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.js.map +7 -0
- package/dist/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.meta.js +14 -0
- package/dist/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.meta.js.map +7 -0
- package/dist/modules/channel_discord/cli.js +142 -0
- package/dist/modules/channel_discord/cli.js.map +7 -0
- package/dist/modules/channel_discord/commands/ai-reply-proposal.js +116 -0
- package/dist/modules/channel_discord/commands/ai-reply-proposal.js.map +7 -0
- package/dist/modules/channel_discord/commands/update-ai-auto-reply.js +104 -0
- package/dist/modules/channel_discord/commands/update-ai-auto-reply.js.map +7 -0
- package/dist/modules/channel_discord/di.js +23 -0
- package/dist/modules/channel_discord/di.js.map +7 -0
- package/dist/modules/channel_discord/index.js +9 -0
- package/dist/modules/channel_discord/index.js.map +7 -0
- package/dist/modules/channel_discord/integration.js +96 -0
- package/dist/modules/channel_discord/integration.js.map +7 -0
- package/dist/modules/channel_discord/lib/adapter.js +248 -0
- package/dist/modules/channel_discord/lib/adapter.js.map +7 -0
- package/dist/modules/channel_discord/lib/ai-agent-directory.js +45 -0
- package/dist/modules/channel_discord/lib/ai-agent-directory.js.map +7 -0
- package/dist/modules/channel_discord/lib/ai-features.js +9 -0
- package/dist/modules/channel_discord/lib/ai-features.js.map +7 -0
- package/dist/modules/channel_discord/lib/ai-proposal-contract.js +9 -0
- package/dist/modules/channel_discord/lib/ai-proposal-contract.js.map +7 -0
- package/dist/modules/channel_discord/lib/ai-proposal.js +109 -0
- package/dist/modules/channel_discord/lib/ai-proposal.js.map +7 -0
- package/dist/modules/channel_discord/lib/ai-reply.js +96 -0
- package/dist/modules/channel_discord/lib/ai-reply.js.map +7 -0
- package/dist/modules/channel_discord/lib/ai-service-principal.js +70 -0
- package/dist/modules/channel_discord/lib/ai-service-principal.js.map +7 -0
- package/dist/modules/channel_discord/lib/capabilities.js +42 -0
- package/dist/modules/channel_discord/lib/capabilities.js.map +7 -0
- package/dist/modules/channel_discord/lib/channel-access.js +62 -0
- package/dist/modules/channel_discord/lib/channel-access.js.map +7 -0
- package/dist/modules/channel_discord/lib/channel-state-store.js +103 -0
- package/dist/modules/channel_discord/lib/channel-state-store.js.map +7 -0
- package/dist/modules/channel_discord/lib/convert-outbound.js +38 -0
- package/dist/modules/channel_discord/lib/convert-outbound.js.map +7 -0
- package/dist/modules/channel_discord/lib/credentials.js +61 -0
- package/dist/modules/channel_discord/lib/credentials.js.map +7 -0
- package/dist/modules/channel_discord/lib/discord-gateway-client.js +289 -0
- package/dist/modules/channel_discord/lib/discord-gateway-client.js.map +7 -0
- package/dist/modules/channel_discord/lib/discord-rest.js +203 -0
- package/dist/modules/channel_discord/lib/discord-rest.js.map +7 -0
- package/dist/modules/channel_discord/lib/failure-reason.js +30 -0
- package/dist/modules/channel_discord/lib/failure-reason.js.map +7 -0
- package/dist/modules/channel_discord/lib/gateway-bridge.js +42 -0
- package/dist/modules/channel_discord/lib/gateway-bridge.js.map +7 -0
- package/dist/modules/channel_discord/lib/health.js +37 -0
- package/dist/modules/channel_discord/lib/health.js.map +7 -0
- package/dist/modules/channel_discord/lib/interactions-dispatch.js +164 -0
- package/dist/modules/channel_discord/lib/interactions-dispatch.js.map +7 -0
- package/dist/modules/channel_discord/lib/interactions-handler.js +115 -0
- package/dist/modules/channel_discord/lib/interactions-handler.js.map +7 -0
- package/dist/modules/channel_discord/lib/interactions-queue.js +17 -0
- package/dist/modules/channel_discord/lib/interactions-queue.js.map +7 -0
- package/dist/modules/channel_discord/lib/interactions-verify.js +84 -0
- package/dist/modules/channel_discord/lib/interactions-verify.js.map +7 -0
- package/dist/modules/channel_discord/lib/normalize-inbound.js +59 -0
- package/dist/modules/channel_discord/lib/normalize-inbound.js.map +7 -0
- package/dist/modules/channel_discord/lib/preset.js +63 -0
- package/dist/modules/channel_discord/lib/preset.js.map +7 -0
- package/dist/modules/channel_discord/lib/queue-strategy.js +12 -0
- package/dist/modules/channel_discord/lib/queue-strategy.js.map +7 -0
- package/dist/modules/channel_discord/lib/slash-commands.js +42 -0
- package/dist/modules/channel_discord/lib/slash-commands.js.map +7 -0
- package/dist/modules/channel_discord/message-types.js +64 -0
- package/dist/modules/channel_discord/message-types.js.map +7 -0
- package/dist/modules/channel_discord/setup.js +62 -0
- package/dist/modules/channel_discord/setup.js.map +7 -0
- package/dist/modules/channel_discord/subscribers/ai-auto-reply.js +185 -0
- package/dist/modules/channel_discord/subscribers/ai-auto-reply.js.map +7 -0
- package/dist/modules/channel_discord/widgets/injection/ai-auto-reply/widget.client.js +79 -0
- package/dist/modules/channel_discord/widgets/injection/ai-auto-reply/widget.client.js.map +7 -0
- package/dist/modules/channel_discord/widgets/injection/ai-auto-reply/widget.js +20 -0
- package/dist/modules/channel_discord/widgets/injection/ai-auto-reply/widget.js.map +7 -0
- package/dist/modules/channel_discord/widgets/injection/connect/widget.client.js +220 -0
- package/dist/modules/channel_discord/widgets/injection/connect/widget.client.js.map +7 -0
- package/dist/modules/channel_discord/widgets/injection/connect/widget.js +17 -0
- package/dist/modules/channel_discord/widgets/injection/connect/widget.js.map +7 -0
- package/dist/modules/channel_discord/widgets/injection-table.js +26 -0
- package/dist/modules/channel_discord/widgets/injection-table.js.map +7 -0
- package/dist/modules/channel_discord/workers/discord-gateway.js +270 -0
- package/dist/modules/channel_discord/workers/discord-gateway.js.map +7 -0
- package/dist/modules/channel_discord/workers/discord-interactions.js +113 -0
- package/dist/modules/channel_discord/workers/discord-interactions.js.map +7 -0
- package/jest.config.cjs +38 -0
- package/package.json +104 -0
- package/src/index.ts +1 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-001.spec.ts +109 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-002.spec.ts +58 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-003.spec.ts +175 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-004.spec.ts +83 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-005.spec.ts +156 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-006.spec.ts +144 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-007.spec.ts +119 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-008.spec.ts +75 -0
- package/src/modules/channel_discord/__integration__/helpers/discordSignature.ts +77 -0
- package/src/modules/channel_discord/__integration__/helpers/eventsQueue.ts +26 -0
- package/src/modules/channel_discord/__integration__/meta.ts +6 -0
- package/src/modules/channel_discord/__tests__/ai-auto-reply.policy.integration.test.ts +286 -0
- package/src/modules/channel_discord/acl.ts +19 -0
- package/src/modules/channel_discord/ai-agents.ts +157 -0
- package/src/modules/channel_discord/api/ai-auto-reply/channels/__tests__/route.test.ts +175 -0
- package/src/modules/channel_discord/api/ai-auto-reply/channels/route.ts +99 -0
- package/src/modules/channel_discord/api/channels/[id]/ai-auto-reply/__tests__/route.test.ts +311 -0
- package/src/modules/channel_discord/api/channels/[id]/ai-auto-reply/route.ts +299 -0
- package/src/modules/channel_discord/api/interactions/route.ts +242 -0
- package/src/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.meta.ts +18 -0
- package/src/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.tsx +288 -0
- package/src/modules/channel_discord/cli.ts +234 -0
- package/src/modules/channel_discord/commands/__tests__/ai-reply-proposal.test.ts +155 -0
- package/src/modules/channel_discord/commands/__tests__/update-ai-auto-reply.test.ts +185 -0
- package/src/modules/channel_discord/commands/ai-reply-proposal.ts +162 -0
- package/src/modules/channel_discord/commands/update-ai-auto-reply.ts +157 -0
- package/src/modules/channel_discord/di.ts +26 -0
- package/src/modules/channel_discord/i18n/de.json +57 -0
- package/src/modules/channel_discord/i18n/en.json +57 -0
- package/src/modules/channel_discord/i18n/es.json +57 -0
- package/src/modules/channel_discord/i18n/ko.json +57 -0
- package/src/modules/channel_discord/i18n/pl.json +57 -0
- package/src/modules/channel_discord/index.ts +6 -0
- package/src/modules/channel_discord/integration.ts +98 -0
- package/src/modules/channel_discord/lib/__tests__/adapter.test.ts +206 -0
- package/src/modules/channel_discord/lib/__tests__/ai-agent-directory.test.ts +61 -0
- package/src/modules/channel_discord/lib/__tests__/ai-reply.test.ts +115 -0
- package/src/modules/channel_discord/lib/__tests__/capabilities.test.ts +170 -0
- package/src/modules/channel_discord/lib/__tests__/channel-quarantine.test.ts +89 -0
- package/src/modules/channel_discord/lib/__tests__/channel-state-store.auto-reply-marker.test.ts +145 -0
- package/src/modules/channel_discord/lib/__tests__/channel-state-store.test.ts +78 -0
- package/src/modules/channel_discord/lib/__tests__/convert-outbound.test.ts +39 -0
- package/src/modules/channel_discord/lib/__tests__/discord-gateway-client.test.ts +243 -0
- package/src/modules/channel_discord/lib/__tests__/failure-reason.test.ts +68 -0
- package/src/modules/channel_discord/lib/__tests__/gateway-bridge.test.ts +62 -0
- package/src/modules/channel_discord/lib/__tests__/interactions-dispatch.test.ts +223 -0
- package/src/modules/channel_discord/lib/__tests__/interactions-handler.test.ts +469 -0
- package/src/modules/channel_discord/lib/__tests__/interactions-verify.test.ts +126 -0
- package/src/modules/channel_discord/lib/__tests__/normalize-inbound.identity.test.ts +64 -0
- package/src/modules/channel_discord/lib/__tests__/normalize-inbound.test.ts +67 -0
- package/src/modules/channel_discord/lib/__tests__/preset.test.ts +110 -0
- package/src/modules/channel_discord/lib/__tests__/queue-strategy.test.ts +33 -0
- package/src/modules/channel_discord/lib/adapter.ts +359 -0
- package/src/modules/channel_discord/lib/ai-agent-directory.ts +124 -0
- package/src/modules/channel_discord/lib/ai-features.ts +23 -0
- package/src/modules/channel_discord/lib/ai-proposal-contract.ts +24 -0
- package/src/modules/channel_discord/lib/ai-proposal.ts +179 -0
- package/src/modules/channel_discord/lib/ai-reply.ts +179 -0
- package/src/modules/channel_discord/lib/ai-service-principal.ts +143 -0
- package/src/modules/channel_discord/lib/capabilities.ts +104 -0
- package/src/modules/channel_discord/lib/channel-access.ts +110 -0
- package/src/modules/channel_discord/lib/channel-state-store.ts +217 -0
- package/src/modules/channel_discord/lib/convert-outbound.ts +88 -0
- package/src/modules/channel_discord/lib/credentials.ts +88 -0
- package/src/modules/channel_discord/lib/discord-gateway-client.ts +432 -0
- package/src/modules/channel_discord/lib/discord-rest.ts +393 -0
- package/src/modules/channel_discord/lib/failure-reason.ts +49 -0
- package/src/modules/channel_discord/lib/gateway-bridge.ts +79 -0
- package/src/modules/channel_discord/lib/health.ts +55 -0
- package/src/modules/channel_discord/lib/interactions-dispatch.ts +288 -0
- package/src/modules/channel_discord/lib/interactions-handler.ts +267 -0
- package/src/modules/channel_discord/lib/interactions-queue.ts +52 -0
- package/src/modules/channel_discord/lib/interactions-verify.ts +174 -0
- package/src/modules/channel_discord/lib/normalize-inbound.ts +89 -0
- package/src/modules/channel_discord/lib/preset.ts +109 -0
- package/src/modules/channel_discord/lib/queue-strategy.ts +29 -0
- package/src/modules/channel_discord/lib/slash-commands.ts +57 -0
- package/src/modules/channel_discord/message-types.ts +75 -0
- package/src/modules/channel_discord/setup.ts +78 -0
- package/src/modules/channel_discord/subscribers/__tests__/ai-auto-reply.armed.test.ts +149 -0
- package/src/modules/channel_discord/subscribers/__tests__/ai-auto-reply.failure-marker.test.ts +133 -0
- package/src/modules/channel_discord/subscribers/__tests__/ai-auto-reply.test.ts +273 -0
- package/src/modules/channel_discord/subscribers/ai-auto-reply.ts +325 -0
- package/src/modules/channel_discord/widgets/injection/ai-auto-reply/widget.client.tsx +143 -0
- package/src/modules/channel_discord/widgets/injection/ai-auto-reply/widget.ts +19 -0
- package/src/modules/channel_discord/widgets/injection/connect/widget.client.tsx +237 -0
- package/src/modules/channel_discord/widgets/injection/connect/widget.ts +16 -0
- package/src/modules/channel_discord/widgets/injection-table.ts +24 -0
- package/src/modules/channel_discord/workers/__tests__/discord-gateway.single-identify.test.ts +88 -0
- package/src/modules/channel_discord/workers/__tests__/discord-gateway.test.ts +179 -0
- package/src/modules/channel_discord/workers/__tests__/discord-interactions.test.ts +152 -0
- package/src/modules/channel_discord/workers/discord-gateway.ts +458 -0
- package/src/modules/channel_discord/workers/discord-interactions.ts +162 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +7 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'
|
|
5
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
7
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
8
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
9
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
10
|
+
import {
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogContent,
|
|
13
|
+
DialogFooter,
|
|
14
|
+
DialogHeader,
|
|
15
|
+
DialogTitle,
|
|
16
|
+
} from '@open-mercato/ui/primitives/dialog'
|
|
17
|
+
import { Input } from '@open-mercato/ui/primitives/input'
|
|
18
|
+
import { Label } from '@open-mercato/ui/primitives/label'
|
|
19
|
+
import { PasswordInput } from '@open-mercato/ui/primitives/password-input'
|
|
20
|
+
|
|
21
|
+
type WidgetContext = Record<string, unknown> & {
|
|
22
|
+
reload?: () => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type ConnectResponse = {
|
|
26
|
+
channelId?: string
|
|
27
|
+
error?: string
|
|
28
|
+
fieldErrors?: Record<string, string>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type FormState = {
|
|
32
|
+
displayName: string
|
|
33
|
+
botToken: string
|
|
34
|
+
applicationId: string
|
|
35
|
+
publicKey: string
|
|
36
|
+
guildId: string
|
|
37
|
+
defaultChannelId: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const INITIAL_FORM: FormState = {
|
|
41
|
+
displayName: '',
|
|
42
|
+
botToken: '',
|
|
43
|
+
applicationId: '',
|
|
44
|
+
publicKey: '',
|
|
45
|
+
guildId: '',
|
|
46
|
+
defaultChannelId: '',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default function ConnectDiscordWidget({
|
|
50
|
+
context,
|
|
51
|
+
}: InjectionWidgetComponentProps<Record<string, unknown>, Record<string, unknown>>) {
|
|
52
|
+
const t = useT()
|
|
53
|
+
const widgetContext = context as WidgetContext | undefined
|
|
54
|
+
const [open, setOpen] = React.useState(false)
|
|
55
|
+
const [pending, setPending] = React.useState(false)
|
|
56
|
+
const [form, setForm] = React.useState<FormState>(INITIAL_FORM)
|
|
57
|
+
const [fieldErrors, setFieldErrors] = React.useState<Record<string, string>>({})
|
|
58
|
+
const { runMutation, retryLastMutation } = useGuardedMutation({
|
|
59
|
+
contextId: 'channel-discord-connect',
|
|
60
|
+
blockedMessage: t('channel_discord.connect.blocked', 'Connection blocked by validation'),
|
|
61
|
+
})
|
|
62
|
+
const mutationContext = React.useMemo(
|
|
63
|
+
() => ({ providerKey: 'discord', retryLastMutation }),
|
|
64
|
+
[retryLastMutation],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const update = React.useCallback(
|
|
68
|
+
<K extends keyof FormState>(key: K, value: FormState[K]) => {
|
|
69
|
+
setForm((current) => ({ ...current, [key]: value }))
|
|
70
|
+
setFieldErrors((current) => {
|
|
71
|
+
if (!current[key]) return current
|
|
72
|
+
const next = { ...current }
|
|
73
|
+
delete next[key]
|
|
74
|
+
return next
|
|
75
|
+
})
|
|
76
|
+
},
|
|
77
|
+
[],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const submit = React.useCallback(async () => {
|
|
81
|
+
if (pending) return
|
|
82
|
+
setPending(true)
|
|
83
|
+
setFieldErrors({})
|
|
84
|
+
try {
|
|
85
|
+
const response = await runMutation({
|
|
86
|
+
context: mutationContext,
|
|
87
|
+
mutationPayload: { providerKey: 'discord', displayName: form.displayName },
|
|
88
|
+
operation: () =>
|
|
89
|
+
apiCall<ConnectResponse>('/api/communication_channels/channels/connect/credentials', {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: { 'content-type': 'application/json' },
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
providerKey: 'discord',
|
|
94
|
+
displayName: form.displayName.trim() || 'Discord',
|
|
95
|
+
credentials: {
|
|
96
|
+
botToken: form.botToken.trim(),
|
|
97
|
+
applicationId: form.applicationId.trim(),
|
|
98
|
+
publicKey: form.publicKey.trim(),
|
|
99
|
+
guildId: form.guildId.trim() || undefined,
|
|
100
|
+
defaultChannelId: form.defaultChannelId.trim() || undefined,
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
}),
|
|
104
|
+
})
|
|
105
|
+
const body = response.result as ConnectResponse | undefined
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
setFieldErrors(body?.fieldErrors ?? {})
|
|
108
|
+
flash(body?.error ?? t('channel_discord.connect.failed', 'Could not connect Discord bot.'), 'error')
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
flash(t('channel_discord.connect.connected', 'Discord channel connected.'), 'success')
|
|
112
|
+
setOpen(false)
|
|
113
|
+
setForm(INITIAL_FORM)
|
|
114
|
+
widgetContext?.reload?.()
|
|
115
|
+
} finally {
|
|
116
|
+
setPending(false)
|
|
117
|
+
}
|
|
118
|
+
}, [form, mutationContext, pending, runMutation, t, widgetContext])
|
|
119
|
+
|
|
120
|
+
const onDialogKeyDown = React.useCallback(
|
|
121
|
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
122
|
+
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
123
|
+
event.preventDefault()
|
|
124
|
+
void submit()
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
[submit],
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<>
|
|
132
|
+
<Button type="button" variant="outline" onClick={() => setOpen(true)}>
|
|
133
|
+
{t('channel_discord.connect.button', 'Connect Discord')}
|
|
134
|
+
</Button>
|
|
135
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
136
|
+
<DialogContent onKeyDown={onDialogKeyDown}>
|
|
137
|
+
<DialogHeader>
|
|
138
|
+
<DialogTitle>{t('channel_discord.connect.title', 'Connect Discord bot')}</DialogTitle>
|
|
139
|
+
</DialogHeader>
|
|
140
|
+
|
|
141
|
+
<div className="grid gap-4 py-2">
|
|
142
|
+
<Field
|
|
143
|
+
label={t('channel_discord.connect.fields.displayName', 'Display name')}
|
|
144
|
+
error={fieldErrors.displayName}
|
|
145
|
+
>
|
|
146
|
+
<Input
|
|
147
|
+
value={form.displayName}
|
|
148
|
+
onChange={(event) => update('displayName', event.target.value)}
|
|
149
|
+
aria-invalid={Boolean(fieldErrors.displayName)}
|
|
150
|
+
/>
|
|
151
|
+
</Field>
|
|
152
|
+
|
|
153
|
+
<Field
|
|
154
|
+
label={t('channel_discord.connect.fields.botToken', 'Bot token')}
|
|
155
|
+
error={fieldErrors.botToken}
|
|
156
|
+
>
|
|
157
|
+
<PasswordInput
|
|
158
|
+
value={form.botToken}
|
|
159
|
+
onChange={(event) => update('botToken', event.target.value)}
|
|
160
|
+
aria-invalid={Boolean(fieldErrors.botToken)}
|
|
161
|
+
/>
|
|
162
|
+
</Field>
|
|
163
|
+
|
|
164
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
165
|
+
<Field
|
|
166
|
+
label={t('channel_discord.connect.fields.applicationId', 'Application ID')}
|
|
167
|
+
error={fieldErrors.applicationId}
|
|
168
|
+
>
|
|
169
|
+
<Input
|
|
170
|
+
value={form.applicationId}
|
|
171
|
+
onChange={(event) => update('applicationId', event.target.value)}
|
|
172
|
+
aria-invalid={Boolean(fieldErrors.applicationId)}
|
|
173
|
+
/>
|
|
174
|
+
</Field>
|
|
175
|
+
<Field
|
|
176
|
+
label={t('channel_discord.connect.fields.publicKey', 'Public key')}
|
|
177
|
+
error={fieldErrors.publicKey}
|
|
178
|
+
>
|
|
179
|
+
<Input
|
|
180
|
+
value={form.publicKey}
|
|
181
|
+
onChange={(event) => update('publicKey', event.target.value)}
|
|
182
|
+
aria-invalid={Boolean(fieldErrors.publicKey)}
|
|
183
|
+
/>
|
|
184
|
+
</Field>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
188
|
+
<Field
|
|
189
|
+
label={t('channel_discord.connect.fields.guildId', 'Guild (server) ID')}
|
|
190
|
+
error={fieldErrors.guildId}
|
|
191
|
+
>
|
|
192
|
+
<Input
|
|
193
|
+
value={form.guildId}
|
|
194
|
+
onChange={(event) => update('guildId', event.target.value)}
|
|
195
|
+
aria-invalid={Boolean(fieldErrors.guildId)}
|
|
196
|
+
/>
|
|
197
|
+
</Field>
|
|
198
|
+
<Field
|
|
199
|
+
label={t('channel_discord.connect.fields.defaultChannelId', 'Default channel ID')}
|
|
200
|
+
error={fieldErrors.defaultChannelId}
|
|
201
|
+
>
|
|
202
|
+
<Input
|
|
203
|
+
value={form.defaultChannelId}
|
|
204
|
+
onChange={(event) => update('defaultChannelId', event.target.value)}
|
|
205
|
+
aria-invalid={Boolean(fieldErrors.defaultChannelId)}
|
|
206
|
+
/>
|
|
207
|
+
</Field>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<DialogFooter>
|
|
212
|
+
<Button type="button" variant="outline" onClick={() => setOpen(false)} disabled={pending}>
|
|
213
|
+
{t('channel_discord.connect.cancel', 'Cancel')}
|
|
214
|
+
</Button>
|
|
215
|
+
<Button type="button" onClick={() => void submit()} disabled={pending}>
|
|
216
|
+
{pending
|
|
217
|
+
? t('channel_discord.connect.connecting', 'Connecting...')
|
|
218
|
+
: t('channel_discord.connect.save', 'Connect')}
|
|
219
|
+
</Button>
|
|
220
|
+
</DialogFooter>
|
|
221
|
+
</DialogContent>
|
|
222
|
+
</Dialog>
|
|
223
|
+
</>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function Field(props: { label: string; error?: string; children: React.ReactNode }) {
|
|
228
|
+
return (
|
|
229
|
+
<label className="grid gap-1.5">
|
|
230
|
+
<Label asChild>
|
|
231
|
+
<span>{props.label}</span>
|
|
232
|
+
</Label>
|
|
233
|
+
{props.children}
|
|
234
|
+
{props.error ? <span className="text-xs text-destructive">{props.error}</span> : null}
|
|
235
|
+
</label>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'
|
|
2
|
+
import ConnectDiscordWidget from './widget.client'
|
|
3
|
+
|
|
4
|
+
const widget: InjectionWidgetModule<Record<string, unknown>, Record<string, unknown>> = {
|
|
5
|
+
metadata: {
|
|
6
|
+
id: 'channel_discord.injection.connect',
|
|
7
|
+
title: 'Connect Discord',
|
|
8
|
+
description: 'Connects a Discord bot as a two-way communication channel.',
|
|
9
|
+
features: ['communication_channels.connect_user_channel'],
|
|
10
|
+
priority: 100,
|
|
11
|
+
enabled: true,
|
|
12
|
+
},
|
|
13
|
+
Widget: ConnectDiscordWidget,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default widget
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ModuleInjectionTable } from '@open-mercato/shared/modules/widgets/injection'
|
|
2
|
+
import { channelDiscordDetailWidgetSpotId } from '../integration'
|
|
3
|
+
|
|
4
|
+
export const injectionTable: ModuleInjectionTable = {
|
|
5
|
+
'profile:communication-channels:connect': [
|
|
6
|
+
{
|
|
7
|
+
widgetId: 'channel_discord.injection.connect',
|
|
8
|
+
priority: 120,
|
|
9
|
+
},
|
|
10
|
+
],
|
|
11
|
+
// The Discord integration's own detail page — the only spot in the product
|
|
12
|
+
// that is Discord-scoped by construction, which is what a per-channel AI
|
|
13
|
+
// setting needs (see the widget's file header).
|
|
14
|
+
[channelDiscordDetailWidgetSpotId]: [
|
|
15
|
+
{
|
|
16
|
+
widgetId: 'channel_discord.injection.ai-auto-reply',
|
|
17
|
+
priority: 40,
|
|
18
|
+
kind: 'group',
|
|
19
|
+
groupLabel: 'channel_discord.aiAutoReply.widget.title',
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default injectionTable
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
botTokenFingerprint,
|
|
3
|
+
findChannelWithSameBot,
|
|
4
|
+
type GatewayConnectionEntry,
|
|
5
|
+
} from '../discord-gateway'
|
|
6
|
+
import type { DiscordGatewayHandle } from '../../lib/discord-gateway-client'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Regression guard for the duplicate-session half of #4977.
|
|
10
|
+
*
|
|
11
|
+
* The hub derives a channel's `externalIdentifier` by sniffing the credential
|
|
12
|
+
* bag for email-shaped keys (`username` / `email` / `fromAddress`), none of
|
|
13
|
+
* which a Discord credential bag has. Every reconnect therefore inserts a second
|
|
14
|
+
* active channel row for the same bot instead of healing the first, and QA
|
|
15
|
+
* observed the gateway opening two parallel WebSocket sessions with two distinct
|
|
16
|
+
* Discord session ids — defeating the single-identify-per-bot discipline that
|
|
17
|
+
* `concurrency: 1` exists to enforce.
|
|
18
|
+
*
|
|
19
|
+
* The real fix is hub-side (let the adapter supply an identity); this guard is
|
|
20
|
+
* the interim one that keeps the second socket from ever opening.
|
|
21
|
+
*/
|
|
22
|
+
function fakeEntry(
|
|
23
|
+
tenantId: string,
|
|
24
|
+
fingerprint: string | undefined,
|
|
25
|
+
active = true,
|
|
26
|
+
): GatewayConnectionEntry {
|
|
27
|
+
const handle: DiscordGatewayHandle = { close: jest.fn(), isActive: () => active }
|
|
28
|
+
return { handle, tenantId, organizationId: null, botTokenFingerprint: fingerprint }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('botTokenFingerprint', () => {
|
|
32
|
+
it('is stable for the same token and different for another', () => {
|
|
33
|
+
expect(botTokenFingerprint('bot-token-a')).toBe(botTokenFingerprint('bot-token-a'))
|
|
34
|
+
expect(botTokenFingerprint('bot-token-a')).not.toBe(botTokenFingerprint('bot-token-b'))
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('never reveals the token it was derived from', () => {
|
|
38
|
+
const token = 'MTIzNDU2Nzg5.super.secret-bot-token'
|
|
39
|
+
const fingerprint = botTokenFingerprint(token)
|
|
40
|
+
expect(fingerprint).not.toContain(token)
|
|
41
|
+
expect(fingerprint).toMatch(/^[0-9a-f]{64}$/)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('findChannelWithSameBot', () => {
|
|
46
|
+
it('reports the channel already serving the same bot', () => {
|
|
47
|
+
const fingerprint = botTokenFingerprint('one-bot')
|
|
48
|
+
const connections = new Map<string, GatewayConnectionEntry>([
|
|
49
|
+
['chan-first', fakeEntry('t1', fingerprint)],
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
expect(findChannelWithSameBot('chan-duplicate', fingerprint, connections)).toBe('chan-first')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('does not treat a channel as its own duplicate', () => {
|
|
56
|
+
const fingerprint = botTokenFingerprint('one-bot')
|
|
57
|
+
const connections = new Map<string, GatewayConnectionEntry>([
|
|
58
|
+
['chan-first', fakeEntry('t1', fingerprint)],
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
expect(findChannelWithSameBot('chan-first', fingerprint, connections)).toBeNull()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('allows a different bot to connect alongside', () => {
|
|
65
|
+
const connections = new Map<string, GatewayConnectionEntry>([
|
|
66
|
+
['chan-first', fakeEntry('t1', botTokenFingerprint('bot-a'))],
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
expect(findChannelWithSameBot('chan-other', botTokenFingerprint('bot-b'), connections)).toBeNull()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('ignores a dead session so a genuinely lost connection can be replaced', () => {
|
|
73
|
+
const fingerprint = botTokenFingerprint('one-bot')
|
|
74
|
+
const connections = new Map<string, GatewayConnectionEntry>([
|
|
75
|
+
['chan-first', fakeEntry('t1', fingerprint, false)],
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
expect(findChannelWithSameBot('chan-second', fingerprint, connections)).toBeNull()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('ignores entries recorded before fingerprints existed', () => {
|
|
82
|
+
const connections = new Map<string, GatewayConnectionEntry>([
|
|
83
|
+
['chan-legacy', fakeEntry('t1', undefined)],
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
expect(findChannelWithSameBot('chan-new', botTokenFingerprint('bot-a'), connections)).toBeNull()
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildGatewayChannelFilter,
|
|
3
|
+
isConnectionLive,
|
|
4
|
+
reconcileGatewayConnections,
|
|
5
|
+
type GatewayConnectionEntry,
|
|
6
|
+
} from '../discord-gateway'
|
|
7
|
+
import type { DiscordGatewayHandle } from '../../lib/discord-gateway-client'
|
|
8
|
+
|
|
9
|
+
function fakeEntry(
|
|
10
|
+
tenantId: string,
|
|
11
|
+
active = true,
|
|
12
|
+
organizationId: string | null = null,
|
|
13
|
+
): { entry: GatewayConnectionEntry; close: jest.Mock } {
|
|
14
|
+
const close = jest.fn()
|
|
15
|
+
const handle: DiscordGatewayHandle = { close, isActive: () => active }
|
|
16
|
+
return { entry: { handle, tenantId, organizationId }, close }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `GatewayJobPayload.organizationId` used to be accepted and silently discarded:
|
|
21
|
+
* the query filtered on tenant only, so a job scoped to one organization
|
|
22
|
+
* connected every organization in the tenant. A scope parameter that is declared
|
|
23
|
+
* and then thrown away is how multi-tenant defects start, so the filter is
|
|
24
|
+
* pinned here.
|
|
25
|
+
*/
|
|
26
|
+
describe('buildGatewayChannelFilter (scoped job payloads)', () => {
|
|
27
|
+
it('connects every active discord channel when the payload carries no scope', () => {
|
|
28
|
+
expect(buildGatewayChannelFilter({})).toEqual({
|
|
29
|
+
providerKey: 'discord',
|
|
30
|
+
isActive: true,
|
|
31
|
+
deletedAt: null,
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('narrows by tenant when the payload names one', () => {
|
|
36
|
+
expect(buildGatewayChannelFilter({ tenantId: 't1' })).toEqual({
|
|
37
|
+
providerKey: 'discord',
|
|
38
|
+
isActive: true,
|
|
39
|
+
deletedAt: null,
|
|
40
|
+
tenantId: 't1',
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('narrows by organization when the payload names one', () => {
|
|
45
|
+
expect(buildGatewayChannelFilter({ tenantId: 't1', organizationId: 'org-a' })).toEqual({
|
|
46
|
+
providerKey: 'discord',
|
|
47
|
+
isActive: true,
|
|
48
|
+
deletedAt: null,
|
|
49
|
+
tenantId: 't1',
|
|
50
|
+
organizationId: 'org-a',
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('treats an explicitly null organization as "no organization filter", not "tenant-wide channels only"', () => {
|
|
55
|
+
// The payload type allows `organizationId: null`; that is the absence of a
|
|
56
|
+
// filter, not a request for rows whose organization_id IS NULL.
|
|
57
|
+
expect(buildGatewayChannelFilter({ tenantId: 't1', organizationId: null })).toEqual({
|
|
58
|
+
providerKey: 'discord',
|
|
59
|
+
isActive: true,
|
|
60
|
+
deletedAt: null,
|
|
61
|
+
tenantId: 't1',
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('reconcileGatewayConnections', () => {
|
|
67
|
+
it('closes + removes a connection whose channel dropped out of the active set', () => {
|
|
68
|
+
const a = fakeEntry('t1')
|
|
69
|
+
const b = fakeEntry('t1')
|
|
70
|
+
const connections = new Map<string, GatewayConnectionEntry>([
|
|
71
|
+
['chan-a', a.entry],
|
|
72
|
+
['chan-b', b.entry],
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
// chan-b is no longer active (deactivated / soft-deleted).
|
|
76
|
+
const removed = reconcileGatewayConnections(new Set(['chan-a']), connections)
|
|
77
|
+
|
|
78
|
+
expect(removed).toEqual(['chan-b'])
|
|
79
|
+
expect(b.close).toHaveBeenCalledTimes(1)
|
|
80
|
+
expect(a.close).not.toHaveBeenCalled()
|
|
81
|
+
expect(connections.has('chan-b')).toBe(false)
|
|
82
|
+
expect(connections.has('chan-a')).toBe(true)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('keeps every connection when all channels are still active', () => {
|
|
86
|
+
const a = fakeEntry('t1')
|
|
87
|
+
const connections = new Map<string, GatewayConnectionEntry>([['chan-a', a.entry]])
|
|
88
|
+
const removed = reconcileGatewayConnections(new Set(['chan-a']), connections)
|
|
89
|
+
expect(removed).toEqual([])
|
|
90
|
+
expect(a.close).not.toHaveBeenCalled()
|
|
91
|
+
expect(connections.has('chan-a')).toBe(true)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('a tenant-scoped refresh never tears down another tenant’s sockets', () => {
|
|
95
|
+
const t1 = fakeEntry('t1')
|
|
96
|
+
const t2 = fakeEntry('t2')
|
|
97
|
+
const connections = new Map<string, GatewayConnectionEntry>([
|
|
98
|
+
['chan-t1', t1.entry],
|
|
99
|
+
['chan-t2', t2.entry],
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
// Scoped refresh for t1 returns no active t1 channels, but must NOT touch t2.
|
|
103
|
+
const removed = reconcileGatewayConnections(new Set<string>(), connections, { tenantId: 't1' })
|
|
104
|
+
|
|
105
|
+
expect(removed).toEqual(['chan-t1'])
|
|
106
|
+
expect(t1.close).toHaveBeenCalledTimes(1)
|
|
107
|
+
expect(t2.close).not.toHaveBeenCalled()
|
|
108
|
+
expect(connections.has('chan-t2')).toBe(true)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('an organization-scoped refresh never tears down a sibling organization’s sockets', () => {
|
|
112
|
+
// A job payload carrying `organizationId` used to be accepted and discarded:
|
|
113
|
+
// the query and the reconciliation both filtered on tenant only, so a refresh
|
|
114
|
+
// scoped to one organization would close every OTHER organization's sockets
|
|
115
|
+
// in the same tenant, because none of their channels appeared in the
|
|
116
|
+
// (tenant-wide) active set it compared against.
|
|
117
|
+
const orgA = fakeEntry('t1', true, 'org-a')
|
|
118
|
+
const orgB = fakeEntry('t1', true, 'org-b')
|
|
119
|
+
const tenantWide = fakeEntry('t1', true, null)
|
|
120
|
+
const connections = new Map<string, GatewayConnectionEntry>([
|
|
121
|
+
['chan-org-a', orgA.entry],
|
|
122
|
+
['chan-org-b', orgB.entry],
|
|
123
|
+
['chan-tenant-wide', tenantWide.entry],
|
|
124
|
+
])
|
|
125
|
+
|
|
126
|
+
const removed = reconcileGatewayConnections(new Set<string>(), connections, {
|
|
127
|
+
tenantId: 't1',
|
|
128
|
+
organizationId: 'org-a',
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
expect(removed).toEqual(['chan-org-a'])
|
|
132
|
+
expect(orgA.close).toHaveBeenCalledTimes(1)
|
|
133
|
+
expect(orgB.close).not.toHaveBeenCalled()
|
|
134
|
+
expect(tenantWide.close).not.toHaveBeenCalled()
|
|
135
|
+
expect(connections.has('chan-org-b')).toBe(true)
|
|
136
|
+
expect(connections.has('chan-tenant-wide')).toBe(true)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('an unscoped refresh still reconciles every tenant and organization', () => {
|
|
140
|
+
const orgA = fakeEntry('t1', true, 'org-a')
|
|
141
|
+
const otherTenant = fakeEntry('t2', true, 'org-z')
|
|
142
|
+
const connections = new Map<string, GatewayConnectionEntry>([
|
|
143
|
+
['chan-org-a', orgA.entry],
|
|
144
|
+
['chan-t2', otherTenant.entry],
|
|
145
|
+
])
|
|
146
|
+
|
|
147
|
+
const removed = reconcileGatewayConnections(new Set<string>(), connections)
|
|
148
|
+
|
|
149
|
+
expect(removed.sort()).toEqual(['chan-org-a', 'chan-t2'])
|
|
150
|
+
expect(connections.size).toBe(0)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('isConnectionLive (refresh must not churn healthy sockets)', () => {
|
|
155
|
+
it('reports a running session as live so the refresh job leaves it alone', () => {
|
|
156
|
+
const live = fakeEntry('t1', true)
|
|
157
|
+
expect(isConnectionLive(live.entry)).toBe(true)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('reports a stopped session as dead so the refresh job replaces it', () => {
|
|
161
|
+
const dead = fakeEntry('t1', false)
|
|
162
|
+
expect(isConnectionLive(dead.entry)).toBe(false)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('treats a missing entry and a throwing handle as dead', () => {
|
|
166
|
+
expect(isConnectionLive(undefined)).toBe(false)
|
|
167
|
+
const throwing: GatewayConnectionEntry = {
|
|
168
|
+
tenantId: 't1',
|
|
169
|
+
organizationId: null,
|
|
170
|
+
handle: {
|
|
171
|
+
close: jest.fn(),
|
|
172
|
+
isActive: () => {
|
|
173
|
+
throw new Error('socket gone')
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
expect(isConnectionLive(throwing)).toBe(false)
|
|
178
|
+
})
|
|
179
|
+
})
|