Merge branch 'main' into pr-736

This commit is contained in:
github-actions[bot]
2026-05-23 19:46:28 +00:00
committed by GitHub
2 changed files with 399 additions and 345 deletions

View File

@@ -2,7 +2,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, act, waitFor } from '@testing-library/react'
import type { VaultEntry, ModifiedFile, GitCommit, FolderNode } from '../types'
import { useVaultLoader, resolveNoteStatus } from './useVaultLoader'
import { workspaceIdentityFromVault } from '../utils/workspaces'
const mockEntries: VaultEntry[] = [
{
@@ -143,27 +142,6 @@ async function enableTauriMode() {
)
}
function buildMountedWorkspaceLoadMock(options: {
pendingEntriesByPath?: Record<string, Promise<VaultEntry[]>>
} = {}) {
return ((cmd: string, args?: Record<string, unknown>) => {
if (isVaultLoadCommand(cmd)) {
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`,
title: path === '/team' ? 'Team Hello' : 'Personal Hello',
}])
}
if (EMPTY_ARRAY_COMMANDS.has(cmd)) {
return Promise.resolve([])
}
return Promise.resolve(null)
}) as typeof defaultMockInvoke
}
describe('useVaultLoader', () => {
beforeEach(() => {
mockIsTauri = false
@@ -177,329 +155,6 @@ describe('useVaultLoader', () => {
expect(result.current.entries[0].title).toBe('Hello')
})
it('loads entries from every mounted workspace and annotates provenance', async () => {
backendInvokeFn.mockImplementation(buildMountedWorkspaceLoadMock())
const vaults = [
{ label: 'Personal', path: '/personal', alias: 'personal', available: true, mounted: true },
{ label: 'Team', path: '/team', alias: 'team', available: true, mounted: true },
]
const { result } = renderHook(() => useVaultLoader('/personal', vaults, '/personal'))
await waitForEntries(result, 2)
expect(result.current.entries.map((entry) => entry.workspace?.alias).sort()).toEqual(['personal', 'team'])
expect(result.current.entries.find((entry) => entry.workspace?.alias === 'team')?.workspace?.defaultForNewNotes).toBe(false)
})
it('loads one folder root per mounted workspace when folder vaults are provided', async () => {
const vaults = [
{ label: 'Personal', path: '/personal', alias: 'personal', available: true, mounted: true },
{ label: 'Team', path: '/team', alias: 'team', available: true, mounted: true },
]
backendInvokeFn.mockImplementation(((cmd: string, args?: Record<string, unknown>) => {
if (isVaultLoadCommand(cmd)) {
const path = args?.path
return Promise.resolve([{ ...mockEntries[0], path: `${path}/note/hello.md` }])
}
if (cmd === 'list_vault_folders') {
const path = args?.path
return Promise.resolve([
{ name: path === '/team' ? 'team-projects' : 'personal-projects', path: 'projects', children: [] },
])
}
if (cmd === 'list_views' || cmd === 'get_modified_files') return Promise.resolve([])
return Promise.resolve(null)
}) as typeof defaultMockInvoke)
const { result } = renderHook(() => useVaultLoader('/personal', vaults, '/personal', vaults))
await waitForEntries(result, 2)
await waitFor(() => {
expect(result.current.folders).toEqual([
{
name: 'Personal',
path: '',
rootPath: '/personal',
children: [{ name: 'personal-projects', path: 'projects', rootPath: '/personal', children: [] }],
},
{
name: 'Team',
path: '',
rootPath: '/team',
children: [{ name: 'team-projects', path: 'projects', rootPath: '/team', children: [] }],
},
])
})
})
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 },
]
const folderVaults = [entryVaults[1]]
backendInvokeFn.mockImplementation(((cmd: string, args?: Record<string, unknown>) => {
if (isVaultLoadCommand(cmd)) {
const path = args?.path
return Promise.resolve([{ ...mockEntries[0], path: `${path}/note/hello.md` }])
}
if (cmd === 'list_vault_folders') {
const path = args?.path
return Promise.resolve([
{ name: path === '/brian' ? 'brian-projects' : 'laputa-projects', path: 'projects', children: [] },
])
}
if (cmd === 'list_views' || cmd === 'get_modified_files') return Promise.resolve([])
return Promise.resolve(null)
}) as typeof defaultMockInvoke)
const { result } = renderHook(() => useVaultLoader('/brian', entryVaults, '/laputa', folderVaults))
await waitForEntries(result, 2)
await waitFor(() => {
expect(result.current.folders).toEqual([
{
name: 'Laputa',
path: '',
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: [] }],
},
])
})
})
it('updates workspace default metadata without reloading vault contents', async () => {
backendInvokeFn.mockImplementation(buildMountedWorkspaceLoadMock())
const vaults = [
{ label: 'Personal', path: '/personal', alias: 'personal', available: true, mounted: true },
{ label: 'Team', path: '/team', alias: 'team', available: true, mounted: true },
]
const { result, rerender } = renderHook(
({ defaultPath }) => useVaultLoader('/personal', vaults, defaultPath, vaults),
{ initialProps: { defaultPath: '/personal' } },
)
await waitForEntries(result, 2)
const vaultLoadCalls = backendInvokeFn.mock.calls.filter(([command]) => isVaultLoadCommand(command)).length
const folderLoadCalls = backendInvokeFn.mock.calls.filter(([command]) => command === 'list_vault_folders').length
rerender({ defaultPath: '/team' })
await waitFor(() => {
expect(result.current.entries.find((entry) => entry.workspace?.path === '/team')?.workspace?.defaultForNewNotes).toBe(true)
})
expect(backendInvokeFn.mock.calls.filter(([command]) => isVaultLoadCommand(command))).toHaveLength(vaultLoadCalls)
expect(backendInvokeFn.mock.calls.filter(([command]) => command === 'list_vault_folders')).toHaveLength(folderLoadCalls)
})
it('loads a newly added workspace incrementally without clearing existing entries', async () => {
backendInvokeFn.mockImplementation(buildMountedWorkspaceLoadMock())
const personal = { label: 'Personal', path: '/personal', alias: 'personal', available: true, mounted: true }
const team = { label: 'Team', path: '/team', alias: 'team', available: true, mounted: true }
const { result, rerender } = renderHook(
({ vaults }) => useVaultLoader('/personal', vaults, '/personal', vaults),
{ initialProps: { vaults: [personal] } },
)
await waitForEntries(result, 1)
expect(result.current.entries.map((entry) => entry.workspace?.path)).toEqual(['/personal'])
rerender({ vaults: [personal, team] })
expect(result.current.entries.map((entry) => entry.workspace?.path)).toContain('/personal')
await waitForEntries(result, 2)
expect(result.current.entries.map((entry) => entry.workspace?.path).sort()).toEqual(['/personal', '/team'])
})
it('keeps preloaded workspace entries mounted when switching the visible vault', async () => {
backendInvokeFn.mockImplementation(buildMountedWorkspaceLoadMock())
const personal = { label: 'Personal', path: '/personal', alias: 'personal', available: true, mounted: true }
const team = { label: 'Team', path: '/team', alias: 'team', available: true, mounted: true }
const vaults = [personal, team]
const { result, rerender } = renderHook(
({ path }) => useVaultLoader(path, vaults, path),
{ initialProps: { path: '/personal' } },
)
await waitForEntries(result, 2)
const vaultLoadCalls = backendInvokeFn.mock.calls.filter(([command]) => isVaultLoadCommand(command)).length
rerender({ path: '/team' })
expect(result.current.isLoading).toBe(false)
expect(result.current.entries.map((entry) => entry.workspace?.path).sort()).toEqual(['/personal', '/team'])
await waitFor(() => {
expect(backendInvokeFn.mock.calls.filter(([command]) => isVaultLoadCommand(command))).toHaveLength(vaultLoadCalls)
})
})
it('reloads scoped folder roots when another mounted workspace is added', async () => {
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 third = { label: 'Third', path: '/third', alias: 'third', available: true, mounted: true }
backendInvokeFn.mockImplementation(((cmd: string, args?: Record<string, unknown>) => {
const path = args?.path
if (isVaultLoadCommand(cmd)) return Promise.resolve([{ ...mockEntries[0], path: `${path}/note/hello.md` }])
if (cmd === 'list_vault_folders') {
const rootName = path === '/laputa'
? 'laputa-root'
: path === '/third'
? 'third-root'
: 'brian-root'
return Promise.resolve([{ name: rootName, path: 'root', children: [] }])
}
if (cmd === 'list_views' || cmd === 'get_modified_files') return Promise.resolve([])
return Promise.resolve(null)
}) as typeof defaultMockInvoke)
const { result, rerender } = renderHook(
({ folderVaults }) => useVaultLoader('/brian', [brian, laputa, third], '/brian', folderVaults),
{ initialProps: { folderVaults: [brian] } },
)
await waitForEntries(result, 3)
await waitFor(() => {
expect(result.current.folders.map((folder) => folder.name)).toEqual(['brian-root'])
})
rerender({ folderVaults: [brian, laputa] })
await waitFor(() => {
expect(result.current.folders.map((folder) => folder.rootPath)).toEqual(['/brian', '/laputa'])
})
rerender({ folderVaults: [brian, laputa, third] })
await waitFor(() => {
expect(result.current.folders.map((folder) => folder.rootPath)).toEqual(['/brian', '/laputa', '/third'])
})
})
it('adds each mounted workspace as soon as that workspace finishes loading', async () => {
const laputaLoad = createDeferred<VaultEntry[]>()
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(buildMountedWorkspaceLoadMock({
pendingEntriesByPath: { '/laputa': laputaLoad.promise },
}))
const { result } = renderHook(() => useVaultLoader('/brian', vaults, '/brian', vaults))
await waitFor(() => {
expect(result.current.entries.map((entry) => entry.workspace?.path).sort()).toEqual(['/brian', '/team'])
})
await act(async () => {
laputaLoad.resolve([{ ...mockEntries[0], path: '/laputa/note/hello.md' }])
})
await waitFor(() => {
expect(result.current.entries.map((entry) => entry.workspace?.path).sort()).toEqual(['/brian', '/laputa', '/team'])
})
})
it('preserves mounted workspace entries that arrive while the active vault scan is pending', async () => {
const field = { label: 'Field Notes', path: '/field', alias: 'field', available: true, mounted: true }
const research = { label: 'Research Lab', path: '/research', alias: 'research', available: true, mounted: true }
const vaults = [field, research]
const fieldLoad = createDeferred<VaultEntry[]>()
backendInvokeFn.mockImplementation(((cmd: string, args?: Record<string, unknown>) => {
const path = args?.path
if (isVaultLoadCommand(cmd)) {
if (path === '/field') return fieldLoad.promise
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)
const { result } = renderHook(() => useVaultLoader('/field', vaults, '/field', vaults))
act(() => {
result.current.addEntry({
...mockEntries[0],
path: '/research/note/hello.md',
title: 'Research Hello',
workspace: workspaceIdentityFromVault(research, { defaultWorkspacePath: '/field' }),
})
})
act(() => {
fieldLoad.resolve([{ ...mockEntries[0], path: '/field/note/hello.md', title: 'Field Hello' }])
})
await waitFor(() => {
expect(result.current.entries.map((entry) => entry.workspace?.path).sort()).toEqual(['/field', '/research'])
})
})
it('uses cached vault listing for background workspace loads in Tauri mode', async () => {
await enableTauriMode()
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 = [brian, laputa]
backendInvokeFn.mockImplementation(((cmd: string, args?: Record<string, unknown>) => {
if (isVaultLoadCommand(cmd)) {
const path = args?.path
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)
const { result } = renderHook(() => useVaultLoader('/brian', vaults, '/brian', vaults))
await waitForEntries(result, 2)
const laputaLoadCommands = backendInvokeFn.mock.calls
.filter(([, args]) => args?.path === '/laputa')
.map(([command]) => command)
expect(laputaLoadCommands).toContain('list_vault')
expect(laputaLoadCommands).not.toContain('reload_vault')
})
it('clears stale views immediately when switching to another preloaded workspace', async () => {
backendInvokeFn.mockImplementation(((cmd: string, args?: Record<string, unknown>) => {
const path = args?.path
if (isVaultLoadCommand(cmd)) return Promise.resolve([{ ...mockEntries[0], path: `${path}/note/hello.md` }])
if (cmd === 'list_views') {
return Promise.resolve(args?.vaultPath === '/brian'
? [{ filename: 'brian.yml', definition: { name: 'Brian View', icon: null, color: null, sort: null, filters: { all: [] } } }]
: [])
}
if (cmd === 'list_vault_folders' || cmd === 'get_modified_files') return Promise.resolve([])
return Promise.resolve(null)
}) as typeof defaultMockInvoke)
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 = [brian, laputa]
const { result, rerender } = renderHook(
({ path }) => useVaultLoader(path, vaults, path),
{ initialProps: { path: '/brian' } },
)
await waitFor(() => {
expect(result.current.views.map((view) => view.filename)).toEqual(['brian.yml'])
})
rerender({ path: '/laputa' })
expect(result.current.views).toEqual([])
await waitFor(() => {
expect(result.current.views).toEqual([])
})
})
it('normalizes missing entry and view string metadata from vault load', async () => {
backendInvokeFn.mockImplementation(((cmd: string) => {
if (isVaultLoadCommand(cmd)) {

View File

@@ -0,0 +1,399 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, act, waitFor } from '@testing-library/react'
import type { VaultEntry } from '../types'
import { useVaultLoader } from './useVaultLoader'
import { workspaceIdentityFromVault } from '../utils/workspaces'
const mockEntries: VaultEntry[] = [
{
path: '/vault/note/hello.md', filename: 'hello.md', title: 'Hello',
isA: 'Note', aliases: [], belongsTo: [], relatedTo: [],
status: 'Active',
archived: false,
modifiedAt: 1700000000, createdAt: 1700000000, fileSize: 100,
snippet: '', wordCount: 0, relationships: {}, icon: null, color: null, order: null, template: null, sort: null, outgoingLinks: [],
sidebarLabel: null, view: null, visible: null, organized: false, favorite: false, favoriteIndex: null,
listPropertiesDisplay: [], properties: {}, hasH1: false,
},
]
type MockCommandHandler = (args?: Record<string, unknown>) => unknown
const defaultMockHandlers: Record<string, MockCommandHandler> = {
list_vault: () => mockEntries,
reload_vault: () => mockEntries,
get_modified_files: () => [],
list_vault_folders: () => [],
list_views: () => [],
}
function defaultMockInvoke(cmd: string, args?: Record<string, unknown>) {
const handler = Reflect.get(defaultMockHandlers, cmd) as ((args?: Record<string, unknown>) => unknown) | undefined
return Promise.resolve(handler ? handler(args) : null)
}
let mockIsTauri = false
const backendInvokeFn = vi.fn(defaultMockInvoke)
function isVaultLoadCommand(cmd: string) {
return cmd === 'list_vault' || cmd === 'reload_vault'
}
function createDeferred<T>() {
let resolve!: (value: T | PromiseLike<T>) => void
let reject!: (reason?: unknown) => void
const promise = new Promise<T>((res, rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn(),
}))
vi.mock('../mock-tauri', () => ({
isTauri: () => mockIsTauri,
mockInvoke: (cmd: string, args?: Record<string, unknown>) => backendInvokeFn(cmd, args),
}))
async function waitForEntries(
result: ReturnType<typeof renderHook<ReturnType<typeof useVaultLoader>, undefined>>['result'],
length = 1,
) {
await waitFor(() => {
expect(result.current.entries).toHaveLength(length)
})
}
async function enableTauriMode() {
mockIsTauri = true
const tauri = await import('@tauri-apps/api/core')
vi.mocked(tauri.invoke).mockImplementation((command: string, args?: Record<string, unknown>) =>
backendInvokeFn(command, args),
)
}
type EntryLoad = VaultEntry[] | Promise<VaultEntry[]>
function entryForPath(path: string): VaultEntry {
return {
...mockEntries[0],
path: `${path}/note/hello.md`,
title: path === '/team' ? 'Team Hello' : 'Personal Hello',
}
}
function backendFolder(name: string) {
return { name, path: 'projects', children: [] }
}
function mountedFolderRoot(label: string, rootPath: string, childName: string) {
return {
name: label,
path: '',
rootPath,
children: [{ name: childName, path: 'projects', rootPath, children: [] }],
}
}
function viewFile(filename: string, name: string) {
return {
filename,
definition: { name, icon: null, color: null, sort: null, filters: { all: [] } },
}
}
function mockWorkspaceBackend(options: {
entriesByPath?: Record<string, EntryLoad>
foldersByPath?: Record<string, ReturnType<typeof backendFolder>[]>
viewsByVaultPath?: Record<string, ReturnType<typeof viewFile>[]>
} = {}) {
backendInvokeFn.mockImplementation(((cmd: string, args?: Record<string, unknown>) => {
const path = typeof args?.path === 'string' ? args.path : ''
const vaultPath = typeof args?.vaultPath === 'string' ? args.vaultPath : ''
if (isVaultLoadCommand(cmd)) {
return Promise.resolve(options.entriesByPath?.[path] ?? [entryForPath(path)])
}
if (cmd === 'list_vault_folders') {
return Promise.resolve(options.foldersByPath?.[path] ?? [])
}
if (cmd === 'list_views') {
return Promise.resolve(options.viewsByVaultPath?.[vaultPath] ?? [])
}
if (cmd === 'get_modified_files') {
return Promise.resolve([])
}
return Promise.resolve(null)
}) as typeof defaultMockInvoke)
}
describe('useVaultLoader workspaces', () => {
beforeEach(() => {
mockIsTauri = false
backendInvokeFn.mockReset()
backendInvokeFn.mockImplementation(defaultMockInvoke)
})
it('loads entries from every mounted workspace and annotates provenance', async () => {
mockWorkspaceBackend()
const vaults = [
{ label: 'Personal', path: '/personal', alias: 'personal', available: true, mounted: true },
{ label: 'Team', path: '/team', alias: 'team', available: true, mounted: true },
]
const { result } = renderHook(() => useVaultLoader('/personal', vaults, '/personal'))
await waitForEntries(result, 2)
expect(result.current.entries.map((entry) => entry.workspace?.alias).sort()).toEqual(['personal', 'team'])
expect(result.current.entries.find((entry) => entry.workspace?.alias === 'team')?.workspace?.defaultForNewNotes).toBe(false)
})
it('loads one folder root per mounted workspace when folder vaults are provided', async () => {
const vaults = [
{ label: 'Personal', path: '/personal', alias: 'personal', available: true, mounted: true },
{ label: 'Team', path: '/team', alias: 'team', available: true, mounted: true },
]
mockWorkspaceBackend({
foldersByPath: {
'/personal': [backendFolder('personal-projects')],
'/team': [backendFolder('team-projects')],
},
})
const { result } = renderHook(() => useVaultLoader('/personal', vaults, '/personal', vaults))
await waitForEntries(result, 2)
await waitFor(() => {
expect(result.current.folders).toEqual([
mountedFolderRoot('Personal', '/personal', 'personal-projects'),
mountedFolderRoot('Team', '/team', 'team-projects'),
])
})
})
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 },
]
const folderVaults = [entryVaults[1]]
mockWorkspaceBackend({
foldersByPath: {
'/brian': [backendFolder('brian-projects')],
'/laputa': [backendFolder('laputa-projects')],
},
})
const { result } = renderHook(() => useVaultLoader('/brian', entryVaults, '/laputa', folderVaults))
await waitForEntries(result, 2)
await waitFor(() => {
expect(result.current.folders).toEqual([
mountedFolderRoot('Laputa', '/laputa', 'laputa-projects'),
mountedFolderRoot('brian', '/brian', 'brian-projects'),
])
})
})
it('updates workspace default metadata without reloading vault contents', async () => {
mockWorkspaceBackend()
const vaults = [
{ label: 'Personal', path: '/personal', alias: 'personal', available: true, mounted: true },
{ label: 'Team', path: '/team', alias: 'team', available: true, mounted: true },
]
const { result, rerender } = renderHook(
({ defaultPath }) => useVaultLoader('/personal', vaults, defaultPath, vaults),
{ initialProps: { defaultPath: '/personal' } },
)
await waitForEntries(result, 2)
const vaultLoadCalls = backendInvokeFn.mock.calls.filter(([command]) => isVaultLoadCommand(command)).length
const folderLoadCalls = backendInvokeFn.mock.calls.filter(([command]) => command === 'list_vault_folders').length
rerender({ defaultPath: '/team' })
await waitFor(() => {
expect(result.current.entries.find((entry) => entry.workspace?.path === '/team')?.workspace?.defaultForNewNotes).toBe(true)
})
expect(backendInvokeFn.mock.calls.filter(([command]) => isVaultLoadCommand(command))).toHaveLength(vaultLoadCalls)
expect(backendInvokeFn.mock.calls.filter(([command]) => command === 'list_vault_folders')).toHaveLength(folderLoadCalls)
})
it('loads a newly added workspace incrementally without clearing existing entries', async () => {
mockWorkspaceBackend()
const personal = { label: 'Personal', path: '/personal', alias: 'personal', available: true, mounted: true }
const team = { label: 'Team', path: '/team', alias: 'team', available: true, mounted: true }
const { result, rerender } = renderHook(
({ vaults }) => useVaultLoader('/personal', vaults, '/personal', vaults),
{ initialProps: { vaults: [personal] } },
)
await waitForEntries(result, 1)
expect(result.current.entries.map((entry) => entry.workspace?.path)).toEqual(['/personal'])
rerender({ vaults: [personal, team] })
expect(result.current.entries.map((entry) => entry.workspace?.path)).toContain('/personal')
await waitForEntries(result, 2)
expect(result.current.entries.map((entry) => entry.workspace?.path).sort()).toEqual(['/personal', '/team'])
})
it('keeps preloaded workspace entries mounted when switching the visible vault', async () => {
mockWorkspaceBackend()
const personal = { label: 'Personal', path: '/personal', alias: 'personal', available: true, mounted: true }
const team = { label: 'Team', path: '/team', alias: 'team', available: true, mounted: true }
const vaults = [personal, team]
const { result, rerender } = renderHook(
({ path }) => useVaultLoader(path, vaults, path),
{ initialProps: { path: '/personal' } },
)
await waitForEntries(result, 2)
const vaultLoadCalls = backendInvokeFn.mock.calls.filter(([command]) => isVaultLoadCommand(command)).length
rerender({ path: '/team' })
expect(result.current.isLoading).toBe(false)
expect(result.current.entries.map((entry) => entry.workspace?.path).sort()).toEqual(['/personal', '/team'])
await waitFor(() => {
expect(backendInvokeFn.mock.calls.filter(([command]) => isVaultLoadCommand(command))).toHaveLength(vaultLoadCalls)
})
})
it('reloads scoped folder roots when another mounted workspace is added', async () => {
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 third = { label: 'Third', path: '/third', alias: 'third', available: true, mounted: true }
mockWorkspaceBackend({
foldersByPath: {
'/brian': [backendFolder('brian-root')],
'/laputa': [backendFolder('laputa-root')],
'/third': [backendFolder('third-root')],
},
})
const { result, rerender } = renderHook(
({ folderVaults }) => useVaultLoader('/brian', [brian, laputa, third], '/brian', folderVaults),
{ initialProps: { folderVaults: [brian] } },
)
await waitForEntries(result, 3)
await waitFor(() => {
expect(result.current.folders.map((folder) => folder.name)).toEqual(['brian-root'])
})
rerender({ folderVaults: [brian, laputa] })
await waitFor(() => {
expect(result.current.folders.map((folder) => folder.rootPath)).toEqual(['/brian', '/laputa'])
})
rerender({ folderVaults: [brian, laputa, third] })
await waitFor(() => {
expect(result.current.folders.map((folder) => folder.rootPath)).toEqual(['/brian', '/laputa', '/third'])
})
})
it('adds each mounted workspace as soon as that workspace finishes loading', async () => {
const laputaLoad = createDeferred<VaultEntry[]>()
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]
mockWorkspaceBackend({
entriesByPath: { '/laputa': laputaLoad.promise },
})
const { result } = renderHook(() => useVaultLoader('/brian', vaults, '/brian', vaults))
await waitFor(() => {
expect(result.current.entries.map((entry) => entry.workspace?.path).sort()).toEqual(['/brian', '/team'])
})
await act(async () => {
laputaLoad.resolve([{ ...mockEntries[0], path: '/laputa/note/hello.md' }])
})
await waitFor(() => {
expect(result.current.entries.map((entry) => entry.workspace?.path).sort()).toEqual(['/brian', '/laputa', '/team'])
})
})
it('preserves mounted workspace entries that arrive while the active vault scan is pending', async () => {
const field = { label: 'Field Notes', path: '/field', alias: 'field', available: true, mounted: true }
const research = { label: 'Research Lab', path: '/research', alias: 'research', available: true, mounted: true }
const vaults = [field, research]
const fieldLoad = createDeferred<VaultEntry[]>()
mockWorkspaceBackend({
entriesByPath: { '/field': fieldLoad.promise },
})
const { result } = renderHook(() => useVaultLoader('/field', vaults, '/field', vaults))
act(() => {
result.current.addEntry({
...mockEntries[0],
path: '/research/note/hello.md',
title: 'Research Hello',
workspace: workspaceIdentityFromVault(research, { defaultWorkspacePath: '/field' }),
})
})
act(() => {
fieldLoad.resolve([{ ...mockEntries[0], path: '/field/note/hello.md', title: 'Field Hello' }])
})
await waitFor(() => {
expect(result.current.entries.map((entry) => entry.workspace?.path).sort()).toEqual(['/field', '/research'])
})
})
it('uses cached vault listing for background workspace loads in Tauri mode', async () => {
await enableTauriMode()
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 = [brian, laputa]
mockWorkspaceBackend()
const { result } = renderHook(() => useVaultLoader('/brian', vaults, '/brian', vaults))
await waitForEntries(result, 2)
const laputaLoadCommands = backendInvokeFn.mock.calls
.filter(([, args]) => args?.path === '/laputa')
.map(([command]) => command)
expect(laputaLoadCommands).toContain('list_vault')
expect(laputaLoadCommands).not.toContain('reload_vault')
})
it('clears stale views immediately when switching to another preloaded workspace', async () => {
mockWorkspaceBackend({
viewsByVaultPath: {
'/brian': [viewFile('brian.yml', 'Brian View')],
},
})
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 = [brian, laputa]
const { result, rerender } = renderHook(
({ path }) => useVaultLoader(path, vaults, path),
{ initialProps: { path: '/brian' } },
)
await waitFor(() => {
expect(result.current.views.map((view) => view.filename)).toEqual(['brian.yml'])
})
rerender({ path: '/laputa' })
expect(result.current.views).toEqual([])
await waitFor(() => {
expect(result.current.views).toEqual([])
})
})
})