Files
makelore/tests/unit/host-api-proxy.test.ts
brother7 5a275b93a7 feat: remove legacy OpenCode runtime
Cut product flows over to Coding/Pi and retain only the migration-owned v1 boundary. Promote supported native optional packages because electron-builder omitted pnpm transitive optional closure from the packaged ASAR.
2026-08-24 12:17:43 +08:00

161 lines
5.7 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
const handleMock = vi.fn();
vi.mock('electron', () => ({
ipcMain: { handle: handleMock },
}));
vi.mock('../../electron/utils/config', () => ({
getPort: () => 13210,
}));
vi.mock('../../electron/api/server', () => ({
getHostApiToken: () => 'host-token',
}));
vi.mock('../../electron/api/renderer-capability', () => ({
getRendererCapability: () => 'renderer-token',
RENDERER_CAPABILITY_HEADER: 'x-niancode-renderer-capability',
}));
describe('Host API IPC proxy', () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
vi.unstubAllGlobals();
});
it('connects to the loopback Host API with direct fetch', async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response(
JSON.stringify({ success: true }),
{ status: 200, headers: { 'content-type': 'application/json' } },
));
vi.stubGlobal('fetch', fetchMock);
const { registerHostApiProxyHandlers } = await import('../../electron/main/ipc/host-api-proxy');
registerHostApiProxyHandlers();
const fetchHandler = handleMock.mock.calls.find(([channel]) => channel === 'hostapi:fetch')?.[1];
expect(fetchHandler).toBeTypeOf('function');
const result = await fetchHandler(undefined, {
path: '/api/coding/projects/create',
method: 'POST',
body: JSON.stringify({ projectName: '测试项目' }),
});
expect(fetchMock).toHaveBeenCalledWith(
'http://127.0.0.1:13210/api/coding/projects/create',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
Authorization: 'Bearer host-token',
'Content-Type': 'application/json',
'x-niancode-renderer-capability': 'renderer-token',
}),
}),
);
expect(result).toEqual({
ok: true,
data: { status: 200, ok: true, json: { success: true } },
});
});
it('preserves ordinary renderer headers but replaces forged security headers', async () => {
const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 204 }));
vi.stubGlobal('fetch', fetchMock);
const { registerHostApiProxyHandlers } = await import('../../electron/main/ipc/host-api-proxy');
registerHostApiProxyHandlers();
const fetchHandler = handleMock.mock.calls.find(([channel]) => channel === 'hostapi:fetch')?.[1];
expect(fetchHandler).toBeTypeOf('function');
await fetchHandler(undefined, {
path: '/api/ai-hardware',
headers: {
Accept: 'application/json',
'X-Renderer-Request': 'hardware-overview',
authorization: 'Bearer renderer-forged-token',
'X-NIANCODE-RENDERER-CAPABILITY': 'renderer-forged-capability',
},
});
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
const headers = new Headers(init.headers);
expect(headers.get('accept')).toBe('application/json');
expect(headers.get('x-renderer-request')).toBe('hardware-overview');
expect(headers.get('authorization')).toBe('Bearer host-token');
expect(headers.get('x-niancode-renderer-capability')).toBe('renderer-token');
});
it('dispatches short JSON requests in Main without opening loopback HTTP', async () => {
const fetchMock = vi.fn();
vi.stubGlobal('fetch', fetchMock);
vi.doMock('../../electron/api/host-api-dispatcher', () => ({
dispatchHostApiRequest: vi.fn().mockResolvedValue({
status: 200,
ok: true,
json: { success: true, transport: 'test' },
}),
}));
const { registerHostApiProxyHandlers } = await import('../../electron/main/ipc/host-api-proxy');
registerHostApiProxyHandlers({} as never);
const fetchHandler = handleMock.mock.calls.find(([channel]) => channel === 'hostapi:fetch')?.[1];
const result = await fetchHandler(undefined, { path: '/api/app/runtime-info' });
expect(fetchMock).not.toHaveBeenCalled();
expect(result).toEqual({
ok: true,
data: { status: 200, ok: true, json: { success: true, transport: 'test' }, transport: 'dispatcher' },
});
});
it('keeps attachment upload bytes raw and returns image bytes through IPC', async () => {
const fetchMock = vi.fn()
.mockResolvedValueOnce(new Response(
JSON.stringify({ attachmentId: 'attachment-1', mime: 'image/png', byteLength: 4 }),
{ status: 201, headers: { 'content-type': 'application/json' } },
))
.mockResolvedValueOnce(new Response(
new Uint8Array([1, 2, 3, 4]),
{ status: 200, headers: { 'content-type': 'image/png' } },
));
vi.stubGlobal('fetch', fetchMock);
const { registerHostApiProxyHandlers } = await import('../../electron/main/ipc/host-api-proxy');
registerHostApiProxyHandlers({} as never);
const fetchHandler = handleMock.mock.calls.find(([channel]) => channel === 'hostapi:fetch')?.[1];
const upload = await fetchHandler(undefined, {
path: '/api/coding/attachments',
method: 'POST',
headers: { 'Content-Type': 'image/png' },
body: new Uint8Array([1, 2, 3, 4]).buffer,
});
const [, uploadInit] = fetchMock.mock.calls[0] as [string, RequestInit];
expect(Array.from(uploadInit.body as Uint8Array)).toEqual([1, 2, 3, 4]);
expect(new Headers(uploadInit.headers).get('content-type')).toBe('image/png');
expect(upload).toMatchObject({
ok: true,
data: { status: 201, json: { attachmentId: 'attachment-1' } },
});
const preview = await fetchHandler(undefined, {
path: '/api/coding/attachments/attachment-1/content',
method: 'GET',
});
expect(preview).toMatchObject({
ok: true,
data: {
status: 200,
contentType: 'image/png',
bytes: new Uint8Array([1, 2, 3, 4]),
},
});
});
});