Files
DeskClaw/tests/unit/plugin-install.test.ts

221 lines
7.1 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const {
mockExistsSync,
mockCpSync,
mockCopyFileSync,
mockStatSync,
mockMkdirSync,
mockRmSync,
mockReadFileSync,
mockWriteFileSync,
mockReaddirSync,
mockRealpathSync,
mockLoggerWarn,
mockLoggerInfo,
mockHomedir,
mockApp,
} = vi.hoisted(() => ({
mockExistsSync: vi.fn(),
mockCpSync: vi.fn(),
mockCopyFileSync: vi.fn(),
mockStatSync: vi.fn(() => ({ isDirectory: () => false })),
mockMkdirSync: vi.fn(),
mockRmSync: vi.fn(),
mockReadFileSync: vi.fn(),
mockWriteFileSync: vi.fn(),
mockReaddirSync: vi.fn(),
mockRealpathSync: vi.fn(),
mockLoggerWarn: vi.fn(),
mockLoggerInfo: vi.fn(),
mockHomedir: vi.fn(() => '/home/test'),
mockApp: {
isPackaged: true,
getAppPath: vi.fn(() => '/mock/app'),
},
}));
const ORIGINAL_PLATFORM_DESCRIPTOR = Object.getOwnPropertyDescriptor(process, 'platform');
vi.mock('node:fs', async () => {
const actual = await vi.importActual<typeof import('node:fs')>('node:fs');
const mocked = {
...actual,
existsSync: mockExistsSync,
cpSync: mockCpSync,
copyFileSync: mockCopyFileSync,
statSync: mockStatSync,
mkdirSync: mockMkdirSync,
rmSync: mockRmSync,
readFileSync: mockReadFileSync,
writeFileSync: mockWriteFileSync,
readdirSync: mockReaddirSync,
realpathSync: mockRealpathSync,
};
return {
...mocked,
default: mocked,
};
});
vi.mock('node:fs/promises', async () => {
const actual = await vi.importActual<typeof import('node:fs/promises')>('node:fs/promises');
return {
...actual,
readdir: vi.fn(),
stat: vi.fn(),
copyFile: vi.fn(),
mkdir: vi.fn(),
};
});
vi.mock('node:os', () => ({
homedir: () => mockHomedir(),
default: {
homedir: () => mockHomedir(),
},
}));
vi.mock('electron', () => ({
app: mockApp,
}));
vi.mock('@electron/utils/logger', () => ({
logger: {
warn: mockLoggerWarn,
info: mockLoggerInfo,
},
}));
function setPlatform(platform: NodeJS.Platform): void {
Object.defineProperty(process, 'platform', {
value: platform,
configurable: true,
});
}
describe('plugin installer diagnostics', () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
mockApp.isPackaged = true;
mockHomedir.mockReturnValue('/home/test');
setPlatform('linux');
mockExistsSync.mockReturnValue(false);
mockCpSync.mockImplementation(() => undefined);
mockMkdirSync.mockImplementation(() => undefined);
mockRmSync.mockImplementation(() => undefined);
mockReadFileSync.mockReturnValue('{}');
mockWriteFileSync.mockImplementation(() => undefined);
mockReaddirSync.mockReturnValue([]);
mockRealpathSync.mockImplementation((input: string) => input);
});
afterEach(() => {
if (ORIGINAL_PLATFORM_DESCRIPTOR) {
Object.defineProperty(process, 'platform', ORIGINAL_PLATFORM_DESCRIPTOR);
}
});
it('returns source-missing warning when bundled mirror cannot be found', async () => {
const { ensurePluginInstalled } = await import('@electron/utils/plugin-install');
const result = ensurePluginInstalled('wecom', ['/bundle/wecom'], 'WeCom');
expect(result.installed).toBe(false);
expect(result.warning).toContain('Bundled WeCom plugin mirror not found');
expect(mockLoggerWarn).not.toHaveBeenCalled();
});
it('retries once on Windows and logs diagnostic details when bundled copy fails', async () => {
setPlatform('win32');
mockHomedir.mockReturnValue('C:\\Users\\test');
const sourceDir = 'C:\\Program Files\\ClawX\\resources\\openclaw-plugins\\wecom';
const sourceManifestSuffix = 'Program Files\\ClawX\\resources\\openclaw-plugins\\wecom\\openclaw.plugin.json';
mockExistsSync.mockImplementation((input: string) => String(input).includes(sourceManifestSuffix));
// On win32, cpSyncSafe uses _copyDirSyncRecursive (readdirSync) instead of cpSync.
// Simulate copy failure by making readdirSync throw during directory traversal.
mockReaddirSync.mockImplementation((_path: string, opts?: unknown) => {
if (opts && typeof opts === 'object' && 'withFileTypes' in (opts as Record<string, unknown>)) {
const error = new Error('path too long') as NodeJS.ErrnoException;
error.code = 'ENAMETOOLONG';
throw error;
}
return [];
});
const { ensurePluginInstalled } = await import('@electron/utils/plugin-install');
const result = ensurePluginInstalled('wecom', [sourceDir], 'WeCom');
expect(result).toEqual({
installed: false,
warning: 'Failed to install bundled WeCom plugin mirror',
});
// On win32, cpSyncSafe walks the directory via readdirSync (with withFileTypes)
const copyAttempts = mockReaddirSync.mock.calls.filter(
(call: unknown[]) => {
const opts = call[1];
return opts && typeof opts === 'object' && 'withFileTypes' in (opts as Record<string, unknown>);
},
);
expect(copyAttempts).toHaveLength(2); // initial + 1 retry
const firstSrcPath = String(copyAttempts[0][0]);
expect(firstSrcPath.startsWith('\\\\?\\')).toBe(true);
expect(mockLoggerWarn).toHaveBeenCalledWith(
'[plugin] Bundled mirror install failed for WeCom',
expect.objectContaining({
pluginDirName: 'wecom',
pluginLabel: 'WeCom',
sourceDir,
platform: 'win32',
attempts: [
expect.objectContaining({ attempt: 1, code: 'ENAMETOOLONG' }),
expect.objectContaining({ attempt: 2, code: 'ENAMETOOLONG' }),
],
}),
);
});
it('logs EPERM diagnostics with source and target paths', async () => {
setPlatform('win32');
mockHomedir.mockReturnValue('C:\\Users\\test');
const sourceDir = 'C:\\Program Files\\ClawX\\resources\\openclaw-plugins\\wecom';
const sourceManifestSuffix = 'Program Files\\ClawX\\resources\\openclaw-plugins\\wecom\\openclaw.plugin.json';
mockExistsSync.mockImplementation((input: string) => String(input).includes(sourceManifestSuffix));
// On win32, cpSyncSafe uses _copyDirSyncRecursive (readdirSync) instead of cpSync.
mockReaddirSync.mockImplementation((_path: string, opts?: unknown) => {
if (opts && typeof opts === 'object' && 'withFileTypes' in (opts as Record<string, unknown>)) {
const error = new Error('access denied') as NodeJS.ErrnoException;
error.code = 'EPERM';
throw error;
}
return [];
});
const { ensurePluginInstalled } = await import('@electron/utils/plugin-install');
const result = ensurePluginInstalled('wecom', [sourceDir], 'WeCom');
expect(result.installed).toBe(false);
expect(result.warning).toBe('Failed to install bundled WeCom plugin mirror');
expect(mockLoggerWarn).toHaveBeenCalledWith(
'[plugin] Bundled mirror install failed for WeCom',
expect.objectContaining({
sourceDir,
targetDir: expect.stringContaining('.openclaw/extensions/wecom'),
platform: 'win32',
attempts: [
expect.objectContaining({ attempt: 1, code: 'EPERM' }),
expect.objectContaining({ attempt: 2, code: 'EPERM' }),
],
}),
);
});
});