feat: update openclaw and polish desktop flows
This commit is contained in:
@@ -37,11 +37,13 @@ describe('App Center', () => {
|
||||
|
||||
expect(screen.getByTestId('app-center-item-product-center')).toHaveTextContent('旅游资源订购');
|
||||
expect(screen.getByTestId('app-center-item-product-center')).toHaveTextContent('旅游资源底价订购');
|
||||
expect(useAppCenterStore.getState().selectedItemId).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByTestId('app-center-item-product-center'));
|
||||
|
||||
expect(screen.getByTestId('product-center-route')).toBeVisible();
|
||||
expect(window.electron.openExternal).not.toHaveBeenCalled();
|
||||
expect(useAppCenterStore.getState().selectedItemId).toBeNull();
|
||||
});
|
||||
|
||||
it('covers built-in apps in English without exposing translation keys', async () => {
|
||||
|
||||
@@ -81,6 +81,21 @@ describe('host-api', () => {
|
||||
await expect(hostApiFetch('/api/test')).rejects.toThrow('Invalid Authentication');
|
||||
});
|
||||
|
||||
it('throws message from unified non-ok Host API responses', async () => {
|
||||
invokeIpcMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
data: {
|
||||
status: 500,
|
||||
ok: false,
|
||||
json: { success: false, error: 'WhatsApp startup failed' },
|
||||
},
|
||||
});
|
||||
|
||||
const { hostApiFetch } = await import('@/lib/host-api');
|
||||
await expect(hostApiFetch('/api/channels/whatsapp/start', { method: 'POST' }))
|
||||
.rejects.toThrow('WhatsApp startup failed');
|
||||
});
|
||||
|
||||
it('falls back to browser fetch only when IPC channel is unavailable', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
|
||||
148
tests/unit/settings-advanced-model-config.test.tsx
Normal file
148
tests/unit/settings-advanced-model-config.test.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { Settings } from '@/pages/Settings';
|
||||
import { useGatewayStore } from '@/stores/gateway';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { useYinianStore } from '@/stores/yinian';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
const hostApiFetchMock = vi.fn();
|
||||
|
||||
vi.mock('@/components/settings/ProvidersSettings', () => ({
|
||||
ProvidersSettings: () => <div data-testid="settings-advanced-model-config-panel">模型配置设置</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@/components/settings/UpdateSettings', () => ({
|
||||
UpdateSettings: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('@/pages/Channels', () => ({
|
||||
Channels: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('@/pages/YinianSkills', () => ({
|
||||
YinianSkills: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/host-api', () => ({
|
||||
hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/api-client', () => ({
|
||||
getGatewayWsDiagnosticEnabled: () => false,
|
||||
invokeIpc: vi.fn().mockResolvedValue({ success: true, command: 'agent' }),
|
||||
setGatewayWsDiagnosticEnabled: vi.fn(),
|
||||
toUserMessage: (error: unknown) => String(error),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/telemetry', () => ({
|
||||
clearUiTelemetry: vi.fn(),
|
||||
getUiTelemetrySnapshot: vi.fn(() => []),
|
||||
subscribeUiTelemetry: vi.fn(() => () => undefined),
|
||||
trackUiEvent: vi.fn(),
|
||||
}));
|
||||
|
||||
const hotel = {
|
||||
id: 'workspace-1',
|
||||
name: '智念空间',
|
||||
city: '杭州',
|
||||
role: 'manager' as const,
|
||||
permissions: [],
|
||||
ota: [],
|
||||
};
|
||||
|
||||
function renderRuntimeSettings() {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={['/settings/runtime']}>
|
||||
<Routes>
|
||||
<Route path="/settings/*" element={<Settings />} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('Settings advanced model config', () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
await i18n.changeLanguage('zh');
|
||||
hostApiFetchMock.mockImplementation((path: string) => {
|
||||
if (path.startsWith('/api/diagnostics/model-config')) {
|
||||
return Promise.resolve({
|
||||
capturedAt: 1,
|
||||
ok: true,
|
||||
model: { primary: null, fallbacks: [], providerKey: null, modelId: null },
|
||||
runtime: { heartbeatEvery: null, heartbeatDisabled: false },
|
||||
providers: [],
|
||||
authProfiles: { path: '', exists: false, providers: [] },
|
||||
checks: [],
|
||||
paths: { openclawConfig: '', authProfiles: '' },
|
||||
});
|
||||
}
|
||||
if (path.startsWith('/api/diagnostics/office-runtime')) {
|
||||
return Promise.resolve({
|
||||
capturedAt: 1,
|
||||
ok: true,
|
||||
repairAttempted: false,
|
||||
python: { executable: null, packages: [] },
|
||||
node: { modules: [] },
|
||||
dotnet: { available: false, version: null },
|
||||
checks: [],
|
||||
});
|
||||
}
|
||||
return Promise.resolve({});
|
||||
});
|
||||
useSettingsStore.setState({
|
||||
devModeUnlocked: false,
|
||||
gatewayAutoStart: false,
|
||||
telemetryEnabled: true,
|
||||
proxyEnabled: false,
|
||||
proxyServer: '',
|
||||
proxyHttpServer: '',
|
||||
proxyHttpsServer: '',
|
||||
proxyAllServer: '',
|
||||
proxyBypassRules: '<local>',
|
||||
});
|
||||
useGatewayStore.setState({
|
||||
status: { state: 'stopped', port: 18789, gatewayReady: false },
|
||||
});
|
||||
useYinianStore.setState({
|
||||
status: 'authenticated',
|
||||
session: {
|
||||
authenticated: true,
|
||||
user: { id: 'user-1', name: '王管理员' },
|
||||
hotels: [hotel],
|
||||
currentHotelId: hotel.id,
|
||||
accessTokenExpiresAt: 100,
|
||||
},
|
||||
config: {
|
||||
serverTime: 1,
|
||||
user: { id: 'user-1', name: '王管理员' },
|
||||
hotel,
|
||||
hotels: [hotel],
|
||||
entitlements: [],
|
||||
notificationChannels: [],
|
||||
featureFlags: {},
|
||||
uiPolicy: { defaultPage: 'today', showAdvancedSettings: false },
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps model configuration settings inside advanced mode', async () => {
|
||||
useSettingsStore.setState({ devModeUnlocked: true });
|
||||
|
||||
renderRuntimeSettings();
|
||||
|
||||
expect(await screen.findByTestId('settings-model-config-section')).toBeVisible();
|
||||
expect(screen.getByTestId('settings-advanced-model-config-panel')).toHaveTextContent('模型配置设置');
|
||||
expect(screen.getByTestId('settings-model-diagnostics')).toBeVisible();
|
||||
});
|
||||
|
||||
it('keeps model configuration settings hidden until advanced mode is opened', () => {
|
||||
renderRuntimeSettings();
|
||||
|
||||
expect(screen.queryByTestId('settings-model-config-section')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('settings-advanced-model-config-panel')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Sidebar } from '@/components/layout/Sidebar';
|
||||
import { useChatStore } from '@/stores/chat';
|
||||
@@ -144,7 +144,7 @@ describe('Sidebar layout', () => {
|
||||
expect(screen.queryByText('快速使用')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides pinned quick tasks when collapsed and keeps hover titles on icon buttons', () => {
|
||||
it('hides pinned quick tasks when collapsed and shows hover labels on icon buttons', async () => {
|
||||
useSettingsStore.setState({
|
||||
sidebarCollapsed: true,
|
||||
devModeUnlocked: false,
|
||||
@@ -159,6 +159,13 @@ describe('Sidebar layout', () => {
|
||||
expect(screen.getByTestId('sidebar-chat-history')).toHaveAttribute('title', '历史会话');
|
||||
expect(screen.getByTestId('sidebar-nav-tasks')).toHaveAttribute('title', '任务中心');
|
||||
expect(screen.getByTestId('sidebar-nav-settings')).toHaveAttribute('title', '设置');
|
||||
|
||||
fireEvent.pointerMove(screen.getByTestId('sidebar-nav-tasks'), { pointerType: 'mouse' });
|
||||
fireEvent.pointerEnter(screen.getByTestId('sidebar-nav-tasks'), { pointerType: 'mouse' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('任务中心').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not mark history active just because a new chat is open', () => {
|
||||
|
||||
63
tests/unit/whatsapp-proxy.test.ts
Normal file
63
tests/unit/whatsapp-proxy.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
isSocksProxyUrl,
|
||||
redactProxyUrlForLog,
|
||||
resolveWhatsAppProxyUrl,
|
||||
shouldBypassWhatsAppProxy,
|
||||
} from '@electron/utils/whatsapp-proxy';
|
||||
import type { ProxySettings } from '@electron/utils/proxy';
|
||||
|
||||
function settings(overrides: Partial<ProxySettings>): ProxySettings {
|
||||
return {
|
||||
proxyEnabled: false,
|
||||
proxyServer: '',
|
||||
proxyHttpServer: '',
|
||||
proxyHttpsServer: '',
|
||||
proxyAllServer: '',
|
||||
proxyBypassRules: '<local>;localhost;127.0.0.1;::1',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('WhatsApp proxy helpers', () => {
|
||||
it('does not use a proxy when app proxy is disabled', () => {
|
||||
expect(resolveWhatsAppProxyUrl(settings({
|
||||
proxyEnabled: false,
|
||||
proxyServer: 'http://127.0.0.1:7890',
|
||||
}))).toBe('');
|
||||
});
|
||||
|
||||
it('uses the all-proxy value first for WhatsApp WebSocket traffic', () => {
|
||||
expect(resolveWhatsAppProxyUrl(settings({
|
||||
proxyEnabled: true,
|
||||
proxyServer: 'http://127.0.0.1:7890',
|
||||
proxyHttpsServer: 'http://127.0.0.1:7892',
|
||||
proxyAllServer: 'socks5://127.0.0.1:7891',
|
||||
}))).toBe('socks5://127.0.0.1:7891');
|
||||
});
|
||||
|
||||
it('falls back to HTTPS/base proxy when no all-proxy is configured', () => {
|
||||
expect(resolveWhatsAppProxyUrl(settings({
|
||||
proxyEnabled: true,
|
||||
proxyServer: '127.0.0.1:7890',
|
||||
proxyHttpsServer: '',
|
||||
proxyAllServer: '',
|
||||
}))).toBe('http://127.0.0.1:7890');
|
||||
});
|
||||
|
||||
it('honors bypass rules for web.whatsapp.com', () => {
|
||||
expect(shouldBypassWhatsAppProxy('*.whatsapp.com')).toBe(true);
|
||||
expect(resolveWhatsAppProxyUrl(settings({
|
||||
proxyEnabled: true,
|
||||
proxyServer: 'http://127.0.0.1:7890',
|
||||
proxyBypassRules: '*.whatsapp.com',
|
||||
}))).toBe('');
|
||||
});
|
||||
|
||||
it('detects SOCKS proxy URLs and redacts credentials for logs', () => {
|
||||
expect(isSocksProxyUrl('socks5://127.0.0.1:7891')).toBe(true);
|
||||
expect(isSocksProxyUrl('http://127.0.0.1:7890')).toBe(false);
|
||||
expect(redactProxyUrlForLog('http://user:secret@127.0.0.1:7890'))
|
||||
.toBe('http://redacted:redacted@127.0.0.1:7890/');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user