4.3 KiB
name, description
| name | description |
|---|---|
| omp-plugin-modal-bats-harness | Drive a real omp-extension settings modal headlessly from bats (ui.custom stub, DV_KEYS key-replay) and ship the bundle with host pi packages external. |
Headless modal testing + host-external bundling for omp plugins
Friction-earned 2026-10-21 on dank/dankvoice PR #31 (select menu → vendored pi-common settings modal). Three failed driver iterations and one 43 MB bundle before green.
The build contract (do this first)
A modal imports runtime values (SelectList, matchesKey, getSelectListTheme) from @oh-my-pi/pi-tui / @oh-my-pi/pi-coding-agent. Bundling the root barrels = 43 MB and trips --target node on bun builtins. The plugin host IS bun and hoists every pi package into ~/.omp/plugins/node_modules/@oh-my-pi/ — the same ground pi-common stands on. So:
bun build src/index.ts --outdir dist --target bun --format esm \
--external @oh-my-pi/pi-coding-agent --external @oh-my-pi/pi-tui \
--external omp-legacy-pi-modules
omp-legacy-pi-modulesis a harness-internal virtual module behind a lazy dynamic import in the pi-coding-agent barrel; externalizing is safe, omitting it fails the build.- Declare the externals in
peerDependencies. - The packed-artifact load test must reproduce the hoisted host layout: extract the tarball, then
ln -s <repo>/node_modules/@oh-my-pi <work>/node_modules/@oh-my-pibefore importing the entry — a bare temp dir has nothing to resolve against and the test fails for the wrong reason.
The driver stub for ctx.ui.custom
The modal mounts through ctx.ui.custom(factory, opts); factory signature is (tui, theme, keybindings, done). The bats driver stub:
custom: (factory: any) => {
customCall += 1;
let settleDone: () => void = () => {};
const closed = new Promise<void>((r) => { settleDone = r; });
// theme: Proxy answering EVERY style method; pass through the LAST arg —
// style calls are (colorName, text); returning args[0] prints color names,
// and a fixed {bg,fg,inverse} trio crashes the frame's theme.bold.
const style = (...a: unknown[]) => String(a[a.length - 1] ?? "");
const theme = new Proxy({} as Record<string, typeof style>, { get: () => style });
const component = factory(
{ requestRender: () => {}, terminal: { rows: 60, cols: 100 } }, // rows feeds list pagination; omit → NaN, nothing scrolls
theme, {}, () => settleDone(),
);
const census = () => { for (const l of component.render?.(100) ?? []) console.log(`ROW ${l}`); };
census();
// keys from DV_KEYS / DV_KEYS<n> per call: up|down|enter|tab|esc|ctrl+u named,
// name*N repeat, anything else typed.
(async () => { /* for each token: beats below; await ~140ms after enter (async
roster fetch), ~20ms otherwise; final ~150ms; settleDone(); */ })();
return closed;
},
Key-replay discipline (each cost a red iteration)
- Never zero-interval. A burst of
\x1b[Bbytes in one tick coalesces in SelectList —down*3lands one step. Beat ~25 ms between repeats. - Type one character at a time. A whole sentence written to the inline editor in one
handleInputis swallowed (paste-shaped); iterate[...text]with ~10 ms beats. - Sleep after every
enter, not just fixed small delays: Enter opens submenus whose list contents arrive from an async fetch. - Row math must be pinned: row order in
settingsFieldsis the UI contract; tests reach row N withdown*N.
Vendored-modal semantics that bite
- Enum fields with
options.length <= cycleThreshold(default) CYCLE inline on Enter instead of opening the picker — a curated list must setcycleThreshold: 0or an "open picker, down, enter" choreography silently cycles and the stray Enter activates the next row. - Submenus render inside the body component's
render()output — one census sees both. - SelectList initial highlight = current value (
items.findIndex(value)); choreography counts from there.
Test layering that held
Component-level pins (field census, commits, audition) against fake deps with bun test; behavior bats drives the REAL bundle through the stub for wiring laws (persistence, byDir, notify strings). Don't duplicate choreographies across layers.
Related managed skills: gitea-pr-api-cycle (landing), plugin-git-install-committed-artifact (install validation).