fix control ui (#1073)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import { PORTS } from '../../utils/config';
|
||||
import { scheduleControlUiDeviceAutoApproval } from '../../utils/control-ui-device-pairing';
|
||||
import { buildOpenClawControlUiUrl } from '../../utils/openclaw-control-ui';
|
||||
import { getSetting } from '../../utils/store';
|
||||
import type { HostApiContext } from '../context';
|
||||
@@ -73,6 +74,7 @@ export async function handleGatewayRoutes(
|
||||
const port = status.port || PORTS.OPENCLAW_GATEWAY;
|
||||
const view = url.searchParams.get('view') === 'dreams' ? 'dreams' : undefined;
|
||||
const urlValue = buildOpenClawControlUiUrl(port, token, { view });
|
||||
scheduleControlUiDeviceAutoApproval(ctx.gatewayManager);
|
||||
sendJson(res, 200, { success: true, url: urlValue, token, port });
|
||||
} catch (error) {
|
||||
sendJson(res, 500, { success: false, error: String(error) });
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
removeProviderFromOpenClaw,
|
||||
} from '../utils/openclaw-auth';
|
||||
import { syncProxyConfigToOpenClaw } from '../utils/openclaw-proxy';
|
||||
import { scheduleControlUiDeviceAutoApproval } from '../utils/control-ui-device-pairing';
|
||||
import { buildOpenClawControlUiUrl } from '../utils/openclaw-control-ui';
|
||||
import { logger } from '../utils/logger';
|
||||
import { resolveAgentIdFromChannel } from '../utils/agent-config';
|
||||
@@ -1395,6 +1396,7 @@ function registerGatewayHandlers(
|
||||
const token = await getSetting('gatewayToken');
|
||||
const port = status.port || 18789;
|
||||
const url = buildOpenClawControlUiUrl(port, token);
|
||||
scheduleControlUiDeviceAutoApproval(gatewayManager);
|
||||
return { success: true, url, port, token };
|
||||
} catch (error) {
|
||||
return { success: false, error: String(error) };
|
||||
|
||||
296
electron/utils/control-ui-device-pairing.ts
Normal file
296
electron/utils/control-ui-device-pairing.ts
Normal file
@@ -0,0 +1,296 @@
|
||||
import { app, utilityProcess } from 'electron';
|
||||
import { existsSync } from 'fs';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { PORTS } from './config';
|
||||
import { prependPathEntry } from './env-path';
|
||||
import { logger } from './logger';
|
||||
import { getOpenClawConfigDir, getOpenClawDir, getOpenClawEntryPath } from './paths';
|
||||
import { getSetting } from './store';
|
||||
import { getUvMirrorEnv } from './uv-env';
|
||||
|
||||
/** Browser Control UI client id used in OpenClaw 2026.5.x connect frames. */
|
||||
export const CONTROL_UI_BROWSER_CLIENT_ID = 'openclaw-control-ui';
|
||||
|
||||
export type PendingDevicePairingRequest = {
|
||||
requestId?: string;
|
||||
clientId?: string;
|
||||
clientMode?: string;
|
||||
role?: string;
|
||||
roles?: string[];
|
||||
scopes?: string[];
|
||||
platform?: string;
|
||||
};
|
||||
|
||||
export type DevicePairingList = {
|
||||
pending?: PendingDevicePairingRequest[];
|
||||
paired?: unknown[];
|
||||
};
|
||||
|
||||
export type GatewayPairingRpcClient = {
|
||||
isConnected: () => boolean;
|
||||
getStatus?: () => { port?: number };
|
||||
rpc: <T>(method: string, params?: unknown, timeoutMs?: number) => Promise<T>;
|
||||
};
|
||||
|
||||
const DEFAULT_POLL_INTERVAL_MS = 800;
|
||||
const DEFAULT_WATCH_TIMEOUT_MS = 90_000;
|
||||
const LIST_RPC_TIMEOUT_MS = 10_000;
|
||||
const APPROVE_RPC_TIMEOUT_MS = 15_000;
|
||||
const CLI_APPROVE_TIMEOUT_MS = 20_000;
|
||||
|
||||
let activeWatcher: { cancel: () => void } | null = null;
|
||||
|
||||
export function isControlUiBrowserPairingRequest(request: PendingDevicePairingRequest): boolean {
|
||||
const clientId = typeof request.clientId === 'string' ? request.clientId.trim() : '';
|
||||
return clientId === CONTROL_UI_BROWSER_CLIENT_ID;
|
||||
}
|
||||
|
||||
function parseDevicePairingList(value: unknown): DevicePairingList {
|
||||
const record = typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : {};
|
||||
return {
|
||||
pending: Array.isArray(record.pending)
|
||||
? (record.pending as PendingDevicePairingRequest[])
|
||||
: [],
|
||||
paired: Array.isArray(record.paired) ? record.paired : [],
|
||||
};
|
||||
}
|
||||
|
||||
function resolveGatewayPort(gateway: GatewayPairingRpcClient): number {
|
||||
return gateway.getStatus?.()?.port ?? PORTS.OPENCLAW_GATEWAY;
|
||||
}
|
||||
|
||||
function resolveWatchTimeoutMs(explicit?: number): number {
|
||||
return typeof explicit === 'number' ? explicit : DEFAULT_WATCH_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
/** Read ~/.openclaw/devices/pending.json (same store the Gateway uses on loopback). */
|
||||
export async function readLocalPendingPairingRequests(): Promise<PendingDevicePairingRequest[]> {
|
||||
const pendingPath = join(getOpenClawConfigDir(), 'devices', 'pending.json');
|
||||
try {
|
||||
const raw = await readFile(pendingPath, 'utf8');
|
||||
const parsed = JSON.parse(raw) as Record<string, PendingDevicePairingRequest>;
|
||||
if (!parsed || typeof parsed !== 'object') return [];
|
||||
return Object.values(parsed).filter((entry) => entry && typeof entry === 'object');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function listPendingPairingRequests(gateway: GatewayPairingRpcClient): Promise<PendingDevicePairingRequest[]> {
|
||||
const merged = new Map<string, PendingDevicePairingRequest>();
|
||||
|
||||
for (const request of await readLocalPendingPairingRequests()) {
|
||||
const requestId = typeof request.requestId === 'string' ? request.requestId.trim() : '';
|
||||
if (requestId) merged.set(requestId, request);
|
||||
}
|
||||
|
||||
if (gateway.isConnected()) {
|
||||
try {
|
||||
const list = parseDevicePairingList(
|
||||
await gateway.rpc<unknown>('device.pair.list', {}, LIST_RPC_TIMEOUT_MS),
|
||||
);
|
||||
for (const request of list.pending ?? []) {
|
||||
const requestId = typeof request.requestId === 'string' ? request.requestId.trim() : '';
|
||||
if (requestId) merged.set(requestId, request);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug(`[control-ui] device.pair.list RPC failed, using local pending file: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
return [...merged.values()];
|
||||
}
|
||||
|
||||
function getBundledBinPath(): string {
|
||||
const target = `${process.platform}-${process.arch}`;
|
||||
return app.isPackaged
|
||||
? join(process.resourcesPath, 'bin')
|
||||
: join(process.cwd(), 'resources', 'bin', target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `openclaw devices approve` in-process (not shown to the user).
|
||||
* OpenClaw falls back to local pending.json on loopback when RPC is unavailable.
|
||||
*/
|
||||
async function approveViaOpenClawCli(requestId: string, _port: number): Promise<boolean> {
|
||||
const entryScript = getOpenClawEntryPath();
|
||||
const openclawDir = getOpenClawDir();
|
||||
if (!existsSync(entryScript)) {
|
||||
logger.warn('[control-ui] Cannot run devices approve: OpenClaw entry missing');
|
||||
return false;
|
||||
}
|
||||
|
||||
const token = await getSetting('gatewayToken');
|
||||
const args = ['devices', 'approve', requestId, '--token', token, '--timeout', String(CLI_APPROVE_TIMEOUT_MS)];
|
||||
|
||||
const binPath = getBundledBinPath();
|
||||
const binPathExists = existsSync(binPath);
|
||||
const baseEnv = (binPathExists
|
||||
? prependPathEntry(process.env as Record<string, string | undefined>, binPath).env
|
||||
: process.env) as Record<string, string | undefined>;
|
||||
const uvEnv = await getUvMirrorEnv();
|
||||
|
||||
return await new Promise<boolean>((resolve) => {
|
||||
const child = utilityProcess.fork(entryScript, args, {
|
||||
cwd: openclawDir,
|
||||
stdio: 'pipe',
|
||||
env: {
|
||||
...baseEnv,
|
||||
...uvEnv,
|
||||
OPENCLAW_NO_RESPAWN: '1',
|
||||
OPENCLAW_EMBEDDED_IN: 'ClawX',
|
||||
} as NodeJS.ProcessEnv,
|
||||
});
|
||||
|
||||
let settled = false;
|
||||
const finish = (ok: boolean) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
resolve(ok);
|
||||
};
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
logger.warn(`[control-ui] devices approve timed out for ${requestId}`);
|
||||
try {
|
||||
child.kill();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
finish(false);
|
||||
}, CLI_APPROVE_TIMEOUT_MS + 5_000);
|
||||
|
||||
child.on('error', (error) => {
|
||||
clearTimeout(timeout);
|
||||
logger.warn(`[control-ui] devices approve spawn failed: ${String(error)}`);
|
||||
finish(false);
|
||||
});
|
||||
|
||||
child.on('exit', (code: number) => {
|
||||
clearTimeout(timeout);
|
||||
finish(code === 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function approvePairingRequest(
|
||||
gateway: GatewayPairingRpcClient,
|
||||
requestId: string,
|
||||
port: number,
|
||||
): Promise<boolean> {
|
||||
if (gateway.isConnected()) {
|
||||
try {
|
||||
await gateway.rpc('device.pair.approve', { requestId }, APPROVE_RPC_TIMEOUT_MS);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.debug(
|
||||
`[control-ui] device.pair.approve RPC failed for ${requestId}, trying CLI fallback: ${String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return approveViaOpenClawCli(requestId, port);
|
||||
}
|
||||
|
||||
function sleep(ms: number, signal: { cancelled: boolean }): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
if (signal.cancelled) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Approve pending Control UI browser pairing requests.
|
||||
* Uses Gateway RPC when available; falls back to local pending.json + embedded CLI on Windows packaged builds.
|
||||
*/
|
||||
export async function approvePendingControlUiPairingRequests(
|
||||
gateway: GatewayPairingRpcClient,
|
||||
options?: { approvedRequestIds?: Set<string> },
|
||||
): Promise<string[]> {
|
||||
const port = resolveGatewayPort(gateway);
|
||||
const approvedRequestIds = options?.approvedRequestIds ?? new Set<string>();
|
||||
const pending = await listPendingPairingRequests(gateway);
|
||||
|
||||
const approved: string[] = [];
|
||||
for (const request of pending) {
|
||||
if (!isControlUiBrowserPairingRequest(request)) continue;
|
||||
|
||||
const requestId = typeof request.requestId === 'string' ? request.requestId.trim() : '';
|
||||
if (!requestId || approvedRequestIds.has(requestId)) continue;
|
||||
|
||||
try {
|
||||
const ok = await approvePairingRequest(gateway, requestId, port);
|
||||
if (!ok) continue;
|
||||
approvedRequestIds.add(requestId);
|
||||
approved.push(requestId);
|
||||
logger.info(
|
||||
`[control-ui] Auto-approved browser device pairing (requestId=${requestId}, mode=${request.clientMode ?? 'unknown'})`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`[control-ui] Failed to auto-approve pairing request ${requestId}: ${String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return approved;
|
||||
}
|
||||
|
||||
async function watchControlUiPairingApprovals(
|
||||
gateway: GatewayPairingRpcClient,
|
||||
signal: { cancelled: boolean },
|
||||
timeoutMs: number,
|
||||
pollIntervalMs: number,
|
||||
): Promise<void> {
|
||||
const approvedRequestIds = new Set<string>();
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
|
||||
while (!signal.cancelled && Date.now() < deadline) {
|
||||
try {
|
||||
await approvePendingControlUiPairingRequests(gateway, { approvedRequestIds });
|
||||
} catch (error) {
|
||||
logger.debug(`[control-ui] Pairing poll error: ${String(error)}`);
|
||||
}
|
||||
|
||||
await sleep(pollIntervalMs, signal);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll for Control UI browser pairing requests and approve them locally.
|
||||
* Safe to call repeatedly; only one watcher runs at a time.
|
||||
*/
|
||||
export function scheduleControlUiDeviceAutoApproval(
|
||||
gateway: GatewayPairingRpcClient,
|
||||
options?: {
|
||||
timeoutMs?: number;
|
||||
pollIntervalMs?: number;
|
||||
},
|
||||
): void {
|
||||
activeWatcher?.cancel();
|
||||
|
||||
const signal = { cancelled: false };
|
||||
const cancel = () => {
|
||||
signal.cancelled = true;
|
||||
};
|
||||
activeWatcher = { cancel };
|
||||
|
||||
const timeoutMs = resolveWatchTimeoutMs(options?.timeoutMs);
|
||||
const pollIntervalMs = options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
||||
|
||||
void watchControlUiPairingApprovals(gateway, signal, timeoutMs, pollIntervalMs)
|
||||
.catch((error) => {
|
||||
logger.warn(`[control-ui] Auto-approval watcher failed: ${String(error)}`);
|
||||
})
|
||||
.finally(() => {
|
||||
if (activeWatcher?.cancel === cancel) {
|
||||
activeWatcher = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
} from './provider-keys';
|
||||
import { normalizePiAiModelCost, type PiAiModelCostRates } from '../shared/pi-ai-model-cost';
|
||||
import { withConfigLock } from './config-mutex';
|
||||
import { PORTS } from './config';
|
||||
import { getSetting } from './store';
|
||||
import {
|
||||
OPENCLAW_API_PROTOCOLS,
|
||||
assertValidApiProtocol,
|
||||
@@ -1997,6 +1999,17 @@ export async function getOpenClawProvidersConfig(): Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
function applyControlUiAllowedOrigins(controlUi: Record<string, unknown>, port: number): void {
|
||||
const allowedOrigins = Array.isArray(controlUi.allowedOrigins)
|
||||
? (controlUi.allowedOrigins as unknown[]).filter((value): value is string => typeof value === 'string')
|
||||
: [];
|
||||
const next = new Set(allowedOrigins);
|
||||
next.add('file://');
|
||||
next.add(`http://127.0.0.1:${port}`);
|
||||
next.add(`http://localhost:${port}`);
|
||||
controlUi.allowedOrigins = [...next];
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the ClawX gateway token into ~/.openclaw/openclaw.json.
|
||||
*/
|
||||
@@ -2020,19 +2033,13 @@ export async function syncGatewayTokenToConfig(token: string): Promise<void> {
|
||||
auth.token = token;
|
||||
gateway.auth = auth;
|
||||
|
||||
// Packaged ClawX loads the renderer from file://, so the gateway must allow
|
||||
// that origin for the chat WebSocket handshake.
|
||||
const controlUi = (
|
||||
gateway.controlUi && typeof gateway.controlUi === 'object'
|
||||
? { ...(gateway.controlUi as Record<string, unknown>) }
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
const allowedOrigins = Array.isArray(controlUi.allowedOrigins)
|
||||
? (controlUi.allowedOrigins as unknown[]).filter((value): value is string => typeof value === 'string')
|
||||
: [];
|
||||
if (!allowedOrigins.includes('file://')) {
|
||||
controlUi.allowedOrigins = [...allowedOrigins, 'file://'];
|
||||
}
|
||||
const gatewayPort = (await getSetting('gatewayPort')) || PORTS.OPENCLAW_GATEWAY;
|
||||
applyControlUiAllowedOrigins(controlUi, gatewayPort);
|
||||
gateway.controlUi = controlUi;
|
||||
|
||||
if (!gateway.mode) gateway.mode = 'local';
|
||||
@@ -2209,12 +2216,8 @@ export async function batchSyncConfigFields(token: string): Promise<void> {
|
||||
? { ...(gateway.controlUi as Record<string, unknown>) }
|
||||
: {}
|
||||
) as Record<string, unknown>;
|
||||
const allowedOrigins = Array.isArray(controlUi.allowedOrigins)
|
||||
? (controlUi.allowedOrigins as unknown[]).filter((v): v is string => typeof v === 'string')
|
||||
: [];
|
||||
if (!allowedOrigins.includes('file://')) {
|
||||
controlUi.allowedOrigins = [...allowedOrigins, 'file://'];
|
||||
}
|
||||
const gatewayPort = (await getSetting('gatewayPort')) || PORTS.OPENCLAW_GATEWAY;
|
||||
applyControlUiAllowedOrigins(controlUi, gatewayPort);
|
||||
gateway.controlUi = controlUi;
|
||||
if (!gateway.mode) gateway.mode = 'local';
|
||||
config.gateway = gateway;
|
||||
|
||||
100
tests/unit/control-ui-device-pairing.test.ts
Normal file
100
tests/unit/control-ui-device-pairing.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: { isPackaged: false },
|
||||
utilityProcess: { fork: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock('@electron/utils/store', () => ({
|
||||
getSetting: vi.fn(async () => 'test-gateway-token'),
|
||||
}));
|
||||
|
||||
vi.mock('@electron/utils/paths', () => ({
|
||||
getOpenClawConfigDir: () => '/tmp/openclaw',
|
||||
getOpenClawDir: () => '/tmp/openclaw/pkg',
|
||||
getOpenClawEntryPath: () => '/tmp/openclaw/pkg/openclaw.mjs',
|
||||
}));
|
||||
|
||||
import {
|
||||
approvePendingControlUiPairingRequests,
|
||||
CONTROL_UI_BROWSER_CLIENT_ID,
|
||||
isControlUiBrowserPairingRequest,
|
||||
} from '@electron/utils/control-ui-device-pairing';
|
||||
|
||||
describe('control-ui-device-pairing', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('detects Control UI browser pairing requests', () => {
|
||||
expect(
|
||||
isControlUiBrowserPairingRequest({
|
||||
clientId: CONTROL_UI_BROWSER_CLIENT_ID,
|
||||
clientMode: 'webchat',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(isControlUiBrowserPairingRequest({ clientId: 'gateway-client' })).toBe(false);
|
||||
expect(isControlUiBrowserPairingRequest({ clientId: 'cli' })).toBe(false);
|
||||
});
|
||||
|
||||
it('approves pending Control UI requests via gateway RPC', async () => {
|
||||
const rpc = vi.fn(async (method: string, params?: unknown) => {
|
||||
if (method === 'device.pair.list') {
|
||||
return {
|
||||
pending: [
|
||||
{
|
||||
requestId: 'req-control-ui',
|
||||
clientId: CONTROL_UI_BROWSER_CLIENT_ID,
|
||||
clientMode: 'webchat',
|
||||
},
|
||||
{
|
||||
requestId: 'req-cli',
|
||||
clientId: 'cli',
|
||||
clientMode: 'cli',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (method === 'device.pair.approve') {
|
||||
expect(params).toEqual({ requestId: 'req-control-ui' });
|
||||
return { requestId: 'req-control-ui' };
|
||||
}
|
||||
throw new Error(`unexpected method: ${method}`);
|
||||
});
|
||||
|
||||
const approved = await approvePendingControlUiPairingRequests({
|
||||
isConnected: () => true,
|
||||
getStatus: () => ({ port: 18789 }),
|
||||
rpc,
|
||||
});
|
||||
|
||||
expect(approved).toEqual(['req-control-ui']);
|
||||
expect(rpc).toHaveBeenCalledTimes(2);
|
||||
expect(rpc.mock.calls[1]?.[0]).toBe('device.pair.approve');
|
||||
});
|
||||
|
||||
it('does not approve the same request twice in one pass', async () => {
|
||||
const rpc = vi.fn(async (method: string) => {
|
||||
if (method === 'device.pair.list') {
|
||||
return {
|
||||
pending: [
|
||||
{
|
||||
requestId: 'req-1',
|
||||
clientId: CONTROL_UI_BROWSER_CLIENT_ID,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const approvedRequestIds = new Set<string>(['req-1']);
|
||||
const approved = await approvePendingControlUiPairingRequests(
|
||||
{ isConnected: () => true, getStatus: () => ({ port: 18789 }), rpc },
|
||||
{ approvedRequestIds },
|
||||
);
|
||||
|
||||
expect(approved).toEqual([]);
|
||||
expect(rpc).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -3,11 +3,16 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { HostApiContext } from '@electron/api/context';
|
||||
import { handleGatewayRoutes } from '@electron/api/routes/gateway';
|
||||
import { scheduleControlUiDeviceAutoApproval } from '@electron/utils/control-ui-device-pairing';
|
||||
|
||||
vi.mock('@electron/utils/store', () => ({
|
||||
getSetting: vi.fn(async () => 'clawx-route-token'),
|
||||
}));
|
||||
|
||||
vi.mock('@electron/utils/control-ui-device-pairing', () => ({
|
||||
scheduleControlUiDeviceAutoApproval: vi.fn(),
|
||||
}));
|
||||
|
||||
function createResponse() {
|
||||
const headers = new Map<string, string>();
|
||||
let body = '';
|
||||
@@ -66,6 +71,7 @@ describe('GET /api/gateway/control-ui', () => {
|
||||
token: 'clawx-route-token',
|
||||
port: 19001,
|
||||
});
|
||||
expect(scheduleControlUiDeviceAutoApproval).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('returns the Dreams Control UI URL', async () => {
|
||||
|
||||
@@ -2,11 +2,12 @@ import { mkdir, readFile, rm, writeFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const { testHome, testUserData } = vi.hoisted(() => {
|
||||
const { testHome, testUserData, getSettingMock } = vi.hoisted(() => {
|
||||
const suffix = Math.random().toString(36).slice(2);
|
||||
return {
|
||||
testHome: `/tmp/clawx-openclaw-auth-${suffix}`,
|
||||
testUserData: `/tmp/clawx-openclaw-auth-user-data-${suffix}`,
|
||||
getSettingMock: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -30,6 +31,10 @@ vi.mock('electron', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@electron/utils/store', () => ({
|
||||
getSetting: getSettingMock,
|
||||
}));
|
||||
|
||||
vi.mock('@electron/utils/paths', async () => {
|
||||
const actual = await vi.importActual<typeof import('@electron/utils/paths')>('@electron/utils/paths');
|
||||
const resolvedDir = join(testHome, '.openclaw-test-openclaw');
|
||||
@@ -1901,6 +1906,10 @@ describe('batchSyncConfigFields', () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.restoreAllMocks();
|
||||
getSettingMock.mockImplementation(async (key: string) => {
|
||||
if (key === 'gatewayPort') return 18789;
|
||||
return undefined;
|
||||
});
|
||||
await rm(testHome, { recursive: true, force: true });
|
||||
await rm(testUserData, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user