-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2719 from ai16z-demirix/tests/client-eliza-home
client-eliza-home: test config and test coverage
- Loading branch information
Showing
5 changed files
with
498 additions
and
2 deletions.
There are no files selected for viewing
234 changes: 234 additions & 0 deletions
234
packages/client-eliza-home/__tests__/services/smart_things_api.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
118
packages/client-eliza-home/__tests__/smart_home.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.