mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
test(filesource): add comprehensive test suite for FileSource feature
Add unit tests covering FileSource functionality including: - Basic FileSource operations and initialization - FileSourceConfig validation and management - Status character mapping and conversion - Recognition strategy testing (metadata, tags, templates, paths) - Edge cases and error handling Tests ensure reliability of file-based task recognition system and provide documentation through test cases for expected behavior.
This commit is contained in:
parent
738d7aa0c0
commit
4c82ab5b0f
3 changed files with 1021 additions and 0 deletions
422
src/__tests__/file-source/FileSource.basic.test.ts
Normal file
422
src/__tests__/file-source/FileSource.basic.test.ts
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
/**
|
||||
* Basic tests for FileSource
|
||||
*/
|
||||
|
||||
import { FileSource } from "../../dataflow/sources/FileSource";
|
||||
import type { FileSourceConfiguration } from "../../types/file-source";
|
||||
|
||||
// Mock Obsidian API
|
||||
const mockApp = {
|
||||
vault: {
|
||||
getAbstractFileByPath: jest.fn(),
|
||||
cachedRead: jest.fn(),
|
||||
offref: jest.fn(),
|
||||
getMarkdownFiles: jest.fn(() => [])
|
||||
},
|
||||
metadataCache: {
|
||||
getFileCache: jest.fn()
|
||||
},
|
||||
workspace: {
|
||||
trigger: jest.fn(),
|
||||
on: jest.fn(() => ({ unload: jest.fn() }))
|
||||
}
|
||||
} as any;
|
||||
|
||||
// Mock file object
|
||||
const mockFile = {
|
||||
path: 'test.md',
|
||||
stat: {
|
||||
ctime: 1000000,
|
||||
mtime: 2000000
|
||||
}
|
||||
};
|
||||
|
||||
describe('FileSource', () => {
|
||||
let fileSource: FileSource;
|
||||
let config: Partial<FileSourceConfiguration>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
|
||||
config = {
|
||||
enabled: false,
|
||||
recognitionStrategies: {
|
||||
metadata: {
|
||||
enabled: true,
|
||||
taskFields: ['dueDate', 'status'],
|
||||
requireAllFields: false
|
||||
},
|
||||
tags: {
|
||||
enabled: true,
|
||||
taskTags: ['#task', '#todo'],
|
||||
matchMode: 'exact'
|
||||
},
|
||||
templates: {
|
||||
enabled: false,
|
||||
templatePaths: [],
|
||||
checkTemplateMetadata: true
|
||||
},
|
||||
paths: {
|
||||
enabled: false,
|
||||
taskPaths: [],
|
||||
matchMode: 'prefix'
|
||||
}
|
||||
},
|
||||
fileTaskProperties: {
|
||||
contentSource: 'filename',
|
||||
stripExtension: true,
|
||||
defaultStatus: ' '
|
||||
},
|
||||
performance: {
|
||||
enableWorkerProcessing: false,
|
||||
enableCaching: true,
|
||||
cacheTTL: 300000
|
||||
},
|
||||
advanced: {
|
||||
excludePatterns: ['**/.obsidian/**'],
|
||||
maxFileSize: 1048576
|
||||
}
|
||||
};
|
||||
|
||||
fileSource = new FileSource(mockApp, config);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (fileSource) {
|
||||
fileSource.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize with provided configuration', () => {
|
||||
expect(fileSource).toBeInstanceOf(FileSource);
|
||||
});
|
||||
|
||||
it('should not initialize when disabled', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||
|
||||
fileSource.initialize();
|
||||
|
||||
expect(consoleSpy).not.toHaveBeenCalledWith(expect.stringContaining('[FileSource] Initializing'));
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should initialize when enabled', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||
|
||||
// Enable FileSource
|
||||
fileSource.updateConfiguration({ enabled: true });
|
||||
fileSource.initialize();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[FileSource] Initializing'));
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not initialize twice', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||
|
||||
fileSource.updateConfiguration({ enabled: true });
|
||||
fileSource.initialize();
|
||||
fileSource.initialize(); // Second call
|
||||
|
||||
// Should only log once
|
||||
const initLogs = consoleSpy.mock.calls.filter((call: any[]) =>
|
||||
call[0]?.includes('[FileSource] Initializing')
|
||||
);
|
||||
expect(initLogs).toHaveLength(1);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('file relevance checking', () => {
|
||||
beforeEach(() => {
|
||||
fileSource.updateConfiguration({ enabled: true });
|
||||
fileSource.initialize();
|
||||
});
|
||||
|
||||
it('should identify markdown files as relevant', async () => {
|
||||
mockApp.vault.getAbstractFileByPath.mockReturnValue(mockFile);
|
||||
mockApp.vault.cachedRead.mockResolvedValue('# Test content');
|
||||
mockApp.metadataCache.getFileCache.mockReturnValue({});
|
||||
|
||||
const result = await fileSource.shouldCreateFileTask('test.md');
|
||||
|
||||
// Should not throw error and should process the file
|
||||
expect(mockApp.vault.getAbstractFileByPath).toHaveBeenCalledWith('test.md');
|
||||
});
|
||||
|
||||
it('should reject non-markdown files', async () => {
|
||||
mockApp.vault.getAbstractFileByPath.mockReturnValue({
|
||||
...mockFile,
|
||||
path: 'test.txt'
|
||||
});
|
||||
|
||||
const result = await fileSource.shouldCreateFileTask('test.txt');
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockApp.vault.getAbstractFileByPath).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject excluded patterns', async () => {
|
||||
const result = await fileSource.shouldCreateFileTask('.obsidian/workspace.json');
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockApp.vault.getAbstractFileByPath).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject hidden files', async () => {
|
||||
const result = await fileSource.shouldCreateFileTask('.hidden-file.md');
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockApp.vault.getAbstractFileByPath).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('metadata-based recognition', () => {
|
||||
beforeEach(() => {
|
||||
fileSource.updateConfiguration({ enabled: true });
|
||||
fileSource.initialize();
|
||||
mockApp.vault.getAbstractFileByPath.mockReturnValue(mockFile);
|
||||
mockApp.vault.cachedRead.mockResolvedValue('# Test content');
|
||||
});
|
||||
|
||||
it('should recognize file with task metadata', async () => {
|
||||
const fileCache = {
|
||||
frontmatter: {
|
||||
dueDate: '2024-01-01',
|
||||
status: 'pending'
|
||||
}
|
||||
};
|
||||
mockApp.metadataCache.getFileCache.mockReturnValue(fileCache);
|
||||
|
||||
const result = await fileSource.shouldCreateFileTask('test.md');
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should recognize file with any task field when requireAllFields is false', async () => {
|
||||
const fileCache = {
|
||||
frontmatter: {
|
||||
dueDate: '2024-01-01'
|
||||
// missing 'status' but requireAllFields is false
|
||||
}
|
||||
};
|
||||
mockApp.metadataCache.getFileCache.mockReturnValue(fileCache);
|
||||
|
||||
const result = await fileSource.shouldCreateFileTask('test.md');
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should not recognize file without task metadata', async () => {
|
||||
const fileCache = {
|
||||
frontmatter: {
|
||||
title: 'Test File',
|
||||
description: 'Just a regular file'
|
||||
}
|
||||
};
|
||||
mockApp.metadataCache.getFileCache.mockReturnValue(fileCache);
|
||||
|
||||
const result = await fileSource.shouldCreateFileTask('test.md');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should not recognize file without frontmatter', async () => {
|
||||
const fileCache = {};
|
||||
mockApp.metadataCache.getFileCache.mockReturnValue(fileCache);
|
||||
|
||||
const result = await fileSource.shouldCreateFileTask('test.md');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tag-based recognition', () => {
|
||||
beforeEach(() => {
|
||||
fileSource.updateConfiguration({
|
||||
enabled: true,
|
||||
recognitionStrategies: {
|
||||
metadata: {
|
||||
enabled: false,
|
||||
taskFields: config.recognitionStrategies!.metadata.taskFields,
|
||||
requireAllFields: config.recognitionStrategies!.metadata.requireAllFields
|
||||
},
|
||||
tags: {
|
||||
enabled: true,
|
||||
taskTags: config.recognitionStrategies!.tags.taskTags,
|
||||
matchMode: config.recognitionStrategies!.tags.matchMode
|
||||
},
|
||||
templates: config.recognitionStrategies!.templates,
|
||||
paths: config.recognitionStrategies!.paths
|
||||
}
|
||||
});
|
||||
fileSource.initialize();
|
||||
mockApp.vault.getAbstractFileByPath.mockReturnValue(mockFile);
|
||||
mockApp.vault.cachedRead.mockResolvedValue('# Test content');
|
||||
});
|
||||
|
||||
it('should recognize file with task tags', async () => {
|
||||
const fileCache = {
|
||||
tags: [
|
||||
{ tag: '#task' },
|
||||
{ tag: '#important' }
|
||||
]
|
||||
};
|
||||
mockApp.metadataCache.getFileCache.mockReturnValue(fileCache);
|
||||
|
||||
const result = await fileSource.shouldCreateFileTask('test.md');
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should not recognize file without task tags', async () => {
|
||||
const fileCache = {
|
||||
tags: [
|
||||
{ tag: '#note' },
|
||||
{ tag: '#reference' }
|
||||
]
|
||||
};
|
||||
mockApp.metadataCache.getFileCache.mockReturnValue(fileCache);
|
||||
|
||||
const result = await fileSource.shouldCreateFileTask('test.md');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should not recognize file without tags', async () => {
|
||||
const fileCache = {};
|
||||
mockApp.metadataCache.getFileCache.mockReturnValue(fileCache);
|
||||
|
||||
const result = await fileSource.shouldCreateFileTask('test.md');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('task creation', () => {
|
||||
beforeEach(() => {
|
||||
fileSource.updateConfiguration({ enabled: true });
|
||||
fileSource.initialize();
|
||||
mockApp.vault.getAbstractFileByPath.mockReturnValue(mockFile);
|
||||
mockApp.vault.cachedRead.mockResolvedValue('# Test content');
|
||||
});
|
||||
|
||||
it('should create file task with correct structure', async () => {
|
||||
const fileCache = {
|
||||
frontmatter: {
|
||||
dueDate: '2024-01-01',
|
||||
status: 'x',
|
||||
priority: 2,
|
||||
project: 'Test Project'
|
||||
}
|
||||
};
|
||||
mockApp.metadataCache.getFileCache.mockReturnValue(fileCache);
|
||||
|
||||
const task = await fileSource.createFileTask('test.md');
|
||||
|
||||
expect(task).toBeTruthy();
|
||||
expect(task!.id).toBe('file-source:test.md');
|
||||
expect(task!.content).toBe('test'); // filename without extension
|
||||
expect(task!.filePath).toBe('test.md');
|
||||
expect(task!.line).toBe(0);
|
||||
expect(task!.completed).toBe(true); // status is 'x'
|
||||
expect(task!.status).toBe('x');
|
||||
expect(task!.metadata.source).toBe('file-source');
|
||||
expect(task!.metadata.recognitionStrategy).toBe('metadata');
|
||||
expect(task!.metadata.fileTimestamps.created).toBe(1000000);
|
||||
expect(task!.metadata.fileTimestamps.modified).toBe(2000000);
|
||||
expect(task!.metadata.priority).toBe(2);
|
||||
expect(task!.metadata.project).toBe('Test Project');
|
||||
});
|
||||
|
||||
it('should use default status when not specified', async () => {
|
||||
const fileCache = {
|
||||
frontmatter: {
|
||||
dueDate: '2024-01-01'
|
||||
}
|
||||
};
|
||||
mockApp.metadataCache.getFileCache.mockReturnValue(fileCache);
|
||||
|
||||
const task = await fileSource.createFileTask('test.md');
|
||||
|
||||
expect(task!.status).toBe(' '); // default status
|
||||
expect(task!.completed).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle missing file gracefully', async () => {
|
||||
mockApp.vault.getAbstractFileByPath.mockReturnValue(null);
|
||||
|
||||
const task = await fileSource.createFileTask('nonexistent.md');
|
||||
|
||||
expect(task).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle file read errors gracefully', async () => {
|
||||
mockApp.vault.cachedRead.mockRejectedValue(new Error('File read error'));
|
||||
|
||||
const task = await fileSource.createFileTask('test.md');
|
||||
|
||||
expect(task).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('statistics', () => {
|
||||
it('should provide initial statistics', () => {
|
||||
const stats = fileSource.getStats();
|
||||
|
||||
expect(stats.initialized).toBe(false);
|
||||
expect(stats.trackedFileCount).toBe(0);
|
||||
expect(stats.recognitionBreakdown.metadata).toBe(0);
|
||||
expect(stats.recognitionBreakdown.tag).toBe(0);
|
||||
expect(stats.lastUpdate).toBe(0);
|
||||
expect(stats.lastUpdateSeq).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('configuration updates', () => {
|
||||
it('should update configuration', () => {
|
||||
const newConfig = { enabled: true };
|
||||
|
||||
fileSource.updateConfiguration(newConfig);
|
||||
|
||||
// Configuration should be updated (we can't directly test it without exposing the config)
|
||||
// But we can test that it doesn't throw errors
|
||||
expect(() => fileSource.updateConfiguration(newConfig)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanup', () => {
|
||||
it('should destroy cleanly', () => {
|
||||
fileSource.updateConfiguration({ enabled: true });
|
||||
fileSource.initialize();
|
||||
|
||||
expect(() => fileSource.destroy()).not.toThrow();
|
||||
|
||||
const stats = fileSource.getStats();
|
||||
expect(stats.initialized).toBe(false);
|
||||
expect(stats.trackedFileCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle destroy when not initialized', () => {
|
||||
expect(() => fileSource.destroy()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('refresh', () => {
|
||||
it('should handle refresh when disabled', async () => {
|
||||
await expect(fileSource.refresh()).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle refresh when enabled', async () => {
|
||||
fileSource.updateConfiguration({ enabled: true });
|
||||
fileSource.initialize();
|
||||
|
||||
await expect(fileSource.refresh()).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
340
src/__tests__/file-source/FileSourceConfig.test.ts
Normal file
340
src/__tests__/file-source/FileSourceConfig.test.ts
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
/**
|
||||
* Tests for FileSourceConfig
|
||||
*/
|
||||
|
||||
import { FileSourceConfig, DEFAULT_FILE_SOURCE_CONFIG } from "../../dataflow/sources/FileSourceConfig";
|
||||
import type { FileSourceConfiguration } from "../../types/file-source";
|
||||
|
||||
describe('FileSourceConfig', () => {
|
||||
let config: FileSourceConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
config = new FileSourceConfig();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up any listeners
|
||||
if (config) {
|
||||
// Config doesn't expose cleanup method, but we can create a new instance
|
||||
config = new FileSourceConfig();
|
||||
}
|
||||
});
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should initialize with default configuration', () => {
|
||||
const result = config.getConfig();
|
||||
expect(result).toEqual(DEFAULT_FILE_SOURCE_CONFIG);
|
||||
});
|
||||
|
||||
it('should initialize with partial configuration', () => {
|
||||
const partial: Partial<FileSourceConfiguration> = {
|
||||
enabled: true,
|
||||
recognitionStrategies: {
|
||||
metadata: {
|
||||
enabled: false,
|
||||
taskFields: ['custom'],
|
||||
requireAllFields: true
|
||||
},
|
||||
tags: {
|
||||
enabled: false,
|
||||
taskTags: [],
|
||||
matchMode: 'exact'
|
||||
},
|
||||
templates: {
|
||||
enabled: false,
|
||||
templatePaths: [],
|
||||
checkTemplateMetadata: true
|
||||
},
|
||||
paths: {
|
||||
enabled: false,
|
||||
taskPaths: [],
|
||||
matchMode: 'prefix'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const customConfig = new FileSourceConfig(partial);
|
||||
const result = customConfig.getConfig();
|
||||
|
||||
expect(result.enabled).toBe(true);
|
||||
expect(result.recognitionStrategies.metadata.enabled).toBe(false);
|
||||
expect(result.recognitionStrategies.metadata.taskFields).toEqual(['custom']);
|
||||
expect(result.recognitionStrategies.metadata.requireAllFields).toBe(true);
|
||||
|
||||
// Other properties should have defaults
|
||||
expect(result.recognitionStrategies.tags.enabled).toBe(false); // Set to false in partial config
|
||||
});
|
||||
});
|
||||
|
||||
describe('configuration updates', () => {
|
||||
it('should update configuration and notify listeners', () => {
|
||||
return new Promise<void>((resolve) => {
|
||||
const updates: Partial<FileSourceConfiguration> = {
|
||||
enabled: true,
|
||||
fileTaskProperties: {
|
||||
contentSource: 'title',
|
||||
stripExtension: false,
|
||||
defaultStatus: 'x'
|
||||
}
|
||||
};
|
||||
|
||||
config.onChange((newConfig) => {
|
||||
expect(newConfig.enabled).toBe(true);
|
||||
expect(newConfig.fileTaskProperties.contentSource).toBe('title');
|
||||
expect(newConfig.fileTaskProperties.stripExtension).toBe(false);
|
||||
expect(newConfig.fileTaskProperties.defaultStatus).toBe('x');
|
||||
resolve();
|
||||
});
|
||||
|
||||
config.updateConfig(updates);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not notify listeners if configuration does not change', () => {
|
||||
const listener = jest.fn();
|
||||
config.onChange(listener);
|
||||
|
||||
// Update with same values
|
||||
config.updateConfig({ enabled: false });
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow multiple listeners', () => {
|
||||
const listener1 = jest.fn();
|
||||
const listener2 = jest.fn();
|
||||
|
||||
config.onChange(listener1);
|
||||
config.onChange(listener2);
|
||||
|
||||
config.updateConfig({ enabled: true });
|
||||
|
||||
expect(listener1).toHaveBeenCalledTimes(1);
|
||||
expect(listener2).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should allow unsubscribing listeners', () => {
|
||||
const listener = jest.fn();
|
||||
const unsubscribe = config.onChange(listener);
|
||||
|
||||
config.updateConfig({ enabled: true });
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
|
||||
unsubscribe();
|
||||
config.updateConfig({ enabled: false });
|
||||
expect(listener).toHaveBeenCalledTimes(1); // Should not be called again
|
||||
});
|
||||
});
|
||||
|
||||
describe('enabled strategies', () => {
|
||||
it('should return empty array when all strategies disabled', () => {
|
||||
config.updateConfig({
|
||||
recognitionStrategies: {
|
||||
metadata: { ...DEFAULT_FILE_SOURCE_CONFIG.recognitionStrategies.metadata, enabled: false },
|
||||
tags: { ...DEFAULT_FILE_SOURCE_CONFIG.recognitionStrategies.tags, enabled: false },
|
||||
templates: { ...DEFAULT_FILE_SOURCE_CONFIG.recognitionStrategies.templates, enabled: false },
|
||||
paths: { ...DEFAULT_FILE_SOURCE_CONFIG.recognitionStrategies.paths, enabled: false }
|
||||
}
|
||||
});
|
||||
|
||||
const strategies = config.getEnabledStrategies();
|
||||
expect(strategies).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return enabled strategies', () => {
|
||||
config.updateConfig({
|
||||
recognitionStrategies: {
|
||||
metadata: { ...DEFAULT_FILE_SOURCE_CONFIG.recognitionStrategies.metadata, enabled: true },
|
||||
tags: { ...DEFAULT_FILE_SOURCE_CONFIG.recognitionStrategies.tags, enabled: true },
|
||||
templates: { ...DEFAULT_FILE_SOURCE_CONFIG.recognitionStrategies.templates, enabled: false },
|
||||
paths: { ...DEFAULT_FILE_SOURCE_CONFIG.recognitionStrategies.paths, enabled: false }
|
||||
}
|
||||
});
|
||||
|
||||
const strategies = config.getEnabledStrategies();
|
||||
expect(strategies).toContain('metadata');
|
||||
expect(strategies).toContain('tags');
|
||||
expect(strategies).not.toContain('templates');
|
||||
expect(strategies).not.toContain('paths');
|
||||
});
|
||||
});
|
||||
|
||||
describe('configuration validation', () => {
|
||||
it('should validate valid configuration', () => {
|
||||
const validConfig: Partial<FileSourceConfiguration> = {
|
||||
enabled: true,
|
||||
recognitionStrategies: {
|
||||
metadata: {
|
||||
enabled: true,
|
||||
taskFields: ['dueDate', 'priority'],
|
||||
requireAllFields: false
|
||||
},
|
||||
tags: {
|
||||
enabled: false,
|
||||
taskTags: ['#test'],
|
||||
matchMode: 'exact'
|
||||
},
|
||||
templates: {
|
||||
enabled: false,
|
||||
templatePaths: ['template.md'],
|
||||
checkTemplateMetadata: true
|
||||
},
|
||||
paths: {
|
||||
enabled: false,
|
||||
taskPaths: ['test/'],
|
||||
matchMode: 'prefix'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const errors = config.validateConfig(validConfig);
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('should detect when no strategies are enabled', () => {
|
||||
const invalidConfig: Partial<FileSourceConfiguration> = {
|
||||
enabled: true,
|
||||
recognitionStrategies: {
|
||||
metadata: { enabled: false, taskFields: [], requireAllFields: false },
|
||||
tags: { enabled: false, taskTags: [], matchMode: 'exact' },
|
||||
templates: { enabled: false, templatePaths: [], checkTemplateMetadata: true },
|
||||
paths: { enabled: false, taskPaths: [], matchMode: 'prefix' }
|
||||
}
|
||||
};
|
||||
|
||||
const errors = config.validateConfig(invalidConfig);
|
||||
expect(errors).toContain('At least one recognition strategy must be enabled');
|
||||
});
|
||||
|
||||
it('should validate empty task fields', () => {
|
||||
const invalidConfig: Partial<FileSourceConfiguration> = {
|
||||
recognitionStrategies: {
|
||||
metadata: {
|
||||
enabled: true,
|
||||
taskFields: [],
|
||||
requireAllFields: false
|
||||
},
|
||||
tags: {
|
||||
enabled: false,
|
||||
taskTags: [],
|
||||
matchMode: 'exact'
|
||||
},
|
||||
templates: {
|
||||
enabled: false,
|
||||
templatePaths: [],
|
||||
checkTemplateMetadata: true
|
||||
},
|
||||
paths: {
|
||||
enabled: false,
|
||||
taskPaths: [],
|
||||
matchMode: 'prefix'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const errors = config.validateConfig(invalidConfig);
|
||||
expect(errors).toContain('Metadata strategy requires at least one task field');
|
||||
});
|
||||
|
||||
it('should validate custom content source', () => {
|
||||
const invalidConfig: Partial<FileSourceConfiguration> = {
|
||||
fileTaskProperties: {
|
||||
contentSource: 'custom',
|
||||
stripExtension: true,
|
||||
defaultStatus: ' '
|
||||
// Missing customContentField
|
||||
}
|
||||
};
|
||||
|
||||
const errors = config.validateConfig(invalidConfig);
|
||||
expect(errors).toContain('Custom content source requires customContentField to be specified');
|
||||
});
|
||||
|
||||
it('should validate cache TTL', () => {
|
||||
const invalidConfig: Partial<FileSourceConfiguration> = {
|
||||
performance: {
|
||||
enableWorkerProcessing: true,
|
||||
enableCaching: true,
|
||||
cacheTTL: -1000
|
||||
}
|
||||
};
|
||||
|
||||
const errors = config.validateConfig(invalidConfig);
|
||||
expect(errors).toContain('Cache TTL must be a positive number');
|
||||
});
|
||||
|
||||
it('should validate maximum file size', () => {
|
||||
const invalidConfig: Partial<FileSourceConfiguration> = {
|
||||
advanced: {
|
||||
excludePatterns: [],
|
||||
maxFileSize: 100 // Too small
|
||||
}
|
||||
};
|
||||
|
||||
const errors = config.validateConfig(invalidConfig);
|
||||
expect(errors).toContain('Maximum file size must be at least 1KB');
|
||||
});
|
||||
|
||||
it('should validate custom recognition function syntax', () => {
|
||||
const invalidConfig: Partial<FileSourceConfiguration> = {
|
||||
advanced: {
|
||||
excludePatterns: [],
|
||||
maxFileSize: 1024,
|
||||
customRecognitionFunction: 'invalid javascript syntax {'
|
||||
}
|
||||
};
|
||||
|
||||
const errors = config.validateConfig(invalidConfig);
|
||||
expect(errors).toContain('Custom recognition function has invalid syntax');
|
||||
});
|
||||
});
|
||||
|
||||
describe('configuration presets', () => {
|
||||
it('should create basic preset', () => {
|
||||
const preset = FileSourceConfig.createPreset('basic');
|
||||
|
||||
expect(preset.enabled).toBe(true);
|
||||
expect(preset.recognitionStrategies?.metadata.enabled).toBe(true);
|
||||
expect(preset.recognitionStrategies?.tags.enabled).toBe(false);
|
||||
expect(preset.recognitionStrategies?.templates.enabled).toBe(false);
|
||||
expect(preset.recognitionStrategies?.paths.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should create metadata-only preset', () => {
|
||||
const preset = FileSourceConfig.createPreset('metadata-only');
|
||||
|
||||
expect(preset.enabled).toBe(true);
|
||||
expect(preset.recognitionStrategies?.metadata.enabled).toBe(true);
|
||||
expect(preset.recognitionStrategies?.tags.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should create tag-only preset', () => {
|
||||
const preset = FileSourceConfig.createPreset('tag-only');
|
||||
|
||||
expect(preset.enabled).toBe(true);
|
||||
expect(preset.recognitionStrategies?.metadata.enabled).toBe(false);
|
||||
expect(preset.recognitionStrategies?.tags.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should create full preset', () => {
|
||||
const preset = FileSourceConfig.createPreset('full');
|
||||
|
||||
expect(preset.enabled).toBe(true);
|
||||
expect(preset.recognitionStrategies?.metadata.enabled).toBe(true);
|
||||
expect(preset.recognitionStrategies?.tags.enabled).toBe(true);
|
||||
expect(preset.recognitionStrategies?.templates.enabled).toBe(false);
|
||||
expect(preset.recognitionStrategies?.paths.enabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEnabled', () => {
|
||||
it('should return false by default', () => {
|
||||
expect(config.isEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when enabled', () => {
|
||||
config.updateConfig({ enabled: true });
|
||||
expect(config.isEnabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
259
src/__tests__/file-source/status-mapping.test.ts
Normal file
259
src/__tests__/file-source/status-mapping.test.ts
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
/**
|
||||
* Status Mapping Tests
|
||||
*
|
||||
* Tests for the status mapping functionality in FileSource
|
||||
*/
|
||||
|
||||
import { FileSourceConfig } from "../../dataflow/sources/FileSourceConfig";
|
||||
import type { FileSourceConfiguration } from "../../types/file-source";
|
||||
|
||||
describe("FileSource Status Mapping", () => {
|
||||
let config: FileSourceConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
config = new FileSourceConfig({
|
||||
enabled: true,
|
||||
statusMapping: {
|
||||
enabled: true,
|
||||
metadataToSymbol: {
|
||||
'completed': 'x',
|
||||
'done': 'x',
|
||||
'in-progress': '/',
|
||||
'in progress': '/',
|
||||
'planned': '?',
|
||||
'todo': '?',
|
||||
'cancelled': '-',
|
||||
'not-started': ' ',
|
||||
'not started': ' '
|
||||
},
|
||||
symbolToMetadata: {
|
||||
'x': 'completed',
|
||||
'X': 'completed',
|
||||
'/': 'in-progress',
|
||||
'?': 'planned',
|
||||
'-': 'cancelled',
|
||||
' ': 'not-started'
|
||||
},
|
||||
autoDetect: true,
|
||||
caseSensitive: false
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Metadata to Symbol Mapping", () => {
|
||||
test("should map common status values to symbols", () => {
|
||||
expect(config.mapMetadataToSymbol("completed")).toBe("x");
|
||||
expect(config.mapMetadataToSymbol("done")).toBe("x");
|
||||
expect(config.mapMetadataToSymbol("in-progress")).toBe("/");
|
||||
expect(config.mapMetadataToSymbol("planned")).toBe("?");
|
||||
expect(config.mapMetadataToSymbol("cancelled")).toBe("-");
|
||||
expect(config.mapMetadataToSymbol("not-started")).toBe(" ");
|
||||
});
|
||||
|
||||
test("should handle case-insensitive matching when configured", () => {
|
||||
expect(config.mapMetadataToSymbol("Completed")).toBe("x");
|
||||
expect(config.mapMetadataToSymbol("DONE")).toBe("x");
|
||||
expect(config.mapMetadataToSymbol("In-Progress")).toBe("/");
|
||||
expect(config.mapMetadataToSymbol("ToDo")).toBe("?");
|
||||
});
|
||||
|
||||
test("should handle values with spaces", () => {
|
||||
expect(config.mapMetadataToSymbol("in progress")).toBe("/");
|
||||
expect(config.mapMetadataToSymbol("not started")).toBe(" ");
|
||||
});
|
||||
|
||||
test("should return original value if no mapping exists", () => {
|
||||
expect(config.mapMetadataToSymbol("unknown")).toBe("unknown");
|
||||
expect(config.mapMetadataToSymbol("custom-status")).toBe("custom-status");
|
||||
});
|
||||
|
||||
test("should return original value when mapping is disabled", () => {
|
||||
const disabledConfig = new FileSourceConfig({
|
||||
enabled: true,
|
||||
statusMapping: {
|
||||
enabled: false,
|
||||
metadataToSymbol: { 'completed': 'x' },
|
||||
symbolToMetadata: { 'x': 'completed' },
|
||||
autoDetect: false,
|
||||
caseSensitive: false
|
||||
}
|
||||
});
|
||||
|
||||
expect(disabledConfig.mapMetadataToSymbol("completed")).toBe("completed");
|
||||
expect(disabledConfig.mapMetadataToSymbol("done")).toBe("done");
|
||||
});
|
||||
|
||||
test("should handle case-sensitive matching when configured", () => {
|
||||
const caseSensitiveConfig = new FileSourceConfig({
|
||||
enabled: true,
|
||||
statusMapping: {
|
||||
enabled: true,
|
||||
metadataToSymbol: {
|
||||
'completed': 'x',
|
||||
'Completed': 'X'
|
||||
},
|
||||
symbolToMetadata: {},
|
||||
autoDetect: false,
|
||||
caseSensitive: true
|
||||
}
|
||||
});
|
||||
|
||||
expect(caseSensitiveConfig.mapMetadataToSymbol("completed")).toBe("x");
|
||||
expect(caseSensitiveConfig.mapMetadataToSymbol("Completed")).toBe("X");
|
||||
expect(caseSensitiveConfig.mapMetadataToSymbol("COMPLETED")).toBe("COMPLETED");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Symbol to Metadata Mapping", () => {
|
||||
test("should map symbols back to metadata values", () => {
|
||||
expect(config.mapSymbolToMetadata("x")).toBe("completed");
|
||||
expect(config.mapSymbolToMetadata("X")).toBe("completed");
|
||||
expect(config.mapSymbolToMetadata("/")).toBe("in-progress");
|
||||
expect(config.mapSymbolToMetadata("?")).toBe("planned");
|
||||
expect(config.mapSymbolToMetadata("-")).toBe("cancelled");
|
||||
expect(config.mapSymbolToMetadata(" ")).toBe("not-started");
|
||||
});
|
||||
|
||||
test("should return original symbol if no mapping exists", () => {
|
||||
expect(config.mapSymbolToMetadata("@")).toBe("@");
|
||||
expect(config.mapSymbolToMetadata("!")).toBe("!");
|
||||
});
|
||||
|
||||
test("should return original symbol when mapping is disabled", () => {
|
||||
const disabledConfig = new FileSourceConfig({
|
||||
enabled: true,
|
||||
statusMapping: {
|
||||
enabled: false,
|
||||
metadataToSymbol: {},
|
||||
symbolToMetadata: { 'x': 'completed' },
|
||||
autoDetect: false,
|
||||
caseSensitive: false
|
||||
}
|
||||
});
|
||||
|
||||
expect(disabledConfig.mapSymbolToMetadata("x")).toBe("x");
|
||||
expect(disabledConfig.mapSymbolToMetadata("/")).toBe("/");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Status Recognition", () => {
|
||||
test("should recognize known status values", () => {
|
||||
expect(config.isRecognizedStatus("completed")).toBe(true);
|
||||
expect(config.isRecognizedStatus("done")).toBe(true);
|
||||
expect(config.isRecognizedStatus("x")).toBe(true);
|
||||
expect(config.isRecognizedStatus("/")).toBe(true);
|
||||
expect(config.isRecognizedStatus("?")).toBe(true);
|
||||
});
|
||||
|
||||
test("should recognize status values case-insensitively", () => {
|
||||
expect(config.isRecognizedStatus("Completed")).toBe(true);
|
||||
expect(config.isRecognizedStatus("DONE")).toBe(true);
|
||||
expect(config.isRecognizedStatus("In-Progress")).toBe(true);
|
||||
});
|
||||
|
||||
test("should not recognize unknown values", () => {
|
||||
expect(config.isRecognizedStatus("unknown")).toBe(false);
|
||||
expect(config.isRecognizedStatus("@")).toBe(false);
|
||||
expect(config.isRecognizedStatus("custom")).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false when mapping is disabled", () => {
|
||||
const disabledConfig = new FileSourceConfig({
|
||||
enabled: true,
|
||||
statusMapping: {
|
||||
enabled: false,
|
||||
metadataToSymbol: { 'completed': 'x' },
|
||||
symbolToMetadata: { 'x': 'completed' },
|
||||
autoDetect: false,
|
||||
caseSensitive: false
|
||||
}
|
||||
});
|
||||
|
||||
expect(disabledConfig.isRecognizedStatus("completed")).toBe(false);
|
||||
expect(disabledConfig.isRecognizedStatus("x")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Task Status Sync", () => {
|
||||
test("should sync with task status configuration", () => {
|
||||
const taskStatuses = {
|
||||
completed: "x|X",
|
||||
inProgress: "/>",
|
||||
planned: "?",
|
||||
abandoned: "-",
|
||||
notStarted: " "
|
||||
};
|
||||
|
||||
config.syncWithTaskStatuses(taskStatuses);
|
||||
const updatedConfig = config.getConfig();
|
||||
|
||||
expect(updatedConfig.statusMapping.symbolToMetadata['x']).toBe('completed');
|
||||
expect(updatedConfig.statusMapping.symbolToMetadata['X']).toBe('completed');
|
||||
expect(updatedConfig.statusMapping.symbolToMetadata['/']).toBe('in-progress');
|
||||
expect(updatedConfig.statusMapping.symbolToMetadata['>']).toBe('in-progress');
|
||||
expect(updatedConfig.statusMapping.symbolToMetadata['?']).toBe('planned');
|
||||
expect(updatedConfig.statusMapping.symbolToMetadata['-']).toBe('cancelled');
|
||||
expect(updatedConfig.statusMapping.symbolToMetadata[' ']).toBe('not-started');
|
||||
});
|
||||
|
||||
test("should not sync when autoDetect is disabled", () => {
|
||||
const noAutoConfig = new FileSourceConfig({
|
||||
enabled: true,
|
||||
statusMapping: {
|
||||
enabled: true,
|
||||
metadataToSymbol: {},
|
||||
symbolToMetadata: { 'x': 'old-value' },
|
||||
autoDetect: false,
|
||||
caseSensitive: false
|
||||
}
|
||||
});
|
||||
|
||||
const taskStatuses = {
|
||||
completed: "x|X",
|
||||
inProgress: "/"
|
||||
};
|
||||
|
||||
noAutoConfig.syncWithTaskStatuses(taskStatuses);
|
||||
const updatedConfig = noAutoConfig.getConfig();
|
||||
|
||||
// Should remain unchanged
|
||||
expect(updatedConfig.statusMapping.symbolToMetadata['x']).toBe('old-value');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Configuration Updates", () => {
|
||||
test("should notify listeners when configuration changes", (done: () => void) => {
|
||||
config.onChange((newConfig) => {
|
||||
expect(newConfig.statusMapping.metadataToSymbol['custom']).toBe('!');
|
||||
done();
|
||||
});
|
||||
|
||||
config.updateConfig({
|
||||
statusMapping: {
|
||||
enabled: true,
|
||||
metadataToSymbol: { 'custom': '!' },
|
||||
symbolToMetadata: { '!': 'custom' },
|
||||
autoDetect: true,
|
||||
caseSensitive: false
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("should merge partial updates with existing configuration", () => {
|
||||
config.updateConfig({
|
||||
statusMapping: {
|
||||
enabled: true,
|
||||
metadataToSymbol: { 'extra': '!' },
|
||||
symbolToMetadata: {},
|
||||
autoDetect: true,
|
||||
caseSensitive: false
|
||||
}
|
||||
});
|
||||
|
||||
const updatedConfig = config.getConfig();
|
||||
expect(updatedConfig.statusMapping.metadataToSymbol['extra']).toBe('!');
|
||||
// Original mappings should be lost with this update pattern
|
||||
// This is expected behavior for the current implementation
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue