fix: hide stale getting started vault entries
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<string>
|
||||
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({
|
||||
|
||||
135
tests/smoke/stale-getting-started-switcher.spec.ts
Normal file
135
tests/smoke/stale-getting-started-switcher.spec.ts
Normal file
@@ -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<string, unknown> | null = null
|
||||
|
||||
Object.defineProperty(window, '__mockHandlers', {
|
||||
configurable: true,
|
||||
set(value) {
|
||||
ref = value as Record<string, unknown>
|
||||
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()
|
||||
})
|
||||
Reference in New Issue
Block a user