From 2c761192396c0334673001a043f456f40ed8a3da Mon Sep 17 00:00:00 2001
From: AbigailDeng <108705114+AbigailDeng@users.noreply.github.com>
Date: Wed, 16 Sep 2026 16:04:54 +0800
Subject: [PATCH] fix(console): Confirm Telegram channel creation automatically
---
.../docs/features/2026-09-14-channel-pages.md | 27 ++-
.../src/locales/channelMessages.en-US.ts | 10 +-
.../src/locales/channelMessages.zh-CN.ts | 9 +-
.../channels/TelegramConnectionPage.test.tsx | 189 ++++++++++++++----
.../channels/TelegramConnectionPage.tsx | 99 ++++-----
.../channels/connectionStyles.ts | 2 -
6 files changed, 228 insertions(+), 108 deletions(-)
diff --git a/apps/aevatar-console-web/docs/features/2026-09-14-channel-pages.md b/apps/aevatar-console-web/docs/features/2026-09-14-channel-pages.md
index ea448aa27..c2f486072 100644
--- a/apps/aevatar-console-web/docs/features/2026-09-14-channel-pages.md
+++ b/apps/aevatar-console-web/docs/features/2026-09-14-channel-pages.md
@@ -10,8 +10,9 @@ links to the editor documented in
[Channel Runtime Editor](../2026-09-15-channel-runtime-editor.md); its edit form
now exposes only Label and Skill name, preserving existing service authorization.
-Channel refresh is explicitly user driven. Do not add background polling or
-refresh on focus/reconnection to these pages.
+Channel browsing refresh is explicitly user driven, with no refresh on
+focus/reconnection. The Telegram creation form alone automatically confirms
+its accepted registration within a bounded window, as described below.
Manual Refresh keeps the connected table mounted and displays the shared
loading overlay over that table while the registration list, bot names, and
@@ -131,16 +132,24 @@ Registration uses existing `POST /api/channels/registrations` with
`platform`, `bot_token`, `label`, `default_skill_name`, `webhook_base_url`, and
the explicit selection above. A `202`/`status: accepted` response must contain
the registration ID. After admission, the form clears its token, retains only
-the safe returned ID, and explicitly refreshes registrations once. A successful
+the safe returned ID, and immediately refreshes registrations. A successful
fresh read must contain that exact ID, route scope and Telegram platform before
the success toast and navigation to the newly created channel's existing detail
page. The form does not return to the collection automatically or infer success
-from cached rows after a failed read. Until confirmation, it preserves the
-submitted choices and shows a pending state with Check again. This action only
-repeats the GET, never the create POST; failed confirmation reads show a safe
-error toast and remain retryable. No polling, window-focus refresh, or reconnect
-refresh runs. Inbound activity continues to use its real status, independently
-of registration creation.
+from cached rows after a failed read. Nyx provisioning completes before admission,
+but the local registration list becomes visible asynchronously; a single read
+can miss a successfully created channel, including a token-only submission.
+Until confirmation, the form preserves submitted choices and shows Connecting
+in the existing submit button. There is no separate pending panel or Check again
+button. An absent row or failed read automatically retries only the GET after
+one second, without overlapping requests or repeating the POST. The entire
+confirmation window, including stalled reads, is limited to 30 seconds.
+Completion or unmount stops the timers and ignores late responses. On timeout,
+the disabled submit button shows Connection pending, one warning explains the
+delay, and Back to channels remains available. No success is claimed and the
+accepted registration cannot be submitted again. Window focus and reconnection
+do not start confirmation. Inbound activity continues to use its real status,
+independently of registration creation.
Registration failures, including
504 and network errors, show a shared error toast and restore editable fields and Connect
Telegram. Inputs and still-authorized selections are preserved for an explicit manual
diff --git a/apps/aevatar-console-web/src/locales/channelMessages.en-US.ts b/apps/aevatar-console-web/src/locales/channelMessages.en-US.ts
index e660df8f4..a7b221517 100644
--- a/apps/aevatar-console-web/src/locales/channelMessages.en-US.ts
+++ b/apps/aevatar-console-web/src/locales/channelMessages.en-US.ts
@@ -155,13 +155,11 @@ export default {
'Only selected services will be available to this bot.',
'channels.connect.selectionChanged':
'Some selected services are no longer available. Deselect them before connecting.',
- 'channels.connect.pending':
- 'Your request was submitted. Check again to open your channel when it is ready.',
- 'channels.connect.check': 'Check again',
+ 'channels.connect.connecting': 'Connecting...',
+ 'channels.connect.awaitingConfirmation': 'Connection pending',
+ 'channels.connect.delayed':
+ 'Your request was submitted, but confirmation is taking longer than expected. You can return to Channels.',
'channels.connect.success': 'Telegram channel created.',
- 'channels.connect.error.confirmation':
- 'Could not check your channel. Please check again.',
- 'channels.connect.submitted': 'Request submitted',
'channels.connect.error.token':
'Check the bot token from BotFather and try again.',
'channels.connect.error.botName':
diff --git a/apps/aevatar-console-web/src/locales/channelMessages.zh-CN.ts b/apps/aevatar-console-web/src/locales/channelMessages.zh-CN.ts
index 67c28c2ca..d4e69a4de 100644
--- a/apps/aevatar-console-web/src/locales/channelMessages.zh-CN.ts
+++ b/apps/aevatar-console-web/src/locales/channelMessages.zh-CN.ts
@@ -133,12 +133,11 @@ export default {
'channels.connect.selectionHelp': '此机器人只能使用选中的服务。',
'channels.connect.selectionChanged':
'部分选中服务已不可用,请取消勾选后再连接。',
- 'channels.connect.pending':
- '请求已提交。可点击再次检查,准备完成后将打开你的渠道详情。',
- 'channels.connect.check': '再次检查',
+ 'channels.connect.connecting': '连接中...',
+ 'channels.connect.awaitingConfirmation': '连接待确认',
+ 'channels.connect.delayed':
+ '请求已提交,但确认所需时间较长。你可以返回渠道列表。',
'channels.connect.success': 'Telegram 渠道已创建。',
- 'channels.connect.error.confirmation': '暂时无法查看渠道,请再次检查。',
- 'channels.connect.submitted': '请求已提交',
'channels.connect.error.token': '请检查 BotFather 提供的令牌后重试。',
'channels.connect.error.botName':
'暂时无法获取 Telegram 机器人名称,请重试。',
diff --git a/apps/aevatar-console-web/src/pages/workflow-activity-vnext/channels/TelegramConnectionPage.test.tsx b/apps/aevatar-console-web/src/pages/workflow-activity-vnext/channels/TelegramConnectionPage.test.tsx
index e5d9ff267..049228a41 100644
--- a/apps/aevatar-console-web/src/pages/workflow-activity-vnext/channels/TelegramConnectionPage.test.tsx
+++ b/apps/aevatar-console-web/src/pages/workflow-activity-vnext/channels/TelegramConnectionPage.test.tsx
@@ -83,25 +83,27 @@ afterEach(() => {
global.fetch = originalFetch;
});
-it('confirms the exact submitted channel with fresh reads and retries only GET without automatic refresh', async () => {
+it('automatically confirms a token-only submission after delayed, failed and unrelated reads without repeating creation', async () => {
jest.useFakeTimers();
try {
- let attempts = 0;
let reads = 0;
+ telegramFetch.mockResolvedValue(
+ response({
+ ok: true,
+ result: { is_bot: true, first_name: 'Token-only bot' },
+ }),
+ );
fetchMock.mockImplementation(async (input, init) => {
- if (init?.method === 'POST') {
- attempts += 1;
- return attempts === 1
- ? response({}, 504)
- : response(
- { status: 'accepted', registration_id: 'registration-new' },
- 202,
- );
- }
+ if (init?.method === 'POST')
+ return response(
+ { status: 'accepted', registration_id: 'registration-new' },
+ 202,
+ );
if (input === listPath) {
reads += 1;
if (reads === 1) return response({}, 503);
- if (reads === 2)
+ if (reads === 2) return response([]);
+ if (reads === 3)
return response([
{
...createdRegistration,
@@ -113,6 +115,7 @@ it('confirms the exact submitted channel with fresh reads and retries only GET w
id: 'registration-new',
platform: 'lark',
},
+ { ...createdRegistration, id: 'registration-new', owned: false },
createdRegistration,
]);
return response([{ ...createdRegistration, id: 'registration-new' }]);
@@ -138,48 +141,53 @@ it('confirms the exact submitted channel with fresh reads and retries only GET w
await screen.findByText(/No services are available/);
await idleAndReturn();
expect(fetchMock.mock.calls.map(([input]) => input)).toEqual([servicePath]);
- enterBotDetails();
- fireEvent.click(screen.getByRole('button', { name: 'Connect Telegram' }));
- await screen.findByText(
- 'Could not confirm the connection. Please try again.',
- );
- await idleAndReturn();
- expect(history.replace).not.toHaveBeenCalled();
- expect(attempts).toBe(1);
+ fireEvent.change(screen.getByLabelText(/^Bot token/), {
+ target: { value: telegramToken },
+ });
fireEvent.click(screen.getByRole('button', { name: 'Connect Telegram' }));
- await screen.findByText(
- 'Could not check your channel. Please check again.',
- );
+ await act(async () => jest.advanceTimersByTimeAsync(1));
+ expect(reads).toBe(1);
expect(history.replace).not.toHaveBeenCalled();
expect(
screen.queryByText('Telegram channel created.'),
).not.toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /Connecting/ })).toBeDisabled();
expect(
- screen.getByRole('button', { name: 'Request submitted' }),
- ).toBeDisabled();
+ screen.queryByRole('button', { name: 'Check again' }),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/Your request was submitted/),
+ ).not.toBeInTheDocument();
expect(screen.getByLabelText(/^Bot token/)).toHaveValue('');
const leaving = new Event('beforeunload', { cancelable: true });
window.dispatchEvent(leaving);
expect(leaving.defaultPrevented).toBe(false);
fireEvent.submit(screen.getByRole('form', { name: 'Connect Telegram' }));
- await idleAndReturn();
- expect(history.replace).not.toHaveBeenCalled();
- expect(attempts).toBe(2);
- expect(reads).toBe(1);
- fireEvent.click(screen.getByRole('button', { name: 'Check again' }));
- await waitFor(() =>
- expect(screen.getByRole('button', { name: 'Check again' })).toBeEnabled(),
- );
- await idleAndReturn();
+ await act(async () => jest.advanceTimersByTimeAsync(1000));
expect(reads).toBe(2);
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
expect(history.replace).not.toHaveBeenCalled();
- fireEvent.click(screen.getByRole('button', { name: 'Check again' }));
- await screen.findByText('Telegram channel created.');
+ await act(async () => jest.advanceTimersByTimeAsync(1000));
+ expect(reads).toBe(3);
+ expect(history.replace).not.toHaveBeenCalled();
+ await act(async () => jest.advanceTimersByTimeAsync(1000));
+ expect(screen.getByText('Telegram channel created.')).toBeInTheDocument();
expect(history.replace).toHaveBeenCalledWith(
'/scopes/scope-alpha/workflow-activity-vnext/channels/registration-new',
);
- expect(attempts).toBe(2);
- expect(reads).toBe(3);
+ const posts = fetchMock.mock.calls.filter(
+ ([, init]) => init?.method === 'POST',
+ );
+ expect(posts).toHaveLength(1);
+ expect(JSON.parse(String(posts[0][1]?.body))).toMatchObject({
+ bot_token: telegramToken,
+ label: expect.stringMatching(/^Token-only bot-[1-9]\d{5}$/),
+ default_skill_name: 'Token-only bot',
+ service_ids: [],
+ });
+ await idleAndReturn();
+ expect(reads).toBe(4);
+ expect(history.replace).toHaveBeenCalledTimes(1);
view.unmount();
} finally {
focusManager.setFocused(undefined);
@@ -191,6 +199,105 @@ it('confirms the exact submitted channel with fresh reads and retries only GET w
}
});
+it('bounds confirmation even when a read stalls and keeps the accepted form from being resubmitted', async () => {
+ jest.useFakeTimers();
+ try {
+ let resolveRead!: (value: Response) => void;
+ const pendingRead = new Promise
- {t( - 'channels.connect.pending', - 'Your request was submitted. Check again to open your channel when it is ready.', - )} -
- -