refactor(new merge) (#369)
Co-authored-by: paisley <8197966+su8su@users.noreply.github.com> Co-authored-by: zuolingxuan <zuolingxuan@bytedance.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
e28eba01e1
commit
3d664c017a
@@ -68,6 +68,16 @@ describe('api-client', () => {
|
||||
expect(msg).toContain('Permission denied');
|
||||
});
|
||||
|
||||
it('returns user-facing message for auth invalid error', () => {
|
||||
const msg = toUserMessage(new AppError('AUTH_INVALID', 'Invalid Authentication'));
|
||||
expect(msg).toContain('Authentication failed');
|
||||
});
|
||||
|
||||
it('returns user-facing message for channel unavailable error', () => {
|
||||
const msg = toUserMessage(new AppError('CHANNEL_UNAVAILABLE', 'Invalid IPC channel'));
|
||||
expect(msg).toContain('Service channel unavailable');
|
||||
});
|
||||
|
||||
it('falls back to legacy channel when unified route is unsupported', async () => {
|
||||
const invoke = vi.mocked(window.electron.ipcRenderer.invoke);
|
||||
invoke
|
||||
|
||||
26
tests/unit/error-model.test.ts
Normal file
26
tests/unit/error-model.test.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { AppError, mapBackendErrorCode, normalizeAppError } from '@/lib/error-model';
|
||||
|
||||
describe('error-model', () => {
|
||||
it('maps backend UNSUPPORTED to CHANNEL_UNAVAILABLE', () => {
|
||||
expect(mapBackendErrorCode('UNSUPPORTED')).toBe('CHANNEL_UNAVAILABLE');
|
||||
});
|
||||
|
||||
it('normalizes auth errors into AUTH_INVALID', () => {
|
||||
const error = normalizeAppError(new Error('HTTP 401: Invalid Authentication'));
|
||||
expect(error.code).toBe('AUTH_INVALID');
|
||||
});
|
||||
|
||||
it('normalizes ipc channel errors into CHANNEL_UNAVAILABLE', () => {
|
||||
const error = normalizeAppError(new Error('Invalid IPC channel: hostapi:fetch'));
|
||||
expect(error.code).toBe('CHANNEL_UNAVAILABLE');
|
||||
});
|
||||
|
||||
it('preserves AppError and merges details', () => {
|
||||
const base = new AppError('TIMEOUT', 'request timeout', undefined, { a: 1 });
|
||||
const normalized = normalizeAppError(base, { b: 2 });
|
||||
expect(normalized.code).toBe('TIMEOUT');
|
||||
expect(normalized.details).toEqual({ a: 1, b: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,14 +44,27 @@ describe('host-api', () => {
|
||||
expect(result.ok).toBe(1);
|
||||
});
|
||||
|
||||
it('throws proxy error from unified envelope', async () => {
|
||||
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);
|
||||
|
||||
invokeIpcMock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
error: { message: 'No handler registered for hostapi:fetch' },
|
||||
});
|
||||
|
||||
const { hostApiFetch } = await import('@/lib/host-api');
|
||||
await expect(hostApiFetch('/api/test')).rejects.toThrow('No handler registered for hostapi:fetch');
|
||||
const result = await hostApiFetch<{ fallback: boolean }>('/api/test');
|
||||
|
||||
expect(result.fallback).toBe(true);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'http://127.0.0.1:3210/api/test',
|
||||
expect.objectContaining({ headers: expect.any(Object) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws message from legacy non-ok envelope', async () => {
|
||||
|
||||
49
tests/unit/skills-errors.test.ts
Normal file
49
tests/unit/skills-errors.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const hostApiFetchMock = vi.fn();
|
||||
const rpcMock = vi.fn();
|
||||
|
||||
vi.mock('@/lib/host-api', () => ({
|
||||
hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@/stores/gateway', () => ({
|
||||
useGatewayStore: {
|
||||
getState: () => ({
|
||||
rpc: (...args: unknown[]) => rpcMock(...args),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('skills store error mapping', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('maps fetchSkills rate-limit error by AppError code', async () => {
|
||||
rpcMock.mockResolvedValueOnce({ skills: [] });
|
||||
hostApiFetchMock.mockRejectedValueOnce(new Error('rate limit exceeded'));
|
||||
|
||||
const { useSkillsStore } = await import('@/stores/skills');
|
||||
await useSkillsStore.getState().fetchSkills();
|
||||
|
||||
expect(useSkillsStore.getState().error).toBe('fetchRateLimitError');
|
||||
});
|
||||
|
||||
it('maps searchSkills timeout error by AppError code', async () => {
|
||||
hostApiFetchMock.mockRejectedValueOnce(new Error('request timeout'));
|
||||
|
||||
const { useSkillsStore } = await import('@/stores/skills');
|
||||
await useSkillsStore.getState().searchSkills('git');
|
||||
|
||||
expect(useSkillsStore.getState().searchError).toBe('searchTimeoutError');
|
||||
});
|
||||
|
||||
it('maps installSkill timeout result into installTimeoutError', async () => {
|
||||
hostApiFetchMock.mockResolvedValueOnce({ success: false, error: 'request timeout' });
|
||||
|
||||
const { useSkillsStore } = await import('@/stores/skills');
|
||||
await expect(useSkillsStore.getState().installSkill('demo-skill')).rejects.toThrow('installTimeoutError');
|
||||
});
|
||||
});
|
||||
53
tests/unit/usage-routes.test.ts
Normal file
53
tests/unit/usage-routes.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
|
||||
const getRecentTokenUsageHistoryMock = vi.fn();
|
||||
const sendJsonMock = vi.fn();
|
||||
|
||||
vi.mock('@electron/utils/token-usage', () => ({
|
||||
getRecentTokenUsageHistory: (...args: unknown[]) => getRecentTokenUsageHistoryMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@electron/api/route-utils', () => ({
|
||||
sendJson: (...args: unknown[]) => sendJsonMock(...args),
|
||||
}));
|
||||
|
||||
describe('handleUsageRoutes', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('passes undefined limit when query param is missing', async () => {
|
||||
getRecentTokenUsageHistoryMock.mockResolvedValueOnce([{ totalTokens: 1 }]);
|
||||
const { handleUsageRoutes } = await import('@electron/api/routes/usage');
|
||||
|
||||
const handled = await handleUsageRoutes(
|
||||
{ method: 'GET' } as IncomingMessage,
|
||||
{} as ServerResponse,
|
||||
new URL('http://127.0.0.1:3210/api/usage/recent-token-history'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(getRecentTokenUsageHistoryMock).toHaveBeenCalledWith(undefined);
|
||||
expect(sendJsonMock).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
200,
|
||||
[{ totalTokens: 1 }],
|
||||
);
|
||||
});
|
||||
|
||||
it('passes sanitized numeric limit when provided', async () => {
|
||||
getRecentTokenUsageHistoryMock.mockResolvedValueOnce([]);
|
||||
const { handleUsageRoutes } = await import('@electron/api/routes/usage');
|
||||
|
||||
await handleUsageRoutes(
|
||||
{ method: 'GET' } as IncomingMessage,
|
||||
{} as ServerResponse,
|
||||
new URL('http://127.0.0.1:3210/api/usage/recent-token-history?limit=50.9'),
|
||||
{} as never,
|
||||
);
|
||||
|
||||
expect(getRecentTokenUsageHistoryMock).toHaveBeenCalledWith(50);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user