Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

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