426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
460 lines
12 KiB
Markdown
460 lines
12 KiB
Markdown
---
|
|
title: "Testing Plugins"
|
|
sidebarTitle: "Testing"
|
|
description: "Unit, integration, and E2E testing patterns for elizaOS plugins using Vitest."
|
|
---
|
|
|
|
This guide covers testing patterns for elizaOS plugins — from unit testing individual actions and providers to integration testing with the runtime, and embedding test suites in your plugin.
|
|
|
|
## Setup
|
|
|
|
Plugins use [Vitest](https://vitest.dev/) as the test runner. Add it to your plugin's dev dependencies:
|
|
|
|
```json
|
|
{
|
|
"devDependencies": {
|
|
"vitest": "^4.0.0"
|
|
},
|
|
"scripts": {
|
|
"test": "vitest run",
|
|
"test:watch": "vitest watch",
|
|
"test:coverage": "vitest run --coverage"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Mock Runtime Factory
|
|
|
|
Most plugin tests need a mock `IAgentRuntime`. Create a shared helper:
|
|
|
|
```typescript
|
|
// tests/helpers.ts
|
|
import { vi } from 'vitest';
|
|
import {
|
|
AgentRuntime,
|
|
createCharacter,
|
|
createMessageMemory,
|
|
InMemoryDatabaseAdapter,
|
|
stringToUuid,
|
|
type IAgentRuntime,
|
|
type Memory,
|
|
type State,
|
|
} from '@elizaos/core';
|
|
|
|
export function createMockRuntime(
|
|
overrides?: Partial<IAgentRuntime>
|
|
): IAgentRuntime {
|
|
const runtime = new AgentRuntime({
|
|
agentId: stringToUuid('test-agent'),
|
|
character: createCharacter({ name: 'Test Agent' }),
|
|
adapter: new InMemoryDatabaseAdapter(),
|
|
plugins: [],
|
|
logLevel: 'error',
|
|
});
|
|
|
|
runtime.getSetting = vi.fn((key: string) => process.env[key]);
|
|
runtime.getService = vi.fn();
|
|
runtime.composeState = vi
|
|
.fn()
|
|
.mockResolvedValue({ values: {}, data: {}, text: '' } satisfies State);
|
|
|
|
Object.assign(runtime, overrides);
|
|
return runtime;
|
|
}
|
|
|
|
export function createMockMessage(
|
|
text: string,
|
|
overrides?: Partial<Memory>
|
|
): Memory {
|
|
return {
|
|
...createMessageMemory({
|
|
id: stringToUuid('test-message'),
|
|
entityId: stringToUuid('test-user'),
|
|
roomId: stringToUuid('test-room'),
|
|
content: { text },
|
|
}),
|
|
...overrides,
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Unit Testing Actions
|
|
|
|
Test the `validate` and `handler` methods independently:
|
|
|
|
```typescript
|
|
// tests/actions/weather.test.ts
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { checkWeatherAction } from '../../src/actions/weather';
|
|
import { createMockRuntime, createMockMessage } from '../helpers';
|
|
|
|
describe('checkWeatherAction', () => {
|
|
let runtime: ReturnType<typeof createMockRuntime>;
|
|
|
|
beforeEach(() => {
|
|
runtime = createMockRuntime({
|
|
getSetting: vi.fn((key) => {
|
|
if (key === 'WEATHER_API_KEY') return 'test-key-123';
|
|
return undefined;
|
|
}),
|
|
});
|
|
});
|
|
|
|
describe('validate', () => {
|
|
it('returns true when API key is configured', async () => {
|
|
const message = createMockMessage('What is the weather?');
|
|
const result = await checkWeatherAction.validate(runtime, message);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('returns false when API key is missing', async () => {
|
|
const noKeyRuntime = createMockRuntime();
|
|
const message = createMockMessage('What is the weather?');
|
|
const result = await checkWeatherAction.validate(noKeyRuntime, message);
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('handler', () => {
|
|
it('returns weather data on success', async () => {
|
|
// Mock the fetch call
|
|
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ temp: 22, condition: 'Sunny' }),
|
|
});
|
|
|
|
const message = createMockMessage('Weather in Tokyo');
|
|
const state: State = { values: {}, data: {}, text: '' };
|
|
const result = await checkWeatherAction.handler(
|
|
runtime,
|
|
message,
|
|
state,
|
|
{ parameters: { city: 'Tokyo' } }
|
|
);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.text).toContain('Tokyo');
|
|
});
|
|
|
|
it('returns error on API failure', async () => {
|
|
globalThis.fetch = vi.fn().mockRejectedValue(new Error('Network error'));
|
|
|
|
const message = createMockMessage('Weather in Tokyo');
|
|
const state: State = { values: {}, data: {}, text: '' };
|
|
const result = await checkWeatherAction.handler(
|
|
runtime,
|
|
message,
|
|
state,
|
|
{ parameters: { city: 'Tokyo' } }
|
|
);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Network error');
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Unit Testing Providers
|
|
|
|
Providers return context strings. Test that the output is well-formatted and contains expected data:
|
|
|
|
```typescript
|
|
// tests/providers/status.test.ts
|
|
import { describe, it, expect } from 'vitest';
|
|
import { pluginStatusProvider } from '../../src/providers/status';
|
|
import { createMockRuntime, createMockMessage } from '../helpers';
|
|
|
|
describe('pluginStatusProvider', () => {
|
|
it('returns active status when API key is set', async () => {
|
|
process.env.WEATHER_API_KEY = 'test-key';
|
|
const runtime = createMockRuntime();
|
|
const message = createMockMessage('hello');
|
|
|
|
const state: State = { values: {}, data: {}, text: '' };
|
|
const result = await pluginStatusProvider.get(runtime, message, state);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(typeof result.text).toBe('string');
|
|
expect(result.text).toContain('active');
|
|
|
|
delete process.env.WEATHER_API_KEY;
|
|
});
|
|
|
|
it('returns inactive status when API key is missing', async () => {
|
|
delete process.env.WEATHER_API_KEY;
|
|
const runtime = createMockRuntime();
|
|
const message = createMockMessage('hello');
|
|
|
|
const state: State = { values: {}, data: {}, text: '' };
|
|
const result = await pluginStatusProvider.get(runtime, message, state);
|
|
|
|
expect(result.text).toContain('missing');
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Unit Testing Services
|
|
|
|
Test that services start and stop cleanly without leaking resources:
|
|
|
|
```typescript
|
|
// tests/services/weather-cache.test.ts
|
|
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import { WeatherCacheService } from '../../src/services/weather-cache';
|
|
import { createMockRuntime } from '../helpers';
|
|
|
|
describe('WeatherCacheService', () => {
|
|
let service: { stop: () => Promise<void> } | undefined;
|
|
|
|
it('starts without errors', async () => {
|
|
const runtime = createMockRuntime();
|
|
service = await WeatherCacheService.start(runtime);
|
|
expect(service).toBeDefined();
|
|
expect(service.stop).toBeTypeOf('function');
|
|
});
|
|
|
|
it('stops cleanly', async () => {
|
|
const runtime = createMockRuntime();
|
|
service = await WeatherCacheService.start(runtime);
|
|
await expect(service.stop()).resolves.toBeUndefined();
|
|
service = undefined;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
if (service) await service.stop();
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Integration Testing
|
|
|
|
For tests that need the full runtime (database, memory, state composition), bootstrap a test runtime:
|
|
|
|
```typescript
|
|
// tests/integration/plugin.test.ts
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import type { IAgentRuntime } from '@elizaos/core';
|
|
import weatherPlugin from '../../src/index';
|
|
|
|
describe('weather plugin integration', () => {
|
|
let runtime: IAgentRuntime;
|
|
|
|
beforeAll(async () => {
|
|
// If your test setup bootstraps a real runtime:
|
|
// runtime = await createTestRuntime({ plugins: [weatherPlugin] });
|
|
|
|
// Or use a mock with real state composition:
|
|
const runtimeMock = {
|
|
agentId: 'test-agent',
|
|
getSetting: (key: string) => process.env[key],
|
|
logger: console,
|
|
// Add other methods your plugin needs
|
|
} satisfies Partial<IAgentRuntime>;
|
|
runtime = runtimeMock as IAgentRuntime;
|
|
|
|
// Initialize the plugin
|
|
if (weatherPlugin.init) {
|
|
await weatherPlugin.init({}, runtime);
|
|
}
|
|
});
|
|
|
|
it('registers all actions', () => {
|
|
expect(weatherPlugin.actions).toHaveLength(1);
|
|
expect(weatherPlugin.actions![0].name).toBe('CHECK_WEATHER');
|
|
});
|
|
|
|
it('registers all providers', () => {
|
|
expect(weatherPlugin.providers).toHaveLength(1);
|
|
expect(weatherPlugin.providers![0].name).toBe('weatherPluginStatus');
|
|
});
|
|
|
|
it('plugin init logs correctly', () => {
|
|
// Verify init was called without errors
|
|
expect(weatherPlugin.name).toBe('weather-plugin');
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Mocking Patterns
|
|
|
|
### Mocking LLM Responses
|
|
|
|
When testing actions that call the LLM via `runtime.useModel`:
|
|
|
|
```typescript
|
|
const runtime = createMockRuntime({
|
|
useModel: vi.fn().mockResolvedValue({
|
|
text: 'The weather in Tokyo is 22°C and sunny.',
|
|
}),
|
|
});
|
|
```
|
|
|
|
### Mocking Database Calls
|
|
|
|
```typescript
|
|
const runtime = createMockRuntime({
|
|
getMemoryManager: vi.fn().mockReturnValue({
|
|
searchMemories: vi.fn().mockResolvedValue([]),
|
|
createMemory: vi.fn().mockResolvedValue(undefined),
|
|
}),
|
|
});
|
|
```
|
|
|
|
### Mocking External APIs
|
|
|
|
Use `vi.fn()` on `globalThis.fetch` or inject a mock HTTP client:
|
|
|
|
```typescript
|
|
globalThis.fetch = vi.fn()
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve({ data: 'first call' }),
|
|
})
|
|
.mockResolvedValueOnce({
|
|
ok: false,
|
|
status: 429,
|
|
statusText: 'Too Many Requests',
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## TestSuite: Embedded Plugin Tests
|
|
|
|
Plugins can embed tests via the `tests` field. These run when users execute `bun test`:
|
|
|
|
```typescript
|
|
import type { Plugin, TestSuite, Memory, State } from '@elizaos/core';
|
|
import { checkWeatherAction } from './actions/weather';
|
|
import { pluginStatusProvider } from './providers/status';
|
|
|
|
const weatherTests: TestSuite = {
|
|
name: 'weather-plugin-tests',
|
|
tests: [
|
|
{
|
|
name: 'action validates with API key',
|
|
fn: async (runtime) => {
|
|
const msg = { content: { text: 'weather' } } as Memory;
|
|
const valid = await checkWeatherAction.validate(runtime, msg);
|
|
if (!valid) throw new Error('Expected validation to pass');
|
|
},
|
|
},
|
|
{
|
|
name: 'provider returns context',
|
|
fn: async (runtime) => {
|
|
const msg = { content: { text: 'status' } } as Memory;
|
|
const state: State = { values: {}, data: {}, text: '' };
|
|
const result = await pluginStatusProvider.get(runtime, msg, state);
|
|
if (!result.text) throw new Error('Expected non-empty text');
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const weatherPlugin: Plugin = {
|
|
name: 'weather-plugin',
|
|
description: 'Weather information plugin',
|
|
actions: [checkWeatherAction],
|
|
providers: [pluginStatusProvider],
|
|
tests: [weatherTests],
|
|
};
|
|
|
|
export default weatherPlugin;
|
|
```
|
|
|
|
---
|
|
|
|
## Running Tests
|
|
|
|
```bash
|
|
# Run all tests
|
|
vitest run
|
|
|
|
# Run with coverage report
|
|
vitest run --coverage
|
|
|
|
# Run a specific test file
|
|
vitest run tests/actions/weather.test.ts
|
|
|
|
# Watch mode (re-runs on file changes)
|
|
vitest watch
|
|
```
|
|
|
|
### Coverage Thresholds
|
|
|
|
The monorepo enforces minimum coverage in `vitest.config.ts`:
|
|
|
|
| Metric | Minimum |
|
|
|--------|---------|
|
|
| Lines | 25% |
|
|
| Functions | 25% |
|
|
| Statements | 25% |
|
|
| Branches | 15% |
|
|
|
|
For standalone published plugins, aim for **80% coverage** — this is the recommended bar for quality.
|
|
|
|
---
|
|
|
|
## E2E Testing
|
|
|
|
For end-to-end testing with a running agent, the starter template includes a Cypress scaffold:
|
|
|
|
```
|
|
my-plugin/
|
|
├── cypress/
|
|
│ ├── e2e/
|
|
│ │ └── plugin.cy.ts
|
|
│ └── support/
|
|
│ └── commands.ts
|
|
└── cypress.config.ts
|
|
```
|
|
|
|
E2E tests start the agent, load the plugin, and verify behavior through the chat or API:
|
|
|
|
```typescript
|
|
// cypress/e2e/plugin.cy.ts
|
|
describe('Weather Plugin E2E', () => {
|
|
it('responds to weather queries', () => {
|
|
cy.request('POST', 'http://localhost:18789/api/conversations', {
|
|
title: 'Weather Plugin Test',
|
|
}).then(({ body }) => {
|
|
const conversationId = body.conversation.id;
|
|
cy.request(
|
|
'POST',
|
|
`http://localhost:18789/api/conversations/${conversationId}/messages`,
|
|
{ text: 'What is the weather in London?' },
|
|
).then((response) => {
|
|
expect(response.status).to.eq(200);
|
|
expect(response.body.text).to.include('London');
|
|
});
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Related
|
|
|
|
- [Create a Plugin](/plugins/create-a-plugin) — Build a plugin from scratch
|
|
- [Plugin Patterns](/plugins/patterns) — Common implementation patterns
|
|
- [Plugin Schemas](/plugins/schemas) — Full type reference
|
|
- [Contributing Guide](https://github.com/elizaOS/eliza/blob/develop/CONTRIBUTING.md) — Test conventions for the monorepo
|