fix(connections): drop account from state before purge so disconnect can't resurrect its webview (#4697)

This commit is contained in:
Mega Mind
2026-07-08 17:52:10 +05:30
committed by GitHub
parent 77786e50c5
commit 8fc9a13da7
9 changed files with 81 additions and 12 deletions
@@ -2,6 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { purgeWebviewAccount } from '../../../services/webviewAccountService';
import SidebarAppRail from './SidebarAppRail';
const mockNavigate = vi.fn();
@@ -96,6 +97,57 @@ describe('SidebarAppRail', () => {
expect(addButton).not.toHaveTextContent('accounts.addApps');
expect(addButton).toHaveAttribute('aria-label', 'accounts.addApps');
});
it('drops the account from state synchronously on disconnect, before the async purge (#4695)', () => {
setAccounts(['acct-whatsapp']);
// Hold the purge pending so we can prove `removeAccount` was dispatched
// *before* the purge (which triggers the app re-mount) resolves. The old
// order (await purge → removeAccount) let the re-mount re-open the
// just-purged webview because the account was still in the store.
let resolvePurge: () => void = () => {};
vi.mocked(purgeWebviewAccount).mockReturnValue(
new Promise<void>(res => {
resolvePurge = res;
})
);
renderRail('/chat');
fireEvent.contextMenu(screen.getByRole('button', { name: 'WhatsApp' }));
fireEvent.click(screen.getByText('accounts.disconnect'));
// removeAccount is dispatched while the purge is still pending.
expect(mockDispatch).toHaveBeenCalledWith({
type: 'accounts/removeAccount',
payload: { accountId: 'acct-whatsapp' },
});
expect(purgeWebviewAccount).toHaveBeenCalledWith('acct-whatsapp');
resolvePurge();
});
it('still drops the account and swallows the error when the purge rejects (#4695)', async () => {
setAccounts(['acct-whatsapp']);
// A purge failure must not leave the user with a zombie icon: the account is
// already removed from state before the await, and the rejection is caught
// and logged (not surfaced) so the disconnect handler resolves cleanly.
vi.mocked(purgeWebviewAccount).mockRejectedValue(new Error('purge failed'));
renderRail('/chat');
fireEvent.contextMenu(screen.getByRole('button', { name: 'WhatsApp' }));
fireEvent.click(screen.getByText('accounts.disconnect'));
expect(mockDispatch).toHaveBeenCalledWith({
type: 'accounts/removeAccount',
payload: { accountId: 'acct-whatsapp' },
});
expect(purgeWebviewAccount).toHaveBeenCalledWith('acct-whatsapp');
// Flush microtasks so the awaited purge rejection reaches the handler's
// catch (which logs and swallows it) — the disconnect must not throw.
await new Promise(resolve => setTimeout(resolve, 0));
});
});
function renderRail(route: string) {
@@ -196,14 +196,23 @@ export default function SidebarAppRail() {
account_status: account.status ?? 'unknown',
});
}
// Drop the account from state FIRST, synchronously, before the async purge
// (#4695). `purgeWebviewAccount` tears down the provider's backend scanners,
// which triggers a full app-state re-fetch and UI re-mount. If the account
// were still in the store at that moment, the re-mount would remount
// `WebviewHost` for it and re-open (reveal) the just-purged provider webview
// — a fresh, logged-out CEF child-view pinned over the content area,
// swallowing all input. Removing it first unmounts the host (whose cleanup
// hides the child-view) and guarantees no re-open ever targets a
// disconnected account.
dispatch(removeAccount({ accountId }));
try {
await purgeWebviewAccount(accountId);
} catch {
// Purge failures are already logged by the service; still drop the
// account from the UI so the user isn't stuck with a zombie icon.
debug('purge failed for %s — dropping from UI anyway', accountId);
// Purge failures are already logged by the service; the account is
// already gone from the UI so the user isn't stuck with a zombie icon.
debug('purge failed for %s — already dropped from UI', accountId);
}
dispatch(removeAccount({ accountId }));
};
// Close the context menu on Escape or any outside click.
+2 -1
View File
@@ -205,7 +205,8 @@ const messages: TranslationMap = {
'orchPage.discover.notDiscoverableGuide':
'Registriere ein @handle, damit andere Agenten dich finden und dir schreiben können.',
'orchPage.discover.linkTitle': 'Neuen Agenten verknüpfen',
'orchPage.discover.linkDescription': 'Füge eine Agenten-ID ein, um eine Verbindungsanfrage zu senden.',
'orchPage.discover.linkDescription':
'Füge eine Agenten-ID ein, um eine Verbindungsanfrage zu senden.',
'orchPage.discover.noRequests': 'Keine eingehenden Anfragen.',
'orchPage.usage.nav': 'Nutzung',
'orchPage.usage.connections': 'Verbindungen',
+2 -1
View File
@@ -202,7 +202,8 @@ const messages: TranslationMap = {
'orchPage.discover.notDiscoverableGuide':
'Registra un @handle para que otros agentes puedan encontrarte y escribirte.',
'orchPage.discover.linkTitle': 'Vincular un nuevo agente',
'orchPage.discover.linkDescription': 'Pega un ID de agente para enviar una solicitud de conexión.',
'orchPage.discover.linkDescription':
'Pega un ID de agente para enviar una solicitud de conexión.',
'orchPage.discover.noRequests': 'No hay solicitudes entrantes.',
'orchPage.usage.nav': 'Uso',
'orchPage.usage.connections': 'Conexiones',
+4 -2
View File
@@ -190,7 +190,8 @@ const messages: TranslationMap = {
'orchPage.agent.description': "Discutez avec l'agent principal et observez son subconscient",
'orchPage.connections.nav': 'Connexions',
'orchPage.connections.title': 'Agents liés',
'orchPage.connections.description': 'Pairs avec lesquels votre agent principal peut se coordonner',
'orchPage.connections.description':
'Pairs avec lesquels votre agent principal peut se coordonner',
'orchPage.connections.empty': 'Aucune connexion pour le moment.',
'orchPage.connections.emptyCta': 'Ajouter une connexion',
'orchPage.connections.statContacts': 'Connexions',
@@ -202,7 +203,8 @@ const messages: TranslationMap = {
'orchPage.discover.notDiscoverableGuide':
"Enregistrez un @handle pour que d'autres agents puissent vous trouver et vous écrire.",
'orchPage.discover.linkTitle': 'Lier un nouvel agent',
'orchPage.discover.linkDescription': "Collez un ID d'agent pour envoyer une demande de connexion.",
'orchPage.discover.linkDescription':
"Collez un ID d'agent pour envoyer une demande de connexion.",
'orchPage.discover.noRequests': 'Aucune demande entrante.',
'orchPage.usage.nav': 'Utilisation',
'orchPage.usage.connections': 'Connexions',
+2 -1
View File
@@ -202,7 +202,8 @@ const messages: TranslationMap = {
'orchPage.discover.notDiscoverableGuide':
'Registra un @handle così altri agenti possono trovarti e scriverti.',
'orchPage.discover.linkTitle': 'Collega un nuovo agente',
'orchPage.discover.linkDescription': 'Incolla un ID agente per inviare una richiesta di connessione.',
'orchPage.discover.linkDescription':
'Incolla un ID agente per inviare una richiesta di connessione.',
'orchPage.discover.noRequests': 'Nessuna richiesta in arrivo.',
'orchPage.usage.nav': 'Utilizzo',
'orchPage.usage.connections': 'Connessioni',
+2 -1
View File
@@ -204,7 +204,8 @@ const messages: TranslationMap = {
'orchPage.discover.notDiscoverableGuide':
'Zarejestruj @handle, aby inni agenci mogli Cię znaleźć i napisać do Ciebie.',
'orchPage.discover.linkTitle': 'Połącz nowego agenta',
'orchPage.discover.linkDescription': 'Wklej identyfikator agenta, aby wysłać prośbę o połączenie.',
'orchPage.discover.linkDescription':
'Wklej identyfikator agenta, aby wysłać prośbę o połączenie.',
'orchPage.discover.noRequests': 'Brak przychodzących próśb.',
'orchPage.usage.nav': 'Wykorzystanie',
'orchPage.usage.connections': 'Połączenia',
+2 -1
View File
@@ -201,7 +201,8 @@ const messages: TranslationMap = {
'orchPage.discover.notDiscoverableGuide':
'Registre um @handle para que outros agentes possam encontrá-lo e enviar mensagens.',
'orchPage.discover.linkTitle': 'Vincular um novo agente',
'orchPage.discover.linkDescription': 'Cole um ID de agente para enviar uma solicitação de conexão.',
'orchPage.discover.linkDescription':
'Cole um ID de agente para enviar uma solicitação de conexão.',
'orchPage.discover.noRequests': 'Nenhuma solicitação recebida.',
'orchPage.usage.nav': 'Uso',
'orchPage.usage.connections': 'Conexões',
+2 -1
View File
@@ -192,7 +192,8 @@ const messages: TranslationMap = {
'orchPage.agent.description': 'Общайтесь с главным агентом и наблюдайте за его подсознанием',
'orchPage.connections.nav': 'Связи',
'orchPage.connections.title': 'Связанные агенты',
'orchPage.connections.description': 'Партнёры, с которыми может координироваться ваш главный агент',
'orchPage.connections.description':
'Партнёры, с которыми может координироваться ваш главный агент',
'orchPage.connections.empty': 'Пока нет связей.',
'orchPage.connections.emptyCta': 'Добавить связь',
'orchPage.connections.statContacts': 'Связи',