Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
431 changes: 431 additions & 0 deletions app/src/components/settings/panels/CoreConnectionPanel.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* Tests for CoreConnectionPanel (GH-4396) — the first-class Settings surface
* that promotes cloud-mode remote-core config and adds a live status
* indicator. Covers: live status rendering per mode, the remote toggle
* revealing the URL/token form, and the save flow persisting + dispatching +
* restarting.
*/
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, test, vi } from 'vitest';

import { renderWithProviders } from '../../../../test/test-utils';

const hoisted = vi.hoisted(() => ({
testCoreRpcConnection: vi.fn(),
clearCoreRpcUrlCache: vi.fn(),
clearCoreRpcTokenCache: vi.fn(),
restartApp: vi.fn(),
}));

vi.mock('../../../../services/coreRpcClient', () => ({
testCoreRpcConnection: hoisted.testCoreRpcConnection,
clearCoreRpcUrlCache: hoisted.clearCoreRpcUrlCache,
clearCoreRpcTokenCache: hoisted.clearCoreRpcTokenCache,
}));

vi.mock('../../../../utils/tauriCommands/core', () => ({
restartApp: hoisted.restartApp,
}));

function okResponse() {
return { ok: true, status: 200, json: async () => ({ jsonrpc: '2.0', id: 1, result: {} }) };
}

async function importPanel() {
const mod = await import('../CoreConnectionPanel');
return mod.default;
}

describe('CoreConnectionPanel', () => {
beforeEach(() => {
vi.resetModules();
hoisted.testCoreRpcConnection.mockReset();
hoisted.clearCoreRpcUrlCache.mockReset();
hoisted.clearCoreRpcTokenCache.mockReset();
hoisted.restartApp.mockReset();
hoisted.restartApp.mockResolvedValue(undefined);
localStorage.clear();
});

test('local mode shows the local connected status once the live check passes', async () => {
hoisted.testCoreRpcConnection.mockResolvedValue(okResponse());
const Panel = await importPanel();
renderWithProviders(<Panel />, { preloadedState: { coreMode: { mode: { kind: 'local' } } } });

await waitFor(() => expect(screen.getByText('Connected to local core')).toBeInTheDocument());
// Remote toggle is off in local mode → no URL field.
expect(screen.queryByLabelText(/Runtime URL/i)).not.toBeInTheDocument();
});

test('cloud mode surfaces the remote URL and remote connected status', async () => {
hoisted.testCoreRpcConnection.mockResolvedValue(okResponse());
const Panel = await importPanel();
renderWithProviders(<Panel />, {
preloadedState: {
coreMode: {
mode: { kind: 'cloud', url: 'https://core.example.com/rpc', token: 'tok-123456' },
},
},
});

await waitFor(() =>
expect(screen.getByText('Connected to remote core')).toBeInTheDocument()
);
// Toggle on → the URL field is pre-filled with the persisted value.
expect(screen.getByDisplayValue('https://core.example.com/rpc')).toBeInTheDocument();
});

test('unreachable core surfaces the failure status', async () => {
hoisted.testCoreRpcConnection.mockRejectedValue(new Error('boom'));
const Panel = await importPanel();
renderWithProviders(<Panel />, { preloadedState: { coreMode: { mode: { kind: 'local' } } } });

await waitFor(() =>
expect(screen.getByText(/Cannot reach the core/i)).toBeInTheDocument()
);
});

test('switching to remote core persists, dispatches, and restarts', async () => {
hoisted.testCoreRpcConnection.mockResolvedValue(okResponse());
const Panel = await importPanel();
const { store } = renderWithProviders(<Panel />, {
preloadedState: { coreMode: { mode: { kind: 'local' } } },
});

await waitFor(() => expect(screen.getByText('Connected to local core')).toBeInTheDocument());

// Flip the remote toggle on to reveal the form.
fireEvent.click(screen.getByTestId('core-use-remote-toggle'));

fireEvent.change(screen.getByLabelText(/Runtime URL/i), {
target: { value: 'https://core.example.com/rpc' },
});
fireEvent.change(screen.getByLabelText(/Auth Token/i), {
target: { value: 'remote-token-xyz' },
});

fireEvent.click(screen.getByTestId('core-save-btn'));

await waitFor(() => expect(hoisted.restartApp).toHaveBeenCalledTimes(1));

// Redux is now in cloud mode with the typed URL + token.
const mode = store.getState().coreMode.mode as {
kind: string;
url?: string;
token?: string;
};
expect(mode.kind).toBe('cloud');
expect(mode.url).toBe('https://core.example.com/rpc');
expect(mode.token).toBe('remote-token-xyz');

// Persisted synchronously to localStorage (mirrors the cloud-mode picker).
expect(localStorage.getItem('openhuman_core_mode')).toBe('cloud');
expect(localStorage.getItem('openhuman_core_rpc_url')).toBe('https://core.example.com/rpc');
expect(localStorage.getItem('openhuman_core_rpc_token')).toBe('remote-token-xyz');

// Caches cleared so the new endpoint takes effect on restart.
expect(hoisted.clearCoreRpcUrlCache).toHaveBeenCalled();
expect(hoisted.clearCoreRpcTokenCache).toHaveBeenCalled();
});
});
4 changes: 4 additions & 0 deletions app/src/components/settings/settingsRouteElements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import AutocompleteDebugPanel from './panels/AutocompleteDebugPanel';
import AutocompletePanel from './panels/AutocompletePanel';
import BillingPanel from './panels/BillingPanel';
import CompanionPanel from './panels/CompanionPanel';
import CoreConnectionPanel from './panels/CoreConnectionPanel';
import ComposioTriagePanel from './panels/ComposioTriagePanel';
import CronJobsPanel from './panels/CronJobsPanel';
import DesktopAgentPanel from './panels/DesktopAgentPanel';
Expand Down Expand Up @@ -154,6 +155,9 @@ export function settingsRouteElements(): ReactNode {
<Route path="autocomplete" element={wrapSettingsPage(<AutocompletePanel />)} />

{/* ── System ──────────────────────────────────────────────── */}
{/* Core connection — promotes cloud-mode remote-core config into a
first-class setting with a live status indicator (GH-4396). */}
<Route path="core" element={wrapSettingsPage(<CoreConnectionPanel />)} />
<Route path="keyboard-shortcuts" element={wrapSettingsPage(<KeyboardShortcutsPanel />)} />
<Route path="developer-options" element={wrapSettingsPage(<DeveloperOptionsPanel />)} />
<Route path="token-usage" element={wrapSettingsPage(<TokenUsagePanel />)} />
Expand Down
24 changes: 24 additions & 0 deletions app/src/components/settings/settingsRouteRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,30 @@ export const SETTINGS_ROUTE_REGISTRY: SettingsRegistryEntry[] = [
navGroup: 'general',
navOrder: 98,
},
{
// Core connection — promotes cloud-mode remote-core config (persisted
// RPC URL + token) into a first-class setting plus a live status
// indicator (GH-4396). Sits just above About in General.
id: 'core',
titleKey: 'settings.core.title',
descriptionKey: 'settings.core.menuDesc',
section: 'home',
searchKeywords: [
'core',
'remote',
'rpc',
'url',
'token',
'cloud',
'local',
'connection',
'server',
'attach',
'self-hosted',
],
navGroup: 'general',
navOrder: 97,
},
{
id: 'about',
titleKey: 'settings.about',
Expand Down
13 changes: 13 additions & 0 deletions app/src/lib/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ import type { TranslationMap } from './types';
// Arabic (العربية) translations. Keys mirror en.ts; missing/
// English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = {
// Core connection panel (GH-4396)
'settings.core.title': 'اتصال النواة',
'settings.core.menuDesc': 'استخدم النواة المحلية المدمجة أو اتصل بنواة بعيدة.',
'settings.core.useRemoteToggle': 'استخدام نواة بعيدة',
'settings.core.useRemoteToggleDesc': 'الاتصال بنواة بعيدة عبر HTTP بدلاً من النواة المحلية المدمجة.',
'settings.core.statusConnectedRemote': 'متصل بالنواة البعيدة',
'settings.core.statusConnectedLocal': 'متصل بالنواة المحلية',
'settings.core.statusChecking': 'جارٍ التحقق من الاتصال…',
'settings.core.statusAuthFailed': 'يمكن الوصول إليه، لكن تم رفض الرمز',
'settings.core.statusUnreachable': 'تعذّر الوصول إلى النواة',
'settings.core.recheck': 'إعادة التحقق',
'settings.core.save': 'حفظ وإعادة التشغيل',
'settings.core.applyRestartNote': 'يؤدي الحفظ إلى إعادة تشغيل OpenHuman لإعادة الاتصال.',
// Cross-host vault (#4278)
'crossHostVault.title': 'الخزنة موجودة على مضيف النواة.',
'crossHostVault.message':
Expand Down
13 changes: 13 additions & 0 deletions app/src/lib/i18n/bn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ import type { TranslationMap } from './types';
// Bengali (বাংলা) translations. Keys mirror en.ts; missing/
// English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = {
// Core connection panel (GH-4396)
'settings.core.title': 'কোর সংযোগ',
'settings.core.menuDesc': 'অন্তর্নির্মিত স্থানীয় কোর ব্যবহার করুন বা একটি রিমোট কোরে সংযোগ করুন।',
'settings.core.useRemoteToggle': 'রিমোট কোর ব্যবহার করুন',
'settings.core.useRemoteToggleDesc': 'অন্তর্নির্মিত স্থানীয় কোরের পরিবর্তে HTTP-এর মাধ্যমে একটি রিমোট কোরে সংযোগ করুন।',
'settings.core.statusConnectedRemote': 'রিমোট কোরে সংযুক্ত',
'settings.core.statusConnectedLocal': 'স্থানীয় কোরে সংযুক্ত',
'settings.core.statusChecking': 'সংযোগ পরীক্ষা করা হচ্ছে…',
'settings.core.statusAuthFailed': 'পৌঁছানো যায়, তবে টোকেন প্রত্যাখ্যাত হয়েছে',
'settings.core.statusUnreachable': 'কোরে পৌঁছানো যাচ্ছে না',
'settings.core.recheck': 'পুনরায় পরীক্ষা করুন',
'settings.core.save': 'সংরক্ষণ করে পুনরায় চালু করুন',
'settings.core.applyRestartNote': 'সংরক্ষণ করলে পুনরায় সংযোগের জন্য OpenHuman পুনরায় চালু হয়।',
// Cross-host vault (#4278)
'crossHostVault.title': 'ভল্টটি কোর হোস্টে রয়েছে।',
'crossHostVault.message':
Expand Down
13 changes: 13 additions & 0 deletions app/src/lib/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ import type { TranslationMap } from './types';
// German (Deutsch) translations. Keys mirror en.ts; missing/
// English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = {
// Core connection panel (GH-4396)
'settings.core.title': 'Core-Verbindung',
'settings.core.menuDesc': 'Den integrierten lokalen Core verwenden oder mit einem Remote-Core verbinden.',
'settings.core.useRemoteToggle': 'Remote-Core verwenden',
'settings.core.useRemoteToggleDesc': 'Über HTTP mit einem Remote-Core verbinden statt den integrierten lokalen Core zu nutzen.',
'settings.core.statusConnectedRemote': 'Mit Remote-Core verbunden',
'settings.core.statusConnectedLocal': 'Mit lokalem Core verbunden',
'settings.core.statusChecking': 'Verbindung wird geprüft…',
'settings.core.statusAuthFailed': 'Erreichbar, aber das Token wurde abgelehnt',
'settings.core.statusUnreachable': 'Core nicht erreichbar',
'settings.core.recheck': 'Erneut prüfen',
'settings.core.save': 'Speichern & neu starten',
'settings.core.applyRestartNote': 'Beim Speichern startet OpenHuman neu, um die Verbindung herzustellen.',
// Cross-host vault (#4278)
'crossHostVault.title': 'Der Vault liegt auf dem Core-Host.',
'crossHostVault.message':
Expand Down
14 changes: 14 additions & 0 deletions app/src/lib/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1479,6 +1479,20 @@ const en: TranslationMap = {
'Spawned in-process by the Tauri shell on app launch. The port is chosen at startup, so this URL changes between launches.',
'settings.about.connectionHelperCloud':
'Connected to a remote core. Change this in BootCheck or the cloud mode picker.',
// Core connection panel (GH-4396)
'settings.core.title': 'Core connection',
'settings.core.menuDesc': 'Use the built-in local core or connect to a remote core.',
'settings.core.useRemoteToggle': 'Use remote core',
'settings.core.useRemoteToggleDesc':
'Connect to a remote core over HTTP instead of the built-in local core.',
'settings.core.statusConnectedRemote': 'Connected to remote core',
'settings.core.statusConnectedLocal': 'Connected to local core',
'settings.core.statusChecking': 'Checking connection…',
'settings.core.statusAuthFailed': 'Reachable, but the token was rejected',
'settings.core.statusUnreachable': 'Cannot reach the core',
'settings.core.recheck': 'Recheck',
'settings.core.save': 'Save & restart',
'settings.core.applyRestartNote': 'Saving restarts OpenHuman to reconnect.',
'settings.heartbeat.title': 'Heartbeat & loops',
'settings.usage.title': 'Usage & Limits',
'settings.usage.menuDesc': 'Costs, token usage, budgets, and background activity',
Expand Down
13 changes: 13 additions & 0 deletions app/src/lib/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ import type { TranslationMap } from './types';
// Spanish (Español) translations. Keys mirror en.ts; missing/
// English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = {
// Core connection panel (GH-4396)
'settings.core.title': 'Conexión del core',
'settings.core.menuDesc': 'Usa el core local integrado o conéctate a un core remoto.',
'settings.core.useRemoteToggle': 'Usar core remoto',
'settings.core.useRemoteToggleDesc': 'Conéctate a un core remoto por HTTP en lugar del core local integrado.',
'settings.core.statusConnectedRemote': 'Conectado al core remoto',
'settings.core.statusConnectedLocal': 'Conectado al core local',
'settings.core.statusChecking': 'Comprobando la conexión…',
'settings.core.statusAuthFailed': 'Accesible, pero el token fue rechazado',
'settings.core.statusUnreachable': 'No se puede conectar con el core',
'settings.core.recheck': 'Volver a comprobar',
'settings.core.save': 'Guardar y reiniciar',
'settings.core.applyRestartNote': 'Al guardar, OpenHuman se reinicia para volver a conectarse.',
// Cross-host vault (#4278)
'crossHostVault.title': 'El vault está en el host del core.',
'crossHostVault.message':
Expand Down
13 changes: 13 additions & 0 deletions app/src/lib/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ import type { TranslationMap } from './types';
// French (Français) translations. Keys mirror en.ts; missing/
// English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = {
// Core connection panel (GH-4396)
'settings.core.title': 'Connexion au core',
'settings.core.menuDesc': 'Utilisez le core local intégré ou connectez-vous à un core distant.',
'settings.core.useRemoteToggle': 'Utiliser un core distant',
'settings.core.useRemoteToggleDesc': 'Se connecter à un core distant via HTTP au lieu du core local intégré.',
'settings.core.statusConnectedRemote': 'Connecté au core distant',
'settings.core.statusConnectedLocal': 'Connecté au core local',
'settings.core.statusChecking': 'Vérification de la connexion…',
'settings.core.statusAuthFailed': 'Accessible, mais le jeton a été refusé',
'settings.core.statusUnreachable': 'Core inaccessible',
'settings.core.recheck': 'Revérifier',
'settings.core.save': 'Enregistrer et redémarrer',
'settings.core.applyRestartNote': 'Enregistrer redémarre OpenHuman pour se reconnecter.',
// Cross-host vault (#4278)
'crossHostVault.title': "Le coffre se trouve sur l'hôte du cœur.",
'crossHostVault.message':
Expand Down
13 changes: 13 additions & 0 deletions app/src/lib/i18n/hi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ import type { TranslationMap } from './types';
// Hindi (हिन्दी) translations. Keys mirror en.ts; missing/
// English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = {
// Core connection panel (GH-4396)
'settings.core.title': 'कोर कनेक्शन',
'settings.core.menuDesc': 'अंतर्निहित स्थानीय कोर का उपयोग करें या किसी रिमोट कोर से कनेक्ट करें।',
'settings.core.useRemoteToggle': 'रिमोट कोर का उपयोग करें',
'settings.core.useRemoteToggleDesc': 'अंतर्निहित स्थानीय कोर के बजाय HTTP के माध्यम से किसी रिमोट कोर से कनेक्ट करें।',
'settings.core.statusConnectedRemote': 'रिमोट कोर से कनेक्ट हो गया',
'settings.core.statusConnectedLocal': 'स्थानीय कोर से कनेक्ट हो गया',
'settings.core.statusChecking': 'कनेक्शन जाँचा जा रहा है…',
'settings.core.statusAuthFailed': 'पहुँच योग्य, लेकिन टोकन अस्वीकृत हो गया',
'settings.core.statusUnreachable': 'कोर तक नहीं पहुँचा जा सकता',
'settings.core.recheck': 'फिर से जाँचें',
'settings.core.save': 'सहेजें और पुनरारंभ करें',
'settings.core.applyRestartNote': 'सहेजने पर OpenHuman फिर से कनेक्ट होने के लिए पुनरारंभ होता है।',
// Cross-host vault (#4278)
'crossHostVault.title': 'वॉल्ट कोर होस्ट पर है।',
'crossHostVault.message':
Expand Down
13 changes: 13 additions & 0 deletions app/src/lib/i18n/id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ import type { TranslationMap } from './types';
// Indonesian (Bahasa Indonesia) translations. Keys mirror en.ts; missing/
// English-identical values fall back to English via I18nContext.resolveEn().
const messages: TranslationMap = {
// Core connection panel (GH-4396)
'settings.core.title': 'Koneksi core',
'settings.core.menuDesc': 'Gunakan core lokal bawaan atau hubungkan ke core jarak jauh.',
'settings.core.useRemoteToggle': 'Gunakan core jarak jauh',
'settings.core.useRemoteToggleDesc': 'Hubungkan ke core jarak jauh melalui HTTP alih-alih core lokal bawaan.',
'settings.core.statusConnectedRemote': 'Terhubung ke core jarak jauh',
'settings.core.statusConnectedLocal': 'Terhubung ke core lokal',
'settings.core.statusChecking': 'Memeriksa koneksi…',
'settings.core.statusAuthFailed': 'Dapat dijangkau, tetapi token ditolak',
'settings.core.statusUnreachable': 'Tidak dapat menjangkau core',
'settings.core.recheck': 'Periksa ulang',
'settings.core.save': 'Simpan & mulai ulang',
'settings.core.applyRestartNote': 'Menyimpan akan memulai ulang OpenHuman untuk menyambung kembali.',
// Cross-host vault (#4278)
'crossHostVault.title': 'Vault berada di host core.',
'crossHostVault.message':
Expand Down
Loading
Loading