Files
hermes-web-ui/tests/client/files-store.test.ts
ekko cc21e0ba6a [codex] Fix session workspace file drawer (#1999)
* Fix session workspace file drawer

* Fix session workspace test mocks
2026-07-09 09:57:16 +08:00

187 lines
5.9 KiB
TypeScript

// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
const mockFilesApi = vi.hoisted(() => ({
listFiles: vi.fn(),
readFile: vi.fn(),
writeFile: vi.fn(),
deleteFile: vi.fn(),
renameFile: vi.fn(),
mkDir: vi.fn(),
copyFile: vi.fn(),
uploadFiles: vi.fn(),
}))
vi.mock('@/api/hermes/files', () => mockFilesApi)
const mockSessionsApi = vi.hoisted(() => ({
copySessionWorkspaceFile: vi.fn(),
deleteSessionWorkspaceFile: vi.fn(),
listSessionWorkspaceFiles: vi.fn(),
mkdirSessionWorkspaceFile: vi.fn(),
readSessionWorkspaceFile: vi.fn(),
renameSessionWorkspaceFile: vi.fn(),
writeSessionWorkspaceFile: vi.fn(),
}))
vi.mock('@/api/hermes/sessions', () => mockSessionsApi)
import { getLanguageFromPath, isPreviewableFile, isTextFile, useFilesStore } from '@/stores/hermes/files'
import type { FileEntry } from '@/api/hermes/files'
function deferred<T>() {
let resolve!: (value: T) => void
let reject!: (error: unknown) => void
const promise = new Promise<T>((res, rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
describe('files store', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
})
it('detects special workspace filenames and extensionless text files', () => {
expect(getLanguageFromPath('Dockerfile')).toBe('dockerfile')
expect(getLanguageFromPath('Makefile')).toBe('makefile')
expect(getLanguageFromPath('CMakeLists.txt')).toBe('cmake')
expect(getLanguageFromPath('.gitignore')).toBe('gitignore')
expect(getLanguageFromPath('.dockerignore')).toBe('gitignore')
expect(getLanguageFromPath('README')).toBe('plaintext')
expect(isTextFile('README')).toBe(true)
expect(isTextFile('LICENSE')).toBe(true)
expect(isTextFile('.env.local')).toBe(true)
expect(isTextFile('script.ts')).toBe(true)
expect(isTextFile('unknown-extensionless-binary')).toBe(false)
expect(isPreviewableFile('README')).toBe(true)
expect(isPreviewableFile('archive.zip')).toBe(false)
expect(isPreviewableFile('font.woff2')).toBe(false)
expect(isPreviewableFile('module.wasm')).toBe(false)
})
it('opens text previews with detected syntax language', async () => {
mockFilesApi.readFile.mockResolvedValue({
content: 'FROM node:20\nRUN npm test\n',
path: 'Dockerfile',
size: 27,
})
const store = useFilesStore()
const entry: FileEntry = {
name: 'Dockerfile',
path: 'Dockerfile',
isDir: false,
size: 27,
modTime: '2026-06-02T00:00:00.000Z',
}
await store.openPreview(entry)
expect(mockFilesApi.readFile).toHaveBeenCalledWith('Dockerfile', null)
expect(store.previewFile).toEqual({
path: 'Dockerfile',
profile: null,
type: 'text',
content: 'FROM node:20\nRUN npm test\n',
language: 'dockerfile',
})
})
it('resets profile scope for unscoped file panels', async () => {
mockFilesApi.listFiles.mockResolvedValue({ entries: [], path: '' })
const store = useFilesStore()
await store.fetchEntries('', { profile: 'reviewer' })
await store.fetchEntries('', { profile: null })
expect(store.currentProfile).toBeNull()
expect(mockFilesApi.listFiles).toHaveBeenLastCalledWith('', null)
})
it('does not let an older root fetch overwrite a later session workspace fetch', async () => {
const rootFetch = deferred<{ entries: FileEntry[]; path: string }>()
const rootEntry: FileEntry = {
name: 'regular-root',
path: 'regular-root',
isDir: true,
size: 0,
modTime: '2026-07-09T00:00:00.000Z',
}
const workspaceEntry: FileEntry = {
name: 'session-workspace',
path: 'session-workspace',
isDir: true,
size: 0,
modTime: '2026-07-09T00:00:00.000Z',
}
mockFilesApi.listFiles.mockReturnValueOnce(rootFetch.promise)
mockSessionsApi.listSessionWorkspaceFiles.mockResolvedValueOnce({
entries: [workspaceEntry],
path: '',
})
const store = useFilesStore()
const firstFetch = store.fetchEntries('', { profile: null })
const secondFetch = store.fetchEntries('', { workspaceSessionId: 'session-1' })
await secondFetch
expect(store.entries).toEqual([workspaceEntry])
expect(store.currentWorkspaceSessionId).toBe('session-1')
rootFetch.resolve({ entries: [rootEntry], path: '' })
await firstFetch
expect(store.entries).toEqual([workspaceEntry])
expect(mockFilesApi.listFiles).toHaveBeenCalledWith('', null)
expect(mockSessionsApi.listSessionWorkspaceFiles).toHaveBeenCalledWith('session-1', '')
})
it('keeps an explicit profile scope for config editor actions', async () => {
mockFilesApi.listFiles.mockResolvedValue({ entries: [], path: '' })
mockFilesApi.readFile.mockResolvedValue({
content: 'model:\n default: gpt-5.4\n',
path: 'config.yaml',
size: 28,
})
const store = useFilesStore()
await store.fetchEntries('', { profile: 'reviewer' })
await store.openEditor('config.yaml')
store.editingFile!.content = 'model:\n default: gpt-5.4-mini\n'
await store.saveEditor()
expect(store.currentProfile).toBe('reviewer')
expect(mockFilesApi.listFiles).toHaveBeenCalledWith('', 'reviewer')
expect(mockFilesApi.readFile).toHaveBeenCalledWith('config.yaml', 'reviewer')
expect(mockFilesApi.writeFile).toHaveBeenCalledWith('config.yaml', 'model:\n default: gpt-5.4-mini\n', 'reviewer')
})
it('opens image previews without reading file contents', async () => {
const store = useFilesStore()
const entry: FileEntry = {
name: 'diagram.png',
path: 'diagram.png',
isDir: false,
size: 128,
modTime: '2026-06-02T00:00:00.000Z',
}
await store.openPreview(entry)
expect(mockFilesApi.readFile).not.toHaveBeenCalled()
expect(store.previewFile).toEqual({
path: 'diagram.png',
profile: null,
type: 'image',
})
})
})