mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 13:32:23 +00:00
fix(connections): drop account from state before purge so disconnect can't resurrect its webview (#4697)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'Связи',
|
||||
|
||||
Reference in New Issue
Block a user