Files
makelore/tests/unit/coding-project-files.test.ts

112 lines
5.3 KiB
TypeScript

// @vitest-environment node
import { execFile } from 'node:child_process';
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { promisify } from 'node:util';
import { afterEach, describe, expect, it } from 'vitest';
import { CodingProjectFileService } from '../../electron/coding-projects/project-files';
const exec = promisify(execFile);
const roots: string[] = [];
afterEach(async () => {
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
});
async function temporaryRoot(prefix: string): Promise<string> {
const root = await mkdtemp(path.join(tmpdir(), prefix));
roots.push(root);
return root;
}
async function git(root: string, ...args: string[]): Promise<void> {
await exec('git', ['-C', root, ...args], { windowsHide: true });
}
async function initializeRepository(root: string): Promise<void> {
await git(root, 'init');
await git(root, 'config', 'user.email', 'pi-files@example.invalid');
await git(root, 'config', 'user.name', 'PI Files');
await mkdir(path.join(root, 'src'), { recursive: true });
await writeFile(path.join(root, 'src', 'app.ts'), 'export const message = "baseline";\n', 'utf8');
await writeFile(path.join(root, '.gitignore'), 'ignored.txt\n', 'utf8');
await git(root, 'add', '.');
await git(root, 'commit', '-m', 'baseline');
}
describe('PI-105 project file service', () => {
it('reports Git status and finds tracked or untracked files using project-relative DTOs', async () => {
const root = await temporaryRoot('makelore-pi-files-git-');
await initializeRepository(root);
await writeFile(path.join(root, 'src', 'app.ts'), 'export const message = "changed";\n', 'utf8');
await writeFile(path.join(root, 'src', 'app.test.ts'), 'test("changed", () => true);\n', 'utf8');
await writeFile(path.join(root, 'ignored.txt'), 'ignored\n', 'utf8');
const service = new CodingProjectFileService();
expect(await service.status(root)).toEqual([
expect.objectContaining({ path: 'src/app.ts', name: 'app.ts', status: 'modified' }),
expect.objectContaining({ path: 'src/app.test.ts', name: 'app.test.ts', status: 'untracked' }),
]);
const found = await service.find(root, 'app', 1);
expect(found).toHaveLength(1);
expect(found[0]).toMatchObject({ path: 'src/app.ts', name: 'app.ts', type: 'file' });
expect(JSON.stringify({ status: await service.status(root), found })).not.toContain(root);
expect((await service.find(root, 'ignored')).map(({ path: filePath }) => filePath)).toEqual([]);
});
it('returns bounded UTF-8 content and rejects absolute, traversal, and binary reads', async () => {
const root = await temporaryRoot('makelore-pi-files-content-');
await writeFile(path.join(root, 'notes.txt'), 'hello project\n', 'utf8');
await writeFile(path.join(root, 'large.txt'), 'x'.repeat((256 * 1024) + 100), 'utf8');
await writeFile(path.join(root, 'large-unicode.txt'), '你'.repeat(90_000), 'utf8');
await writeFile(path.join(root, 'binary.bin'), Buffer.from([1, 0, 2]));
const service = new CodingProjectFileService();
expect(await service.content(root, 'notes.txt')).toEqual({
path: 'notes.txt', content: 'hello project\n', truncated: false,
});
const large = await service.content(root, 'large.txt');
expect(Buffer.byteLength(large.content, 'utf8')).toBe(256 * 1024);
expect(large.truncated).toBe(true);
const largeUnicode = await service.content(root, 'large-unicode.txt');
expect(Buffer.byteLength(largeUnicode.content, 'utf8')).toBeLessThanOrEqual(256 * 1024);
expect(largeUnicode.content).toMatch(/^+$/u);
expect(largeUnicode.truncated).toBe(true);
await expect(service.content(root, '../secret.txt')).rejects.toThrow('escapes the active project');
await expect(service.content(root, path.resolve(root, 'notes.txt'))).rejects.toThrow('must be relative');
await expect(service.content(root, 'binary.bin')).rejects.toThrow('Binary project files');
});
it('searches literal text with bounded relative matches and falls back outside Git', async () => {
const root = await temporaryRoot('makelore-pi-files-search-');
await mkdir(path.join(root, 'docs'), { recursive: true });
await mkdir(path.join(root, 'node_modules', 'private-package'), { recursive: true });
await writeFile(path.join(root, 'docs', 'guide.md'), 'First line\nNeedle and needle again\n', 'utf8');
await writeFile(path.join(root, 'node_modules', 'private-package', 'secret.txt'), 'needle\n', 'utf8');
const service = new CodingProjectFileService({
async run() {
throw Object.assign(new Error('spawn git ENOENT'), { code: 'ENOENT' });
},
});
expect((await service.find(root, 'guide')).map(({ path: filePath }) => filePath)).toEqual([
'docs/guide.md',
]);
const matches = await service.search(root, 'needle');
expect(matches).toEqual([{
path: 'docs/guide.md',
name: 'guide.md',
lineNumber: 2,
lineText: 'Needle and needle again',
submatches: [
{ text: 'Needle', start: 0, end: 6 },
{ text: 'needle', start: 11, end: 17 },
],
}]);
expect(JSON.stringify(matches)).not.toContain(root);
expect(JSON.stringify(matches)).not.toContain('private-package');
});
});