diff --git a/app/src/components/intelligence/MemorySyncConnections.tsx b/app/src/components/intelligence/MemorySyncConnections.tsx index 1ce79c634..514a39b91 100644 --- a/app/src/components/intelligence/MemorySyncConnections.tsx +++ b/app/src/components/intelligence/MemorySyncConnections.tsx @@ -100,7 +100,7 @@ function SourceCard({ status }: SourceCardProps) { {FRESHNESS_LABEL[status.freshness]} -
+
{lifetime.toLocaleString()} chunks @@ -187,7 +187,7 @@ export function MemorySyncConnections({ pollIntervalMs }: MemorySyncConnectionsP return (

Memory sources

-

+

Failed to load sync status: {loadError}

diff --git a/app/src/components/settings/components/MemoryWindowControl.tsx b/app/src/components/settings/components/MemoryWindowControl.tsx index 8460c6584..0a3906e18 100644 --- a/app/src/components/settings/components/MemoryWindowControl.tsx +++ b/app/src/components/settings/components/MemoryWindowControl.tsx @@ -137,7 +137,7 @@ const MemoryWindowControl = ({ onError, onSaved }: Props) => {
+ className="grid grid-cols-2 gap-2"> {MEMORY_CONTEXT_WINDOWS.map(option => { const optionMeta = MEMORY_WINDOW_PRESET_META[option]; const isActive = activeForUi === option; @@ -154,9 +154,9 @@ const MemoryWindowControl = ({ onError, onSaved }: Props) => { className={`text-left rounded-md border px-3 py-2 transition-colors ${ isActive ? 'border-primary bg-primary/10' : 'border-border hover:bg-accent/40' } disabled:opacity-60 disabled:cursor-not-allowed`}> -
- {optionMeta.label} - +
+ {optionMeta.label} + {optionMeta.badge}
diff --git a/app/src/components/settings/panels/__tests__/MemoryDataPanel.test.tsx b/app/src/components/settings/panels/__tests__/MemoryDataPanel.test.tsx new file mode 100644 index 000000000..0de04fe65 --- /dev/null +++ b/app/src/components/settings/panels/__tests__/MemoryDataPanel.test.tsx @@ -0,0 +1,126 @@ +/** + * Tests for the Settings → Memory Data panel. + * + * Verifies that all four memory-window preset buttons render, the memory + * sources section is present, and that a sync-connection error does not + * hide or disable the memory-window controls. + */ +import { screen, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { renderWithProviders } from '../../../../test/test-utils'; +import MemoryDataPanel from '../MemoryDataPanel'; + +// ── Mocks ──────────────────────────────────────────────────────────────────── + +const hoisted = vi.hoisted(() => ({ + mockGetConfig: vi.fn(), + mockUpdateMemorySettings: vi.fn(), + mockIsTauri: vi.fn(() => false), + mockMemoryTreeListChunks: vi.fn(), + mockMemoryTreeListSources: vi.fn(), + mockMemoryTreeTopEntities: vi.fn(), + mockMemoryTreeEntityIndexFor: vi.fn(), + mockMemoryTreeChunkScore: vi.fn(), + mockMemoryTreeChunksForEntity: vi.fn(), + mockStatusList: vi.fn(), +})); + +vi.mock('../../../../utils/tauriCommands', () => ({ + isTauri: hoisted.mockIsTauri, + openhumanGetConfig: hoisted.mockGetConfig, + openhumanUpdateMemorySettings: hoisted.mockUpdateMemorySettings, + MEMORY_CONTEXT_WINDOWS: ['minimal', 'balanced', 'extended', 'maximum'], + memoryTreeListChunks: hoisted.mockMemoryTreeListChunks, + memoryTreeListSources: hoisted.mockMemoryTreeListSources, + memoryTreeTopEntities: hoisted.mockMemoryTreeTopEntities, + memoryTreeEntityIndexFor: hoisted.mockMemoryTreeEntityIndexFor, + memoryTreeChunkScore: hoisted.mockMemoryTreeChunkScore, + memoryTreeChunksForEntity: hoisted.mockMemoryTreeChunksForEntity, +})); + +vi.mock('../../../../services/memorySyncService', async () => { + const actual = await vi.importActual( + '../../../../services/memorySyncService' + ); + return { + ...actual, + memorySyncStatusList: (...args: unknown[]) => hoisted.mockStatusList(...args), + }; +}); + +vi.mock('../../hooks/useSettingsNavigation', () => ({ + useSettingsNavigation: () => ({ navigateBack: vi.fn(), breadcrumbs: [] }), +})); + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +const resolveConfigWith = (memory_window = 'balanced') => { + hoisted.mockGetConfig.mockResolvedValue({ + result: { + config: { agent: { memory_window } }, + workspace_dir: '/tmp/ws', + config_path: '/tmp/cfg.toml', + }, + }); +}; + +// ── Tests ───────────────────────────────────────────────────────────────────── + +describe('MemoryDataPanel', () => { + beforeEach(() => { + hoisted.mockGetConfig.mockReset(); + hoisted.mockUpdateMemorySettings.mockReset(); + hoisted.mockIsTauri.mockReturnValue(false); + hoisted.mockStatusList.mockReset(); + hoisted.mockMemoryTreeListChunks.mockReset(); + hoisted.mockMemoryTreeListSources.mockReset(); + hoisted.mockMemoryTreeTopEntities.mockReset(); + hoisted.mockMemoryTreeEntityIndexFor.mockReset(); + hoisted.mockMemoryTreeChunkScore.mockReset(); + hoisted.mockMemoryTreeChunksForEntity.mockReset(); + + // Default: no sources yet, no errors + hoisted.mockStatusList.mockResolvedValue([]); + hoisted.mockMemoryTreeListChunks.mockResolvedValue({ chunks: [], total: 0, cursor: null }); + hoisted.mockMemoryTreeListSources.mockResolvedValue([]); + hoisted.mockMemoryTreeTopEntities.mockResolvedValue([]); + }); + + it('renders all four preset buttons', async () => { + resolveConfigWith('balanced'); + renderWithProviders(); + + for (const label of ['Minimal', 'Balanced', 'Extended', 'Maximum']) { + expect(screen.getByText(label)).toBeInTheDocument(); + } + }); + + it('renders the memory sources section', async () => { + resolveConfigWith('balanced'); + renderWithProviders(); + + await waitFor(() => { + expect(screen.getByTestId('memory-sync-connections')).toBeInTheDocument(); + }); + }); + + it('keeps all preset buttons accessible when sync connections returns an error', async () => { + resolveConfigWith('balanced'); + hoisted.mockStatusList.mockRejectedValue(new Error('network timeout')); + + renderWithProviders(); + + // Wait for the error state to appear in the sync connections section + await waitFor(() => { + expect(screen.getByText(/network timeout/i)).toBeInTheDocument(); + }); + + // All four preset buttons must still be in the DOM and not disabled + for (const preset of ['minimal', 'balanced', 'extended', 'maximum']) { + const btn = screen.getByTestId(`memory-window-option-${preset}`); + expect(btn).toBeInTheDocument(); + expect(btn).not.toBeDisabled(); + } + }); +});