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

103 lines
4.6 KiB
TypeScript

// @vitest-environment node
import path from 'node:path';
import { describe, expect, it, vi } from 'vitest';
import type { CodingProductHost } from '../../electron/api/coding-product-services';
import type { HostApiContext } from '../../electron/api/context';
import { dispatchHostApiRequest } from '../../electron/api/host-api-dispatcher';
function context(host?: CodingProductHost): HostApiContext {
return (host
? { codingProducts: { host, attachments: {}, productTools: {} } }
: {}) as HostApiContext;
}
function productHost(): CodingProductHost {
return {
fileStatus: vi.fn(async () => [{
path: 'src/app.ts', name: 'app.ts', type: 'file' as const, status: 'modified' as const,
}]),
findFiles: vi.fn(async () => [{
path: 'src/app.ts', name: 'app.ts', type: 'file' as const, size: 12,
}]),
fileContent: vi.fn(async () => ({
path: 'src/app.ts', content: 'export {};\n', truncated: false,
})),
searchText: vi.fn(async () => [{
path: 'src/app.ts', name: 'app.ts', lineNumber: 1, lineText: 'export {};', submatches: [],
}]),
listSkills: vi.fn(async () => [{
id: 'agent-browser' as const, name: 'Agent Browser', description: 'Browser', selected: true,
}]),
listCommands: vi.fn(async () => [{
name: 'compact', title: '压缩会话', description: 'Compact', source: 'makelore' as const,
}]),
getChanges: vi.fn(async () => ({
conversationId: 'conversation/a', runId: 'run-a', git: false, baselineHead: null, files: [],
})),
};
}
describe('PI-105 coding product routes', () => {
it('dispatches the seven vendor-neutral GET routes with stable DTOs', async () => {
const host = productHost();
const cases = [
['/api/coding/files/status', { files: [expect.objectContaining({ path: 'src/app.ts' })] }],
['/api/coding/files/find?query=app&limit=2', { files: [expect.objectContaining({ size: 12 })] }],
['/api/coding/files/content?path=src%2Fapp.ts', { file: expect.objectContaining({ content: 'export {};\n' }) }],
['/api/coding/search?pattern=export', { matches: [expect.objectContaining({ lineNumber: 1 })] }],
['/api/coding/skills?agentId=builder', { skills: [expect.objectContaining({ selected: true })] }],
['/api/coding/conversations/conversation%2Fa/commands', {
commands: [expect.objectContaining({ name: 'compact', source: 'makelore' })],
}],
['/api/coding/conversations/conversation%2Fa/changes', {
changes: expect.objectContaining({ conversationId: 'conversation/a', runId: 'run-a' }),
}],
] as const;
for (const [route, expected] of cases) {
const response = await dispatchHostApiRequest(context(host), { path: route });
expect(response).toMatchObject({ status: 200, ok: true, json: expected });
const serialized = JSON.stringify(response.json);
expect(serialized).not.toMatch(/"(todo|share|revert|unrevert)"/);
}
expect(host.findFiles).toHaveBeenCalledWith('app', 2);
expect(host.fileContent).toHaveBeenCalledWith('src/app.ts');
expect(host.listSkills).toHaveBeenCalledWith('builder');
expect(host.listCommands).toHaveBeenCalledWith('conversation/a');
expect(host.getChanges).toHaveBeenCalledWith('conversation/a');
});
it('reports unavailable composition and redacts absolute-path service failures', async () => {
expect(await dispatchHostApiRequest(context(), { path: '/api/coding/files/status' }))
.toMatchObject({
status: 503,
json: { success: false, code: 'CODING_PRODUCT_TOOLS_UNAVAILABLE' },
});
const host = productHost();
const absolute = path.resolve('private', 'secret.txt');
vi.mocked(host.fileContent).mockRejectedValueOnce(
Object.assign(new Error(`ENOENT: ${absolute}`), { code: 'ENOENT' }),
);
const response = await dispatchHostApiRequest(context(host), {
path: '/api/coding/files/content?path=missing.txt',
});
expect(response).toMatchObject({
status: 404,
json: { success: false, code: 'CODING_FILE_NOT_FOUND', error: 'Project file does not exist' },
});
expect(JSON.stringify(response.json)).not.toContain(absolute);
});
it('does not claim non-GET methods or unrelated coding routes', async () => {
expect(await dispatchHostApiRequest(context(productHost()), {
path: '/api/coding/files/status', method: 'POST',
headers: { 'content-type': 'application/json' }, body: '{}',
})).toMatchObject({ status: 404 });
expect(await dispatchHostApiRequest(context(productHost()), {
path: '/api/coding/conversations/conversation-a/prompt',
})).toMatchObject({ status: 404 });
});
});