# Testing

Plugins are plain Dart classes, so most plugin testing is just Dart testing. The framework-specific question is: when does a test need a stub context, and when does it need a full `PluginRuntime`? Three questions cover almost every case.

## What we test, what we don't

- Fake at your boundaries: repositories, IO clients, external APIs. Don't fake the framework primitives (bus, registry, context, runtime); use them.
- Use the blessed factories. `PluginContext.stub()`, `SessionPluginContext.stub()`, and `GlobalPluginContext.stub()` already give you a real registry and bus wired together. If you find yourself hand-constructing a context, there's a helper.
- Each test owns its setup. Don't share a context, a registry, or a bus across tests.
- `StatefulPluginService.attach` only runs inside the runtime. A stub bypasses `_runAttach`, so the service's `context` getter throws when accessed. Drive stateful services through a real `PluginRuntime`, or test their pure methods in isolation.

## Does this service compute correctly?

Construct the service, inject settings if it reads any, call the method, assert. No framework primitives needed.

```dart
extractRegion(testingSnippets, 'testing-level-1-service')
```

This is the cheapest tier and where the most plugin tests should land: config readers, formatters, query builders, anything whose behavior is a pure function of its inputs.
**`pluginId` is not set until resolution:** `PluginService.pluginId` and `serviceId` are stamped by the registry on resolve. If your test reads those values, resolve the service through a registry instead of calling the constructor directly.

## Does it interact correctly with the bus and registry?

When the test cares about event subscriptions, request handlers, or how the service composes with peers in the registry, use a `.stub()` context. It gives you a real registry and bus without spinning up a runtime.

### Inject a fake for a peer service

The named-arg form of `registerSingleton` carries a priority, so a fake registered at `Priority.system` outranks anything the system under test contributes.

```dart
extractRegion(testingSnippets, 'testing-stub-inject-fake')
```

### Drive a plugin's attach handlers

For plugins whose reactive logic lives in `Plugin.attach` (using the inherited `on` / `onRequest` helpers), wire the plugin against a stub context and emit on the stub bus.

```dart
extractRegion(testingSnippets, 'testing-level-2-plugin')
```

`Plugin.attach` is what the test calls directly, so handlers registered inside it land on the stub bus. Plugins that delegate reactive work to a `StatefulPluginService` cannot be exercised this way; jump to the next section.

### Assert on cascade behavior

`emit` returns the post-cascade envelope. Inspect `event` for mutations applied by handlers, `stopped` for halts.

```dart
extractRegion(testingSnippets, 'testing-assert-cascade')
```

For request/response: `bus.maybeRequest` returns null on no-handler-or-all-conceded; `bus.request` throws a `NoRequestAnswerException` subtype (`RequestNotWiredException` or `AllConcededException`). Use `maybeRequest` to assert the absence path; reach for `request` only when at least one handler is guaranteed to claim.

## Does the whole plugin work end-to-end?

When the test depends on `StatefulPluginService.attach`, lifecycle order, settings reconciliation, or exception aggregation, use a real `PluginRuntime`. This is the default tier for "does my plugin work?", not an escape hatch.

### Lifecycle order

A small helper plugin that records lifecycle calls is enough to assert order.

```dart
extractRegion(testingSnippets, 'testing-tracking-plugin')
```

```dart
extractRegion(testingSnippets, 'testing-update-settings-disable')
```

Toggling a plugin off runs its `detach`. Toggling back on runs `register` and `attach` again.

### Stateful services

For stateful services, attach them through the runtime, exercise whatever the service reacts to, then detach to verify cleanup.

```dart
extractRegion(testingSnippets, 'testing-stateful-service')
```

Resolve the service through the registry once before exercising it. That is what stamps `pluginId` and `serviceId` onto the instance.

### Where failures surface

The runtime aggregates lifecycle errors into `PluginLifecycleException` and throws after the phase finishes. The exception carries a `phase` string (`'attachGlobal'`, `'attachSession'`, `'detachSession'`, `'updateSessionSettings'`, etc.) and a list of `(pluginId, error, stackTrace)` tuples.

```dart
extractRegion(testingSnippets, 'testing-throws-lifecycle-exception')
```

```dart
extractRegion(testingSnippets, 'testing-lifecycle-exception')
```

## Patterns that age well

| Pattern | Severity | Why |
|---|---|---|
| Don't fake the bus, registry, or context. | High | They are framework primitives. Use `.stub()` if you want a real one with sane defaults. |
| Don't hand-construct a `SessionPluginContext` from `ServiceRegistry()` and `EventBus()`. | High | `SessionPluginContext.stub()` already does this with sane defaults. |
| Don't drive a `StatefulPluginService.attach` directly. | High | Its context binding only happens in the runtime's `_runAttach`. Calling `attach()` outside a runtime leaves the service's `context` getter throwing. |
| Don't share a registry, bus, or context across tests. | Medium | Fresh state per test prevents handler leaks and priority surprises. |
| Don't mock the framework primitives. | Medium | The bus is small, fast, and well-exercised. A real bus gives better signal than a mock. |
| Don't assert on private state when an emitted event covers the same behavior. | Low | The event is your test contract; private state is implementation. |

## Related reading

[Adding a Plugin](https://plugin-kit.saad-ardati.dev/guides/adding-a-plugin/)
  [Plugin Services](https://plugin-kit.saad-ardati.dev/concepts/plugin-services/)
  [Logging](https://plugin-kit.saad-ardati.dev/guides/logging/)