Skip to content
DocsTest API

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.