diff --git a/src/App.test.tsx b/src/App.test.tsx
index 18506ce2..3eddbb63 100644
--- a/src/App.test.tsx
+++ b/src/App.test.tsx
@@ -525,7 +525,7 @@ describe('App', () => {
// Entries appear in both Sidebar and NoteList
expect(screen.getAllByText('Test Project').length).toBeGreaterThan(0)
expect(screen.getAllByText('Software Development').length).toBeGreaterThan(0)
- })
+ }, { timeout: SLOW_APP_READY_TIMEOUT_MS })
})
it('keeps the app shell usable while the vault note scan is pending', async () => {
@@ -1301,7 +1301,7 @@ describe('App', () => {
render()
- expect(await screen.findByTestId('status-missing-git')).toBeInTheDocument()
+ expect(await screen.findByTestId('status-missing-git', {}, { timeout: SLOW_APP_READY_TIMEOUT_MS })).toBeInTheDocument()
fireEvent.click(screen.getByTestId('status-missing-git'))
expect(await screen.findByText('Enable Git for this vault?')).toBeInTheDocument()
diff --git a/src/hooks/useVaultLoader.test.ts b/src/hooks/useVaultLoader.test.ts
index 0050c5c4..fe62fc60 100644
--- a/src/hooks/useVaultLoader.test.ts
+++ b/src/hooks/useVaultLoader.test.ts
@@ -143,10 +143,14 @@ async function enableTauriMode() {
)
}
-function buildMountedWorkspaceLoadMock() {
+function buildMountedWorkspaceLoadMock(options: {
+ pendingEntriesByPath?: Record>
+} = {}) {
return ((cmd: string, args?: Record) => {
if (isVaultLoadCommand(cmd)) {
- const path = args?.path
+ const path = typeof args?.path === 'string' ? args.path : ''
+ const pendingEntries = options.pendingEntriesByPath?.[path]
+ if (pendingEntries) return pendingEntries
return Promise.resolve([{
...mockEntries[0],
path: `${path}/note/hello.md`,
@@ -229,7 +233,7 @@ describe('useVaultLoader', () => {
})
})
- it('does not re-add the base vault as a folder root after it is unmounted', async () => {
+ it('keeps the active vault folder root visible when it is absent from mounted folder vaults', async () => {
const entryVaults = [
{ label: 'Brian', path: '/brian', alias: 'brian', available: true, mounted: false },
{ label: 'Laputa', path: '/laputa', alias: 'laputa', available: true, mounted: true },
@@ -261,6 +265,12 @@ describe('useVaultLoader', () => {
rootPath: '/laputa',
children: [{ name: 'laputa-projects', path: 'projects', rootPath: '/laputa', children: [] }],
},
+ {
+ name: 'brian',
+ path: '',
+ rootPath: '/brian',
+ children: [{ name: 'brian-projects', path: 'projects', rootPath: '/brian', children: [] }],
+ },
])
})
})
@@ -372,25 +382,15 @@ describe('useVaultLoader', () => {
})
it('adds each mounted workspace as soon as that workspace finishes loading', async () => {
- let resolveLaputa: ((entries: VaultEntry[]) => void) | null = null
+ const laputaLoad = createDeferred()
const brian = { label: 'Brian', path: '/brian', alias: 'brian', available: true, mounted: true }
const laputa = { label: 'Laputa', path: '/laputa', alias: 'laputa', available: true, mounted: true }
const team = { label: 'Team', path: '/team', alias: 'team', available: true, mounted: true }
const vaults = [brian, laputa, team]
- backendInvokeFn.mockImplementation(((cmd: string, args?: Record) => {
- const path = args?.path
- if (isVaultLoadCommand(cmd)) {
- if (path === '/laputa') {
- return new Promise((resolve) => {
- resolveLaputa = resolve
- })
- }
- return Promise.resolve([{ ...mockEntries[0], path: `${path}/note/hello.md` }])
- }
- if (cmd === 'list_vault_folders' || cmd === 'list_views' || cmd === 'get_modified_files') return Promise.resolve([])
- return Promise.resolve(null)
- }) as typeof defaultMockInvoke)
+ backendInvokeFn.mockImplementation(buildMountedWorkspaceLoadMock({
+ pendingEntriesByPath: { '/laputa': laputaLoad.promise },
+ }))
const { result } = renderHook(() => useVaultLoader('/brian', vaults, '/brian', vaults))
@@ -399,7 +399,7 @@ describe('useVaultLoader', () => {
})
await act(async () => {
- resolveLaputa?.([{ ...mockEntries[0], path: '/laputa/note/hello.md' }])
+ laputaLoad.resolve([{ ...mockEntries[0], path: '/laputa/note/hello.md' }])
})
await waitFor(() => {
@@ -662,15 +662,17 @@ describe('useVaultLoader', () => {
const brian = { label: 'Brian', path: '/brian', alias: 'brian', available: true, mounted: true }
const laputa = { label: 'Laputa', path: '/laputa', alias: 'laputa', available: true, mounted: true }
const vaults = [laputa, brian]
+ const laputaStartupResponses: Partial> = {
+ reload_vault: [
+ { ...mockEntries[0], path: '/laputa/note/alpha.md', filename: 'alpha.md', title: 'Alpha' },
+ ],
+ list_vault: [],
+ }
backendInvokeFn.mockImplementation(((cmd: string, args?: Record) => {
- if (cmd === 'reload_vault' && args?.path === '/laputa') {
- return Promise.resolve([
- { ...mockEntries[0], path: '/laputa/note/alpha.md', filename: 'alpha.md', title: 'Alpha' },
- ])
- }
- if (cmd === 'list_vault' && args?.path === '/laputa') return Promise.resolve([])
- if (cmd === 'list_vault_folders' || cmd === 'list_views' || cmd === 'get_modified_files') return Promise.resolve([])
+ const response = args?.path === '/laputa' ? laputaStartupResponses[cmd] : undefined
+ if (response) return Promise.resolve(response)
+ if (EMPTY_ARRAY_COMMANDS.has(cmd)) return Promise.resolve([])
return Promise.resolve(null)
}) as typeof defaultMockInvoke)
diff --git a/src/hooks/vaultLoaderCommands.ts b/src/hooks/vaultLoaderCommands.ts
index 1f5e10b5..6587a779 100644
--- a/src/hooks/vaultLoaderCommands.ts
+++ b/src/hooks/vaultLoaderCommands.ts
@@ -173,7 +173,7 @@ export function loadVaultFolders({ vaultPath }: VaultPathOptions): Promise {
- const mountedVaults = uniqueMountedVaults({ ...options, includeFallbackVault: false })
+ const mountedVaults = uniqueMountedVaults(options)
if (mountedVaults.length === 0) return []
if (mountedVaults.length === 1) {
const [vault] = mountedVaults
@@ -213,7 +213,7 @@ export function loadVaultViews({ vaultPath }: VaultPathOptions): Promise {
- const mountedVaults = uniqueMountedVaults({ ...options, includeFallbackVault: false })
+ const mountedVaults = uniqueMountedVaults(options)
if (mountedVaults.length === 0) return []
if (mountedVaults.length === 1 && mountedVaults[0].path === options.vaultPath) {
return loadVaultViews({ vaultPath: options.vaultPath })
diff --git a/src/hooks/vaultLoaderCommands.views.test.ts b/src/hooks/vaultLoaderCommands.views.test.ts
index 5a0e6ae6..07a5baa8 100644
--- a/src/hooks/vaultLoaderCommands.views.test.ts
+++ b/src/hooks/vaultLoaderCommands.views.test.ts
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { loadMountedVaultViews } from './vaultLoaderCommands'
+import { loadMountedVaultFolders, loadMountedVaultViews } from './vaultLoaderCommands'
import type { ViewDefinition } from '../types'
const mockInvoke = vi.fn()
@@ -86,4 +86,93 @@ describe('loadMountedVaultViews', () => {
expect(views.map((view) => view.definition.name)).toEqual(['Default View', 'Mounted View'])
})
+
+ it('loads saved views from the active workspace when folder vaults omit it', async () => {
+ mockInvoke.mockImplementation((command: string, args?: Record) => {
+ if (command !== 'list_views') return Promise.resolve([])
+
+ if (args?.vaultPath === '/active') {
+ return Promise.resolve([{ filename: 'active.yml', definition: viewDefinition('Active View') }])
+ }
+ if (args?.vaultPath === '/mounted') {
+ return Promise.resolve([{ filename: 'mounted.yml', definition: viewDefinition('Mounted View') }])
+ }
+ throw new Error(`Unexpected vaultPath ${String(args?.vaultPath)}`)
+ })
+
+ const views = await loadMountedVaultViews({
+ defaultWorkspacePath: '/mounted',
+ vaultPath: '/active',
+ vaults: [
+ { label: 'Mounted', path: '/mounted', alias: 'mounted', mounted: true, available: true },
+ ],
+ })
+
+ expect(views.map((view) => [view.definition.name, view.rootPath])).toEqual([
+ ['Mounted View', '/mounted'],
+ ['Active View', '/active'],
+ ])
+ })
+})
+
+describe('loadMountedVaultFolders', () => {
+ beforeEach(() => {
+ mockInvoke.mockReset()
+ })
+
+ it('loads folders from the active workspace when folder vaults omit it', async () => {
+ mockInvoke.mockImplementation((command: string, args?: Record) => {
+ if (command !== 'list_vault_folders') return Promise.resolve([])
+
+ const path = String(args?.path)
+ if (path === '/active') {
+ return Promise.resolve([{ name: 'active-folder', path: 'active-folder', children: [] }])
+ }
+ if (path === '/mounted') {
+ return Promise.resolve([{ name: 'mounted-folder', path: 'mounted-folder', children: [] }])
+ }
+ throw new Error(`Unexpected path ${path}`)
+ })
+
+ const folders = await loadMountedVaultFolders({
+ defaultWorkspacePath: '/mounted',
+ vaultPath: '/active',
+ vaults: [
+ { label: 'Mounted', path: '/mounted', alias: 'mounted', mounted: true, available: true },
+ ],
+ })
+
+ expect(folders).toEqual([
+ {
+ name: 'Mounted',
+ path: '',
+ rootPath: '/mounted',
+ children: [{ name: 'mounted-folder', path: 'mounted-folder', rootPath: '/mounted', children: [] }],
+ },
+ {
+ name: 'active',
+ path: '',
+ rootPath: '/active',
+ children: [{ name: 'active-folder', path: 'active-folder', rootPath: '/active', children: [] }],
+ },
+ ])
+ })
+
+ it('does not duplicate the active workspace when folder vaults already include it', async () => {
+ mockInvoke.mockImplementation((command: string, args?: Record) => {
+ if (command !== 'list_vault_folders') return Promise.resolve([])
+ return Promise.resolve([{ name: `${String(args?.path)}-folder`, path: 'folder', children: [] }])
+ })
+
+ const folders = await loadMountedVaultFolders({
+ defaultWorkspacePath: '/active',
+ vaultPath: '/active',
+ vaults: [
+ { label: 'Active', path: '/active', alias: 'active', mounted: true, available: true },
+ ],
+ })
+
+ expect(mockInvoke).toHaveBeenCalledTimes(1)
+ expect(folders).toEqual([{ name: '/active-folder', path: 'folder', children: [] }])
+ })
})