Skip to content
DocsTest and debug

PluginsEarly

Test and debug

Check a plugin, test it on fixtures with fake time, draw its views as text, work on it live, and read its logs and status.

A plugin is checked and tested without the app, and without a network. Everything on this page runs on your machine with gr.

Check

gr plugin check .

gr plugin check reads the folder, the manifest and the script, and names each problem with its file, line and code:

plugin.toml:6:1: error api.newer: events[0]: the event "files.changed" arrived in api 0.2; this plugin says api = "0.1" (set api = "0.2")

It finds a misspelt key, an id that does not start with the plugin’s name, a key bound to a command the plugin does not have, a handler for an event the manifest does not list, a method whose request is missing from [allow], and a script that uses a global plugins do not have (fetch, process, require). --json prints the same as data, with the folder’s hash and every handler the script registers. Errors lists every code.

Tests

A plugin’s tests are the *.test.ts files beside its plugin.toml. Run them with the checks:

gr plugin check . --test
ok   plugin.test.ts: hello says hello (5 ms)
ok   plugin.test.ts: the panel lists Markdown files only (5 ms)
ok   plugin.test.ts: Enter opens the chosen file (6 ms)
the tests used: fs:read:workspace
greeter passes (hash 6f5b8e0a0cb9)

gr plugin test . does the same. Each test gets a fresh copy of the plugin in a pretend host:

  • Time is fake. It starts at 2026-01-01 and moves only when the test says.
  • There is no network and no program to run. A file, request or program the test’s world does not answer for is refused.
  • The workspace is the world you give it, starting from the folder’s fixtures/.

The last line before the result says which requests the tests used, so you can see a test reach for something you did not expect.

The world a test runs in

fixtures/files/ is the pretend workspace: every file in it is a file of the project. fixtures/world.json holds the rest. A test lays more over both with t.fixtures:

t.fixtures({
  files: { "README.md": "# Hi", "docs/guide.md": "Guide" },
  settings: { max_notes: 10 },
  fetch: { "https://api.github.com/repos/o/r": { body: '{"stars": 3}' } },
  exec: { "rg": { stdout: "a.go:1:TODO\n" } },
});

files adds to fixtures/files/; every other key replaces what the folder had. A world can also hold the map’s nodes and edges, your agents and what they said, the state and store the plugin keeps, open docs, worktrees, git changes and what a language server reports. Test API has every key and its shape.

Writing tests

t drives the plugin as a person and the app would:

import { test } from "graphide/test";

test("Enter opens the chosen file", async (t) => {
  t.fixtures({ files: { "a.md": "A", "b.md": "B" } });
  const v = await t.mount("greeter.notes");
  await v.keys("j", "<CR>");
  t.expect(t.commands()).toEqual([{ cmd: "open.code", path: "b.md" }]);
});
To Use
Run a command await t.command("name", ...args)
Show a view const v = await t.mount("id", { size: [w, h] })
Read a view v.text(), the view as text; v.tree(), its elements; v.selected(key)
Press keys in it await v.keys("j", "<CR>"), in Vim’s notation
Type in its input await v.keys("/"), then await v.type("words")
Open a picker, choose await t.pick("id", "query"), await t.choose("id", entry)
Send any event await t.event("files.changed", { ... })
Call a tool as an agent await t.tool("name", { ... })
Move time on await t.advance(ms)
Answer a question it asked t.asks(), then t.answer(value)
See what it did t.commands(), t.messages(), t.status(id), t.badges(id), t.state(key), t.logs()

t.commands() is everything the plugin asked the app to do, in order. Opening b.md above needed no handler: the item’s open did it, and the test sees it all the same.

t.expect(value) takes toBe, toEqual, toContain, toMatch, toHaveLength, toBeGreaterThan, toBeLessThan, toBeTruthy, toBeFalsy, toBeDefined, toBeUndefined and toBeNull, each also after .not.

Draw a view without the app

gr plugin render . --view greeter.notes
Markdown in this project
> notes.md

This is the same text v.text() returns and the review shows before anyone approves. --state world.json draws it over another world:

echo '{ "files": { "docs/guide.md": "Guide", "CHANGELOG.md": "Changes" } }' > world.json
gr plugin render . --view greeter.notes --state world.json
Markdown in this project
> CHANGELOG.md
  docs/guide.md
  notes.md

--png shot.png also draws a picture, --size 400x600 sets its size and --theme ink or --theme marble its colours.

Work on it live

An installed plugin runs the version you approved. Each change to its files is a new version, and waits for approval. While you work on it, run it as it stands:

gr plugin dev .

The plugin loads now, and again each time you save. Dev mode grants nothing new: the plugin may do what you approved, and nothing if you never approved it. A save that asks for more runs without it until you approve. A save that does not load leaves the last good version running.

Dev mode ends with gr plugin dev --off, after eight hours, or when the daemon stops. Only a person can switch it on, on their own terminal.

When it goes wrong in the app

gr plugin status greeter          # its process, its handlers' times, its last errors
gr plugin status greeter --logs   # with its log lines
gr plugin logs greeter --since 2026-10-08T12:00:00Z
gr plugin reload greeter          # load it afresh, and give it another go

console.log and the other console methods write to the plugin’s log, with the file and line, and so does a handler that throws. gr plugin status with no name lists every plugin’s state.

A plugin that keeps failing, runs past its time or memory, or crashes over and over is switched off for the session. Fix it, then gr plugin reload gives it another go. When this happens within an hour of approving a new version, the version approved before it comes back by itself; gr plugin rollback does the same by hand.

These docs describe Graphide 0.2.16, the latest release. Something wrong or missing? Tell us onDiscord or at hello@graphide.com.