From 74e50c64bb0f831dbaf821d43c9ab4a2d20c54cd Mon Sep 17 00:00:00 2001 From: lucaronin Date: Mon, 20 Apr 2026 01:02:08 +0200 Subject: [PATCH] fix: hide stale getting started vault entries --- src/hooks/useVaultSwitcher.test.ts | 49 +++++++ src/hooks/useVaultSwitcher.ts | 86 ++++++++++- .../stale-getting-started-switcher.spec.ts | 135 ++++++++++++++++++ 3 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 tests/smoke/stale-getting-started-switcher.spec.ts diff --git a/src/hooks/useVaultSwitcher.test.ts b/src/hooks/useVaultSwitcher.test.ts index 196b388f..14e09693 100644 --- a/src/hooks/useVaultSwitcher.test.ts +++ b/src/hooks/useVaultSwitcher.test.ts @@ -195,6 +195,55 @@ describe('useVaultSwitcher', () => { expect(result.current.isGettingStartedHidden).toBe(false) }) + it('drops stale canonical Getting Started entries when the starter path is missing', async () => { + mockVaultListStore = { + vaults: [ + { label: 'Getting Started', path: expectedDefaultVaultPath }, + { label: 'Work', path: '/work/vault' }, + ], + active_vault: '/work/vault', + hidden_defaults: [], + } + setMockInvokeBehavior({ + checkVaultExists: ({ path }) => path === '/work/vault', + }) + + const { result } = await renderLoadedVaultSwitcher() + + expect(result.current.allVaults).toEqual([{ label: 'Work', path: '/work/vault', available: true }]) + expect(result.current.vaultPath).toBe('/work/vault') + + await waitFor(() => { + expect(mockVaultListStore).toEqual({ + vaults: [{ label: 'Work', path: '/work/vault' }], + active_vault: '/work/vault', + hidden_defaults: [], + }) + }) + }) + + it('clears a stale canonical Getting Started selection when the starter path is missing', async () => { + mockVaultListStore = { + vaults: [{ label: 'Getting Started', path: expectedDefaultVaultPath }], + active_vault: expectedDefaultVaultPath, + hidden_defaults: [], + } + setMockInvokeBehavior({ checkVaultExists: false }) + + const { result } = await renderLoadedVaultSwitcher() + + expect(result.current.allVaults).toEqual([]) + expect(result.current.selectedVaultPath).toBeNull() + + await waitFor(() => { + expect(mockVaultListStore).toEqual({ + vaults: [], + active_vault: null, + hidden_defaults: [], + }) + }) + }) + it('handles load error gracefully', async () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) mockInvokeFn.mockImplementation((cmd: string) => { diff --git a/src/hooks/useVaultSwitcher.ts b/src/hooks/useVaultSwitcher.ts index 865b3469..08204b21 100644 --- a/src/hooks/useVaultSwitcher.ts +++ b/src/hooks/useVaultSwitcher.ts @@ -137,6 +137,18 @@ function syncDefaultVaultExport(path: string) { DEFAULT_VAULTS[0] = { label: GETTING_STARTED_LABEL, path } } +function isCanonicalGettingStartedPath(path: string, resolvedDefaultPath: string): boolean { + return path === resolvedDefaultPath +} + +function isUnavailableGettingStartedVault(vault: VaultOption): boolean { + return vault.label === GETTING_STARTED_LABEL && vault.available === false +} + +function shouldDropPersistedGettingStartedVault(vault: VaultOption, resolvedDefaultPath: string): boolean { + return isCanonicalGettingStartedPath(vault.path, resolvedDefaultPath) || isUnavailableGettingStartedVault(vault) +} + async function checkVaultAvailability(path: string): Promise { if (!path) { return false @@ -166,7 +178,79 @@ async function loadInitialVaultState() { console.warn('Failed to load vault list:', vaultListResult.reason) } - return { activeVault, defaultAvailable, hiddenDefaults, resolvedDefaultPath, vaults } + return sanitizeCanonicalGettingStartedState({ + activeVault, + defaultAvailable, + hiddenDefaults, + resolvedDefaultPath, + vaults, + }) +} + +function sanitizeCanonicalGettingStartedState({ + activeVault, + defaultAvailable, + hiddenDefaults, + resolvedDefaultPath, + vaults, +}: { + activeVault: string | null + defaultAvailable: boolean + hiddenDefaults: string[] + resolvedDefaultPath: string + vaults: VaultOption[] +}) { + if (!resolvedDefaultPath) { + return { activeVault, defaultAvailable, hiddenDefaults, resolvedDefaultPath, vaults } + } + + const filteredVaults = vaults.filter( + (vault) => !shouldDropPersistedGettingStartedVault(vault, resolvedDefaultPath), + ) + const removedStarterPaths = new Set( + vaults + .filter((vault) => shouldDropPersistedGettingStartedVault(vault, resolvedDefaultPath)) + .map((vault) => vault.path), + ) + const sanitizedActiveVault = resolveSanitizedGettingStartedSelection({ + activeVault, + defaultAvailable, + filteredVaults, + removedStarterPaths, + resolvedDefaultPath, + }) + + return { + activeVault: sanitizedActiveVault, + defaultAvailable, + hiddenDefaults, + resolvedDefaultPath, + vaults: filteredVaults, + } +} + +function resolveSanitizedGettingStartedSelection({ + activeVault, + defaultAvailable, + filteredVaults, + removedStarterPaths, + resolvedDefaultPath, +}: { + activeVault: string | null + defaultAvailable: boolean + filteredVaults: VaultOption[] + removedStarterPaths: Set + resolvedDefaultPath: string +}): string | null { + if (!activeVault || !removedStarterPaths.has(activeVault)) { + return activeVault + } + + if (isCanonicalGettingStartedPath(activeVault, resolvedDefaultPath) && defaultAvailable) { + return activeVault + } + + return filteredVaults[0]?.path ?? null } function buildDefaultVaults({ diff --git a/tests/smoke/stale-getting-started-switcher.spec.ts b/tests/smoke/stale-getting-started-switcher.spec.ts new file mode 100644 index 00000000..51133ad9 --- /dev/null +++ b/tests/smoke/stale-getting-started-switcher.spec.ts @@ -0,0 +1,135 @@ +import { test, expect, type Page } from '@playwright/test' + +interface StaleStarterPaths { + gettingStartedPath: string + personalVaultPath: string + workVaultPath: string +} + +function buildPaths(): StaleStarterPaths { + return { + gettingStartedPath: '/Users/mock/Documents/Getting Started', + personalVaultPath: '/Users/mock/Personal', + workVaultPath: '/Users/mock/Work', + } +} + +async function installStaleStarterMocks(page: Page) { + const paths = buildPaths() + + await page.addInitScript((data: StaleStarterPaths) => { + localStorage.clear() + localStorage.setItem('tolaria:claude-code-onboarding-dismissed', '1') + + let ref: Record | null = null + + Object.defineProperty(window, '__mockHandlers', { + configurable: true, + set(value) { + ref = value as Record + ref.load_vault_list = () => ({ + vaults: [ + { label: 'Getting Started', path: data.gettingStartedPath }, + { label: 'Work Vault', path: data.workVaultPath }, + { label: 'Personal Vault', path: data.personalVaultPath }, + ], + active_vault: data.workVaultPath, + hidden_defaults: [], + }) + ref.get_default_vault_path = () => data.gettingStartedPath + ref.check_vault_exists = (args: { path?: string }) => + args?.path === data.workVaultPath || args?.path === data.personalVaultPath + ref.list_vault = (args: { path?: string }) => { + if (args?.path === data.workVaultPath) { + return [{ + path: `${data.workVaultPath}/work-home.md`, + filename: 'work-home.md', + title: 'Work Home', + isA: 'Note', + aliases: [], + belongsTo: [], + relatedTo: [], + status: null, + archived: false, + modifiedAt: 1700000000, + createdAt: null, + fileSize: 256, + snippet: 'Work Home snippet', + wordCount: 12, + relationships: {}, + outgoingLinks: [], + properties: {}, + template: null, + sort: null, + }] + } + + if (args?.path === data.personalVaultPath) { + return [{ + path: `${data.personalVaultPath}/personal-home.md`, + filename: 'personal-home.md', + title: 'Personal Home', + isA: 'Note', + aliases: [], + belongsTo: [], + relatedTo: [], + status: null, + archived: false, + modifiedAt: 1700000000, + createdAt: null, + fileSize: 256, + snippet: 'Personal Home snippet', + wordCount: 12, + relationships: {}, + outgoingLinks: [], + properties: {}, + template: null, + sort: null, + }] + } + + return [] + } + ref.list_vault_folders = () => [] + ref.list_views = () => [] + ref.get_all_content = () => ({ + [`${data.workVaultPath}/work-home.md`]: '# Work Home\n\nWork Home snippet', + [`${data.personalVaultPath}/personal-home.md`]: '# Personal Home\n\nPersonal Home snippet', + }) + ref.get_note_content = (args: { path?: string }) => { + if (args?.path === `${data.workVaultPath}/work-home.md`) { + return '# Work Home\n\nWork Home snippet' + } + + if (args?.path === `${data.personalVaultPath}/personal-home.md`) { + return '# Personal Home\n\nPersonal Home snippet' + } + + return '' + } + ref.get_modified_files = () => [] + ref.get_file_history = () => [] + }, + get() { + return ref + }, + }) + }, paths) +} + +test('stale persisted Getting Started entries stay hidden when the starter path is missing @smoke', async ({ page }) => { + await installStaleStarterMocks(page) + + await page.goto('/', { waitUntil: 'domcontentloaded' }) + + const trigger = page.getByTestId('status-vault-trigger') + await expect(trigger).toContainText('Work Vault') + + await trigger.focus() + await expect(trigger).toBeFocused() + await page.keyboard.press('Enter') + + await expect(page.getByTestId('vault-menu-item-Getting Started')).toHaveCount(0) + await expect(page.getByTestId('vault-menu-item-Work Vault')).toBeVisible() + await expect(page.getByTestId('vault-menu-item-Personal Vault')).toBeVisible() +})