Skip to content

Commit 9bcd23b

Browse files
author
Omni Loop Labs
committed
fix: make Telegram setup provision and verify its adapter
1 parent d10e790 commit 9bcd23b

20 files changed

Lines changed: 828 additions & 72 deletions

apps/desktop/src/app/command-center/index.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,9 @@ export function CommandCenterView({
358358
const started = kind === 'restart' ? await restartGateway() : await updateClio()
359359
let nextStatus: ActionStatusResponse | null = null
360360

361-
for (let attempt = 0; attempt < 18; attempt += 1) {
361+
const maxAttempts = kind === 'restart' ? 65 : 300
362+
363+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
362364
await new Promise(resolve => window.setTimeout(resolve, 1200))
363365
const polled = await getActionStatus(started.name, 180)
364366
nextStatus = polled
@@ -370,6 +372,11 @@ export function CommandCenterView({
370372
}
371373
}
372374

375+
if (nextStatus && !nextStatus.running && nextStatus.exit_code !== 0) {
376+
const detail = nextStatus.lines.filter(Boolean).slice(-6).join('\n')
377+
throw new Error(detail || `${nextStatus.name} failed (exit ${String(nextStatus.exit_code)}).`)
378+
}
379+
373380
if (!nextStatus) {
374381
const pendingStatus = {
375382
exit_code: null,

apps/desktop/src/app/messaging/telegram-qr-setup.test.tsx

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,21 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
55
const mocks = vi.hoisted(() => ({
66
applyTelegramOnboarding: vi.fn(),
77
cancelTelegramOnboarding: vi.fn(),
8+
getActionStatus: vi.fn(),
89
getTelegramOnboardingStatus: vi.fn(),
910
restartGateway: vi.fn(),
10-
startTelegramOnboarding: vi.fn()
11+
startTelegramOnboarding: vi.fn(),
12+
testMessagingPlatform: vi.fn()
1113
}))
1214

1315
vi.mock('@/clio', () => ({
1416
applyTelegramOnboarding: (id: string, body: unknown) => mocks.applyTelegramOnboarding(id, body),
1517
cancelTelegramOnboarding: (id: string) => mocks.cancelTelegramOnboarding(id),
18+
getActionStatus: (name: string, lines: number) => mocks.getActionStatus(name, lines),
1619
getTelegramOnboardingStatus: (id: string) => mocks.getTelegramOnboardingStatus(id),
1720
restartGateway: () => mocks.restartGateway(),
18-
startTelegramOnboarding: (body: unknown) => mocks.startTelegramOnboarding(body)
21+
startTelegramOnboarding: (body: unknown) => mocks.startTelegramOnboarding(body),
22+
testMessagingPlatform: (id: string) => mocks.testMessagingPlatform(id)
1923
}))
2024
vi.mock('qrcode', () => ({ default: { toDataURL: vi.fn().mockResolvedValue('data:image/png;base64,QR') } }))
2125

@@ -37,7 +41,15 @@ describe('TelegramQrSetup', () => {
3741
status: 'ready'
3842
})
3943
mocks.applyTelegramOnboarding.mockResolvedValue({ needs_restart: true, ok: true, platform: 'telegram' })
40-
mocks.restartGateway.mockResolvedValue({ ok: true })
44+
mocks.restartGateway.mockResolvedValue({ name: 'gateway-restart', ok: true, pid: 42 })
45+
mocks.getActionStatus.mockResolvedValue({
46+
exit_code: 0,
47+
lines: ['Gateway started'],
48+
name: 'gateway-restart',
49+
pid: 42,
50+
running: false
51+
})
52+
mocks.testMessagingPlatform.mockResolvedValue({ message: 'Telegram is connected.', ok: true, state: 'connected' })
4153
mocks.cancelTelegramOnboarding.mockResolvedValue({ ok: true })
4254
})
4355

@@ -52,7 +64,7 @@ describe('TelegramQrSetup', () => {
5264
expect(screen.getByText('Set up with QR')).toBeTruthy()
5365
})
5466

55-
it('runs the full flow: start → QR → ready → apply → restart', async () => {
67+
it('runs the full flow and reports success only after Telegram connects', async () => {
5668
const onApplied = vi.fn()
5769
render(<TelegramQrSetup onApplied={onApplied} />)
5870

@@ -73,7 +85,39 @@ describe('TelegramQrSetup', () => {
7385
expect(mocks.applyTelegramOnboarding).toHaveBeenCalledWith('p1', { allowed_user_ids: ['4242'] }),
7486
)
7587
await waitFor(() => expect(mocks.restartGateway).toHaveBeenCalled())
88+
await waitFor(() => expect(mocks.getActionStatus).toHaveBeenCalledWith('gateway-restart', 200), { timeout: 4000 })
89+
await waitFor(() => expect(mocks.testMessagingPlatform).toHaveBeenCalledWith('telegram'))
7690
await waitFor(() => expect(onApplied).toHaveBeenCalled())
91+
}, 6000)
92+
93+
it('surfaces restart action failure and offers retry instead of reporting success', async () => {
94+
mocks.getActionStatus.mockResolvedValue({
95+
exit_code: 1,
96+
lines: ['Telegram support is unavailable'],
97+
name: 'gateway-restart',
98+
pid: 42,
99+
running: false
100+
})
101+
const onApplied = vi.fn()
102+
render(<TelegramQrSetup onApplied={onApplied} />)
103+
fireEvent.click(screen.getByText('Set up with QR'))
104+
await waitFor(() => expect(screen.getByText(/Bot created/)).toBeTruthy(), { timeout: 3000 })
105+
106+
fireEvent.click(screen.getByText('Save & restart gateway'))
107+
108+
await waitFor(() => expect(screen.getByText('Retry gateway connection')).toBeTruthy(), { timeout: 4000 })
109+
expect(screen.getByText(/Telegram support is unavailable/)).toBeTruthy()
110+
expect(onApplied).not.toHaveBeenCalled()
111+
}, 6000)
112+
113+
it('shows dependency preparation failure before creating a pairing', async () => {
114+
mocks.startTelegramOnboarding.mockRejectedValue(new Error('Telegram support is unavailable'))
115+
render(<TelegramQrSetup />)
116+
117+
fireEvent.click(screen.getByText('Set up with QR'))
118+
119+
await waitFor(() => expect(screen.getByText('Telegram support is unavailable')).toBeTruthy())
120+
expect(mocks.getTelegramOnboardingStatus).not.toHaveBeenCalled()
77121
})
78122

79123
it('validates that allowed user IDs are numeric', async () => {

apps/desktop/src/app/messaging/telegram-qr-setup.tsx

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,28 @@ import { useCallback, useEffect, useRef, useState } from 'react'
44
import {
55
applyTelegramOnboarding,
66
cancelTelegramOnboarding,
7+
getActionStatus,
78
getTelegramOnboardingStatus,
89
restartGateway,
910
startTelegramOnboarding,
10-
type TelegramOnboardingStart
11+
type TelegramOnboardingStart,
12+
testMessagingPlatform
1113
} from '@/clio'
1214
import { Button } from '@/components/ui/button'
1315
import { Codicon } from '@/components/ui/codicon'
1416
import { Input } from '@/components/ui/input'
15-
import { notify, notifyError } from '@/store/notifications'
17+
import { notify } from '@/store/notifications'
1618

1719
const TELEGRAM_USER_ID_RE = /^\d{1,15}$/
1820
const POLL_MS = 2000
21+
const RESTART_POLL_MS = 1200
22+
const RESTART_POLL_ATTEMPTS = 65
23+
const CONNECTION_POLL_ATTEMPTS = 45
1924

20-
type Phase = 'applying' | 'idle' | 'ready' | 'starting' | 'waiting'
25+
type Phase = 'applying' | 'connecting' | 'idle' | 'ready' | 'restart_failed' | 'starting' | 'waiting'
2126

2227
interface TelegramQrSetupProps {
23-
/** Called after the bot token is saved + the gateway restart is requested. */
28+
/** Called only after the gateway reports Telegram connected. */
2429
onApplied?: () => void
2530
}
2631

@@ -143,6 +148,61 @@ export function TelegramQrSetup({ onApplied }: TelegramQrSetupProps) {
143148
setNewAllowedId('')
144149
}, [newAllowedId])
145150

151+
const restartAndVerify = useCallback(async () => {
152+
setPhase('connecting')
153+
setError('')
154+
155+
try {
156+
const started = await restartGateway()
157+
let completed = false
158+
159+
for (let attempt = 0; attempt < RESTART_POLL_ATTEMPTS; attempt += 1) {
160+
await new Promise(resolve => window.setTimeout(resolve, RESTART_POLL_MS))
161+
const status = await getActionStatus(started.name, 200)
162+
163+
if (status.running) {continue}
164+
completed = true
165+
166+
if (status.exit_code !== 0) {
167+
const detail = status.lines.filter(Boolean).slice(-6).join('\n')
168+
throw new Error(detail || `Gateway restart failed (exit ${String(status.exit_code)}).`)
169+
}
170+
171+
break
172+
}
173+
174+
if (!completed) {
175+
throw new Error('Gateway restart is taking too long. Check the gateway logs, then retry connection.')
176+
}
177+
178+
let lastMessage = 'Telegram has not reported a connection yet.'
179+
180+
for (let attempt = 0; attempt < CONNECTION_POLL_ATTEMPTS; attempt += 1) {
181+
const result = await testMessagingPlatform('telegram')
182+
lastMessage = result.message || lastMessage
183+
184+
if (result.ok) {
185+
reset()
186+
notify({ kind: 'success', message: 'Telegram bot connected', title: 'Telegram' })
187+
onApplied?.()
188+
189+
return
190+
}
191+
192+
if (result.state === 'fatal') {
193+
throw new Error(lastMessage)
194+
}
195+
196+
await new Promise(resolve => window.setTimeout(resolve, POLL_MS))
197+
}
198+
199+
throw new Error(`${lastMessage} Retry the gateway connection.`)
200+
} catch (err) {
201+
setPhase('restart_failed')
202+
setError(err instanceof Error ? err.message : String(err))
203+
}
204+
}, [onApplied, reset])
205+
146206
const apply = useCallback(async () => {
147207
if (!setup) {return}
148208

@@ -157,22 +217,12 @@ export function TelegramQrSetup({ onApplied }: TelegramQrSetupProps) {
157217

158218
try {
159219
await applyTelegramOnboarding(setup.pairing_id, { allowed_user_ids: allowedIds })
160-
reset()
161-
notify({ kind: 'success', message: 'Telegram bot saved', title: 'Telegram' })
162-
163-
try {
164-
await restartGateway()
165-
notify({ kind: 'success', message: 'Gateway restarting…', title: 'Telegram' })
166-
} catch (restartErr) {
167-
notifyError(restartErr, 'Telegram saved, but the gateway restart failed — restart it manually.')
168-
}
169-
170-
onApplied?.()
220+
await restartAndVerify()
171221
} catch (err) {
172222
setPhase('ready')
173223
setError(err instanceof Error ? err.message : String(err))
174224
}
175-
}, [setup, allowedIds, reset, onApplied])
225+
}, [setup, allowedIds, restartAndVerify])
176226

177227
return (
178228
<div
@@ -195,7 +245,26 @@ export function TelegramQrSetup({ onApplied }: TelegramQrSetupProps) {
195245
</Button>
196246
)}
197247

198-
{phase === 'starting' && <p className="mt-3 text-[0.8125rem] text-(--ui-text-secondary)">Starting…</p>}
248+
{phase === 'starting' && (
249+
<p className="mt-3 text-[0.8125rem] text-(--ui-text-secondary)">Preparing Telegram support…</p>
250+
)}
251+
252+
{phase === 'connecting' && (
253+
<p className="mt-3 text-[0.8125rem] text-(--ui-text-secondary)">
254+
Restarting the gateway and verifying Telegram…
255+
</p>
256+
)}
257+
258+
{phase === 'restart_failed' && (
259+
<div className="mt-3 flex items-center gap-2">
260+
<Button onClick={() => void restartAndVerify()} size="sm">
261+
Retry gateway connection
262+
</Button>
263+
<Button onClick={reset} size="sm" variant="ghost">
264+
Close
265+
</Button>
266+
</div>
267+
)}
199268

200269
{(phase === 'waiting' || phase === 'starting') && qrDataUrl && (
201270
<div className="mt-3 flex flex-col items-center gap-2">

apps/desktop/src/clio.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import type {
4141
} from '@/types/clio'
4242

4343
const DEFAULT_GATEWAY_REQUEST_TIMEOUT_MS = 30_000
44+
const PLATFORM_PROVISION_TIMEOUT_MS = 330_000
4445

4546
export type {
4647
ActionResponse,
@@ -455,7 +456,8 @@ export function updateMessagingPlatform(
455456
return window.clioDesktop.api<{ ok: boolean; platform: string }>({
456457
path: `/api/messaging/platforms/${encodeURIComponent(platformId)}`,
457458
method: 'PUT',
458-
body
459+
body,
460+
timeoutMs: PLATFORM_PROVISION_TIMEOUT_MS
459461
})
460462
}
461463

@@ -756,7 +758,8 @@ export function startTelegramOnboarding(body: { bot_name?: string }): Promise<Te
756758
return window.clioDesktop.api<TelegramOnboardingStart>({
757759
body,
758760
method: 'POST',
759-
path: '/api/messaging/telegram/onboarding/start'
761+
path: '/api/messaging/telegram/onboarding/start',
762+
timeoutMs: PLATFORM_PROVISION_TIMEOUT_MS
760763
})
761764
}
762765

@@ -773,7 +776,8 @@ export function applyTelegramOnboarding(
773776
return window.clioDesktop.api<TelegramOnboardingApplyResult>({
774777
body,
775778
method: 'POST',
776-
path: `/api/messaging/telegram/onboarding/${encodeURIComponent(pairingId)}/apply`
779+
path: `/api/messaging/telegram/onboarding/${encodeURIComponent(pairingId)}/apply`,
780+
timeoutMs: PLATFORM_PROVISION_TIMEOUT_MS
777781
})
778782
}
779783

clio_cli/gateway.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4383,6 +4383,18 @@ def _setup_standard_platform(platform: dict):
43834383
label = platform["label"]
43844384
token_var = platform["token_var"]
43854385

4386+
if platform.get("key") == "telegram":
4387+
from clio_cli.platform_dependencies import (
4388+
PlatformDependencyError,
4389+
ensure_platform_ready,
4390+
)
4391+
4392+
try:
4393+
ensure_platform_ready("telegram", prompt=True)
4394+
except PlatformDependencyError as exc:
4395+
print_error(f" {exc}")
4396+
return
4397+
43864398
print()
43874399
print(color(f" ─── {emoji} {label} Setup ───", Colors.CYAN))
43884400

0 commit comments

Comments
 (0)