Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client-eliza-home: test config and test coverage #2719

Merged
merged 9 commits into from
Jan 24, 2025
234 changes: 234 additions & 0 deletions packages/client-eliza-home/__tests__/services/smart_things_api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { IAgentRuntime } from '@elizaos/core';

// Mock global fetch
const mockFetch = vi.fn();
global.fetch = mockFetch;

// Create a mock class that matches the SmartThingsApi interface
class MockSmartThingsApi {
private baseUrl = 'https://api.smartthings.com/v1';
private token: string;

constructor(runtime: IAgentRuntime) {
this.token = runtime.getSetting("SMARTTHINGS_TOKEN");
if (!this.token) {
throw new Error("SmartThings token is required");
}
}

private async request(endpoint: string, options: RequestInit = {}) {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json',
...options.headers,
},
});

if (!response.ok) {
throw new Error(`SmartThings API error: ${response.statusText}`);
}

return response.json();
}

devices = {
list: () => this.request('/devices'),
get: (deviceId: string) => this.request(`/devices/${deviceId}`),
getStatus: (deviceId: string) => this.request(`/devices/${deviceId}/status`),
executeCommand: (deviceId: string, command: any) =>
this.request(`/devices/${deviceId}/commands`, {
method: 'POST',
body: JSON.stringify({
commands: [command]
})
}),
executeCommands: (deviceId: string, commands: any[]) =>
this.request(`/devices/${deviceId}/commands`, {
method: 'POST',
body: JSON.stringify({ commands })
}),
getComponents: (deviceId: string) =>
this.request(`/devices/${deviceId}/components`),
getCapabilities: (deviceId: string) =>
this.request(`/devices/${deviceId}/capabilities`)
};

scenes = {
list: () => this.request('/scenes'),
execute: (sceneId: string) =>
this.request(`/scenes/${sceneId}/execute`, {
method: 'POST'
})
};

rooms = {
list: () => this.request('/rooms'),
get: (roomId: string) => this.request(`/rooms/${roomId}`)
};
}

describe('SmartThingsApi', () => {
let api: MockSmartThingsApi;
let mockRuntime: IAgentRuntime;

beforeEach(() => {
vi.clearAllMocks();
mockRuntime = {
getSetting: vi.fn().mockReturnValue('mock-token'),
} as unknown as IAgentRuntime;
api = new MockSmartThingsApi(mockRuntime);
});

it('should throw error if token is not provided', () => {
const runtimeWithoutToken = {
getSetting: vi.fn().mockReturnValue(null),
} as unknown as IAgentRuntime;

expect(() => new MockSmartThingsApi(runtimeWithoutToken))
.toThrow('SmartThings token is required');
});

describe('devices', () => {
beforeEach(() => {
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: 'success' }),
});
});

it('should list devices', async () => {
await api.devices.list();

expect(mockFetch).toHaveBeenCalledWith(
'https://api.smartthings.com/v1/devices',
expect.objectContaining({
headers: expect.objectContaining({
'Authorization': 'Bearer mock-token',
'Content-Type': 'application/json',
}),
})
);
});

it('should get device details', async () => {
const deviceId = 'device123';
await api.devices.get(deviceId);

expect(mockFetch).toHaveBeenCalledWith(
`https://api.smartthings.com/v1/devices/${deviceId}`,
expect.objectContaining({
headers: expect.objectContaining({
'Authorization': 'Bearer mock-token',
}),
})
);
});

it('should execute device command', async () => {
const deviceId = 'device123';
const command = { capability: 'switch', command: 'on' };

await api.devices.executeCommand(deviceId, command);

expect(mockFetch).toHaveBeenCalledWith(
`https://api.smartthings.com/v1/devices/${deviceId}/commands`,
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ commands: [command] }),
headers: expect.objectContaining({
'Authorization': 'Bearer mock-token',
'Content-Type': 'application/json',
}),
})
);
});

it('should handle API errors', async () => {
mockFetch.mockResolvedValue({
ok: false,
statusText: 'Not Found',
});

await expect(api.devices.list())
.rejects
.toThrow('SmartThings API error: Not Found');
});
});

describe('scenes', () => {
beforeEach(() => {
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: 'success' }),
});
});

it('should list scenes', async () => {
await api.scenes.list();

expect(mockFetch).toHaveBeenCalledWith(
'https://api.smartthings.com/v1/scenes',
expect.objectContaining({
headers: expect.objectContaining({
'Authorization': 'Bearer mock-token',
}),
})
);
});

it('should execute scene', async () => {
const sceneId = 'scene123';
await api.scenes.execute(sceneId);

expect(mockFetch).toHaveBeenCalledWith(
`https://api.smartthings.com/v1/scenes/${sceneId}/execute`,
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Authorization': 'Bearer mock-token',
}),
})
);
});
});

describe('rooms', () => {
beforeEach(() => {
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: 'success' }),
});
});

it('should list rooms', async () => {
await api.rooms.list();

expect(mockFetch).toHaveBeenCalledWith(
'https://api.smartthings.com/v1/rooms',
expect.objectContaining({
headers: expect.objectContaining({
'Authorization': 'Bearer mock-token',
}),
})
);
});

it('should get room details', async () => {
const roomId = 'room123';
await api.rooms.get(roomId);

expect(mockFetch).toHaveBeenCalledWith(
`https://api.smartthings.com/v1/rooms/${roomId}`,
expect.objectContaining({
headers: expect.objectContaining({
'Authorization': 'Bearer mock-token',
}),
})
);
});
});
});
118 changes: 118 additions & 0 deletions packages/client-eliza-home/__tests__/smart_home.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { SmartHomeManager } from '../src/smart_home';
import { SmartThingsApi } from '../src/services/smart_things_api';
import { CommandParser } from '../src/utils/command_parser';
import type { IAgentRuntime } from '@elizaos/core';

// Define mock interface that extends IAgentRuntime
interface MockAgentRuntime extends IAgentRuntime {
llm: {
shouldRespond: ReturnType<typeof vi.fn>;
complete: ReturnType<typeof vi.fn>;
};
}

// Mock dependencies
vi.mock('../src/services/smart_things_api', () => ({
SmartThingsApi: vi.fn().mockImplementation(() => ({
devices: {
list: vi.fn().mockResolvedValue([]),
executeCommand: vi.fn().mockResolvedValue({ status: 'success' })
}
}))
}));
vi.mock('../src/utils/command_parser');
vi.mock('@elizaos/core', () => ({
elizaLogger: {
error: vi.fn(),
},
}));

describe('SmartHomeManager', () => {
let smartHomeManager: SmartHomeManager;
let mockRuntime: MockAgentRuntime;

beforeEach(() => {
// Reset all mocks
vi.clearAllMocks();

// Create mock runtime with proper typing
mockRuntime = {
llm: {
shouldRespond: vi.fn(),
complete: vi.fn(),
},
getSetting: vi.fn().mockReturnValue('mock-token'),
// Add required IAgentRuntime properties
agentId: 'test-agent-id',
serverUrl: 'http://test-server',
databaseAdapter: {
init: vi.fn(),
close: vi.fn(),
// Add other required database methods as needed
},
token: 'test-token',
modelProvider: 'test-provider',
} as MockAgentRuntime;

smartHomeManager = new SmartHomeManager(mockRuntime);
});

describe('handleCommand', () => {
it('should return null when shouldRespond returns IGNORE', async () => {
// Arrange
vi.mocked(mockRuntime.llm.shouldRespond).mockResolvedValue('IGNORE');

// Act
const result = await smartHomeManager.handleCommand('turn on lights', 'user123');

// Assert
expect(result).toBeNull();
expect(mockRuntime.llm.shouldRespond).toHaveBeenCalledWith(
expect.any(String),
'turn on lights'
);
});

it('should execute command and return response when shouldRespond returns RESPOND', async () => {
// Arrange
const mockResponse = 'Command executed successfully';
vi.mocked(mockRuntime.llm.shouldRespond).mockResolvedValue('RESPOND');
vi.mocked(mockRuntime.llm.complete).mockResolvedValue(mockResponse);
vi.mocked(CommandParser.parseCommand).mockReturnValue({
command: 'turn_on',
args: { device: 'lights' }
});
vi.mocked(CommandParser.mapToDeviceCommand).mockReturnValue({
deviceId: 'device123',
capability: 'switch',
command: 'on'
});

// Act
const result = await smartHomeManager.handleCommand('turn on lights', 'user123');

// Assert
expect(result).toEqual({
success: true,
message: mockResponse,
data: { status: 'success' }
});
expect(mockRuntime.llm.shouldRespond).toHaveBeenCalled();
expect(mockRuntime.llm.complete).toHaveBeenCalled();
expect(CommandParser.parseCommand).toHaveBeenCalledWith('turn on lights');
expect(CommandParser.mapToDeviceCommand).toHaveBeenCalled();
});

it('should handle errors gracefully', async () => {
// Arrange
const mockError = new Error('Test error');
vi.mocked(mockRuntime.llm.shouldRespond).mockRejectedValue(mockError);

// Act & Assert
await expect(smartHomeManager.handleCommand('turn on lights', 'user123'))
.rejects
.toThrow(mockError);
});
});
});
Loading
Loading