Added tests for extensions functionality (#3794)

This commit is contained in:
Zane
2025-08-19 09:52:51 -07:00
committed by GitHub
parent 2bc297c763
commit 93949bb7f8
3 changed files with 1131 additions and 0 deletions
@@ -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);
});
});
});