detectFileOperation silently failed when tool input was undefined (timing issues with NDJSON event ordering). Added onVaultChanged callback as fallback: when Write/Edit/Bash tools complete but specific file can't be determined, vault.reloadVault() is triggered. Also added safety net in onDone handler. Threaded onVaultChanged through AiPanel → EditorRightPanel → Editor → App. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
179 lines
6.8 KiB
TypeScript
179 lines
6.8 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest'
|
|
import { detectFileOperation, parseBashFileCreation } from './useAiAgent'
|
|
import type { AgentFileCallbacks } from './useAiAgent'
|
|
|
|
const VAULT = '/Users/luca/Laputa'
|
|
|
|
function makeCallbacks() {
|
|
return {
|
|
onFileCreated: vi.fn(),
|
|
onFileModified: vi.fn(),
|
|
onVaultChanged: vi.fn(),
|
|
} satisfies AgentFileCallbacks
|
|
}
|
|
|
|
describe('detectFileOperation', () => {
|
|
it('calls onFileCreated for Write tool with .md in vault', () => {
|
|
const cb = makeCallbacks()
|
|
detectFileOperation('Write', JSON.stringify({ file_path: `${VAULT}/note/test.md` }), VAULT, cb)
|
|
expect(cb.onFileCreated).toHaveBeenCalledWith('note/test.md')
|
|
expect(cb.onFileModified).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('calls onFileModified for Edit tool with .md in vault', () => {
|
|
const cb = makeCallbacks()
|
|
detectFileOperation('Edit', JSON.stringify({ file_path: `${VAULT}/note/test.md` }), VAULT, cb)
|
|
expect(cb.onFileModified).toHaveBeenCalledWith('note/test.md')
|
|
expect(cb.onFileCreated).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('ignores non-md files', () => {
|
|
const cb = makeCallbacks()
|
|
detectFileOperation('Write', JSON.stringify({ file_path: `${VAULT}/image.png` }), VAULT, cb)
|
|
expect(cb.onFileCreated).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('ignores files outside vault', () => {
|
|
const cb = makeCallbacks()
|
|
detectFileOperation('Write', JSON.stringify({ file_path: '/tmp/other.md' }), VAULT, cb)
|
|
expect(cb.onFileCreated).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('ignores unknown tool names', () => {
|
|
const cb = makeCallbacks()
|
|
detectFileOperation('Read', JSON.stringify({ file_path: `${VAULT}/note/test.md` }), VAULT, cb)
|
|
expect(cb.onFileCreated).not.toHaveBeenCalled()
|
|
expect(cb.onFileModified).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('calls onVaultChanged when Write input is undefined', () => {
|
|
const cb = makeCallbacks()
|
|
detectFileOperation('Write', undefined, VAULT, cb)
|
|
expect(cb.onFileCreated).not.toHaveBeenCalled()
|
|
expect(cb.onVaultChanged).toHaveBeenCalled()
|
|
})
|
|
|
|
it('calls onVaultChanged when Edit input is undefined', () => {
|
|
const cb = makeCallbacks()
|
|
detectFileOperation('Edit', undefined, VAULT, cb)
|
|
expect(cb.onFileModified).not.toHaveBeenCalled()
|
|
expect(cb.onVaultChanged).toHaveBeenCalled()
|
|
})
|
|
|
|
it('calls onVaultChanged when Write has malformed JSON input', () => {
|
|
const cb = makeCallbacks()
|
|
detectFileOperation('Write', 'not-json', VAULT, cb)
|
|
expect(cb.onFileCreated).not.toHaveBeenCalled()
|
|
expect(cb.onVaultChanged).toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not call onVaultChanged when Write detects specific file', () => {
|
|
const cb = makeCallbacks()
|
|
detectFileOperation('Write', JSON.stringify({ file_path: `${VAULT}/note/test.md` }), VAULT, cb)
|
|
expect(cb.onFileCreated).toHaveBeenCalledWith('note/test.md')
|
|
expect(cb.onVaultChanged).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not call onVaultChanged for Read tool', () => {
|
|
const cb = makeCallbacks()
|
|
detectFileOperation('Read', undefined, VAULT, cb)
|
|
expect(cb.onVaultChanged).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('calls onVaultChanged when Bash input is undefined', () => {
|
|
const cb = makeCallbacks()
|
|
detectFileOperation('Bash', undefined, VAULT, cb)
|
|
expect(cb.onFileCreated).not.toHaveBeenCalled()
|
|
expect(cb.onVaultChanged).toHaveBeenCalled()
|
|
})
|
|
|
|
it('handles undefined callbacks gracefully', () => {
|
|
expect(() => detectFileOperation('Write', JSON.stringify({ file_path: `${VAULT}/note/test.md` }), VAULT, undefined)).not.toThrow()
|
|
})
|
|
|
|
it('detects Bash redirect creating .md file in vault', () => {
|
|
const cb = makeCallbacks()
|
|
detectFileOperation('Bash', JSON.stringify({ command: `echo "# Note" > ${VAULT}/note/new.md` }), VAULT, cb)
|
|
expect(cb.onFileCreated).toHaveBeenCalledWith('note/new.md')
|
|
})
|
|
|
|
it('detects Bash append redirect creating .md file', () => {
|
|
const cb = makeCallbacks()
|
|
detectFileOperation('Bash', JSON.stringify({ command: `cat content.txt >> ${VAULT}/daily/2026-03-07.md` }), VAULT, cb)
|
|
expect(cb.onFileCreated).toHaveBeenCalledWith('daily/2026-03-07.md')
|
|
})
|
|
|
|
it('detects Bash tee command creating .md file', () => {
|
|
const cb = makeCallbacks()
|
|
detectFileOperation('Bash', JSON.stringify({ command: `echo "content" | tee ${VAULT}/note/tee-note.md` }), VAULT, cb)
|
|
expect(cb.onFileCreated).toHaveBeenCalledWith('note/tee-note.md')
|
|
})
|
|
|
|
it('ignores Bash commands without file creation', () => {
|
|
const cb = makeCallbacks()
|
|
detectFileOperation('Bash', JSON.stringify({ command: 'ls -la' }), VAULT, cb)
|
|
expect(cb.onFileCreated).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('ignores Bash creating non-md files', () => {
|
|
const cb = makeCallbacks()
|
|
detectFileOperation('Bash', JSON.stringify({ command: `echo "data" > ${VAULT}/config.json` }), VAULT, cb)
|
|
expect(cb.onFileCreated).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('ignores Bash creating .md files outside vault', () => {
|
|
const cb = makeCallbacks()
|
|
detectFileOperation('Bash', JSON.stringify({ command: 'echo "text" > /tmp/other.md' }), VAULT, cb)
|
|
expect(cb.onFileCreated).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('uses path field when file_path is missing', () => {
|
|
const cb = makeCallbacks()
|
|
detectFileOperation('Write', JSON.stringify({ path: `${VAULT}/note/alt.md` }), VAULT, cb)
|
|
expect(cb.onFileCreated).toHaveBeenCalledWith('note/alt.md')
|
|
})
|
|
})
|
|
|
|
describe('parseBashFileCreation', () => {
|
|
it('returns null for undefined input', () => {
|
|
expect(parseBashFileCreation(undefined, VAULT)).toBeNull()
|
|
})
|
|
|
|
it('returns null for malformed JSON', () => {
|
|
expect(parseBashFileCreation('not json', VAULT)).toBeNull()
|
|
})
|
|
|
|
it('returns null when no command field', () => {
|
|
expect(parseBashFileCreation(JSON.stringify({ other: 'value' }), VAULT)).toBeNull()
|
|
})
|
|
|
|
it('returns null when command is not a string', () => {
|
|
expect(parseBashFileCreation(JSON.stringify({ command: 42 }), VAULT)).toBeNull()
|
|
})
|
|
|
|
it('parses simple redirect', () => {
|
|
const input = JSON.stringify({ command: `echo "# Title" > ${VAULT}/note.md` })
|
|
expect(parseBashFileCreation(input, VAULT)).toBe('note.md')
|
|
})
|
|
|
|
it('parses append redirect', () => {
|
|
const input = JSON.stringify({ command: `echo "line" >> ${VAULT}/sub/note.md` })
|
|
expect(parseBashFileCreation(input, VAULT)).toBe('sub/note.md')
|
|
})
|
|
|
|
it('parses tee command', () => {
|
|
const input = JSON.stringify({ command: `echo "data" | tee ${VAULT}/new.md` })
|
|
expect(parseBashFileCreation(input, VAULT)).toBe('new.md')
|
|
})
|
|
|
|
it('parses tee -a (append) command', () => {
|
|
const input = JSON.stringify({ command: `echo "extra" | tee -a ${VAULT}/new.md` })
|
|
expect(parseBashFileCreation(input, VAULT)).toBe('new.md')
|
|
})
|
|
|
|
it('returns null for non-md redirect', () => {
|
|
const input = JSON.stringify({ command: `echo "x" > ${VAULT}/file.txt` })
|
|
expect(parseBashFileCreation(input, VAULT)).toBeNull()
|
|
})
|
|
})
|