103 lines
4.6 KiB
TypeScript
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 });
|
|
});
|
|
});
|