diff --git a/app/src/components/settings/panels/EmbeddingsPanel.tsx b/app/src/components/settings/panels/EmbeddingsPanel.tsx index 97bef7703..a0d09b1cb 100644 --- a/app/src/components/settings/panels/EmbeddingsPanel.tsx +++ b/app/src/components/settings/panels/EmbeddingsPanel.tsx @@ -8,6 +8,7 @@ import { useCallback, useEffect, useState } from 'react'; import { useT } from '../../../lib/i18n/I18nContext'; +import { useCoreState } from '../../../providers/CoreStateProvider'; import { clearEmbeddingsApiKey, type EmbeddingProviderEntry, @@ -18,6 +19,7 @@ import { testEmbeddingsConnection, updateEmbeddingsSettings, } from '../../../services/api/embeddingsApi'; +import { isLocalSessionToken } from '../../../utils/localSession'; import PanelPage from '../../layout/PanelPage'; import Button from '../../ui/Button'; import SettingsBackButton from '../components/SettingsBackButton'; @@ -38,6 +40,16 @@ type Status = | { kind: 'saved' } | { kind: 'error'; message: string }; +function isBackendSessionError(message: string | undefined): boolean { + const text = message ?? ''; + return ( + /no backend session/i.test(text) || + /SESSION_EXPIRED/i.test(text) || + /session expired/i.test(text) || + (/invalid token/i.test(text) && /(401|unauthorized)/i.test(text)) + ); +} + interface EmbeddingsPanelProps { embedded?: boolean; } @@ -45,9 +57,12 @@ interface EmbeddingsPanelProps { const EmbeddingsPanel = ({ embedded = false }: EmbeddingsPanelProps = {}) => { const { t } = useT(); const { navigateBack } = useSettingsNavigation(); + const { snapshot, clearSession } = useCoreState(); + const isLocalSession = isLocalSessionToken(snapshot.sessionToken); const [settings, setSettings] = useState(null); const [status, setStatus] = useState({ kind: 'loading' }); + const [managedSessionMissing, setManagedSessionMissing] = useState(false); // Setup popup state const [setupProvider, setSetupProvider] = useState(null); @@ -110,10 +125,22 @@ const EmbeddingsPanel = ({ embedded = false }: EmbeddingsPanelProps = {}) => { const currentModels = currentEntry?.models ?? []; const currentModel = currentModels.find(m => m.id === settings.model) ?? currentModels[0]; const allowedDims = currentModel?.allowed_dimensions ?? []; + const managedLoginMessage = t('settings.embeddings.managedLoginRequired'); + const managedRequiresLogin = isLocalSession && selectedProvider === 'managed'; + const showManagedLoginPrompt = + (selectedProvider === 'managed' && (managedRequiresLogin || managedSessionMissing)) || + (isLocalSession && managedSessionMissing); function handleProviderClick(entry: EmbeddingProviderEntry) { + if (entry.slug !== 'managed') setManagedSessionMissing(false); if (entry.slug === selectedProvider) return; + if (entry.slug === 'managed' && isLocalSession) { + setManagedSessionMissing(true); + setStatus({ kind: 'error', message: managedLoginMessage }); + return; + } + if (entry.slug === 'custom') { // For custom, open setup popup to enter endpoint setSetupProvider(entry); @@ -143,6 +170,7 @@ const EmbeddingsPanel = ({ embedded = false }: EmbeddingsPanelProps = {}) => { const newModel = model ?? defaultModel?.id ?? settings!.model; const newDims = dims ?? defaultModel?.default_dimensions ?? settings!.dimensions; + if (slug !== 'managed') setManagedSessionMissing(false); setStatus({ kind: 'saving' }); try { const result = await updateEmbeddingsSettings({ @@ -338,12 +366,25 @@ const EmbeddingsPanel = ({ embedded = false }: EmbeddingsPanelProps = {}) => { try { const result = await testEmbeddingsConnection(); if (result.success) { + setManagedSessionMissing(false); setStatus({ kind: 'saved' }); } else { - setStatus({ kind: 'error', message: result.error ?? 'Test failed' }); + const message = result.error ?? t('settings.embeddings.connectionTestFailed'); + if (selectedProvider === 'managed' && isBackendSessionError(message)) { + setManagedSessionMissing(true); + setStatus({ kind: 'error', message: managedLoginMessage }); + } else { + setStatus({ kind: 'error', message }); + } } } catch (err) { - setStatus({ kind: 'error', message: err instanceof Error ? err.message : String(err) }); + const message = err instanceof Error ? err.message : String(err); + if (selectedProvider === 'managed' && isBackendSessionError(message)) { + setManagedSessionMissing(true); + setStatus({ kind: 'error', message: managedLoginMessage }); + } else { + setStatus({ kind: 'error', message }); + } } } @@ -389,6 +430,11 @@ const EmbeddingsPanel = ({ embedded = false }: EmbeddingsPanelProps = {}) => { : t('settings.embeddings.statusNeedsKey')} )} + {isLocalSession && entry.slug === 'managed' && ( + + {t('settings.embeddings.requiresSignIn')} + + )} {entry.description} @@ -415,6 +461,29 @@ const EmbeddingsPanel = ({ embedded = false }: EmbeddingsPanelProps = {}) => { + {showManagedLoginPrompt && ( +
+
+

+ {t('settings.embeddings.managedBannerIntro')}{' '} + {isLocalSession + ? t('settings.embeddings.managedBannerLocalSession') + : t('settings.embeddings.managedBannerRemoteSession')} +

+ +
+
+ )} + {/* Vector search disabled notice */} {selectedProvider === 'none' && (
@@ -487,7 +556,7 @@ const EmbeddingsPanel = ({ embedded = false }: EmbeddingsPanelProps = {}) => { variant="secondary" size="xs" onClick={() => void handleTestConnection()} - disabled={selectedProvider === 'none'}> + disabled={selectedProvider === 'none' || managedRequiresLogin}> {t('settings.embeddings.testConnection')}
diff --git a/app/src/components/settings/panels/__tests__/EmbeddingsPanel.test.tsx b/app/src/components/settings/panels/__tests__/EmbeddingsPanel.test.tsx index 705352266..b1380447e 100644 --- a/app/src/components/settings/panels/__tests__/EmbeddingsPanel.test.tsx +++ b/app/src/components/settings/panels/__tests__/EmbeddingsPanel.test.tsx @@ -10,6 +10,7 @@ import { fireEvent, screen, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { setCoreStateSnapshot } from '../../../../lib/coreState/store'; import { clearEmbeddingsApiKey, type EmbeddingProviderEntry, @@ -73,9 +74,41 @@ const makeSettings = (overrides: Partial = {}): EmbeddingsSe ...overrides, }); +const setCoreSession = ({ + sessionToken = 'header.payload.remote', + userId = 'u-1', + profileId = 'p-1', +}: { sessionToken?: string; userId?: string; profileId?: string | null } = {}) => { + setCoreStateSnapshot({ + isBootstrapping: false, + isReady: true, + snapshot: { + auth: { isAuthenticated: true, userId, user: null, profileId }, + sessionToken, + currentUser: null, + onboardingCompleted: true, + chatOnboardingCompleted: true, + analyticsEnabled: false, + meetAutoOrchestratorHandoff: false, + localState: { encryptionKey: null, onboardingTasks: null, keyringConsent: null }, + keyringStatus: { + available: true, + failureReason: null, + activeMode: 'os_keyring', + backendName: 'os', + }, + runtime: { screenIntelligence: null, localAi: null, autocomplete: null, service: null }, + }, + teams: [], + teamMembersById: {}, + teamInvitesById: {}, + }); +}; + describe('EmbeddingsPanel', () => { beforeEach(() => { vi.clearAllMocks(); + setCoreSession(); vi.mocked(loadEmbeddingsSettings).mockResolvedValue(makeSettings()); vi.mocked(updateEmbeddingsSettings).mockResolvedValue({ provider: 'managed', @@ -101,6 +134,46 @@ describe('EmbeddingsPanel', () => { expect(screen.getByText('Custom')).toBeInTheDocument(); }); + it('marks Managed embeddings as requiring OpenHuman sign-in for local sessions', async () => { + setCoreSession({ sessionToken: 'header.payload.local', userId: 'local', profileId: null }); + + renderWithProviders(); + + expect(await screen.findByText('Managed')).toBeInTheDocument(); + expect(screen.getByText(/requires OpenHuman sign-in/i)).toBeInTheDocument(); + expect( + screen.getByText(/Managed embeddings route through the OpenHuman backend/i) + ).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Test Connection/i })).toBeDisabled(); + }); + + it('blocks switching to Managed embeddings during a local session', async () => { + setCoreSession({ sessionToken: 'header.payload.local', userId: 'local', profileId: null }); + vi.mocked(loadEmbeddingsSettings).mockResolvedValue( + makeSettings({ + provider: 'openai', + model: 'openai-model-v1', + providers: [ + makeProvider('managed', { requires_api_key: false }), + makeProvider('openai', { requires_api_key: true, has_api_key: true }), + ], + }) + ); + + renderWithProviders(); + await screen.findByText('Managed'); + + fireEvent.click(screen.getByRole('radio', { name: /managed/i })); + + await waitFor(() => + expect(screen.getByText(/Managed embeddings require OpenHuman sign-in/i)).toBeInTheDocument() + ); + expect( + screen.getByText(/Managed embeddings route through the OpenHuman backend/i) + ).toBeInTheDocument(); + expect(vi.mocked(updateEmbeddingsSettings)).not.toHaveBeenCalled(); + }); + it('shows loading state then settings', async () => { let resolveLoad!: (s: EmbeddingsSettings) => void; vi.mocked(loadEmbeddingsSettings).mockReturnValue( @@ -634,6 +707,52 @@ describe('EmbeddingsPanel', () => { await waitFor(() => expect(screen.getByText(/connection refused/i)).toBeInTheDocument()); }); + it.each([ + ['missing backend session', 'No backend session for cloud embeddings: log in to OpenHuman'], + ['session-expired sentinel', 'SESSION_EXPIRED: backend session not active'], + [ + 'backend invalid token', + 'Embedding API error (401 Unauthorized): {"success":false,"error":"Invalid token"}', + ], + ])('turns Managed %s test failures into sign-in guidance', async (_case, error) => { + const settings = makeSettings({ + provider: 'managed', + providers: [ + makeProvider('managed', { + requires_api_key: false, + models: [ + { + id: 'managed-model-v1', + label: 'Managed Model v1', + default_dimensions: 1536, + allowed_dimensions: [1536], + }, + ], + }), + ], + }); + vi.mocked(loadEmbeddingsSettings).mockResolvedValue(settings); + vi.mocked(testEmbeddingsConnection).mockResolvedValueOnce({ + success: false, + provider: 'managed', + model: 'managed-model-v1', + error, + }); + + renderWithProviders(); + await screen.findByText('Managed'); + + fireEvent.click(await screen.findByRole('button', { name: /test connection/i })); + + await waitFor(() => + expect(screen.getByText(/Managed embeddings require OpenHuman sign-in/i)).toBeInTheDocument() + ); + expect( + screen.getByText(/Managed embeddings route through the OpenHuman backend/i) + ).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: /sign in again/i })); + }); + // ─── Model select (multiple catalog models) ─────────────────────────────── it('shows model select when provider has multiple models', async () => { diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index a1f8caab7..8a2f7c8a7 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -1144,6 +1144,16 @@ const messages: TranslationMap = { 'settings.embeddings.providerAria': 'مزود التضمينات', 'settings.embeddings.statusConfigured': 'تم التهيئة', 'settings.embeddings.statusNeedsKey': 'يحتاج مفتاح API', + 'settings.embeddings.requiresSignIn': 'يتطلب تسجيل الدخول إلى OpenHuman', + 'settings.embeddings.managedLoginRequired': + 'تتطلب التضمينات المُدارة تسجيل الدخول إلى OpenHuman. سجّل الدخول لاستخدام خلفية OpenHuman.', + 'settings.embeddings.managedBannerIntro': + 'تمر التضمينات المُدارة عبر خلفية OpenHuman وتتطلب جلسة حساب OpenHuman.', + 'settings.embeddings.managedBannerLocalSession': + 'اخرج من الجلسة المحلية وسجّل الدخول لاستخدام هذا المزوّد، أو انتقل إلى مزوّد تضمينات محلي أو بمفتاحك الخاص.', + 'settings.embeddings.managedBannerRemoteSession': + 'سجّل الدخول مرة أخرى لتحديث جلسة OpenHuman، أو انتقل إلى مزوّد تضمينات محلي أو بمفتاحك الخاص.', + 'settings.embeddings.signInAgain': 'تسجيل الدخول مرة أخرى', 'settings.embeddings.apiKeyLabel': 'مفتاح API لـ {provider}', 'settings.embeddings.placeholderStored': '•••••••• (مخزن)', 'settings.embeddings.placeholderKey': 'الصق مفتاح API الخاص بك…', @@ -1161,6 +1171,7 @@ const messages: TranslationMap = { 'settings.embeddings.testConnection': 'اختبار الاتصال', 'settings.embeddings.testing': 'جارٍ الاختبار…', 'settings.embeddings.testSuccess': 'متصل — {dims} بُعد', + 'settings.embeddings.connectionTestFailed': 'فشل الاختبار', 'settings.embeddings.testFailed': 'فشل: {error}', 'settings.embeddings.saving': 'جارٍ الحفظ…', 'settings.embeddings.saved': 'تم الحفظ.', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index d8bf168d0..7e2b2465e 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -1163,6 +1163,16 @@ const messages: TranslationMap = { 'settings.embeddings.providerAria': 'এমবেডিং প্রদানকারী', 'settings.embeddings.statusConfigured': 'কনফিগার করা হয়েছে', 'settings.embeddings.statusNeedsKey': 'API কী প্রয়োজন', + 'settings.embeddings.requiresSignIn': 'OpenHuman-এ সাইন-ইন প্রয়োজন', + 'settings.embeddings.managedLoginRequired': + 'ম্যানেজড এমবেডিংসের জন্য OpenHuman সাইন-ইন প্রয়োজন। OpenHuman ব্যাকএন্ড ব্যবহার করতে সাইন ইন করুন।', + 'settings.embeddings.managedBannerIntro': + 'ম্যানেজড এমবেডিংস OpenHuman ব্যাকএন্ডের মাধ্যমে চলে এবং একটি OpenHuman অ্যাকাউন্ট সেশন প্রয়োজন।', + 'settings.embeddings.managedBannerLocalSession': + 'এই প্রোভাইডার ব্যবহার করতে স্থানীয় সেশন থেকে বেরিয়ে সাইন ইন করুন, অথবা স্থানীয়/নিজস্ব-কী এমবেডিং প্রোভাইডারে যান।', + 'settings.embeddings.managedBannerRemoteSession': + 'OpenHuman সেশন রিফ্রেশ করতে আবার সাইন ইন করুন, অথবা স্থানীয়/নিজস্ব-কী এমবেডিং প্রোভাইডারে যান।', + 'settings.embeddings.signInAgain': 'আবার সাইন ইন করুন', 'settings.embeddings.apiKeyLabel': '{provider} API কী', 'settings.embeddings.placeholderStored': '•••••••• (সঞ্চিত)', 'settings.embeddings.placeholderKey': 'আপনার API কী পেস্ট করুন…', @@ -1180,6 +1190,7 @@ const messages: TranslationMap = { 'settings.embeddings.testConnection': 'সংযোগ পরীক্ষা', 'settings.embeddings.testing': 'পরীক্ষা হচ্ছে…', 'settings.embeddings.testSuccess': 'সংযুক্ত — {dims} মাত্রা', + 'settings.embeddings.connectionTestFailed': 'পরীক্ষা ব্যর্থ হয়েছে', 'settings.embeddings.testFailed': 'ব্যর্থ: {error}', 'settings.embeddings.saving': 'সংরক্ষণ হচ্ছে…', 'settings.embeddings.saved': 'সংরক্ষিত।', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index 606a9c735..85e6da7dd 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -1195,6 +1195,16 @@ const messages: TranslationMap = { 'settings.embeddings.providerAria': 'Embedding-Anbieter', 'settings.embeddings.statusConfigured': 'Konfiguriert', 'settings.embeddings.statusNeedsKey': 'API-Schlüssel benötigt', + 'settings.embeddings.requiresSignIn': 'OpenHuman-Anmeldung erforderlich', + 'settings.embeddings.managedLoginRequired': + 'Verwaltete Einbettungen erfordern eine OpenHuman-Anmeldung. Melde dich an, um das OpenHuman-Backend zu verwenden.', + 'settings.embeddings.managedBannerIntro': + 'Verwaltete Einbettungen laufen über das OpenHuman-Backend und erfordern eine OpenHuman-Kontositzung.', + 'settings.embeddings.managedBannerLocalSession': + 'Beende die lokale Sitzung und melde dich an, um diesen Anbieter zu verwenden, oder wechsle zu einem lokalen oder eigenen Einbettungsanbieter.', + 'settings.embeddings.managedBannerRemoteSession': + 'Melde dich erneut an, um deine OpenHuman-Sitzung zu aktualisieren, oder wechsle zu einem lokalen oder eigenen Einbettungsanbieter.', + 'settings.embeddings.signInAgain': 'Erneut anmelden', 'settings.embeddings.apiKeyLabel': '{provider} API-Schlüssel', 'settings.embeddings.placeholderStored': '•••••••• (gespeichert)', 'settings.embeddings.placeholderKey': 'API-Schlüssel einfügen…', @@ -1213,6 +1223,7 @@ const messages: TranslationMap = { 'settings.embeddings.testConnection': 'Verbindung testen', 'settings.embeddings.testing': 'Wird getestet…', 'settings.embeddings.testSuccess': 'Verbunden — {dims} Dimensionen', + 'settings.embeddings.connectionTestFailed': 'Test fehlgeschlagen', 'settings.embeddings.testFailed': 'Fehlgeschlagen: {error}', 'settings.embeddings.saving': 'Wird gespeichert…', 'settings.embeddings.saved': 'Gespeichert.', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index 42b2842e5..efd1d32f7 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -1511,6 +1511,16 @@ const en: TranslationMap = { 'settings.embeddings.providerAria': 'Embedding provider', 'settings.embeddings.statusConfigured': 'Configured', 'settings.embeddings.statusNeedsKey': 'Needs API key', + 'settings.embeddings.requiresSignIn': 'Requires OpenHuman sign-in', + 'settings.embeddings.managedLoginRequired': + 'Managed embeddings require OpenHuman sign-in. Sign in to use the OpenHuman backend.', + 'settings.embeddings.managedBannerIntro': + 'Managed embeddings route through the OpenHuman backend and require an OpenHuman account session.', + 'settings.embeddings.managedBannerLocalSession': + 'Exit local session and sign in to use this provider, or switch to a local or bring-your-own embeddings provider.', + 'settings.embeddings.managedBannerRemoteSession': + 'Sign in again to refresh your OpenHuman session, or switch to a local or bring-your-own embeddings provider.', + 'settings.embeddings.signInAgain': 'Sign in again', 'settings.embeddings.apiKeyLabel': '{provider} API key', 'settings.embeddings.placeholderStored': '•••••••• (stored)', 'settings.embeddings.placeholderKey': 'Paste your API key…', @@ -1528,6 +1538,7 @@ const en: TranslationMap = { 'settings.embeddings.testConnection': 'Test connection', 'settings.embeddings.testing': 'Testing…', 'settings.embeddings.testSuccess': 'Connected — {dims} dimensions', + 'settings.embeddings.connectionTestFailed': 'Test failed', 'settings.embeddings.testFailed': 'Failed: {error}', 'settings.embeddings.saving': 'Saving…', 'settings.embeddings.saved': 'Saved.', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 9f72c54d8..a45f51cc9 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -1190,6 +1190,16 @@ const messages: TranslationMap = { 'settings.embeddings.providerAria': 'Proveedor de embeddings', 'settings.embeddings.statusConfigured': 'Configurado', 'settings.embeddings.statusNeedsKey': 'Necesita clave API', + 'settings.embeddings.requiresSignIn': 'Requiere iniciar sesión en OpenHuman', + 'settings.embeddings.managedLoginRequired': + 'Los embeddings administrados requieren iniciar sesión en OpenHuman. Inicia sesión para usar el backend de OpenHuman.', + 'settings.embeddings.managedBannerIntro': + 'Los embeddings administrados se enrutan por el backend de OpenHuman y requieren una sesión de cuenta de OpenHuman.', + 'settings.embeddings.managedBannerLocalSession': + 'Sal de la sesión local e inicia sesión para usar este proveedor, o cambia a un proveedor de embeddings local o con tu propia clave.', + 'settings.embeddings.managedBannerRemoteSession': + 'Vuelve a iniciar sesión para actualizar tu sesión de OpenHuman, o cambia a un proveedor de embeddings local o con tu propia clave.', + 'settings.embeddings.signInAgain': 'Volver a iniciar sesión', 'settings.embeddings.apiKeyLabel': 'Clave API de {provider}', 'settings.embeddings.placeholderStored': '•••••••• (almacenado)', 'settings.embeddings.placeholderKey': 'Pega tu clave API…', @@ -1207,6 +1217,7 @@ const messages: TranslationMap = { 'settings.embeddings.testConnection': 'Probar conexión', 'settings.embeddings.testing': 'Probando…', 'settings.embeddings.testSuccess': 'Conectado — {dims} dimensiones', + 'settings.embeddings.connectionTestFailed': 'La prueba falló', 'settings.embeddings.testFailed': 'Fallido: {error}', 'settings.embeddings.saving': 'Guardando…', 'settings.embeddings.saved': 'Guardado.', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index cde0bc198..a7b44072b 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -1195,6 +1195,16 @@ const messages: TranslationMap = { 'settings.embeddings.providerAria': "Fournisseur d'embeddings", 'settings.embeddings.statusConfigured': 'Configuré', 'settings.embeddings.statusNeedsKey': 'Clé API requise', + 'settings.embeddings.requiresSignIn': 'Connexion OpenHuman requise', + 'settings.embeddings.managedLoginRequired': + 'Les embeddings gérés nécessitent une connexion OpenHuman. Connectez-vous pour utiliser le backend OpenHuman.', + 'settings.embeddings.managedBannerIntro': + 'Les embeddings gérés passent par le backend OpenHuman et nécessitent une session de compte OpenHuman.', + 'settings.embeddings.managedBannerLocalSession': + 'Quittez la session locale et connectez-vous pour utiliser ce fournisseur, ou passez à un fournisseur local ou avec votre propre clé.', + 'settings.embeddings.managedBannerRemoteSession': + 'Reconnectez-vous pour actualiser votre session OpenHuman, ou passez à un fournisseur local ou avec votre propre clé.', + 'settings.embeddings.signInAgain': 'Se reconnecter', 'settings.embeddings.apiKeyLabel': 'Clé API {provider}', 'settings.embeddings.placeholderStored': '•••••••• (stocké)', 'settings.embeddings.placeholderKey': 'Collez votre clé API…', @@ -1213,6 +1223,7 @@ const messages: TranslationMap = { 'settings.embeddings.testConnection': 'Tester la connexion', 'settings.embeddings.testing': 'Test en cours…', 'settings.embeddings.testSuccess': 'Connecté — {dims} dimensions', + 'settings.embeddings.connectionTestFailed': 'Test échoué', 'settings.embeddings.testFailed': 'Échec : {error}', 'settings.embeddings.saving': 'Enregistrement…', 'settings.embeddings.saved': 'Enregistré.', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 6e73f84dd..a3035b1df 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -1160,6 +1160,16 @@ const messages: TranslationMap = { 'settings.embeddings.providerAria': 'एम्बेडिंग प्रदाता', 'settings.embeddings.statusConfigured': 'कॉन्फ़िगर किया गया', 'settings.embeddings.statusNeedsKey': 'API कुंजी चाहिए', + 'settings.embeddings.requiresSignIn': 'OpenHuman साइन-इन आवश्यक', + 'settings.embeddings.managedLoginRequired': + 'प्रबंधित एम्बेडिंग के लिए OpenHuman साइन-इन आवश्यक है। OpenHuman बैकएंड का उपयोग करने के लिए साइन इन करें।', + 'settings.embeddings.managedBannerIntro': + 'प्रबंधित एम्बेडिंग OpenHuman बैकएंड से होकर चलते हैं और OpenHuman खाता सत्र की आवश्यकता होती है।', + 'settings.embeddings.managedBannerLocalSession': + 'इस प्रदाता का उपयोग करने के लिए स्थानीय सत्र से बाहर निकलें और साइन इन करें, या स्थानीय/अपनी-कुंजी वाले एम्बेडिंग प्रदाता पर स्विच करें।', + 'settings.embeddings.managedBannerRemoteSession': + 'अपने OpenHuman सत्र को रीफ़्रेश करने के लिए फिर से साइन इन करें, या स्थानीय/अपनी-कुंजी वाले एम्बेडिंग प्रदाता पर स्विच करें।', + 'settings.embeddings.signInAgain': 'फिर से साइन इन करें', 'settings.embeddings.apiKeyLabel': '{provider} API कुंजी', 'settings.embeddings.placeholderStored': '•••••••• (संग्रहीत)', 'settings.embeddings.placeholderKey': 'अपनी API कुंजी पेस्ट करें…', @@ -1178,6 +1188,7 @@ const messages: TranslationMap = { 'settings.embeddings.testConnection': 'कनेक्शन परीक्षण', 'settings.embeddings.testing': 'परीक्षण हो रहा है…', 'settings.embeddings.testSuccess': 'कनेक्ट — {dims} आयाम', + 'settings.embeddings.connectionTestFailed': 'परीक्षण विफल', 'settings.embeddings.testFailed': 'विफल: {error}', 'settings.embeddings.saving': 'सहेजा जा रहा है…', 'settings.embeddings.saved': 'सहेजा गया।', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 71551410c..9a9c4a2b8 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -1168,6 +1168,16 @@ const messages: TranslationMap = { 'settings.embeddings.providerAria': 'Penyedia embedding', 'settings.embeddings.statusConfigured': 'Dikonfigurasi', 'settings.embeddings.statusNeedsKey': 'Perlu kunci API', + 'settings.embeddings.requiresSignIn': 'Memerlukan masuk OpenHuman', + 'settings.embeddings.managedLoginRequired': + 'Embedding terkelola memerlukan masuk OpenHuman. Masuk untuk menggunakan backend OpenHuman.', + 'settings.embeddings.managedBannerIntro': + 'Embedding terkelola dirutekan melalui backend OpenHuman dan memerlukan sesi akun OpenHuman.', + 'settings.embeddings.managedBannerLocalSession': + 'Keluar dari sesi lokal dan masuk untuk menggunakan penyedia ini, atau beralih ke penyedia embedding lokal atau bawa-kunci-sendiri.', + 'settings.embeddings.managedBannerRemoteSession': + 'Masuk lagi untuk menyegarkan sesi OpenHuman Anda, atau beralih ke penyedia embedding lokal atau bawa-kunci-sendiri.', + 'settings.embeddings.signInAgain': 'Masuk lagi', 'settings.embeddings.apiKeyLabel': 'Kunci API {provider}', 'settings.embeddings.placeholderStored': '•••••••• (disimpan)', 'settings.embeddings.placeholderKey': 'Tempel kunci API Anda…', @@ -1185,6 +1195,7 @@ const messages: TranslationMap = { 'settings.embeddings.testConnection': 'Uji koneksi', 'settings.embeddings.testing': 'Menguji…', 'settings.embeddings.testSuccess': 'Terhubung — {dims} dimensi', + 'settings.embeddings.connectionTestFailed': 'Pengujian gagal', 'settings.embeddings.testFailed': 'Gagal: {error}', 'settings.embeddings.saving': 'Menyimpan…', 'settings.embeddings.saved': 'Tersimpan.', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index f6c1f0fea..75ba80ddf 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -1186,6 +1186,16 @@ const messages: TranslationMap = { 'settings.embeddings.providerAria': 'Fornitore di embeddings', 'settings.embeddings.statusConfigured': 'Configurato', 'settings.embeddings.statusNeedsKey': 'Chiave API necessaria', + 'settings.embeddings.requiresSignIn': 'Accesso OpenHuman richiesto', + 'settings.embeddings.managedLoginRequired': + 'Gli embedding gestiti richiedono l’accesso a OpenHuman. Accedi per usare il backend OpenHuman.', + 'settings.embeddings.managedBannerIntro': + 'Gli embedding gestiti passano dal backend OpenHuman e richiedono una sessione account OpenHuman.', + 'settings.embeddings.managedBannerLocalSession': + 'Esci dalla sessione locale e accedi per usare questo provider, oppure passa a un provider di embedding locale o con chiave personale.', + 'settings.embeddings.managedBannerRemoteSession': + 'Accedi di nuovo per aggiornare la sessione OpenHuman, oppure passa a un provider di embedding locale o con chiave personale.', + 'settings.embeddings.signInAgain': 'Accedi di nuovo', 'settings.embeddings.apiKeyLabel': 'Chiave API {provider}', 'settings.embeddings.placeholderStored': '•••••••• (memorizzato)', 'settings.embeddings.placeholderKey': 'Incolla la tua chiave API…', @@ -1204,6 +1214,7 @@ const messages: TranslationMap = { 'settings.embeddings.testConnection': 'Testa connessione', 'settings.embeddings.testing': 'Test in corso…', 'settings.embeddings.testSuccess': 'Connesso — {dims} dimensioni', + 'settings.embeddings.connectionTestFailed': 'Test non riuscito', 'settings.embeddings.testFailed': 'Fallito: {error}', 'settings.embeddings.saving': 'Salvataggio…', 'settings.embeddings.saved': 'Salvato.', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 32224c6b2..2a7e9af1d 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -1161,6 +1161,16 @@ const messages: TranslationMap = { 'settings.embeddings.providerAria': '임베딩 제공자', 'settings.embeddings.statusConfigured': '구성됨', 'settings.embeddings.statusNeedsKey': 'API 키 필요', + 'settings.embeddings.requiresSignIn': 'OpenHuman 로그인 필요', + 'settings.embeddings.managedLoginRequired': + '관리형 임베딩을 사용하려면 OpenHuman 로그인이 필요합니다. OpenHuman 백엔드를 사용하려면 로그인하세요.', + 'settings.embeddings.managedBannerIntro': + '관리형 임베딩은 OpenHuman 백엔드를 통해 라우팅되며 OpenHuman 계정 세션이 필요합니다.', + 'settings.embeddings.managedBannerLocalSession': + '이 공급자를 사용하려면 로컬 세션을 종료하고 로그인하거나, 로컬 또는 자체 키 임베딩 공급자로 전환하세요.', + 'settings.embeddings.managedBannerRemoteSession': + 'OpenHuman 세션을 새로 고치려면 다시 로그인하거나, 로컬 또는 자체 키 임베딩 공급자로 전환하세요.', + 'settings.embeddings.signInAgain': '다시 로그인', 'settings.embeddings.apiKeyLabel': '{provider} API 키', 'settings.embeddings.placeholderStored': '•••••••(저장됨)', 'settings.embeddings.placeholderKey': 'API 키를 붙여넣으세요…', @@ -1178,6 +1188,7 @@ const messages: TranslationMap = { 'settings.embeddings.testConnection': '연결 테스트', 'settings.embeddings.testing': '테스트 중…', 'settings.embeddings.testSuccess': '연결됨 — {dims} 차원', + 'settings.embeddings.connectionTestFailed': '테스트 실패', 'settings.embeddings.testFailed': '실패: {error}', 'settings.embeddings.saving': '저장 중…', 'settings.embeddings.saved': '저장됨.', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 581aeac2f..e72c20880 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -1180,6 +1180,16 @@ const messages: TranslationMap = { 'settings.embeddings.providerAria': 'Dostawca embeddings', 'settings.embeddings.statusConfigured': 'Skonfigurowano', 'settings.embeddings.statusNeedsKey': 'Wymaga klucza API', + 'settings.embeddings.requiresSignIn': 'Wymagane logowanie do OpenHuman', + 'settings.embeddings.managedLoginRequired': + 'Zarządzane osadzania wymagają logowania do OpenHuman. Zaloguj się, aby użyć backendu OpenHuman.', + 'settings.embeddings.managedBannerIntro': + 'Zarządzane osadzania są kierowane przez backend OpenHuman i wymagają sesji konta OpenHuman.', + 'settings.embeddings.managedBannerLocalSession': + 'Zakończ sesję lokalną i zaloguj się, aby użyć tego dostawcy, albo przełącz się na lokalnego dostawcę osadzań lub dostawcę z własnym kluczem.', + 'settings.embeddings.managedBannerRemoteSession': + 'Zaloguj się ponownie, aby odświeżyć sesję OpenHuman, albo przełącz się na lokalnego dostawcę osadzań lub dostawcę z własnym kluczem.', + 'settings.embeddings.signInAgain': 'Zaloguj się ponownie', 'settings.embeddings.apiKeyLabel': 'Klucz API {provider}', 'settings.embeddings.placeholderStored': '•••••••• (zapisane)', 'settings.embeddings.placeholderKey': 'Wklej swój klucz API…', @@ -1197,6 +1207,7 @@ const messages: TranslationMap = { 'settings.embeddings.testConnection': 'Przetestuj połączenie', 'settings.embeddings.testing': 'Testowanie…', 'settings.embeddings.testSuccess': 'Połączono — wymiarów: {dims}', + 'settings.embeddings.connectionTestFailed': 'Test nie powiódł się', 'settings.embeddings.testFailed': 'Niepowodzenie: {error}', 'settings.embeddings.saving': 'Zapisywanie…', 'settings.embeddings.saved': 'Zapisano.', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 8f6c685cb..7d7962866 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -1193,6 +1193,16 @@ const messages: TranslationMap = { 'settings.embeddings.providerAria': 'Provedor de embeddings', 'settings.embeddings.statusConfigured': 'Configurado', 'settings.embeddings.statusNeedsKey': 'Precisa de chave API', + 'settings.embeddings.requiresSignIn': 'Requer login no OpenHuman', + 'settings.embeddings.managedLoginRequired': + 'Embeddings gerenciados requerem login no OpenHuman. Faça login para usar o backend do OpenHuman.', + 'settings.embeddings.managedBannerIntro': + 'Embeddings gerenciados passam pelo backend do OpenHuman e exigem uma sessão de conta OpenHuman.', + 'settings.embeddings.managedBannerLocalSession': + 'Saia da sessão local e faça login para usar este provedor, ou mude para um provedor de embeddings local ou com sua própria chave.', + 'settings.embeddings.managedBannerRemoteSession': + 'Faça login novamente para atualizar sua sessão OpenHuman, ou mude para um provedor de embeddings local ou com sua própria chave.', + 'settings.embeddings.signInAgain': 'Entrar novamente', 'settings.embeddings.apiKeyLabel': 'Chave API {provider}', 'settings.embeddings.placeholderStored': '•••••••• (armazenado)', 'settings.embeddings.placeholderKey': 'Cole sua chave API…', @@ -1211,6 +1221,7 @@ const messages: TranslationMap = { 'settings.embeddings.testConnection': 'Testar conexão', 'settings.embeddings.testing': 'Testando…', 'settings.embeddings.testSuccess': 'Conectado — {dims} dimensões', + 'settings.embeddings.connectionTestFailed': 'Teste falhou', 'settings.embeddings.testFailed': 'Falhou: {error}', 'settings.embeddings.saving': 'Salvando…', 'settings.embeddings.saved': 'Salvo.', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index ba0b7470e..d816b591d 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -1177,6 +1177,16 @@ const messages: TranslationMap = { 'settings.embeddings.providerAria': 'Провайдер эмбеддингов', 'settings.embeddings.statusConfigured': 'Настроено', 'settings.embeddings.statusNeedsKey': 'Нужен API-ключ', + 'settings.embeddings.requiresSignIn': 'Требуется вход в OpenHuman', + 'settings.embeddings.managedLoginRequired': + 'Для управляемых эмбеддингов требуется вход в OpenHuman. Войдите, чтобы использовать backend OpenHuman.', + 'settings.embeddings.managedBannerIntro': + 'Управляемые эмбеддинги проходят через backend OpenHuman и требуют сеанс учетной записи OpenHuman.', + 'settings.embeddings.managedBannerLocalSession': + 'Выйдите из локального сеанса и войдите, чтобы использовать этого провайдера, или переключитесь на локального провайдера эмбеддингов либо провайдера с собственным ключом.', + 'settings.embeddings.managedBannerRemoteSession': + 'Войдите снова, чтобы обновить сеанс OpenHuman, или переключитесь на локального провайдера эмбеддингов либо провайдера с собственным ключом.', + 'settings.embeddings.signInAgain': 'Войти снова', 'settings.embeddings.apiKeyLabel': 'API-ключ {provider}', 'settings.embeddings.placeholderStored': '•••••••• (сохранено)', 'settings.embeddings.placeholderKey': 'Вставьте API-ключ…', @@ -1195,6 +1205,7 @@ const messages: TranslationMap = { 'settings.embeddings.testConnection': 'Проверить подключение', 'settings.embeddings.testing': 'Проверка…', 'settings.embeddings.testSuccess': 'Подключено — {dims} измерений', + 'settings.embeddings.connectionTestFailed': 'Проверка не удалась', 'settings.embeddings.testFailed': 'Ошибка: {error}', 'settings.embeddings.saving': 'Сохранение…', 'settings.embeddings.saved': 'Сохранено.', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 7fe79fdba..4ea416670 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -1110,6 +1110,16 @@ const messages: TranslationMap = { 'settings.embeddings.providerAria': '嵌入提供商', 'settings.embeddings.statusConfigured': '已配置', 'settings.embeddings.statusNeedsKey': '需要 API 密钥', + 'settings.embeddings.requiresSignIn': '需要登录 OpenHuman', + 'settings.embeddings.managedLoginRequired': + '托管嵌入需要登录 OpenHuman。请登录后使用 OpenHuman 后端。', + 'settings.embeddings.managedBannerIntro': + '托管嵌入会通过 OpenHuman 后端,并且需要 OpenHuman 账号会话。', + 'settings.embeddings.managedBannerLocalSession': + '退出本地会话并登录即可使用此提供商,或切换到本地/自带密钥的嵌入提供商。', + 'settings.embeddings.managedBannerRemoteSession': + '重新登录以刷新 OpenHuman 会话,或切换到本地/自带密钥的嵌入提供商。', + 'settings.embeddings.signInAgain': '重新登录', 'settings.embeddings.apiKeyLabel': '{provider} API 密钥', 'settings.embeddings.placeholderStored': '••••••••(已存储)', 'settings.embeddings.placeholderKey': '粘贴您的 API 密钥…', @@ -1127,6 +1137,7 @@ const messages: TranslationMap = { 'settings.embeddings.testConnection': '测试连接', 'settings.embeddings.testing': '测试中…', 'settings.embeddings.testSuccess': '已连接 — {dims} 维度', + 'settings.embeddings.connectionTestFailed': '测试失败', 'settings.embeddings.testFailed': '失败:{error}', 'settings.embeddings.saving': '保存中…', 'settings.embeddings.saved': '已保存。', diff --git a/docs/WEEKLY-CODE-REVIEW.md b/docs/WEEKLY-CODE-REVIEW.md index 87b9372aa..50e23cbc9 100644 --- a/docs/WEEKLY-CODE-REVIEW.md +++ b/docs/WEEKLY-CODE-REVIEW.md @@ -21,11 +21,10 @@ Each sub-check is **best-effort**: a missing tool or transient failure is reported inline in the Markdown, not fatal. A full lane going red never stops the rest of the report from being produced. -## Schedule + manual trigger +## Scheduling -- Cron: every Monday at **06:00 UTC** (`0 6 * * 1`). -- Manual: **Actions → Weekly Code Review → Run workflow**. -- Concurrency: one run at a time; subsequent triggers queue rather than cancel. +No scheduled GitHub Actions workflow is currently checked in for this report. +Run the script locally when a weekly code-health snapshot is needed. ## Outputs @@ -59,7 +58,7 @@ for the JSON shaping. Missing tools are skipped with a note in the report. - **TODO backlog** — the counter is a direction signal, not an action item on its own. Watch for a rising trend over successive weeks. -## Disabling / overrides +## Retiring - **One-off skip** — cancel the scheduled run from the Actions tab. - **Pause indefinitely** — no scheduled workflow is currently installed. diff --git a/scripts/ci-cancel-aware.sh b/scripts/ci-cancel-aware.sh old mode 100644 new mode 100755 diff --git a/src/openhuman/agentbox/test_support.rs b/src/openhuman/agentbox/test_support.rs index a7405934b..101c02e8b 100644 --- a/src/openhuman/agentbox/test_support.rs +++ b/src/openhuman/agentbox/test_support.rs @@ -1,5 +1,5 @@ -static TEST_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); - pub(super) fn test_env_lock() -> std::sync::MutexGuard<'static, ()> { - TEST_ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner()) + crate::openhuman::config::TEST_ENV_LOCK + .lock() + .unwrap_or_else(|p| p.into_inner()) } diff --git a/src/openhuman/wallet/chains/btc.rs b/src/openhuman/wallet/chains/btc.rs index 399ccafae..6859deb6c 100644 --- a/src/openhuman/wallet/chains/btc.rs +++ b/src/openhuman/wallet/chains/btc.rs @@ -541,12 +541,9 @@ mod tests { #[tokio::test] async fn execute_btc_quote_builds_psbt_signs_and_broadcasts() { let _guard = TEST_LOCK.lock(); - let _env_guard = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); reset_quote_store_for_tests(); let temp = TempDir::new().unwrap(); - setup_wallet_in(&temp).await.unwrap(); + let _workspace_guard = setup_wallet_in(&temp).await.unwrap(); // Mock state: collect raw tx hex posted to /tx. let raw_txs: Arc>> = @@ -630,12 +627,9 @@ mod tests { #[tokio::test] async fn execute_btc_quote_rejects_insufficient_utxos() { let _guard = TEST_LOCK.lock(); - let _env_guard = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); reset_quote_store_for_tests(); let temp = TempDir::new().unwrap(); - setup_wallet_in(&temp).await.unwrap(); + let _workspace_guard = setup_wallet_in(&temp).await.unwrap(); // Empty UTXO set — must error. let app = Router::new().route( diff --git a/src/openhuman/wallet/chains/evm.rs b/src/openhuman/wallet/chains/evm.rs index 01f5f70de..62d7dd00a 100644 --- a/src/openhuman/wallet/chains/evm.rs +++ b/src/openhuman/wallet/chains/evm.rs @@ -511,11 +511,8 @@ mod tests { #[tokio::test] async fn sign_and_broadcast_evm_signs_raw_calldata() { let _guard = TEST_LOCK.lock(); - let _env = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); let temp = TempDir::new().unwrap(); - setup_wallet_in(&temp).await.unwrap(); + let _workspace_guard = setup_wallet_in(&temp).await.unwrap(); let (addr, calls) = start_evm_mock(JsonValue::Null, JsonValue::Null).await; set_evm_rpc(addr); let result = sign_and_broadcast_evm( diff --git a/src/openhuman/wallet/chains/solana.rs b/src/openhuman/wallet/chains/solana.rs index 8ede0af42..49a1f40a5 100644 --- a/src/openhuman/wallet/chains/solana.rs +++ b/src/openhuman/wallet/chains/solana.rs @@ -808,12 +808,9 @@ mod tests { #[tokio::test] async fn execute_solana_quote_signs_and_broadcasts_native_transfer() { let _guard = TEST_LOCK.lock(); - let _env_guard = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); reset_quote_store_for_tests(); let temp = TempDir::new().unwrap(); - setup_wallet_in(&temp).await.unwrap(); + let _workspace_guard = setup_wallet_in(&temp).await.unwrap(); let fake_sig = "5xS9pXmqVz8R1nuRZTfsdsAxBdBFmtnAtuYbCsmK5DYzGn5vR4VqWGmiR5McLnYx8oFqLdo62q4qiUZpQyR4Hkn3"; let (addr, calls) = start_solana_mock(fake_sig).await; @@ -864,12 +861,9 @@ mod tests { #[tokio::test] async fn execute_solana_quote_signs_and_broadcasts_spl_transfer() { let _guard = TEST_LOCK.lock(); - let _env_guard = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); reset_quote_store_for_tests(); let temp = TempDir::new().unwrap(); - setup_wallet_in(&temp).await.unwrap(); + let _workspace_guard = setup_wallet_in(&temp).await.unwrap(); let fake_sig = "5xS9pXmqVz8R1nuRZTfsdsAxBdBFmtnAtuYbCsmK5DYzGn5vR4VqWGmiR5McLnYx8oFqLdo62q4qiUZpQyR4Hkn3"; let (addr, calls) = start_solana_mock(fake_sig).await; @@ -937,12 +931,9 @@ mod tests { #[tokio::test] async fn execute_solana_quote_refuses_spl_when_destination_ata_missing() { let _guard = TEST_LOCK.lock(); - let _env_guard = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); reset_quote_store_for_tests(); let temp = TempDir::new().unwrap(); - setup_wallet_in(&temp).await.unwrap(); + let _workspace_guard = setup_wallet_in(&temp).await.unwrap(); // Custom mock that returns null for getAccountInfo — simulates an ATA // that was never created on-chain. @@ -1069,12 +1060,9 @@ mod tests { #[tokio::test] async fn sign_and_broadcast_versioned_fills_signature_and_broadcasts() { let _guard = TEST_LOCK.lock(); - let _env_guard = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); reset_quote_store_for_tests(); let temp = TempDir::new().unwrap(); - setup_wallet_in(&temp).await.unwrap(); + let _workspace_guard = setup_wallet_in(&temp).await.unwrap(); let fake_sig = "5xS9pXmqVz8R1nuRZTfsdsAxBdBFmtnAtuYbCsmK5DYzGn5vR4VqWGmiR5McLnYx8oFqLdo62q4qiUZpQyR4Hkn3"; let (addr, calls) = start_solana_mock(fake_sig).await; @@ -1108,12 +1096,9 @@ mod tests { #[tokio::test] async fn sign_and_broadcast_versioned_rejects_non_signer() { let _guard = TEST_LOCK.lock(); - let _env_guard = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); reset_quote_store_for_tests(); let temp = TempDir::new().unwrap(); - setup_wallet_in(&temp).await.unwrap(); + let _workspace_guard = setup_wallet_in(&temp).await.unwrap(); // A signer pubkey that is NOT our wallet — sign must refuse. let other = [7u8; 32]; diff --git a/src/openhuman/wallet/chains/tron.rs b/src/openhuman/wallet/chains/tron.rs index 4577dcf9e..e4753ea0f 100644 --- a/src/openhuman/wallet/chains/tron.rs +++ b/src/openhuman/wallet/chains/tron.rs @@ -542,12 +542,9 @@ mod tests { #[tokio::test] async fn execute_tron_quote_signs_and_broadcasts_native_transfer() { let _guard = TEST_LOCK.lock(); - let _env_guard = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); reset_quote_store_for_tests(); let temp = TempDir::new().unwrap(); - setup_wallet_in(&temp).await.unwrap(); + let _workspace_guard = setup_wallet_in(&temp).await.unwrap(); let record = TronMockRecord::default(); let addr = start_tron_mock(record.clone()).await; @@ -597,12 +594,9 @@ mod tests { #[tokio::test] async fn execute_tron_quote_signs_and_broadcasts_trc20_transfer() { let _guard = TEST_LOCK.lock(); - let _env_guard = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); reset_quote_store_for_tests(); let temp = TempDir::new().unwrap(); - setup_wallet_in(&temp).await.unwrap(); + let _workspace_guard = setup_wallet_in(&temp).await.unwrap(); let record = TronMockRecord::default(); let addr = start_tron_mock(record.clone()).await; @@ -651,12 +645,9 @@ mod tests { #[tokio::test] async fn execute_tron_quote_surfaces_node_rejection() { let _guard = TEST_LOCK.lock(); - let _env_guard = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); reset_quote_store_for_tests(); let temp = TempDir::new().unwrap(); - setup_wallet_in(&temp).await.unwrap(); + let _workspace_guard = setup_wallet_in(&temp).await.unwrap(); // Custom mock returning result=false on broadcast. let app = Router::new() diff --git a/src/openhuman/wallet/execution_tests.rs b/src/openhuman/wallet/execution_tests.rs index 09ad02075..22f0e4205 100644 --- a/src/openhuman/wallet/execution_tests.rs +++ b/src/openhuman/wallet/execution_tests.rs @@ -182,12 +182,9 @@ async fn supported_assets_lists_default_erc20s_and_l2() { #[tokio::test] async fn prepare_transfer_rejects_unknown_asset_symbol() { let _guard = TEST_LOCK.lock(); - let _env_guard = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); reset_quote_store_for_tests(); let temp = TempDir::new().unwrap(); - setup_wallet_in(&temp).await.unwrap(); + let _workspace_guard = setup_wallet_in(&temp).await.unwrap(); let err = prepare_transfer(PrepareTransferParams { chain: WalletChain::Evm, to_address: "0x1111111111111111111111111111111111111111".into(), @@ -203,11 +200,8 @@ async fn prepare_transfer_rejects_unknown_asset_symbol() { #[tokio::test] async fn balances_fans_evm_account_into_eth_base_bsc_rows() { let _guard = TEST_LOCK.lock(); - let _env_guard = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); let temp = TempDir::new().unwrap(); - setup_wallet_in(&temp).await.unwrap(); + let _workspace_guard = setup_wallet_in(&temp).await.unwrap(); // Point all three displayed EVM networks at a mock returning 1e18 wei. let (addr, _estimate_calls, _raw_txs) = start_mock_rpc().await.unwrap(); for var in [ @@ -266,12 +260,9 @@ async fn tx_status_rejects_empty_hash() { #[tokio::test] async fn execute_prepared_broadcasts_native_evm_transaction() { let _guard = TEST_LOCK.lock(); - let _env_guard = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); reset_quote_store_for_tests(); let temp = TempDir::new().unwrap(); - setup_wallet_in(&temp).await.unwrap(); + let _workspace_guard = setup_wallet_in(&temp).await.unwrap(); let (addr, estimate_calls, raw_txs) = start_mock_rpc().await.unwrap(); std::env::set_var("OPENHUMAN_WALLET_RPC_EVM", format!("http://{addr}")); @@ -306,12 +297,9 @@ async fn execute_prepared_broadcasts_native_evm_transaction() { #[tokio::test] async fn execute_prepared_broadcasts_erc20_transfer_using_default_token_catalog() { let _guard = TEST_LOCK.lock(); - let _env_guard = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); reset_quote_store_for_tests(); let temp = TempDir::new().unwrap(); - setup_wallet_in(&temp).await.unwrap(); + let _workspace_guard = setup_wallet_in(&temp).await.unwrap(); let (addr, estimate_calls, raw_txs) = start_mock_rpc().await.unwrap(); std::env::set_var("OPENHUMAN_WALLET_RPC_EVM", format!("http://{addr}")); @@ -350,12 +338,9 @@ async fn execute_prepared_broadcasts_erc20_transfer_using_default_token_catalog( #[tokio::test] async fn execute_prepared_broadcasts_native_evm_on_base_with_chain_id_8453() { let _guard = TEST_LOCK.lock(); - let _env_guard = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); reset_quote_store_for_tests(); let temp = TempDir::new().unwrap(); - setup_wallet_in(&temp).await.unwrap(); + let _workspace_guard = setup_wallet_in(&temp).await.unwrap(); // Base uses chain_id 8453 = 0x2105. let (addr, _estimate_calls, raw_txs) = start_mock_rpc_with_chain_id("0x2105").await.unwrap(); std::env::set_var("OPENHUMAN_WALLET_RPC_BASE", format!("http://{addr}")); @@ -585,12 +570,9 @@ async fn execute_prepared_owner_mismatch_error_matches_not_found_shape() { async fn prepare_stamps_owner_via_task_local() { use crate::openhuman::approval::APPROVAL_CHAT_CONTEXT; let _guard = TEST_LOCK.lock(); - let _env_guard = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); reset_quote_store_for_tests(); let temp = TempDir::new().unwrap(); - setup_wallet_in(&temp).await.unwrap(); + let _workspace_guard = setup_wallet_in(&temp).await.unwrap(); let expected = owner_a(); let ctx = chat_ctx_from(&expected); @@ -620,12 +602,9 @@ async fn prepare_stamps_owner_via_task_local() { #[tokio::test] async fn execute_prepared_rejects_evm_chain_id_mismatch() { let _guard = TEST_LOCK.lock(); - let _env_guard = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); reset_quote_store_for_tests(); let temp = TempDir::new().unwrap(); - setup_wallet_in(&temp).await.unwrap(); + let _workspace_guard = setup_wallet_in(&temp).await.unwrap(); // Quote says Base; mock reports Ethereum (0x1) — must fail. let (addr, _e, _r) = start_mock_rpc_with_chain_id("0x1").await.unwrap(); std::env::set_var("OPENHUMAN_WALLET_RPC_BASE", format!("http://{addr}")); diff --git a/src/openhuman/wallet/ops.rs b/src/openhuman/wallet/ops.rs index 8d4181264..1a110394f 100644 --- a/src/openhuman/wallet/ops.rs +++ b/src/openhuman/wallet/ops.rs @@ -995,11 +995,8 @@ mod tests { async fn reveal_recovery_phrase_returns_error_when_no_wallet() { let temp = tempfile::tempdir().expect("temp dir"); let _wallet_lock = crate::openhuman::wallet::test_support::TEST_LOCK.lock(); - let _env_lock = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); - let _workspace = - crate::openhuman::wallet::test_support::WorkspaceEnvGuard::set(temp.path()); + let _workspace_guard = + crate::openhuman::wallet::test_support::set_workspace_env_for_test(&temp); let result = reveal_recovery_phrase().await; let err = result.expect_err("should error when no wallet configured"); assert!( @@ -1012,12 +1009,7 @@ mod tests { async fn reveal_recovery_phrase_returns_phrase_for_existing_wallet() { let temp = tempfile::tempdir().expect("temp dir"); let _wallet_lock = crate::openhuman::wallet::test_support::TEST_LOCK.lock(); - let _env_lock = crate::openhuman::config::TEST_ENV_LOCK - .lock() - .unwrap_or_else(|e| e.into_inner()); - let _workspace = - crate::openhuman::wallet::test_support::WorkspaceEnvGuard::set(temp.path()); - crate::openhuman::wallet::test_support::setup_wallet_in(&temp) + let _workspace_guard = crate::openhuman::wallet::test_support::setup_wallet_in(&temp) .await .expect("setup wallet"); let result = reveal_recovery_phrase() diff --git a/src/openhuman/wallet/test_support.rs b/src/openhuman/wallet/test_support.rs index e86b74507..71662a9d4 100644 --- a/src/openhuman/wallet/test_support.rs +++ b/src/openhuman/wallet/test_support.rs @@ -17,6 +17,7 @@ use tempfile::TempDir; use super::ops::{setup, WalletAccount, WalletChain, WalletSetupParams, WalletSetupSource}; use crate::openhuman::config::rpc as config_rpc; +use crate::openhuman::config::TEST_ENV_LOCK; pub(crate) static TEST_LOCK: Lazy> = Lazy::new(|| Mutex::new(())); @@ -61,13 +62,20 @@ pub(crate) fn sample_account(chain: WalletChain) -> WalletAccount { /// out of scope at the end of the test. pub(crate) struct WorkspaceEnvGuard { prev: Option, + _env_lock: std::sync::MutexGuard<'static, ()>, } impl WorkspaceEnvGuard { pub(crate) fn set(path: impl AsRef) -> Self { + // OPENHUMAN_WORKSPACE is process-global, so hold the shared config env + // lock for the full lifetime of the test workspace override. + let env_lock = TEST_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let prev = std::env::var_os("OPENHUMAN_WORKSPACE"); std::env::set_var("OPENHUMAN_WORKSPACE", path.as_ref()); - Self { prev } + Self { + prev, + _env_lock: env_lock, + } } } @@ -80,11 +88,15 @@ impl Drop for WorkspaceEnvGuard { } } -pub(crate) async fn setup_wallet_in(temp: &TempDir) -> Result<(), String> { - // We intentionally leak the env-var change for the duration of the test - // (wallet state lookups rely on it). Callers that run under the full lib - // suite hold the repo-wide config TEST_ENV_LOCK while this env var is set. - std::env::set_var("OPENHUMAN_WORKSPACE", temp.path()); +pub(crate) fn set_workspace_env_for_test(temp: &TempDir) -> WorkspaceEnvGuard { + WorkspaceEnvGuard::set(temp.path()) +} + +pub(crate) async fn setup_wallet_in(temp: &TempDir) -> Result { + // Wallet state lookups rely on OPENHUMAN_WORKSPACE for the duration of + // each test. Return a guard so the tempdir path does not leak into later + // parallel tests after this test's TempDir has been dropped. + let workspace_guard = set_workspace_env_for_test(temp); let config = config_rpc::load_config_with_timeout().await?; let encrypted = crate::openhuman::encryption::rpc::encrypt_secret(&config, TEST_MNEMONIC) .await? @@ -107,5 +119,43 @@ pub(crate) async fn setup_wallet_in(temp: &TempDir) -> Result<(), String> { force: true, }) .await?; - Ok(()) + Ok(workspace_guard) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn workspace_env_guard_restores_workspace_env_when_dropped() { + let env_lock = TEST_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let previous = std::env::var_os("OPENHUMAN_WORKSPACE"); + std::env::set_var("OPENHUMAN_WORKSPACE", "/tmp/openhuman-existing-workspace"); + + let temp = TempDir::new().expect("temp dir"); + let prev = std::env::var_os("OPENHUMAN_WORKSPACE"); + std::env::set_var("OPENHUMAN_WORKSPACE", temp.path()); + let workspace_guard = WorkspaceEnvGuard { + prev, + _env_lock: env_lock, + }; + assert_eq!( + std::env::var_os("OPENHUMAN_WORKSPACE"), + Some(temp.path().as_os_str().to_os_string()) + ); + + drop(workspace_guard); + let _cleanup_lock = TEST_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + assert_eq!( + std::env::var_os("OPENHUMAN_WORKSPACE"), + Some(std::ffi::OsString::from( + "/tmp/openhuman-existing-workspace" + )) + ); + + match previous { + Some(value) => std::env::set_var("OPENHUMAN_WORKSPACE", value), + None => std::env::remove_var("OPENHUMAN_WORKSPACE"), + } + } }