feat: update openclaw and polish desktop flows

This commit is contained in:
inman
2026-05-13 21:52:17 +08:00
parent 7c8781a6e3
commit 86795078f7
22 changed files with 1145 additions and 186 deletions

View File

@@ -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 () => {

View File

@@ -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,

View 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();
});
});

View File

@@ -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', () => {

View 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/');
});
});