* feat: track project-level .takt/pieces in version control * feat: track project-level takt facets for customizable resources * chore: include project .takt/config.yaml in git-tracked subset * takt: github-issue-284-faceted-prompting
175 lines
5.8 KiB
TypeScript
175 lines
5.8 KiB
TypeScript
/**
|
|
* Unit tests for faceted-prompting DataEngine implementations.
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { FileDataEngine, CompositeDataEngine } from '../../faceted-prompting/index.js';
|
|
import type { DataEngine, FacetKind } from '../../faceted-prompting/index.js';
|
|
|
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
|
|
vi.mock('node:fs', () => ({
|
|
existsSync: vi.fn(),
|
|
readFileSync: vi.fn(),
|
|
readdirSync: vi.fn(),
|
|
}));
|
|
|
|
const mockExistsSync = vi.mocked(existsSync);
|
|
const mockReadFileSync = vi.mocked(readFileSync);
|
|
const mockReaddirSync = vi.mocked(readdirSync);
|
|
|
|
beforeEach(() => {
|
|
vi.resetAllMocks();
|
|
});
|
|
|
|
describe('FileDataEngine', () => {
|
|
const engine = new FileDataEngine('/root');
|
|
|
|
describe('resolve', () => {
|
|
it('should return FacetContent when file exists', async () => {
|
|
mockExistsSync.mockReturnValue(true);
|
|
mockReadFileSync.mockReturnValue('persona body');
|
|
|
|
const result = await engine.resolve('personas', 'coder');
|
|
expect(result).toEqual({
|
|
body: 'persona body',
|
|
sourcePath: '/root/personas/coder.md',
|
|
});
|
|
});
|
|
|
|
it('should return undefined when file does not exist', async () => {
|
|
mockExistsSync.mockReturnValue(false);
|
|
|
|
const result = await engine.resolve('policies', 'missing');
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it('should resolve correct directory for each facet kind', async () => {
|
|
const kinds: FacetKind[] = ['personas', 'policies', 'knowledge', 'instructions', 'output-contracts'];
|
|
mockExistsSync.mockReturnValue(true);
|
|
mockReadFileSync.mockReturnValue('content');
|
|
|
|
for (const kind of kinds) {
|
|
const result = await engine.resolve(kind, 'test');
|
|
expect(result?.sourcePath).toBe(`/root/${kind}/test.md`);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('list', () => {
|
|
it('should return facet keys from directory', async () => {
|
|
mockExistsSync.mockReturnValue(true);
|
|
mockReaddirSync.mockReturnValue(['coder.md', 'architect.md', 'readme.txt'] as unknown as ReturnType<typeof readdirSync>);
|
|
|
|
const result = await engine.list('personas');
|
|
expect(result).toEqual(['coder', 'architect']);
|
|
});
|
|
|
|
it('should return empty array when directory does not exist', async () => {
|
|
mockExistsSync.mockReturnValue(false);
|
|
|
|
const result = await engine.list('policies');
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('should filter non-.md files', async () => {
|
|
mockExistsSync.mockReturnValue(true);
|
|
mockReaddirSync.mockReturnValue(['a.md', 'b.txt', 'c.md'] as unknown as ReturnType<typeof readdirSync>);
|
|
|
|
const result = await engine.list('knowledge');
|
|
expect(result).toEqual(['a', 'c']);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('CompositeDataEngine', () => {
|
|
it('should throw when constructed with empty engines array', () => {
|
|
expect(() => new CompositeDataEngine([])).toThrow(
|
|
'CompositeDataEngine requires at least one engine',
|
|
);
|
|
});
|
|
|
|
describe('resolve', () => {
|
|
it('should return result from first engine that resolves', async () => {
|
|
const engine1: DataEngine = {
|
|
resolve: vi.fn().mockResolvedValue(undefined),
|
|
list: vi.fn().mockResolvedValue([]),
|
|
};
|
|
const engine2: DataEngine = {
|
|
resolve: vi.fn().mockResolvedValue({ body: 'from engine2', sourcePath: '/e2/p.md' }),
|
|
list: vi.fn().mockResolvedValue([]),
|
|
};
|
|
|
|
const composite = new CompositeDataEngine([engine1, engine2]);
|
|
const result = await composite.resolve('personas', 'coder');
|
|
|
|
expect(result).toEqual({ body: 'from engine2', sourcePath: '/e2/p.md' });
|
|
expect(engine1.resolve).toHaveBeenCalledWith('personas', 'coder');
|
|
expect(engine2.resolve).toHaveBeenCalledWith('personas', 'coder');
|
|
});
|
|
|
|
it('should return first match (first-wins)', async () => {
|
|
const engine1: DataEngine = {
|
|
resolve: vi.fn().mockResolvedValue({ body: 'from engine1' }),
|
|
list: vi.fn().mockResolvedValue([]),
|
|
};
|
|
const engine2: DataEngine = {
|
|
resolve: vi.fn().mockResolvedValue({ body: 'from engine2' }),
|
|
list: vi.fn().mockResolvedValue([]),
|
|
};
|
|
|
|
const composite = new CompositeDataEngine([engine1, engine2]);
|
|
const result = await composite.resolve('personas', 'coder');
|
|
|
|
expect(result?.body).toBe('from engine1');
|
|
expect(engine2.resolve).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return undefined when no engine resolves', async () => {
|
|
const engine1: DataEngine = {
|
|
resolve: vi.fn().mockResolvedValue(undefined),
|
|
list: vi.fn().mockResolvedValue([]),
|
|
};
|
|
|
|
const composite = new CompositeDataEngine([engine1]);
|
|
const result = await composite.resolve('policies', 'missing');
|
|
|
|
expect(result).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('list', () => {
|
|
it('should return deduplicated keys from all engines', async () => {
|
|
const engine1: DataEngine = {
|
|
resolve: vi.fn(),
|
|
list: vi.fn().mockResolvedValue(['a', 'b']),
|
|
};
|
|
const engine2: DataEngine = {
|
|
resolve: vi.fn(),
|
|
list: vi.fn().mockResolvedValue(['b', 'c']),
|
|
};
|
|
|
|
const composite = new CompositeDataEngine([engine1, engine2]);
|
|
const result = await composite.list('personas');
|
|
|
|
expect(result).toEqual(['a', 'b', 'c']);
|
|
});
|
|
|
|
it('should preserve order with first-seen priority', async () => {
|
|
const engine1: DataEngine = {
|
|
resolve: vi.fn(),
|
|
list: vi.fn().mockResolvedValue(['x', 'y']),
|
|
};
|
|
const engine2: DataEngine = {
|
|
resolve: vi.fn(),
|
|
list: vi.fn().mockResolvedValue(['y', 'z']),
|
|
};
|
|
|
|
const composite = new CompositeDataEngine([engine1, engine2]);
|
|
const result = await composite.list('knowledge');
|
|
|
|
expect(result).toEqual(['x', 'y', 'z']);
|
|
});
|
|
});
|
|
});
|