Fix semantic cache distance calculation, tool path validation, and add deferred settings persistence
- Convert Chroma cosine distance to similarity (1 - distance) for correct threshold comparison - Simplify forbidden directory check to match path segments instead of string prefixes - Add debounced settings save via onPersist callback in ChatView to prevent data loss - Fix workflow engine to pass availableTools to LLM when includeToolCalls is enabled - Add targetFolder support to AutoLinker config and update tests - Harden vault file indexing to await background indexing before processing create/modify/rename events
This commit is contained in:
@@ -366,6 +366,25 @@ describe('AutoLinker', () => {
|
||||
expect(mockVault.read).toHaveBeenCalledWith(files[0]);
|
||||
});
|
||||
|
||||
it('should update targetFolder through updateConfig', async () => {
|
||||
linker.setTargetFolder('Projects');
|
||||
linker.updateConfig({
|
||||
enabled: true,
|
||||
maxLinksPerNote: 3,
|
||||
similarityThreshold: 0.5,
|
||||
targetFolder: 'Archive',
|
||||
});
|
||||
const files = [{ path: 'Projects/note1.md' }, { path: 'Archive/note2.md' }] as any[];
|
||||
mockVault.getMarkdownFiles.mockReturnValue(files);
|
||||
mockVault.read.mockResolvedValue('Content');
|
||||
mockIndexer.searchVault!.mockResolvedValue([]);
|
||||
|
||||
await linker.run();
|
||||
|
||||
expect(mockVault.read).toHaveBeenCalledTimes(1);
|
||||
expect(mockVault.read).toHaveBeenCalledWith(files[1]);
|
||||
});
|
||||
|
||||
it('should return dry-run proposals when dryRun is true', async () => {
|
||||
const files = [{ path: 'note.md' }] as any[];
|
||||
mockVault.getMarkdownFiles.mockReturnValue(files);
|
||||
|
||||
@@ -119,7 +119,7 @@ describe('SemanticCacheService', () => {
|
||||
mockCollection.query.mockResolvedValue({
|
||||
ids: [['test-id']],
|
||||
documents: [[cachedContent]],
|
||||
distances: [[0.9]], // Above threshold
|
||||
distances: [[0.1]], // Similarity 0.9, above threshold
|
||||
});
|
||||
|
||||
const result = await cacheService.getCache('test query');
|
||||
@@ -127,6 +127,18 @@ describe('SemanticCacheService', () => {
|
||||
expect(result).toBe(cachedContent);
|
||||
expect(mockCollection.query).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return null when cosine distance is too high', async () => {
|
||||
mockCollection.query.mockResolvedValue({
|
||||
ids: [['test-id']],
|
||||
documents: [['unrelated cached response']],
|
||||
distances: [[0.9]], // Similarity 0.1, below threshold
|
||||
});
|
||||
|
||||
const result = await cacheService.getCache('test query');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCache', () => {
|
||||
|
||||
@@ -213,6 +213,38 @@ describe('ToolExecutor', () => {
|
||||
expect(mockVault.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject exact forbidden directory paths', async () => {
|
||||
const call: ToolCall = {
|
||||
id: 'call_forbidden_exact',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'create_file',
|
||||
arguments: JSON.stringify({
|
||||
path: '.obsidian',
|
||||
content: 'Test content',
|
||||
}),
|
||||
},
|
||||
};
|
||||
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
||||
expect(mockVault.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject nested forbidden directory paths', async () => {
|
||||
const call: ToolCall = {
|
||||
id: 'call_forbidden_nested',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'create_file',
|
||||
arguments: JSON.stringify({
|
||||
path: 'Notes/.git',
|
||||
content: 'Test content',
|
||||
}),
|
||||
},
|
||||
};
|
||||
await expect(executor.handleToolCall(call)).rejects.toThrow();
|
||||
expect(mockVault.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject path traversal attempts with .\\', async () => {
|
||||
const call: ToolCall = {
|
||||
id: 'call_8',
|
||||
@@ -1214,6 +1246,22 @@ describe('ToolExecutor', () => {
|
||||
expect(mockVault.createFolder).toHaveBeenCalledWith('Archive');
|
||||
expect(mockVault.rename).toHaveBeenCalledWith(file, 'Archive/old.md');
|
||||
});
|
||||
|
||||
it('should reject moving a note into a forbidden folder', async () => {
|
||||
const call: ToolCall = {
|
||||
id: 'call_mn_forbidden',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'move_note',
|
||||
arguments: JSON.stringify({
|
||||
path: 'Projects/old.md',
|
||||
folder: '.obsidian',
|
||||
}),
|
||||
},
|
||||
};
|
||||
await expect(executor.handleToolCall(call)).rejects.toThrow('Invalid folder path detected');
|
||||
expect(mockVault.rename).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete_note tool', () => {
|
||||
|
||||
Reference in New Issue
Block a user