# Testing a plugin

Run a plugin test target with loam plugin test, write logic and UI tests with the harnesses, and check the plugin on a vault.

A generated plugin comes with one test. This page shows how to run the tests with `loam plugin test`, how to write a logic test and a UI test, how to check the plugin on a vault, and what to avoid.

## Run the tests

`loam plugin test <id>` runs the package tests against the app SDK and prints JSON with `ok` and the test log. The first run also compiles the test target, so it takes a few minutes.

```bash
loam --vault ~/Notes plugin test word-count
```

## A logic test

A logic test builds a kernel on a temporary vault, activates the plugin, runs a command, and checks the result. `PluginTestHarness` comes from `PluginKitTesting`. This is the generated test:

**Tests/WordCountTests/WordCountPluginTests.swift**

```swift
import PluginKitTesting
import XCTest
@testable import WordCount

final class WordCountPluginTests: XCTestCase {
    func testSaysHelloWithTheNoteCount() async throws {
        let harness = try PluginTestHarness([WordCountPlugin.self], files: ["one.md": "# One", "two.md": "# Two"])
        let output = try await harness.run("word-count.hello", ["name": "Ada"])
        XCTAssertEqual(output, "Hello Ada. Notes in this vault: 2.")
    }
}
```

The `files` argument creates notes in the temporary vault before the test runs. `harness.run` runs a command by id. End each test with `harness.host.deactivateAll()`.

## A UI test

A UI test uses `UIPluginHarness` from `PluginKitUITesting`. It gives you the host services, a recording workspace and quick pick, and `render(_:)`, which lays a view out offscreen. This is the generated UI test:

**Tests/WordPanelTests/WordPanelPluginTests.swift**

```swift
import PluginKitUITesting
import XCTest
@testable import WordPanel

@MainActor
final class WordPanelPluginTests: XCTestCase {
    func testAddsItsPanelAndCommand() async throws {
        let harness = try UIPluginHarness(uiPlugins: [WordPanelPlugin.self], files: ["one.md": "# One"])
        defer { harness.close() }
        XCTAssertTrue(harness.report.failed.isEmpty, "\(harness.report.failed)")
        let panel = try XCTUnwrap(harness.panels.all.first { $0.id == "word-panel.panel" })
        _ = harness.render(panel.makeView())
        let output = try await harness.run("word-panel.hello")
        XCTAssertEqual(output, "Hello from Word Panel. Notes in this vault: 1.")
    }
}
```

## Check it on a vault

Build the plugin, turn it on, and run its command on a test vault:

```bash
loam --vault ~/Notes plugin build word-count
loam --vault ~/Notes plugin enable word-count
loam --vault ~/Notes word-count.hello
```

Use a test note for your checks, and delete it afterwards. Keep your own notes unchanged.

## Pitfalls

- In the CLI, each run loads the latest build. An open app keeps the old code until it restarts: `loam --vault ~/Notes app relaunch`.
- Read a note right before you write it. The editor may hold text that is not saved yet, and reading through `context.vault` saves that text first.
- When `ok` is `false`, the last lines of `log` say why. Fix the first error, then build again.
- Finish each test with `harness.host.deactivateAll()`, then check that nothing stays registered.
- After an app update that changes the SDK major version, rebuild the plugin.

> **Tip:** For AI agents: `loam guide test` has these steps. `loam plugin test <id>` prints JSON with `ok` and `log`.
