Makelore 2.0 initial clean snapshot
This commit is contained in:
201
tests/unit/host-api.test.ts
Normal file
201
tests/unit/host-api.test.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const invokeIpcMock = vi.fn();
|
||||
|
||||
vi.mock('@/lib/api-client', () => ({
|
||||
invokeIpc: (...args: unknown[]) => invokeIpcMock(...args),
|
||||
}));
|
||||
|
||||
describe('host-api', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.resetAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
window.localStorage.removeItem('niancode:allow-localhost-fallback');
|
||||
});
|
||||
|
||||
it('uses IPC proxy and returns unified envelope json', async () => {
|
||||
invokeIpcMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
data: {
|
||||
status: 200,
|
||||
ok: true,
|
||||
json: { success: true },
|
||||
},
|
||||
});
|
||||
|
||||
const { hostApiFetch } = await import('@/lib/host-api');
|
||||
const result = await hostApiFetch<{ success: boolean }>('/api/settings');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(invokeIpcMock).toHaveBeenCalledWith(
|
||||
'hostapi:fetch',
|
||||
expect.objectContaining({ path: '/api/settings', method: 'GET' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws the Host API error from a unified non-ok HTTP response', async () => {
|
||||
invokeIpcMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
data: {
|
||||
status: 403,
|
||||
ok: false,
|
||||
json: { success: false, error: 'Game asset review is unavailable for this project template' },
|
||||
},
|
||||
});
|
||||
|
||||
const { hostApiFetch } = await import('@/lib/host-api');
|
||||
|
||||
const promise = hostApiFetch('/api/files/game-asset-review?invocationId=legacy-review');
|
||||
|
||||
await expect(promise).rejects.toMatchObject({
|
||||
message: 'Game asset review is unavailable for this project template',
|
||||
details: expect.objectContaining({ status: 403 }),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns undefined for a unified 204 response', async () => {
|
||||
invokeIpcMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
data: { status: 204, ok: true },
|
||||
});
|
||||
|
||||
const { hostApiFetch } = await import('@/lib/host-api');
|
||||
|
||||
await expect(hostApiFetch('/api/test')).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('supports legacy proxy envelope response', async () => {
|
||||
invokeIpcMock.mockResolvedValueOnce({
|
||||
success: true,
|
||||
status: 200,
|
||||
ok: true,
|
||||
json: { ok: 1 },
|
||||
});
|
||||
|
||||
const { hostApiFetch } = await import('@/lib/host-api');
|
||||
const result = await hostApiFetch<{ ok: number }>('/api/settings');
|
||||
expect(result.ok).toBe(1);
|
||||
});
|
||||
|
||||
it('returns undefined for a legacy 204 response', async () => {
|
||||
invokeIpcMock.mockResolvedValueOnce({
|
||||
success: true,
|
||||
status: 204,
|
||||
ok: true,
|
||||
});
|
||||
|
||||
const { hostApiFetch } = await import('@/lib/host-api');
|
||||
|
||||
await expect(hostApiFetch('/api/test')).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('falls back to browser fetch when hostapi handler is not registered', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ fallback: true }),
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
window.localStorage.setItem('niancode:allow-localhost-fallback', '1');
|
||||
|
||||
invokeIpcMock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
error: { message: 'No handler registered for hostapi:fetch' },
|
||||
});
|
||||
|
||||
const { hostApiFetch } = await import('@/lib/host-api');
|
||||
const result = await hostApiFetch<{ fallback: boolean }>('/api/test');
|
||||
|
||||
expect(result.fallback).toBe(true);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'http://127.0.0.1:13210/api/test',
|
||||
expect.objectContaining({ headers: expect.any(Object) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws message from legacy non-ok envelope', async () => {
|
||||
invokeIpcMock.mockResolvedValueOnce({
|
||||
success: true,
|
||||
ok: false,
|
||||
status: 401,
|
||||
json: { error: 'Invalid Authentication' },
|
||||
});
|
||||
|
||||
const { hostApiFetch } = await import('@/lib/host-api');
|
||||
await expect(hostApiFetch('/api/test')).rejects.toThrow('Invalid Authentication');
|
||||
});
|
||||
|
||||
it('preserves HTTP status from a legacy non-ok envelope', async () => {
|
||||
invokeIpcMock.mockResolvedValueOnce({
|
||||
success: true,
|
||||
ok: false,
|
||||
status: 503,
|
||||
json: { error: 'Service unavailable' },
|
||||
});
|
||||
|
||||
const { hostApiFetch } = await import('@/lib/host-api');
|
||||
|
||||
await expect(hostApiFetch('/api/test')).rejects.toMatchObject({
|
||||
message: 'Service unavailable',
|
||||
details: expect.objectContaining({ status: 503 }),
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to browser fetch only when IPC channel is unavailable', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ fallback: true }),
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
window.localStorage.setItem('niancode:allow-localhost-fallback', '1');
|
||||
|
||||
invokeIpcMock.mockRejectedValueOnce(new Error('Invalid IPC channel: hostapi:fetch'));
|
||||
|
||||
const { hostApiFetch } = await import('@/lib/host-api');
|
||||
const result = await hostApiFetch<{ fallback: boolean }>('/api/test');
|
||||
|
||||
expect(result.fallback).toBe(true);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'http://127.0.0.1:13210/api/test',
|
||||
expect.objectContaining({ headers: expect.any(Object) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not use localhost fallback when policy flag is disabled', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ fallback: true }),
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
invokeIpcMock.mockRejectedValueOnce(new Error('Invalid IPC channel: hostapi:fetch'));
|
||||
|
||||
const { hostApiFetch } = await import('@/lib/host-api');
|
||||
await expect(hostApiFetch('/api/test')).rejects.toThrow('Invalid IPC channel: hostapi:fetch');
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('primes the host api token and base url before creating an event stream url', async () => {
|
||||
const eventSourceMock = vi.fn();
|
||||
vi.stubGlobal('EventSource', eventSourceMock);
|
||||
invokeIpcMock.mockImplementation(async (channel: string) => {
|
||||
if (channel === 'hostapi:token') return 'secret-token';
|
||||
if (channel === 'hostapi:base-url') return 'http://127.0.0.1:4567';
|
||||
return null;
|
||||
});
|
||||
|
||||
const { createHostEventSource, ensureHostApiToken, getHostApiBase } = await import('@/lib/host-api');
|
||||
await ensureHostApiToken();
|
||||
createHostEventSource('/api/opencode/events?sessionId=ses_1');
|
||||
|
||||
expect(invokeIpcMock).toHaveBeenCalledWith('hostapi:token');
|
||||
expect(invokeIpcMock).toHaveBeenCalledWith('hostapi:base-url');
|
||||
expect(getHostApiBase()).toBe('http://127.0.0.1:4567');
|
||||
expect(eventSourceMock).toHaveBeenCalledWith(
|
||||
'http://127.0.0.1:4567/api/opencode/events?sessionId=ses_1&token=secret-token',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user