Skip to main content

Testing Plugins

The @ever-works/plugin package provides a comprehensive testing toolkit in @ever-works/plugin/testing. It includes mock implementations of all platform services, a test harness for lifecycle testing, and contract test suites that verify plugins conform to their capability interfaces.

Test Infrastructure​

All plugins use Vitest as their test runner. Each plugin package has:

packages/plugins/my-plugin/
src/
__tests__/
my-plugin.spec.ts
my-plugin.plugin.ts
index.ts
package.json
tsconfig.json
tsup.config.ts
vitest.config.ts

Run tests for a single plugin:

cd packages/plugins/my-plugin
pnpm test # Run all tests
pnpm test:watch # Watch mode
pnpm test:coverage # With coverage report
npx vitest run src/__tests__/my-plugin.spec.ts # Single file

Mock Plugin Context​

The createMockPluginContext() function creates a fully functional mock of the PluginContext that plugins receive during onLoad(). Every service is mocked with working implementations:

import { createMockPluginContext } from '@ever-works/plugin/testing';

const context = createMockPluginContext({
pluginId: 'my-plugin',
settings: {
apiKey: 'test-key-123',
defaultModel: 'test-model'
},
envVars: {
NODE_ENV: 'test',
PLUGIN_MY_API_KEY: 'env-key-456'
}
});

// Use in tests
await myPlugin.onLoad(context);

Mock Context Options​

interface MockPluginContextOptions {
pluginId?: string; // Plugin ID (default: 'test-plugin')
settings?: PluginSettings; // Pre-configured settings
env?: MockPluginEnvironmentOptions; // Environment overrides
envVars?: Record<string, string>; // Environment variables
works?: Map<string, WorkInfo>; // Mock work data
users?: Map<string, UserInfo>; // Mock user data
currentUser?: UserInfo; // Current user for the session
httpResponses?: Map<string, HttpResponse>; // Pre-configured HTTP responses
}

What Gets Mocked​

The mock context includes functional implementations of every platform service:

ServiceMock Behavior
LoggerRecords all log calls; accessible via context.logger.log.mock.calls
CacheIn-memory Map with TTL support
HTTP ClientReturns pre-configured responses by method + URL key
EnvironmentConfigurable platform version, environment flags, paths
Environment VariablesIn-memory variable store
Work ServiceLookup by ID or slug from provided Map
User ServiceLookup by ID; returns configurable current user
EventsWorking event subscription and emission
Custom CapabilitiesFull registration and retrieval
SettingsReturns configured settings for any scope

Testing Event Handlers​

The mock context exposes internal helpers for testing event-driven behavior:

const context = createMockPluginContext();
await myPlugin.onLoad(context);

// Simulate an event from outside the plugin
context._triggerEvent('work.created', {
workId: 'dir-123',
timestamp: new Date().toISOString()
});

// Inspect registered handlers
const handlers = context._eventHandlers.get('work.created');
expect(handlers?.size).toBe(1);

Testing HTTP Interactions​

Pre-configure HTTP responses for specific endpoints:

const httpResponses = new Map([
[
'GET:https://api.example.com/models',
{
status: 200,
statusText: 'OK',
headers: {},
data: { models: [{ id: 'model-1', name: 'Test Model' }] }
}
],
[
'POST:https://api.example.com/chat',
{
status: 200,
statusText: 'OK',
headers: {},
data: { id: 'resp-1', choices: [{ message: { content: 'Hello' } }] }
}
]
]);

const context = createMockPluginContext({ httpResponses });

Mock Plugin Environment​

Create environment configurations for different testing scenarios:

import {
createMockPluginEnvironment,
createProductionEnvironment,
createDevelopmentEnvironment
} from '@ever-works/plugin/testing';

// Default test environment
const testEnv = createMockPluginEnvironment();
// testEnv.isTest === true, testEnv.isDevelopment === true

// Production-like environment
const prodEnv = createProductionEnvironment();
// prodEnv.isProduction === true, prodEnv.baseUrl === 'https://example.com'

// Development environment
const devEnv = createDevelopmentEnvironment();
// devEnv.isDevelopment === true, devEnv.isTest === false

// Custom environment
const customEnv = createMockPluginEnvironment({
platform: 'ever-works',
platformVersion: '2.0.0',
isProduction: false,
features: new Set(['feature-flag-a', 'feature-flag-b']),
baseUrl: 'http://localhost:3000',
tempDir: '/tmp/test'
});

Plugin Test Harness​

The PluginTestHarness wraps a plugin with lifecycle management and assertion utilities:

import { createTestHarness } from '@ever-works/plugin/testing';
import { MyPlugin } from '../my-plugin.plugin';

const plugin = new MyPlugin();
const harness = createTestHarness(plugin, {
settings: { apiKey: 'test-key' }
});

// Lifecycle
await harness.load(); // Calls plugin.onLoad(mockContext)
await harness.unload(); // Calls plugin.onUnload()
harness.isLoaded; // Check load state

// Health check
const health = await harness.healthCheck();

// Run a named test
const result = await harness.test('should process items', async (h) => {
h.assert(h.isLoaded, 'Plugin must be loaded');
// ... test logic ...
});

// Built-in lifecycle tests
const lifecycleResults = await harness.testLifecycle();

// Get all test results
const suite = harness.getResults();
// { plugin: 'my-plugin', passed: 5, failed: 0, total: 5, ... }

Assertion Methods​

The harness provides assertion methods that work without a test framework:

harness.assert(condition, 'message'); // Boolean assertion
harness.assertEqual(actual, expected, 'msg'); // Strict equality
harness.assertDeepEqual(actual, expected); // JSON deep equality
await harness.assertRejects(asyncFn, 'msg'); // Assert promise rejects

Contract Tests​

Contract test suites verify that a plugin correctly implements its capability interface. They check property types, method signatures, and basic behavior:

import {
testBasePluginContract,
testAiProviderContract,
testSearchContract,
testGitProviderContract,
testDeploymentContract,
testScreenshotContract,
testPipelineContract,
testPipelineModifierContract,
runContractTests // Auto-detects capabilities and runs all applicable suites
} from '@ever-works/plugin/testing';

Base Plugin Contract Tests​

Every plugin is tested for:

  • Has a valid id (non-empty string)
  • Has a valid name (non-empty string)
  • Has a valid version (semver format)
  • Has a valid category (one of PLUGIN_CATEGORIES)
  • Has a capabilities array
  • Has a settingsSchema object
  • Implements onLoad() and onUnload() functions
  • Successfully completes load/unload lifecycle

Capability-Specific Contract Tests​

Each capability adds additional checks:

AI Provider (testAiProviderContract):

  • Has ai-provider capability
  • Has providerType and providerName properties
  • Implements createChatCompletion(), listModels(), getCapabilities()

Search (testSearchContract):

  • Has search capability
  • Has providerName property
  • Implements search(), isAvailable()

Pipeline (testPipelineContract):

  • Has pipeline capability
  • Implements getStepDefinitions() returning a non-empty array
  • Implements execute()
  • Step definitions have valid id, name, and position fields

Pipeline Modifier (testPipelineModifierContract):

  • Has pipeline-modifier capability
  • Has non-empty targetPipelines array
  • Implements execute()
  • getStepDefinition() returns a valid definition

Auto-Running Contract Tests​

The runContractTests() function automatically detects a plugin's capabilities and runs all applicable test suites:

import { runContractTests } from '@ever-works/plugin/testing';
import { MyPlugin } from '../my-plugin.plugin';

describe('MyPlugin contracts', () => {
it('should pass all contract tests', async () => {
const plugin = new MyPlugin();
const results = await runContractTests(plugin);
const failures = results.filter((r) => !r.passed);
expect(failures).toEqual([]);
});
});

Writing Plugin Unit Tests​

A typical plugin test file:

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { createMockPluginContext } from '@ever-works/plugin/testing';
import { MySearchPlugin } from '../my-search.plugin';

describe('MySearchPlugin', () => {
let plugin: MySearchPlugin;

beforeEach(async () => {
plugin = new MySearchPlugin();
const context = createMockPluginContext({
settings: { apiKey: 'test-key' }
});
await plugin.onLoad(context);
});

afterEach(async () => {
await plugin.onUnload();
});

it('should have correct metadata', () => {
expect(plugin.id).toBe('my-search');
expect(plugin.category).toBe('search');
expect(plugin.capabilities).toContain('search');
});

it('should return search results', async () => {
const response = await plugin.search({
query: 'test query',
limit: 5,
settings: { apiKey: 'test-key' }
});

expect(response.results).toBeDefined();
expect(response.query).toBe('test query');
expect(response.hasMore).toBeDefined();
});

it('should report availability', async () => {
const available = await plugin.isAvailable();
expect(typeof available).toBe('boolean');
});

it('should pass contract tests', async () => {
const { runContractTests } = await import('@ever-works/plugin/testing');
const results = await runContractTests(plugin);
for (const result of results) {
expect(result.passed).toBe(true);
}
});
});

Integration Testing with Pipeline​

To test a plugin's integration with the pipeline, create a minimal pipeline context:

import { createMockPluginContext } from '@ever-works/plugin/testing';

// Test a pipeline modifier step
const modifier = new MyPipelineModifier();
await modifier.onLoad(createMockPluginContext());

const mockContext = {
prompt: 'Test prompt',
subject: 'test-subject',
searchResults: [{ title: 'Result 1', url: 'https://example.com' }],
shouldStop: false
};

const result = await modifier.execute(mockContext as any);
expect(result.customData).toBeDefined();

Mock Function Utilities​

The testing package includes a lightweight mock function implementation compatible with both Vitest and Jest:

import { createMockFn } from '@ever-works/plugin/testing';

const mockFn = createMockFn<(x: number) => string>();
mockFn.mockReturnValue('hello');
mockFn.mockResolvedValue('async-hello');
mockFn.mockImplementation((x) => `value: ${x}`);

// Inspect calls
console.log(mockFn.mock.calls); // [[1], [2], [3]]

This is used internally by createMockLogger() and other mock factories, and can be used directly in plugin tests without importing from the test framework.