feat(workspace): 接入真实Vault数据链路

This commit is contained in:
2026-08-31 21:38:56 +08:00
parent 84077feb18
commit 8da75d4420
23 changed files with 1055 additions and 442 deletions
@@ -0,0 +1,126 @@
// @vitest-environment happy-dom
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ApiErrorClass } from './apiClient'
import * as workspaceService from './workspaceService'
function jsonResponse(body: unknown, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { 'Content-Type': 'application/json' },
})
}
const workspaceSnapshot = {
workspace: {
vault_id: 'default',
name: 'vault',
path: 'C:\\data\\vault',
file_count: 1,
indexed_note_count: 1,
requires_refresh: false,
},
items: [
{
entry_id: 'folder-course',
name: '课程',
path: '/课程',
type: 'folder',
note_id: null,
children: [
{
entry_id: 'note-os',
note_id: 'note-os',
name: '操作系统.md',
path: '/课程/操作系统.md',
type: 'file',
children: [],
},
],
},
],
}
beforeEach(() => {
vi.stubGlobal('fetch', vi.fn())
})
afterEach(() => {
vi.unstubAllGlobals()
vi.restoreAllMocks()
})
describe('workspaceService backend adapter', () => {
it('opens the configured Vault and reads/saves Markdown through Note API', async () => {
const fetchMock = vi.mocked(fetch)
fetchMock.mockImplementation(async (input, init) => {
const url = String(input)
if (url === '/api/workspace/open') return jsonResponse(workspaceSnapshot)
if (url === '/api/notes/note-os' && init?.method === 'GET') {
return jsonResponse({
note_id: 'note-os', title: '操作系统', file_path: '课程/操作系统.md', tags: [],
created_at: '2026-08-31T00:00:00Z', updated_at: '2026-08-31T00:00:00Z',
markdown: '# 操作系统\n', blocks: [],
})
}
if (url === '/api/notes/note-os' && init?.method === 'PATCH') {
return jsonResponse({})
}
throw new Error(`Unexpected request: ${init?.method} ${url}`)
})
const vault = await workspaceService.openVault('C:\\data\\vault')
const tree = await workspaceService.getFileTree()
const markdown = await workspaceService.readFileContent('/课程/操作系统.md')
await workspaceService.saveFileContent('/课程/操作系统.md', '# 已更新\n')
expect(vault).toEqual({ path: 'C:\\data\\vault', name: 'vault' })
expect(tree[0].children?.[0]).toMatchObject({
id: 'note-os', note_id: 'note-os', path: '/课程/操作系统.md', type: 'file',
})
expect(markdown).toBe('# 操作系统\n')
const patchCall = fetchMock.mock.calls.find(([, init]) => init?.method === 'PATCH')
expect(JSON.parse(String(patchCall?.[1]?.body))).toEqual({ markdown: '# 已更新\n' })
})
it('creates notes and folders with Vault-relative paths', async () => {
const fetchMock = vi.mocked(fetch)
fetchMock.mockImplementation(async (input, init) => {
const url = String(input)
if (url === '/api/workspace/open') return jsonResponse(workspaceSnapshot)
if (url === '/api/notes' && init?.method === 'POST') {
return jsonResponse({
note_id: 'note-new', title: '新笔记', file_path: '课程/新笔记.md', tags: [],
created_at: '2026-08-31T00:00:00Z', updated_at: '2026-08-31T00:00:00Z',
markdown: '# 新笔记\n', blocks: [],
})
}
if (url === '/api/workspace/folders' && init?.method === 'POST') {
return jsonResponse({
entry_id: 'folder-child', name: '子目录', path: '/课程/子目录', type: 'folder',
note_id: null, children: [],
})
}
throw new Error(`Unexpected request: ${init?.method} ${url}`)
})
await workspaceService.openVault('C:\\data\\vault')
const note = await workspaceService.createFile('/课程', '新笔记.md', '# 新笔记\n')
const folder = await workspaceService.createFolder('/课程', '子目录')
expect(note).toMatchObject({ id: 'note-new', path: '/课程/新笔记.md' })
expect(folder).toMatchObject({ id: 'folder-child', path: '/课程/子目录' })
const bodies = fetchMock.mock.calls
.filter(([, init]) => init?.method === 'POST')
.map(([, init]) => JSON.parse(String(init?.body)))
expect(bodies).toContainEqual({ title: '新笔记', folder: '课程', markdown: '# 新笔记\n' })
expect(bodies).toContainEqual({ parent: '课程', name: '子目录' })
})
it('reports backend connectivity errors instead of falling back to Mock data', async () => {
vi.mocked(fetch).mockRejectedValue(new Error('offline'))
await expect(workspaceService.getWorkspaceInfo()).rejects.toEqual(
expect.objectContaining<Partial<ApiErrorClass>>({ code: 'NETWORK_ERROR' }),
)
})
})