every-plugin
Version:
163 lines (130 loc) • 4.59 kB
Markdown
---
name: plugin-testing
description: Test every-plugin modules with vitest and the plugin runtime. Use when writing or modifying plugin tests under plugins/*/src/__tests__/ or plugins/*/tests/.
metadata:
sources: "src/testing/index.ts,src/runtime/index.ts"
---
# every-plugin Testing
## Test Structure
```
plugins/your-plugin/
├── src/__tests__/
│ ├── integration.test.ts # Full plugin lifecycle via runtime
│ └── unit.test.ts # Service class in isolation
└── vitest.config.ts
```
## Unit Tests (Service Only)
Test the service class directly without the plugin runtime:
```typescript
import { describe, expect, it } from "vitest";
import { Effect } from "every-plugin/effect";
import { MyService } from "../service";
describe("MyService", () => {
const service = new MyService("https://api.example.com", "test-key");
it("ping returns ok", async () => {
const result = await Effect.runPromise(service.ping());
expect(result.status).toBe("ok");
});
it("getById returns item", async () => {
const result = await Effect.runPromise(service.getById("item-1"));
expect(result.item.id).toBe("item-1");
});
it("getById throws on missing item", async () => {
await expect(Effect.runPromise(service.getById("missing"))).rejects.toThrow();
});
});
```
## Integration Tests (Plugin Runtime)
Test the full plugin lifecycle — initialization, router execution, shutdown:
```typescript
import { describe, expect, it, beforeAll, afterAll } from "vitest";
import { createPluginRuntime } from "every-plugin";
import Plugin from "../index";
describe("MyPlugin integration", () => {
let runtime: ReturnType<typeof createPluginRuntime>;
let client: any;
beforeAll(async () => {
runtime = createPluginRuntime({
registry: {
"my-plugin": { module: Plugin },
},
});
const result = await runtime.usePlugin("my-plugin", {
variables: { baseUrl: "https://api.example.com" },
secrets: { apiKey: "test-key" },
});
client = result.createClient();
});
afterAll(async () => {
await runtime.shutdown();
});
it("ping responds", async () => {
const result = await client.ping();
expect(result.status).toBe("ok");
});
it("getById returns item", async () => {
const result = await client.getById({ id: "item-1" });
expect(result.item.id).toBe("item-1");
});
it("getById without auth throws UNAUTHORIZED", async () => {
await expect(client.getById({ id: "item-1" })).rejects.toThrow();
});
});
```
## Testing Plugin Composition
When testing an API plugin that uses `withPlugins`, mock the `pluginsClient`:
```typescript
import { createPluginRuntime } from "every-plugin";
import ApiPlugin from "../index";
const mockRegistryClient = {
getRegistryStatus: vi.fn().mockResolvedValue({ status: "ok" }),
listRegistryApps: vi.fn().mockResolvedValue({ apps: [] }),
};
describe("API with mock registry", () => {
it("pluginDemo returns composed data", async () => {
const runtime = createPluginRuntime({
registry: {
api: { module: ApiPlugin },
},
});
const result = await runtime.usePlugin(
"api",
{ variables: {}, secrets: {} },
{ registry: () => mockRegistryClient },
);
const client = result.createClient();
const data = await client.pluginDemo();
expect(data.registryStatus.status).toBe("ok");
await runtime.shutdown();
});
});
```
## Testing Streaming (eventIterator)
Use `for await` to collect streaming results:
```typescript
it("search streams results", async () => {
const chunks: any[] = [];
const stream = await client.search({ query: "test", limit: 5 });
for await (const chunk of stream) {
chunks.push(chunk);
}
expect(chunks.length).toBeGreaterThan(0);
});
```
## Vitest Config
```typescript
import { defineConfig } from "vitest/config";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [tsconfigPaths()],
test: {
include: ["src/__tests__/**/*.test.ts"],
},
});
```
## Common Mistakes
- Not calling `runtime.shutdown()` in `afterAll` — leaves plugin scopes/resources running
- Using `vi.fn()` without `.mockResolvedValue()` — unhandled promise rejections
- Forgetting `vite-tsconfig-paths` plugin — path aliases like `every-plugin/orpc` won't resolve
- Omitting the `registry` object when calling `createPluginRuntime(...)` — the runtime requires explicit plugin entries
- Testing only happy paths — always test error channels (Effect failures, ORPCError throws)