PluginsEarly
Test API
The graphide/test module a plugin's own tests import: the fake world, the harness and the matchers.
From the plugin interface of gr 0.2.16 (API 0.1, 0.2, 0.3). gr plugin api test prints it on your machine.
A plugin's own tests are *.test.ts files beside plugin.toml: import { test } from "graphide/test" and call test(name, async (t) => { ... }). gr plugin test (and plugin_check with test) runs each test in the fake host with a fresh instance of the plugin: fake time starting 2026-01-01, no network, no programs, and a world of fixtures in place of the workspace. A call no fixture answers is refused (fs.refused, net.refused, exec.unsandboxed). The same world is what gr plugin render draws a view on.
import { test } from "graphide/test";
test("shows the greeting", async (t) => {
// ...
});The world
Fixtures a test lays over the folder's own, with t.fixtures(...).
files: Record<string, string>
Workspace files by path; added to the folder's own fixtures/files/, where every other key replaces
data: Record<string, string>
The plugin's data folder (fs.readData, fs.writeData), by name
nodes: GraphNode[]
The graph's nodes; parent links them for graph.children, name is what graph.search matches
edges: { from: string; to: string; kind: string }[]
The graph's edges, for graph.neighbors
agents: (Agent & { transcript?: TranscriptBlock[] })[]
The roster; an agent's transcript (0.2) is what agents.transcript answers of it, and is not in agents.list
exec: Record<string, { code?: number; stdout?: string; stderr?: string }>
What a program answers, by the name exec.run is given, or by the name and its first arguments (git status), the longest winning
fetch: Record<string, { status?: number; headers?: Record<string, string>; body?: string }>
What a URL answers, by the whole URL; status defaults to 200
settings: Record<string, Json>
The person's settings, over the manifest's defaults
ctx: Partial<Context>
Fields of the context every event carries, over the fake editor's
state: Record<string, Json>
What $.state holds
store: Record<string, Json>
What $.store holds
worktrees: Worktree[]
The repository's worktrees, for worktrees.list
changes: GitChange[]
The working tree's changes, for git.changes
graph: boolean
False: the root has no graph, and every graph.* call answers graph.none
docs: Record<string, { text: string; version?: number; language?: string; dirty?: boolean }>
Documents open in the fake editor, by path, for $.docs (version 1 and the file's extension as language when not said)
lsp: { diagnostics?: Record<string, (Omit<Diagnostic, "path" | "server"> & { server?: string })[]>; symbols?: Record<string, DocSymbol[]>; off?: string; fails?: Record<string, "lsp.unsupported" | "lsp.failed"> }
What the fake language servers say, by path: diagnostics for lsp.diagnostics (server "fake" when not said), symbols for lsp.symbols, which fails lsp.no_server for a path not listed and, for a path in fails, with the code given there. off is why no server may run in the folder: with it both methods fail lsp.no_server, saying it. With no lsp the world has no server, and both fail lsp.no_server
setup: HarnessSetup
What setup.read answers, whatever its options; an empty setup when not given. setup.layer answers on with one holder, no nodes
The harness
The methods on t.
t.fixtures(world: World): void
Lays a world over the folder's own (fixtures/files/ and fixtures/world.json). The load handlers run again on it. Await t.advance(0), a command, an event or a mount before reading their results; a synchronous read immediately after fixtures still sees the previous load.
t.command(name: string, ...args: Json[]): Promise<Cmd[]>
Runs a command of the plugin; resolves with the commands its event produced, writes first.
t.mount(view: string, opts?: { size?: [number, number] }): Promise<MountedView>
Shows a view and resolves once it is drawn.
t.event(kind: K, fields?: Partial<Events[K]>): Promise<EventResult>
Sends any event; resolves with its result.
t.pick(picker: string, query?: string): Promise<Entry[]>
Opens a picker; resolves with its entries.
t.choose(picker: string, entry: string, how?: "open" | "split" | "vsplit" | "tab"): Promise<Cmd[]>
Chooses a picker entry by its key; resolves with the commands that produced.
t.commands(): Cmd[]
Every command the plugin has produced in this test, in order: those an event returned, those written with $.editor.run, and an item's open followed without the plugin.
t.messages(): string[]
The text of each message command so far.
t.state(key: string): T | undefined
A key of $.state as the plugin last wrote it.
t.store(key: string): T | undefined
A key of $.store as the plugin last wrote it.
t.status(segment: string): Segment | null | undefined
A status segment as the plugin last set it.
t.badges(set: string): Record<string, Badge | null> | undefined
A badge set as the plugin last wrote it.
t.logs(): { level: string; text: string; file: string; line: number }[]
The plugin's log lines, and its handlers' errors.
t.asks(): { kind: "select" | "input"; args: Json[] }[]
The ui.select and ui.input calls waiting for an answer.
t.answer(value: Json): void
Answers the oldest waiting ask, or the next one made: the item chosen, the text typed, or null when dismissed.
t.advance(ms: number): Promise<void>
Moves fake time on, running each timer that comes due.
t.expect(actual: unknown): Expect
An assertion; a failed one throws with both values.
t.agents(): { sent: { agent: string; text: string }[]; started: { id: string; prompt?: string; harness?: string; worktree?: string; name?: string }[] }
What the plugin wrote to agents (agents.send) and the agents it started (agents.start), in order. A started agent joins the world's roster as running, startedBy the plugin.
t.open(path: string, text: string, opts?: { language?: string; version?: number; dirty?: boolean }): Promise<void>
Opens a document in the fake editor: $.docs holds it, and the plugin hears document.opened when it lists it.
t.change(path: string, edits: TextEdit[]): Promise<number>
Changes an open document as the person would; the plugin hears document.changed. Resolves with its new version.
t.save(path: string): Promise<void>
Saves an open document; the plugin hears document.saved.
t.close(path: string): Promise<void>
Closes an open document; the plugin hears document.closed.
t.tool(name: string, input?: Record<string, Json>): Promise<ToolAnswer>
Calls one of the plugin's [[tools]] as an agent would: the input is checked against its [tools.input] first (refused with tool.input and a path, as the daemon refuses it), then its tool handler runs; resolves with what it answered, checked as the daemon checks it.
t.mention(source: string, query?: string): Promise<MentionEntry[]>
Asks one of the plugin's [[mentions]] sources as the composer's @ does; resolves with the entries the daemon would show, each checked against the world as the daemon checks it: one it would leave out is not there, and t.logs() says why (mention.entry). Choosing one is t.event("mention.pick", { source, entry }).
t.flow(page?: string): FlowShown | undefined
One of the plugin's flow pages as the editor shows it now (the one opened last when none is named); undefined when it is not open.
t.flowEdit(page: string, edit: FlowEdit): Promise<EventResult>
Does edit to a flow page as the person would: the plugin hears flow.edit, and its page's onEdit handlers run. { op: "closed" } closes the page.
t.decorations(path: string, set?: string): Decoration[]
The marks the plugin's sets hold on a document now (of one set, when named), the sets in their ids' order; [] when none.
t.decorationsVersion(path: string, set: string): number | undefined
The version a set's marks on a document were last set against (0 for the file as saved); undefined when the set holds none there.
t.graph(): { nodes: GraphNode[]; edges: { from: string; to: string; kind: string }[] }
The fake world's current nodes, tags and edges after graph writes. The returned picture is a copy, so a test cannot change the world by editing it.
Views
What a mounted view answers.
v.text(): string
The view as text, as gr plugin render prints it, in its current widget state.
v.tree(): Element
The view's last tree.
v.selected(element: string): string | undefined
The selected item of a list, node of a tree or tab of a tabs, by the element's key.
v.keys(...keys: string[]): Promise<void>
Types keys in Vim notation into the view: the panel's own keys first, then the widgets' (j, k, gg, G, <CR>, l, h, <Space>, <Tab>, i, /, <Esc>); a view opens with the keys on its first list or tree, and / takes them into its first input; on a table h and l move to a column and s sorts by it; <Space> flips a toggle, <Space> or <CR> opens a select's options, and while they are open every key is theirs (j, k, g, G, <CR> chooses, <Esc> or q puts them away); resolves when the plugin has heard each act.
v.type(text: string): Promise<void>
Types text into the focused input: press / first to reach the view's first input.
v.refresh(): Promise<void>
Draws the view again.
Matchers
After expect(value).
.toBe(expected: unknown): void
The same value (Object.is).
.toEqual(expected: unknown): void
Equal in every field, undefined fields aside.
.toContain(part: unknown): void
A string holding a substring, or an array holding an equal element.
.toMatch(pattern: RegExp | string): void
A string matching a pattern.
.toBeTruthy(): void
Truthy.
.toBeFalsy(): void
Falsy.
.toBeDefined(): void
Not undefined.
.toBeUndefined(): void
Undefined.
.toBeNull(): void
Null.
.toHaveLength(length: number): void
A string or an array of that length.
.toBeGreaterThan(n: number): void
Greater than n.
.toBeLessThan(n: number): void
Less than n.
These docs describe Graphide 0.2.16, the latest release. Something wrong or missing? Tell us onDiscord or at hello@graphide.com.