Added tests for extensions functionality (#3794)
This commit is contained in:
@@ -0,0 +1,326 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { extensionApiCall, addToAgent, removeFromAgent, sanitizeName } from './agent-api';
|
||||||
|
import * as config from '../../../config';
|
||||||
|
import * as toasts from '../../../toasts';
|
||||||
|
import { ExtensionConfig } from '../../../api/types.gen';
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock('../../../config');
|
||||||
|
vi.mock('../../../toasts');
|
||||||
|
vi.mock('./utils');
|
||||||
|
|
||||||
|
const mockGetApiUrl = vi.mocked(config.getApiUrl);
|
||||||
|
const mockToastService = vi.mocked(toasts.toastService);
|
||||||
|
|
||||||
|
// Mock window.electron
|
||||||
|
const mockElectron = {
|
||||||
|
getSecretKey: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.defineProperty(window, 'electron', {
|
||||||
|
value: mockElectron,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock fetch
|
||||||
|
const mockFetch = vi.fn();
|
||||||
|
(globalThis as typeof globalThis & { fetch: typeof mockFetch }).fetch = mockFetch;
|
||||||
|
|
||||||
|
describe('Agent API', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockGetApiUrl.mockImplementation((path: string) => `http://localhost:8080${path}`);
|
||||||
|
mockElectron.getSecretKey.mockResolvedValue('secret-key');
|
||||||
|
mockToastService.configure = vi.fn();
|
||||||
|
mockToastService.loading = vi.fn().mockReturnValue('toast-id');
|
||||||
|
mockToastService.success = vi.fn();
|
||||||
|
mockToastService.error = vi.fn();
|
||||||
|
mockToastService.dismiss = vi.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sanitizeName', () => {
|
||||||
|
it('should sanitize extension names correctly', () => {
|
||||||
|
expect(sanitizeName('Test Extension')).toBe('testextension');
|
||||||
|
expect(sanitizeName('My-Extension_Name')).toBe('myextensionname');
|
||||||
|
expect(sanitizeName('UPPERCASE')).toBe('uppercase');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('extensionApiCall', () => {
|
||||||
|
const mockExtensionConfig: ExtensionConfig = {
|
||||||
|
type: 'stdio',
|
||||||
|
name: 'test-extension',
|
||||||
|
cmd: 'python',
|
||||||
|
args: ['script.py'],
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should make successful API call for adding extension', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
ok: true,
|
||||||
|
text: vi.fn().mockResolvedValue('{"error": false}'),
|
||||||
|
};
|
||||||
|
mockFetch.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const response = await extensionApiCall('/extensions/add', mockExtensionConfig);
|
||||||
|
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith('http://localhost:8080/extensions/add', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Secret-Key': 'secret-key',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(mockExtensionConfig),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockToastService.loading).toHaveBeenCalledWith({
|
||||||
|
title: 'test-extension',
|
||||||
|
msg: 'Activating test-extension extension...',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockToastService.success).toHaveBeenCalledWith({
|
||||||
|
title: 'test-extension',
|
||||||
|
msg: 'Successfully activated extension',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toBe(mockResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should make successful API call for removing extension', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
ok: true,
|
||||||
|
text: vi.fn().mockResolvedValue('{"error": false}'),
|
||||||
|
};
|
||||||
|
mockFetch.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const response = await extensionApiCall('/extensions/remove', 'test-extension');
|
||||||
|
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith('http://localhost:8080/extensions/remove', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Secret-Key': 'secret-key',
|
||||||
|
},
|
||||||
|
body: JSON.stringify('test-extension'),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockToastService.loading).not.toHaveBeenCalled(); // No loading toast for removal
|
||||||
|
expect(mockToastService.success).toHaveBeenCalledWith({
|
||||||
|
title: 'test-extension',
|
||||||
|
msg: 'Successfully deactivated extension',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toBe(mockResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle HTTP error responses', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
statusText: 'Internal Server Error',
|
||||||
|
};
|
||||||
|
mockFetch.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
await expect(extensionApiCall('/extensions/add', mockExtensionConfig)).rejects.toThrow(
|
||||||
|
'Server returned 500: Internal Server Error'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockToastService.error).toHaveBeenCalledWith({
|
||||||
|
title: 'test-extension',
|
||||||
|
msg: 'Failed to add test-extension extension: Server returned 500: Internal Server Error',
|
||||||
|
traceback: 'Server returned 500: Internal Server Error',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle 428 error specially', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
ok: false,
|
||||||
|
status: 428,
|
||||||
|
statusText: 'Precondition Required',
|
||||||
|
};
|
||||||
|
mockFetch.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
await expect(extensionApiCall('/extensions/add', mockExtensionConfig)).rejects.toThrow(
|
||||||
|
'Agent is not initialized. Please initialize the agent first.'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockToastService.error).toHaveBeenCalledWith({
|
||||||
|
title: 'test-extension',
|
||||||
|
msg: 'Failed to add extension. Goose Agent was still starting up. Please try again.',
|
||||||
|
traceback: 'Server returned 428: Precondition Required',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle API error responses', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
ok: true,
|
||||||
|
text: vi.fn().mockResolvedValue('{"error": true, "message": "Extension not found"}'),
|
||||||
|
};
|
||||||
|
mockFetch.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
await expect(extensionApiCall('/extensions/remove', 'test-extension')).rejects.toThrow(
|
||||||
|
'Error deactivating extension: Extension not found'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockToastService.error).toHaveBeenCalledWith({
|
||||||
|
title: 'test-extension',
|
||||||
|
msg: 'Error deactivating extension: Extension not found',
|
||||||
|
traceback: 'Error deactivating extension: Extension not found',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle JSON parse errors', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
ok: true,
|
||||||
|
text: vi.fn().mockResolvedValue('invalid json'),
|
||||||
|
};
|
||||||
|
mockFetch.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const response = await extensionApiCall('/extensions/add', mockExtensionConfig);
|
||||||
|
|
||||||
|
expect(mockToastService.success).toHaveBeenCalledWith({
|
||||||
|
title: 'test-extension',
|
||||||
|
msg: 'Successfully activated extension',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response).toBe(mockResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle network errors', async () => {
|
||||||
|
const networkError = new Error('Network error');
|
||||||
|
mockFetch.mockRejectedValue(networkError);
|
||||||
|
|
||||||
|
await expect(extensionApiCall('/extensions/add', mockExtensionConfig)).rejects.toThrow(
|
||||||
|
'Network error'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockToastService.error).toHaveBeenCalledWith({
|
||||||
|
title: 'test-extension',
|
||||||
|
msg: 'Network error',
|
||||||
|
traceback: 'Network error',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure toast service with options', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
ok: true,
|
||||||
|
text: vi.fn().mockResolvedValue('{"error": false}'),
|
||||||
|
};
|
||||||
|
mockFetch.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
await extensionApiCall('/extensions/add', mockExtensionConfig, { silent: true });
|
||||||
|
|
||||||
|
expect(mockToastService.configure).toHaveBeenCalledWith({ silent: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addToAgent', () => {
|
||||||
|
const mockExtensionConfig: ExtensionConfig = {
|
||||||
|
type: 'stdio',
|
||||||
|
name: 'Test Extension',
|
||||||
|
cmd: 'python',
|
||||||
|
args: ['script.py'],
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should add stdio extension to agent with shim replacement', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
ok: true,
|
||||||
|
text: vi.fn().mockResolvedValue('{"error": false}'),
|
||||||
|
};
|
||||||
|
mockFetch.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
// Mock replaceWithShims
|
||||||
|
const { replaceWithShims } = await import('./utils');
|
||||||
|
vi.mocked(replaceWithShims).mockResolvedValue('/path/to/python');
|
||||||
|
|
||||||
|
await addToAgent(mockExtensionConfig);
|
||||||
|
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith('http://localhost:8080/extensions/add', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Secret-Key': 'secret-key',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...mockExtensionConfig,
|
||||||
|
name: 'testextension',
|
||||||
|
cmd: '/path/to/python',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle 428 error with enhanced message', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
ok: false,
|
||||||
|
status: 428,
|
||||||
|
statusText: 'Precondition Required',
|
||||||
|
};
|
||||||
|
mockFetch.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
await expect(addToAgent(mockExtensionConfig)).rejects.toThrow(
|
||||||
|
'Agent is not initialized. Please initialize the agent first.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add non-stdio extension without shim replacement', async () => {
|
||||||
|
const sseConfig: ExtensionConfig = {
|
||||||
|
type: 'sse',
|
||||||
|
name: 'SSE Extension',
|
||||||
|
uri: 'http://localhost:8080/events',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
ok: true,
|
||||||
|
text: vi.fn().mockResolvedValue('{"error": false}'),
|
||||||
|
};
|
||||||
|
mockFetch.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
await addToAgent(sseConfig);
|
||||||
|
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith('http://localhost:8080/extensions/add', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Secret-Key': 'secret-key',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...sseConfig,
|
||||||
|
name: 'sseextension',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeFromAgent', () => {
|
||||||
|
it('should remove extension from agent', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
ok: true,
|
||||||
|
text: vi.fn().mockResolvedValue('{"error": false}'),
|
||||||
|
};
|
||||||
|
mockFetch.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
await removeFromAgent('Test Extension');
|
||||||
|
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith('http://localhost:8080/extensions/remove', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Secret-Key': 'secret-key',
|
||||||
|
},
|
||||||
|
body: JSON.stringify('testextension'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle removal errors', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
ok: false,
|
||||||
|
status: 404,
|
||||||
|
statusText: 'Not Found',
|
||||||
|
};
|
||||||
|
mockFetch.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
await expect(removeFromAgent('Test Extension')).rejects.toThrow();
|
||||||
|
|
||||||
|
expect(mockToastService.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import {
|
||||||
|
activateExtension,
|
||||||
|
addToAgentOnStartup,
|
||||||
|
updateExtension,
|
||||||
|
toggleExtension,
|
||||||
|
deleteExtension,
|
||||||
|
} from './extension-manager';
|
||||||
|
import * as agentApi from './agent-api';
|
||||||
|
import * as toasts from '../../../toasts';
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock('./agent-api');
|
||||||
|
vi.mock('../../../toasts');
|
||||||
|
|
||||||
|
const mockAddToAgent = vi.mocked(agentApi.addToAgent);
|
||||||
|
const mockRemoveFromAgent = vi.mocked(agentApi.removeFromAgent);
|
||||||
|
const mockSanitizeName = vi.mocked(agentApi.sanitizeName);
|
||||||
|
const mockToastService = vi.mocked(toasts.toastService);
|
||||||
|
|
||||||
|
describe('Extension Manager', () => {
|
||||||
|
const mockAddToConfig = vi.fn();
|
||||||
|
const mockRemoveFromConfig = vi.fn();
|
||||||
|
|
||||||
|
const mockExtensionConfig = {
|
||||||
|
type: 'stdio' as const,
|
||||||
|
name: 'test-extension',
|
||||||
|
cmd: 'python',
|
||||||
|
args: ['script.py'],
|
||||||
|
timeout: 300,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockSanitizeName.mockImplementation((name: string) => name.toLowerCase());
|
||||||
|
mockAddToConfig.mockResolvedValue(undefined);
|
||||||
|
mockRemoveFromConfig.mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('activateExtension', () => {
|
||||||
|
it('should successfully activate extension', async () => {
|
||||||
|
mockAddToAgent.mockResolvedValue({} as Response);
|
||||||
|
|
||||||
|
await activateExtension({
|
||||||
|
addToConfig: mockAddToConfig,
|
||||||
|
extensionConfig: mockExtensionConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockAddToAgent).toHaveBeenCalledWith(mockExtensionConfig, { silent: false });
|
||||||
|
expect(mockAddToConfig).toHaveBeenCalledWith('test-extension', mockExtensionConfig, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add to config as disabled if agent fails', async () => {
|
||||||
|
const agentError = new Error('Agent failed');
|
||||||
|
mockAddToAgent.mockRejectedValue(agentError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
activateExtension({
|
||||||
|
addToConfig: mockAddToConfig,
|
||||||
|
extensionConfig: mockExtensionConfig,
|
||||||
|
})
|
||||||
|
).rejects.toThrow('Agent failed');
|
||||||
|
|
||||||
|
expect(mockAddToAgent).toHaveBeenCalledWith(mockExtensionConfig, { silent: false });
|
||||||
|
expect(mockAddToConfig).toHaveBeenCalledWith('test-extension', mockExtensionConfig, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove from agent if config fails', async () => {
|
||||||
|
const configError = new Error('Config failed');
|
||||||
|
mockAddToAgent.mockResolvedValue({} as Response);
|
||||||
|
mockAddToConfig.mockRejectedValue(configError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
activateExtension({
|
||||||
|
addToConfig: mockAddToConfig,
|
||||||
|
extensionConfig: mockExtensionConfig,
|
||||||
|
})
|
||||||
|
).rejects.toThrow('Config failed');
|
||||||
|
|
||||||
|
expect(mockAddToAgent).toHaveBeenCalledWith(mockExtensionConfig, { silent: false });
|
||||||
|
expect(mockAddToConfig).toHaveBeenCalledWith('test-extension', mockExtensionConfig, true);
|
||||||
|
expect(mockRemoveFromAgent).toHaveBeenCalledWith('test-extension');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addToAgentOnStartup', () => {
|
||||||
|
it('should successfully add extension on startup', async () => {
|
||||||
|
mockAddToAgent.mockResolvedValue({} as Response);
|
||||||
|
|
||||||
|
await addToAgentOnStartup({
|
||||||
|
addToConfig: mockAddToConfig,
|
||||||
|
extensionConfig: mockExtensionConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockAddToAgent).toHaveBeenCalledWith(mockExtensionConfig, { silent: true });
|
||||||
|
expect(mockAddToConfig).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retry on 428 errors', async () => {
|
||||||
|
const error428 = new Error('428 Precondition Required');
|
||||||
|
mockAddToAgent
|
||||||
|
.mockRejectedValueOnce(error428)
|
||||||
|
.mockRejectedValueOnce(error428)
|
||||||
|
.mockResolvedValue({} as Response);
|
||||||
|
|
||||||
|
await addToAgentOnStartup({
|
||||||
|
addToConfig: mockAddToConfig,
|
||||||
|
extensionConfig: mockExtensionConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockAddToAgent).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disable extension after max retries', async () => {
|
||||||
|
const error428 = new Error('428 Precondition Required');
|
||||||
|
mockAddToAgent.mockRejectedValue(error428);
|
||||||
|
mockToastService.configure = vi.fn();
|
||||||
|
mockToastService.error = vi.fn();
|
||||||
|
|
||||||
|
await addToAgentOnStartup({
|
||||||
|
addToConfig: mockAddToConfig,
|
||||||
|
extensionConfig: mockExtensionConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockAddToAgent).toHaveBeenCalledTimes(4); // Initial + 3 retries
|
||||||
|
expect(mockToastService.error).toHaveBeenCalledWith({
|
||||||
|
title: 'test-extension',
|
||||||
|
msg: 'Extension failed to start and will be disabled.',
|
||||||
|
traceback: '428 Precondition Required',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateExtension', () => {
|
||||||
|
it('should update extension without name change', async () => {
|
||||||
|
mockAddToAgent.mockResolvedValue({} as Response);
|
||||||
|
mockAddToConfig.mockResolvedValue(undefined);
|
||||||
|
mockToastService.success = vi.fn();
|
||||||
|
|
||||||
|
await updateExtension({
|
||||||
|
enabled: true,
|
||||||
|
addToConfig: mockAddToConfig,
|
||||||
|
removeFromConfig: mockRemoveFromConfig,
|
||||||
|
extensionConfig: mockExtensionConfig,
|
||||||
|
originalName: 'test-extension',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockAddToAgent).toHaveBeenCalledWith(
|
||||||
|
{ ...mockExtensionConfig, name: 'test-extension' },
|
||||||
|
{ silent: true }
|
||||||
|
);
|
||||||
|
expect(mockAddToConfig).toHaveBeenCalledWith(
|
||||||
|
'test-extension',
|
||||||
|
{ ...mockExtensionConfig, name: 'test-extension' },
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(mockToastService.success).toHaveBeenCalledWith({
|
||||||
|
title: 'Update extension',
|
||||||
|
msg: 'Successfully updated test-extension extension',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle name change by removing old and adding new', async () => {
|
||||||
|
mockAddToAgent.mockResolvedValue({} as Response);
|
||||||
|
mockRemoveFromAgent.mockResolvedValue({} as Response);
|
||||||
|
mockRemoveFromConfig.mockResolvedValue(undefined);
|
||||||
|
mockAddToConfig.mockResolvedValue(undefined);
|
||||||
|
mockToastService.success = vi.fn();
|
||||||
|
|
||||||
|
await updateExtension({
|
||||||
|
enabled: true,
|
||||||
|
addToConfig: mockAddToConfig,
|
||||||
|
removeFromConfig: mockRemoveFromConfig,
|
||||||
|
extensionConfig: { ...mockExtensionConfig, name: 'new-extension' },
|
||||||
|
originalName: 'old-extension',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockRemoveFromAgent).toHaveBeenCalledWith('old-extension', { silent: true });
|
||||||
|
expect(mockRemoveFromConfig).toHaveBeenCalledWith('old-extension');
|
||||||
|
expect(mockAddToAgent).toHaveBeenCalledWith(
|
||||||
|
{ ...mockExtensionConfig, name: 'new-extension' },
|
||||||
|
{ silent: true }
|
||||||
|
);
|
||||||
|
expect(mockAddToConfig).toHaveBeenCalledWith(
|
||||||
|
'new-extension',
|
||||||
|
{ ...mockExtensionConfig, name: 'new-extension' },
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update disabled extension without calling agent', async () => {
|
||||||
|
mockAddToConfig.mockResolvedValue(undefined);
|
||||||
|
mockToastService.success = vi.fn();
|
||||||
|
|
||||||
|
await updateExtension({
|
||||||
|
enabled: false,
|
||||||
|
addToConfig: mockAddToConfig,
|
||||||
|
removeFromConfig: mockRemoveFromConfig,
|
||||||
|
extensionConfig: mockExtensionConfig,
|
||||||
|
originalName: 'test-extension',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockAddToAgent).not.toHaveBeenCalled();
|
||||||
|
expect(mockAddToConfig).toHaveBeenCalledWith(
|
||||||
|
'test-extension',
|
||||||
|
{ ...mockExtensionConfig, name: 'test-extension' },
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(mockToastService.success).toHaveBeenCalledWith({
|
||||||
|
title: 'Update extension',
|
||||||
|
msg: 'Successfully updated test-extension extension',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toggleExtension', () => {
|
||||||
|
it('should toggle extension on successfully', async () => {
|
||||||
|
mockAddToAgent.mockResolvedValue({} as Response);
|
||||||
|
mockAddToConfig.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await toggleExtension({
|
||||||
|
toggle: 'toggleOn',
|
||||||
|
extensionConfig: mockExtensionConfig,
|
||||||
|
addToConfig: mockAddToConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockAddToAgent).toHaveBeenCalledWith(mockExtensionConfig, {});
|
||||||
|
expect(mockAddToConfig).toHaveBeenCalledWith('test-extension', mockExtensionConfig, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle extension off successfully', async () => {
|
||||||
|
mockRemoveFromAgent.mockResolvedValue({} as Response);
|
||||||
|
mockAddToConfig.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await toggleExtension({
|
||||||
|
toggle: 'toggleOff',
|
||||||
|
extensionConfig: mockExtensionConfig,
|
||||||
|
addToConfig: mockAddToConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockRemoveFromAgent).toHaveBeenCalledWith('test-extension', {});
|
||||||
|
expect(mockAddToConfig).toHaveBeenCalledWith('test-extension', mockExtensionConfig, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should rollback on agent failure when toggling on', async () => {
|
||||||
|
const agentError = new Error('Agent failed');
|
||||||
|
mockAddToAgent.mockRejectedValue(agentError);
|
||||||
|
mockAddToConfig.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
toggleExtension({
|
||||||
|
toggle: 'toggleOn',
|
||||||
|
extensionConfig: mockExtensionConfig,
|
||||||
|
addToConfig: mockAddToConfig,
|
||||||
|
})
|
||||||
|
).rejects.toThrow('Agent failed');
|
||||||
|
|
||||||
|
expect(mockAddToAgent).toHaveBeenCalledWith(mockExtensionConfig, {});
|
||||||
|
// addToConfig is called during the rollback (toggleOff)
|
||||||
|
expect(mockAddToConfig).toHaveBeenCalledWith('test-extension', mockExtensionConfig, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove from agent if config update fails when toggling on', async () => {
|
||||||
|
const configError = new Error('Config failed');
|
||||||
|
mockAddToAgent.mockResolvedValue({} as Response);
|
||||||
|
mockAddToConfig.mockRejectedValue(configError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
toggleExtension({
|
||||||
|
toggle: 'toggleOn',
|
||||||
|
extensionConfig: mockExtensionConfig,
|
||||||
|
addToConfig: mockAddToConfig,
|
||||||
|
})
|
||||||
|
).rejects.toThrow('Config failed');
|
||||||
|
|
||||||
|
expect(mockAddToAgent).toHaveBeenCalledWith(mockExtensionConfig, {});
|
||||||
|
expect(mockAddToConfig).toHaveBeenCalledWith('test-extension', mockExtensionConfig, true);
|
||||||
|
expect(mockRemoveFromAgent).toHaveBeenCalledWith('test-extension', {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update config even if agent removal fails when toggling off', async () => {
|
||||||
|
const agentError = new Error('Agent removal failed');
|
||||||
|
mockRemoveFromAgent.mockRejectedValue(agentError);
|
||||||
|
mockAddToConfig.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
toggleExtension({
|
||||||
|
toggle: 'toggleOff',
|
||||||
|
extensionConfig: mockExtensionConfig,
|
||||||
|
addToConfig: mockAddToConfig,
|
||||||
|
})
|
||||||
|
).rejects.toThrow('Agent removal failed');
|
||||||
|
|
||||||
|
expect(mockRemoveFromAgent).toHaveBeenCalledWith('test-extension', {});
|
||||||
|
expect(mockAddToConfig).toHaveBeenCalledWith('test-extension', mockExtensionConfig, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteExtension', () => {
|
||||||
|
it('should delete extension successfully', async () => {
|
||||||
|
mockRemoveFromAgent.mockResolvedValue({} as Response);
|
||||||
|
mockRemoveFromConfig.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await deleteExtension({
|
||||||
|
name: 'test-extension',
|
||||||
|
removeFromConfig: mockRemoveFromConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockRemoveFromAgent).toHaveBeenCalledWith('test-extension', { isDelete: true });
|
||||||
|
expect(mockRemoveFromConfig).toHaveBeenCalledWith('test-extension');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove from config even if agent removal fails', async () => {
|
||||||
|
const agentError = new Error('Agent removal failed');
|
||||||
|
mockRemoveFromAgent.mockRejectedValue(agentError);
|
||||||
|
mockRemoveFromConfig.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
deleteExtension({
|
||||||
|
name: 'test-extension',
|
||||||
|
removeFromConfig: mockRemoveFromConfig,
|
||||||
|
})
|
||||||
|
).rejects.toThrow('Agent removal failed');
|
||||||
|
|
||||||
|
expect(mockRemoveFromAgent).toHaveBeenCalledWith('test-extension', { isDelete: true });
|
||||||
|
expect(mockRemoveFromConfig).toHaveBeenCalledWith('test-extension');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw config error if both agent and config fail', async () => {
|
||||||
|
const agentError = new Error('Agent removal failed');
|
||||||
|
const configError = new Error('Config removal failed');
|
||||||
|
mockRemoveFromAgent.mockRejectedValue(agentError);
|
||||||
|
mockRemoveFromConfig.mockRejectedValue(configError);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
deleteExtension({
|
||||||
|
name: 'test-extension',
|
||||||
|
removeFromConfig: mockRemoveFromConfig,
|
||||||
|
})
|
||||||
|
).rejects.toThrow('Config removal failed');
|
||||||
|
|
||||||
|
expect(mockRemoveFromAgent).toHaveBeenCalledWith('test-extension', { isDelete: true });
|
||||||
|
expect(mockRemoveFromConfig).toHaveBeenCalledWith('test-extension');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,459 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import {
|
||||||
|
nameToKey,
|
||||||
|
getDefaultFormData,
|
||||||
|
extensionToFormData,
|
||||||
|
createExtensionConfig,
|
||||||
|
splitCmdAndArgs,
|
||||||
|
combineCmdAndArgs,
|
||||||
|
extractExtensionConfig,
|
||||||
|
replaceWithShims,
|
||||||
|
removeShims,
|
||||||
|
extractCommand,
|
||||||
|
extractExtensionName,
|
||||||
|
DEFAULT_EXTENSION_TIMEOUT,
|
||||||
|
} from './utils';
|
||||||
|
import type { FixedExtensionEntry } from '../../ConfigContext';
|
||||||
|
|
||||||
|
// Mock window.electron
|
||||||
|
const mockElectron = {
|
||||||
|
getBinaryPath: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.defineProperty(window, 'electron', {
|
||||||
|
value: mockElectron,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Extension Utils', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('nameToKey', () => {
|
||||||
|
it('should convert name to lowercase key format', () => {
|
||||||
|
expect(nameToKey('My Extension')).toBe('myextension');
|
||||||
|
expect(nameToKey('Test-Extension_Name')).toBe('test-extension_name');
|
||||||
|
expect(nameToKey('UPPERCASE')).toBe('uppercase');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove spaces', () => {
|
||||||
|
expect(nameToKey('Extension With Spaces')).toBe('extensionwithspaces');
|
||||||
|
expect(nameToKey(' Multiple Spaces ')).toBe('multiplespaces');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getDefaultFormData', () => {
|
||||||
|
it('should return default form data structure', () => {
|
||||||
|
const defaultData = getDefaultFormData();
|
||||||
|
|
||||||
|
expect(defaultData).toEqual({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
type: 'stdio',
|
||||||
|
cmd: '',
|
||||||
|
endpoint: '',
|
||||||
|
enabled: true,
|
||||||
|
timeout: 300,
|
||||||
|
envVars: [],
|
||||||
|
headers: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('extensionToFormData', () => {
|
||||||
|
it('should convert stdio extension to form data', () => {
|
||||||
|
const extension: FixedExtensionEntry = {
|
||||||
|
type: 'stdio',
|
||||||
|
name: 'test-extension',
|
||||||
|
description: 'Test description',
|
||||||
|
cmd: 'python',
|
||||||
|
args: ['script.py', '--flag'],
|
||||||
|
enabled: true,
|
||||||
|
timeout: 600,
|
||||||
|
env_keys: ['API_KEY', 'SECRET'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const formData = extensionToFormData(extension);
|
||||||
|
|
||||||
|
expect(formData).toEqual({
|
||||||
|
name: 'test-extension',
|
||||||
|
description: 'Test description',
|
||||||
|
type: 'stdio',
|
||||||
|
cmd: 'python script.py --flag',
|
||||||
|
endpoint: undefined,
|
||||||
|
enabled: true,
|
||||||
|
timeout: 600,
|
||||||
|
envVars: [
|
||||||
|
{ key: 'API_KEY', value: '••••••••', isEdited: false },
|
||||||
|
{ key: 'SECRET', value: '••••••••', isEdited: false },
|
||||||
|
],
|
||||||
|
headers: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert sse extension to form data', () => {
|
||||||
|
const extension: FixedExtensionEntry = {
|
||||||
|
type: 'sse',
|
||||||
|
name: 'sse-extension',
|
||||||
|
description: 'SSE description',
|
||||||
|
uri: 'http://localhost:8080/events',
|
||||||
|
enabled: false,
|
||||||
|
env_keys: ['TOKEN'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const formData = extensionToFormData(extension);
|
||||||
|
|
||||||
|
expect(formData).toEqual({
|
||||||
|
name: 'sse-extension',
|
||||||
|
description: 'SSE description',
|
||||||
|
type: 'sse',
|
||||||
|
cmd: undefined,
|
||||||
|
endpoint: 'http://localhost:8080/events',
|
||||||
|
enabled: false,
|
||||||
|
timeout: undefined,
|
||||||
|
envVars: [{ key: 'TOKEN', value: '••••••••', isEdited: false }],
|
||||||
|
headers: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert streamable_http extension to form data', () => {
|
||||||
|
const extension: FixedExtensionEntry = {
|
||||||
|
type: 'streamable_http',
|
||||||
|
name: 'http-extension',
|
||||||
|
description: 'HTTP description',
|
||||||
|
uri: 'http://api.example.com',
|
||||||
|
enabled: true,
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer token',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
env_keys: ['API_KEY'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const formData = extensionToFormData(extension);
|
||||||
|
|
||||||
|
expect(formData).toEqual({
|
||||||
|
name: 'http-extension',
|
||||||
|
description: 'HTTP description',
|
||||||
|
type: 'streamable_http',
|
||||||
|
cmd: undefined,
|
||||||
|
endpoint: 'http://api.example.com',
|
||||||
|
enabled: true,
|
||||||
|
timeout: undefined,
|
||||||
|
envVars: [{ key: 'API_KEY', value: '••••••••', isEdited: false }],
|
||||||
|
headers: [
|
||||||
|
{ key: 'Authorization', value: 'Bearer token', isEdited: false },
|
||||||
|
{ key: 'Content-Type', value: 'application/json', isEdited: false },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle legacy envs field', () => {
|
||||||
|
const extension: FixedExtensionEntry = {
|
||||||
|
type: 'stdio',
|
||||||
|
name: 'legacy-extension',
|
||||||
|
cmd: 'node',
|
||||||
|
args: ['app.js'],
|
||||||
|
enabled: true,
|
||||||
|
envs: {
|
||||||
|
OLD_KEY: 'old_value',
|
||||||
|
LEGACY_TOKEN: 'legacy_token',
|
||||||
|
},
|
||||||
|
env_keys: ['NEW_KEY'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const formData = extensionToFormData(extension);
|
||||||
|
|
||||||
|
expect(formData.envVars).toEqual([
|
||||||
|
{ key: 'OLD_KEY', value: 'old_value', isEdited: true },
|
||||||
|
{ key: 'LEGACY_TOKEN', value: 'legacy_token', isEdited: true },
|
||||||
|
{ key: 'NEW_KEY', value: '••••••••', isEdited: false },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle builtin extension', () => {
|
||||||
|
const extension: FixedExtensionEntry = {
|
||||||
|
type: 'builtin',
|
||||||
|
name: 'developer',
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const formData = extensionToFormData(extension);
|
||||||
|
|
||||||
|
expect(formData).toEqual({
|
||||||
|
name: 'developer',
|
||||||
|
description: '',
|
||||||
|
type: 'builtin',
|
||||||
|
cmd: undefined,
|
||||||
|
endpoint: undefined,
|
||||||
|
enabled: true,
|
||||||
|
timeout: undefined,
|
||||||
|
envVars: [],
|
||||||
|
headers: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createExtensionConfig', () => {
|
||||||
|
it('should create stdio extension config', () => {
|
||||||
|
const formData = {
|
||||||
|
name: 'test-stdio',
|
||||||
|
description: 'Test stdio extension',
|
||||||
|
type: 'stdio' as const,
|
||||||
|
cmd: 'python script.py --arg1 --arg2',
|
||||||
|
endpoint: '',
|
||||||
|
enabled: true,
|
||||||
|
timeout: 300,
|
||||||
|
envVars: [
|
||||||
|
{ key: 'API_KEY', value: 'secret123', isEdited: true },
|
||||||
|
{ key: '', value: '', isEdited: false }, // Should be filtered out
|
||||||
|
],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = createExtensionConfig(formData);
|
||||||
|
|
||||||
|
expect(config).toEqual({
|
||||||
|
type: 'stdio',
|
||||||
|
name: 'test-stdio',
|
||||||
|
description: 'Test stdio extension',
|
||||||
|
cmd: 'python',
|
||||||
|
args: ['script.py', '--arg1', '--arg2'],
|
||||||
|
timeout: 300,
|
||||||
|
env_keys: ['API_KEY'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create sse extension config', () => {
|
||||||
|
const formData = {
|
||||||
|
name: 'test-sse',
|
||||||
|
description: 'Test SSE extension',
|
||||||
|
type: 'sse' as const,
|
||||||
|
cmd: '',
|
||||||
|
endpoint: 'http://localhost:8080/events',
|
||||||
|
enabled: true,
|
||||||
|
timeout: 600,
|
||||||
|
envVars: [{ key: 'TOKEN', value: 'abc123', isEdited: true }],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = createExtensionConfig(formData);
|
||||||
|
|
||||||
|
expect(config).toEqual({
|
||||||
|
type: 'sse',
|
||||||
|
name: 'test-sse',
|
||||||
|
description: 'Test SSE extension',
|
||||||
|
timeout: 600,
|
||||||
|
uri: 'http://localhost:8080/events',
|
||||||
|
env_keys: ['TOKEN'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create streamable_http extension config', () => {
|
||||||
|
const formData = {
|
||||||
|
name: 'test-http',
|
||||||
|
description: 'Test HTTP extension',
|
||||||
|
type: 'streamable_http' as const,
|
||||||
|
cmd: '',
|
||||||
|
endpoint: 'http://api.example.com',
|
||||||
|
enabled: true,
|
||||||
|
timeout: 300,
|
||||||
|
envVars: [{ key: 'API_KEY', value: 'key123', isEdited: true }],
|
||||||
|
headers: [
|
||||||
|
{ key: 'Authorization', value: 'Bearer token', isEdited: true },
|
||||||
|
{ key: '', value: '', isEdited: false }, // Should be filtered out
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = createExtensionConfig(formData);
|
||||||
|
|
||||||
|
expect(config).toEqual({
|
||||||
|
type: 'streamable_http',
|
||||||
|
name: 'test-http',
|
||||||
|
description: 'Test HTTP extension',
|
||||||
|
timeout: 300,
|
||||||
|
uri: 'http://api.example.com',
|
||||||
|
env_keys: ['API_KEY'],
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer token',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create builtin extension config', () => {
|
||||||
|
const formData = {
|
||||||
|
name: 'developer',
|
||||||
|
description: '',
|
||||||
|
type: 'builtin' as const,
|
||||||
|
cmd: '',
|
||||||
|
endpoint: '',
|
||||||
|
enabled: true,
|
||||||
|
timeout: 300,
|
||||||
|
envVars: [],
|
||||||
|
headers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = createExtensionConfig(formData);
|
||||||
|
|
||||||
|
expect(config).toEqual({
|
||||||
|
type: 'builtin',
|
||||||
|
name: 'developer',
|
||||||
|
timeout: 300,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('splitCmdAndArgs', () => {
|
||||||
|
it('should split command and arguments correctly', () => {
|
||||||
|
expect(splitCmdAndArgs('python script.py --flag value')).toEqual({
|
||||||
|
cmd: 'python',
|
||||||
|
args: ['script.py', '--flag', 'value'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(splitCmdAndArgs('node')).toEqual({
|
||||||
|
cmd: 'node',
|
||||||
|
args: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(splitCmdAndArgs('')).toEqual({
|
||||||
|
cmd: '',
|
||||||
|
args: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(splitCmdAndArgs(' multiple spaces between args ')).toEqual({
|
||||||
|
cmd: 'multiple',
|
||||||
|
args: ['spaces', 'between', 'args'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('combineCmdAndArgs', () => {
|
||||||
|
it('should combine command and arguments correctly', () => {
|
||||||
|
expect(combineCmdAndArgs('python', ['script.py', '--flag', 'value'])).toBe(
|
||||||
|
'python script.py --flag value'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(combineCmdAndArgs('node', [])).toBe('node');
|
||||||
|
|
||||||
|
expect(combineCmdAndArgs('', ['arg1', 'arg2'])).toBe(' arg1 arg2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('extractExtensionConfig', () => {
|
||||||
|
it('should extract extension config from fixed entry', () => {
|
||||||
|
const fixedEntry: FixedExtensionEntry = {
|
||||||
|
type: 'stdio',
|
||||||
|
name: 'test-extension',
|
||||||
|
cmd: 'python',
|
||||||
|
args: ['script.py'],
|
||||||
|
enabled: true,
|
||||||
|
timeout: 300,
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = extractExtensionConfig(fixedEntry);
|
||||||
|
|
||||||
|
expect(config).toEqual({
|
||||||
|
type: 'stdio',
|
||||||
|
name: 'test-extension',
|
||||||
|
cmd: 'python',
|
||||||
|
args: ['script.py'],
|
||||||
|
enabled: true,
|
||||||
|
timeout: 300,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('replaceWithShims', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockElectron.getBinaryPath.mockImplementation((binary: string) => {
|
||||||
|
const paths: Record<string, string> = {
|
||||||
|
goosed: '/path/to/goosed',
|
||||||
|
jbang: '/path/to/jbang',
|
||||||
|
npx: '/path/to/npx',
|
||||||
|
uvx: '/path/to/uvx',
|
||||||
|
};
|
||||||
|
return Promise.resolve(paths[binary] || binary);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace known commands with shim paths', async () => {
|
||||||
|
expect(await replaceWithShims('goosed')).toBe('/path/to/goosed');
|
||||||
|
expect(await replaceWithShims('jbang')).toBe('/path/to/jbang');
|
||||||
|
expect(await replaceWithShims('npx')).toBe('/path/to/npx');
|
||||||
|
expect(await replaceWithShims('uvx')).toBe('/path/to/uvx');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should leave unknown commands unchanged', async () => {
|
||||||
|
expect(await replaceWithShims('python')).toBe('python');
|
||||||
|
expect(await replaceWithShims('node')).toBe('node');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeShims', () => {
|
||||||
|
it('should remove shim paths and return command name', () => {
|
||||||
|
expect(removeShims('/path/to/goosed')).toBe('goosed');
|
||||||
|
expect(removeShims('/usr/local/bin/jbang')).toBe('jbang');
|
||||||
|
expect(removeShims('/Applications/Docker.app/Contents/Resources/bin/docker')).toBe('docker');
|
||||||
|
expect(removeShims('/path/to/npx.cmd')).toBe('npx.cmd');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle paths with trailing slashes', () => {
|
||||||
|
// The removeShims function only works if the path ends with the shim pattern
|
||||||
|
// Trailing slashes prevent the pattern from matching
|
||||||
|
expect(removeShims('/path/to/goosed/')).toBe('/path/to/goosed/');
|
||||||
|
expect(removeShims('/path/to/uvx//')).toBe('/path/to/uvx//');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should leave non-shim commands unchanged', () => {
|
||||||
|
expect(removeShims('python')).toBe('python');
|
||||||
|
expect(removeShims('node')).toBe('node');
|
||||||
|
expect(removeShims('/usr/bin/python3')).toBe('/usr/bin/python3');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('extractCommand', () => {
|
||||||
|
it('should extract command from extension link', () => {
|
||||||
|
const link = 'goose://extension/add?name=Test&cmd=python&arg=script.py&arg=--flag';
|
||||||
|
expect(extractCommand(link)).toBe('python script.py --flag');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle encoded arguments', () => {
|
||||||
|
const link = 'goose://extension/add?cmd=echo&arg=hello%20world&arg=--test%3Dvalue';
|
||||||
|
expect(extractCommand(link)).toBe('echo hello world --test=value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing command', () => {
|
||||||
|
const link = 'goose://extension/add?name=Test';
|
||||||
|
expect(extractCommand(link)).toBe('Unknown Command');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle command without arguments', () => {
|
||||||
|
const link = 'goose://extension/add?cmd=python';
|
||||||
|
expect(extractCommand(link)).toBe('python');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('extractExtensionName', () => {
|
||||||
|
it('should extract extension name from link', () => {
|
||||||
|
const link = 'goose://extension/add?name=Test%20Extension&cmd=python';
|
||||||
|
expect(extractExtensionName(link)).toBe('Test Extension');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing name', () => {
|
||||||
|
const link = 'goose://extension/add?cmd=python';
|
||||||
|
expect(extractExtensionName(link)).toBe('Unknown Extension');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should decode URL encoded names', () => {
|
||||||
|
const link = 'goose://extension/add?name=My%20Special%20Extension%21';
|
||||||
|
expect(extractExtensionName(link)).toBe('My Special Extension!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DEFAULT_EXTENSION_TIMEOUT', () => {
|
||||||
|
it('should have correct default timeout value', () => {
|
||||||
|
expect(DEFAULT_EXTENSION_TIMEOUT).toBe(300);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user