mirror of
https://github.com/block/buzz.git
synced 2026-11-08 18:31:56 +00:00
feat(beehive): add the Harnesses tracer
Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
@@ -7,7 +7,7 @@ import { spawnOwned } from './owned.ts';
|
||||
|
||||
/** Public capabilities, never login evidence. Non-Buzz adapters remain visible,
|
||||
* but cannot consume this catalog's OS-backed provider contract without an adapter. */
|
||||
export type DetectedHarness = { id: string; label: string; executable?: string; cli?: string; state: 'available' | 'not-installed' | 'cli-missing' | 'incompatible'; providers: string[]; reason: string };
|
||||
export type DetectedHarness = { id: string; label: string; executable?: string; cli?: string; version?: string; state: 'available' | 'not-installed' | 'cli-missing' | 'incompatible'; providers: string[]; reason: string };
|
||||
const executable = (path: string) => { try { const p = realpathSync(path); accessSync(p, constants.X_OK); return statSync(p).isFile() ? p : undefined; } catch { return undefined; } };
|
||||
const version = (s: string) => { const last = s.trim().split(/\s+/).at(-1) ?? ''; return /^\d+\.\d+\.\d+$/.test(last) ? last.split('.').map(BigInt) : undefined; };
|
||||
/** Desktop's strict adapter floor: unknown/prerelease/partial versions fail closed. */
|
||||
@@ -76,11 +76,17 @@ export async function discoverHarnesses(signal: AbortSignal, options: { home?: s
|
||||
for (const command of row.commands) { binary = await resolve(command); if (binary) break; }
|
||||
const cli = row.underlyingCli ? await resolve(row.underlyingCli) : undefined;
|
||||
let state: DetectedHarness['state'] = !binary ? 'not-installed' : row.underlyingCli && !cli ? 'cli-missing' : 'available';
|
||||
if (state === 'available' && row.id === 'codex' && !compatibleCodex(await probe(binary!, ['--version'], [...managed, path].join(delimiter), home, signal) ?? '')) state = 'incompatible';
|
||||
let detectedVersion: string | undefined;
|
||||
if (state === 'available') {
|
||||
const output = await probe(binary!, ['--version'], [...managed, path].join(delimiter), home, signal);
|
||||
detectedVersion = output?.trim().split(/\s+/).at(-1);
|
||||
if (!detectedVersion || !version(detectedVersion)) detectedVersion = undefined;
|
||||
if (row.id === 'codex' && !compatibleCodex(output ?? '')) state = 'incompatible';
|
||||
}
|
||||
signal.throwIfAborted();
|
||||
const providers = state === 'available' && ['buzz-agent','pi'].includes(row.id) ? row.id === 'buzz-agent' ? ['openai', 'anthropic', 'openai-compat', 'openrouter', 'databricks_v2'] : ['openai', 'databricks_v2'] : state === 'available' && row.id === 'codex' ? ['openai'] : [];
|
||||
const reason = state === 'not-installed' ? 'Not installed' : state === 'cli-missing' ? `Underlying ${row.underlyingCli} CLI not installed` : state === 'incompatible' ? 'Codex ACP 1.10.0 or newer required; version unknown/outdated' : providers.length ? 'Detected; provider/model access unverified' : 'Detected; this catalog’s OS-backed providers are not supported by this adapter. Use existing local setup for native authentication.';
|
||||
result.push({ id: row.id, label: row.label!, executable: binary, cli, state, providers, reason });
|
||||
result.push({ id: row.id, label: row.label!, executable: binary, cli, ...(detectedVersion ? { version: detectedVersion } : {}), state, providers, reason });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
100
beehive/src/harness-inventory.ts
Normal file
100
beehive/src/harness-inventory.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { discoverHarnesses, type DetectedHarness } from './harness-discovery.ts';
|
||||
import { readSettings, saveSettings, type Settings } from './settings.ts';
|
||||
|
||||
export type HarnessInventoryPhase = 'idle' | 'refreshing' | 'error';
|
||||
export type HarnessInventorySnapshot = Readonly<{
|
||||
harnesses: readonly DetectedHarness[];
|
||||
savedConfigurations: Readonly<Record<string, number>>;
|
||||
phase: HarnessInventoryPhase;
|
||||
message: string;
|
||||
generation: number;
|
||||
}>;
|
||||
export type HarnessInventoryStore = {
|
||||
read(): Settings;
|
||||
save(candidate: Settings, expectedRevision: number): Settings;
|
||||
};
|
||||
export type HarnessInventoryDiscovery = (signal: AbortSignal) => Promise<DetectedHarness[]>;
|
||||
|
||||
const counts = (settings: Settings) => Object.freeze(settings.runtimes.reduce<Record<string, number>>((result, runtime) => {
|
||||
result[runtime.harness] = (result[runtime.harness] ?? 0) + 1;
|
||||
return result;
|
||||
}, {}));
|
||||
|
||||
/** Harness-only controller. It owns durable inventory refresh without constructing
|
||||
* owner, relay, Host, provider, credential, or agent state. */
|
||||
export class HarnessInventoryController {
|
||||
private current: HarnessInventorySnapshot;
|
||||
private active?: AbortController;
|
||||
private serial = 0;
|
||||
private disposed = false;
|
||||
private listener?: (snapshot: HarnessInventorySnapshot) => void;
|
||||
|
||||
constructor(private readonly store: HarnessInventoryStore, private readonly discover: HarnessInventoryDiscovery) {
|
||||
try {
|
||||
const settings = store.read();
|
||||
this.current = Object.freeze({ harnesses: Object.freeze([...(settings.harnesses ?? [])]), savedConfigurations: counts(settings), phase: 'idle', message: settings.harnesses ? 'Saved local harness inventory.' : 'No saved harness inventory. Run Refresh harnesses.', generation: 0 });
|
||||
} catch {
|
||||
this.current = Object.freeze({ harnesses: Object.freeze([]), savedConfigurations: Object.freeze({}), phase: 'error', message: 'Saved harness inventory could not be read. Repair the local settings file, then try again.', generation: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
snapshot() { return this.current; }
|
||||
subscribe(listener: (snapshot: HarnessInventorySnapshot) => void) { this.listener = listener; listener(this.current); return () => { if (this.listener === listener) this.listener = undefined; }; }
|
||||
private publish(next: Omit<HarnessInventorySnapshot, 'generation'>) {
|
||||
if (this.disposed) return;
|
||||
this.current = Object.freeze({ ...next, harnesses: Object.freeze([...next.harnesses]), savedConfigurations: Object.freeze({ ...next.savedConfigurations }), generation: this.serial });
|
||||
this.listener?.(this.current);
|
||||
}
|
||||
|
||||
/** One bounded refresh at a time. Completion is fenced before durable commit
|
||||
* and again before presentation, so leaving the screen or disposal wins. */
|
||||
async refresh(): Promise<boolean> {
|
||||
if (this.disposed || this.active) return false;
|
||||
const abort = new AbortController();
|
||||
this.active = abort;
|
||||
const generation = ++this.serial;
|
||||
this.publish({ ...this.current, phase: 'refreshing', message: 'Refreshing local harness inventory…' });
|
||||
try {
|
||||
const harnesses = await this.discover(abort.signal);
|
||||
if (this.disposed || abort.signal.aborted || generation !== this.serial) return false;
|
||||
const previous = this.store.read();
|
||||
if (this.disposed || abort.signal.aborted || generation !== this.serial) return false;
|
||||
const committed = this.store.save({ ...previous, harnesses: [...harnesses] }, previous.revision);
|
||||
if (this.disposed || abort.signal.aborted || generation !== this.serial) return false;
|
||||
this.publish({ harnesses, savedConfigurations: counts(committed), phase: 'idle', message: `Harness inventory refreshed. ${harnesses.length} harnesses checked.` });
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (this.disposed || abort.signal.aborted || generation !== this.serial) return false;
|
||||
const changed = error instanceof Error && error.message === 'Settings changed. Open the form again.';
|
||||
this.publish({ ...this.current, phase: 'error', message: changed ? 'Local settings changed during refresh. Existing inventory was kept; try Refresh harnesses again.' : 'Harness refresh failed. Existing inventory was kept; check local executable access and try again.' });
|
||||
return false;
|
||||
} finally {
|
||||
if (this.active === abort) this.active = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
if (!this.active) return;
|
||||
this.serial++;
|
||||
this.active.abort();
|
||||
this.active = undefined;
|
||||
this.publish({ ...this.current, phase: 'idle', message: 'Harness refresh stopped. Existing inventory was kept.' });
|
||||
}
|
||||
dispose() { if (this.disposed) return; this.disposed = true; this.serial++; this.active?.abort(); this.active = undefined; this.listener = undefined; }
|
||||
}
|
||||
|
||||
/** Production adapter. BEEHIVE_HOME and the isolated switch are explicit seams
|
||||
* for product-shaped tests; normal launch uses the ordinary local HOME/PATH. */
|
||||
export function localHarnessInventory(environment: NodeJS.ProcessEnv = process.env) {
|
||||
const isolated = environment.BEEHIVE_HARNESS_ISOLATED === '1';
|
||||
if (isolated && (!environment.BEEHIVE_HOME || environment.BEEHIVE_HARNESS_PATH === undefined)) throw Error('Isolated harness discovery requires an explicit BEEHIVE_HOME and BEEHIVE_HARNESS_PATH');
|
||||
const home = environment.BEEHIVE_HOME || homedir();
|
||||
const path = environment.BEEHIVE_HARNESS_PATH ?? environment.PATH ?? '';
|
||||
const directory = join(home, '.beehive', 'host');
|
||||
return new HarnessInventoryController(
|
||||
{ read: () => readSettings(directory), save: (candidate, revision) => saveSettings(directory, candidate, revision) },
|
||||
signal => discoverHarnesses(signal, { home, path, ...(isolated ? { bundled: [], common: [], loginShells: [] } : {}) }),
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createCliRenderer } from '@opentui/core';
|
||||
import { OpenTuiShell } from './opentui-shell.ts';
|
||||
import { localHarnessInventory } from './harness-inventory.ts';
|
||||
|
||||
const renderer = await createCliRenderer({
|
||||
exitOnCtrlC: false,
|
||||
@@ -7,7 +8,7 @@ const renderer = await createCliRenderer({
|
||||
consoleMode: 'disabled',
|
||||
openConsoleOnError: false,
|
||||
});
|
||||
const shell = new OpenTuiShell(renderer);
|
||||
const shell = new OpenTuiShell(renderer, localHarnessInventory());
|
||||
const stop = () => shell.close();
|
||||
process.once('SIGINT', stop);
|
||||
process.once('SIGTERM', stop);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BoxRenderable, ScrollBoxRenderable, TextRenderable, type CliRenderer, type KeyEvent } from '@opentui/core';
|
||||
import { brand, destinations, listWidth, ownerLabel, ShellState, type FocusRegion } from './shell-state.ts';
|
||||
import { HarnessInventoryController, type HarnessInventorySnapshot } from './harness-inventory.ts';
|
||||
|
||||
export const palette = {
|
||||
surface: '#0d100e', text: '#dce1d8', muted: '#7d867c', divider: '#4a5149',
|
||||
@@ -61,6 +62,14 @@ export class OpenTuiShell {
|
||||
private readonly detailPane: BoxRenderable;
|
||||
private readonly divider: TextRenderable;
|
||||
private readonly focusPerimeter: BoxRenderable;
|
||||
private readonly listTitle: TextRenderable;
|
||||
private readonly detailTitle: TextRenderable;
|
||||
private readonly detailText: TextRenderable;
|
||||
private readonly detailAction: TextRenderable;
|
||||
private readonly listRows: TextRenderable[] = [];
|
||||
private inventorySnapshot: HarnessInventorySnapshot;
|
||||
private unsubscribeInventory: () => void;
|
||||
private screenGeneration = 0;
|
||||
private readonly footerRule: TextRenderable;
|
||||
private readonly footer: TextRenderable;
|
||||
private helpScrim?: BoxRenderable;
|
||||
@@ -70,7 +79,8 @@ export class OpenTuiShell {
|
||||
private helpTitle?: TextRenderable;
|
||||
private helpActions?: TextRenderable;
|
||||
|
||||
constructor(readonly renderer: CliRenderer, private readonly helpContent = helpBody) {
|
||||
constructor(readonly renderer: CliRenderer, private readonly inventory: HarnessInventoryController, private readonly helpContent = helpBody) {
|
||||
this.inventorySnapshot = inventory.snapshot();
|
||||
this.done = new Promise(resolve => { this.finish = resolve; });
|
||||
this.root = new BoxRenderable(renderer, { width: '100%', height: '100%', flexDirection: 'column', backgroundColor: palette.surface });
|
||||
renderer.root.add(this.root);
|
||||
@@ -95,8 +105,29 @@ export class OpenTuiShell {
|
||||
this.root.add(this.body);
|
||||
this.listPane = this.pane('list'); this.detailPane = this.pane('detail');
|
||||
this.divider = new TextRenderable(renderer, { width: 1, height: '100%', fg: palette.divider, content: '│' });
|
||||
this.focusPerimeter = new BoxRenderable(renderer, { position: 'absolute', visible: false, border: true, borderColor: palette.focus, backgroundColor: 'transparent' });
|
||||
this.focusPerimeter = new BoxRenderable(renderer, { position: 'absolute', visible: false, border: true, borderColor: palette.focus, backgroundColor: 'transparent',
|
||||
onMouseDown: event => {
|
||||
if (this.state.focus === 'list') this.clickListSlot(Math.floor((event.y - 6) / 2));
|
||||
else if (this.state.focus === 'detail' && event.y === 15) this.activateSelectedRow();
|
||||
},
|
||||
onMouseScroll: event => {
|
||||
if (this.state.focus !== 'list') return;
|
||||
if (event.scroll?.direction === 'up') this.state.key('up');
|
||||
else if (event.scroll?.direction === 'down') this.state.key('down');
|
||||
this.paint();
|
||||
} });
|
||||
this.body.add(this.listPane); this.body.add(this.divider); this.body.add(this.detailPane); this.body.add(this.focusPerimeter);
|
||||
this.listTitle = new TextRenderable(renderer, { position: 'absolute', left: 2, top: 1, height: 1, fg: palette.muted, content: 'LIST' });
|
||||
this.detailTitle = new TextRenderable(renderer, { position: 'absolute', left: 2, top: 1, height: 1, fg: palette.muted, content: 'DETAILS' });
|
||||
this.detailText = new TextRenderable(renderer, { position: 'absolute', left: 2, top: 3, width: '90%', height: '80%', fg: palette.text, wrapMode: 'word', content: '' });
|
||||
this.detailAction = new TextRenderable(renderer, { position: 'absolute', left: 2, top: 12, height: 1, fg: palette.selectedText, bg: palette.selected, content: ' Refresh harnesses ', visible: false,
|
||||
onMouseDown: () => this.activateSelectedRow() });
|
||||
this.listPane.add(this.listTitle); this.detailPane.add(this.detailTitle); this.detailPane.add(this.detailText); this.detailPane.add(this.detailAction);
|
||||
for (let slot = 0; slot < 32; slot++) {
|
||||
const row = new TextRenderable(renderer, { position: 'absolute', left: 2, top: slot * 2 + 3, height: 2, fg: palette.text, content: '', visible: false,
|
||||
onMouseDown: () => this.clickListSlot(slot) });
|
||||
this.listPane.add(row); this.listRows.push(row);
|
||||
}
|
||||
|
||||
this.footerRule = new TextRenderable(renderer, { height: 1, fg: palette.divider }); this.root.add(this.footerRule);
|
||||
this.footer = new TextRenderable(renderer, { height: 1, fg: palette.muted, content: wideFooter }); this.root.add(this.footer);
|
||||
@@ -104,11 +135,22 @@ export class OpenTuiShell {
|
||||
renderer.keyInput.on('keypress', key => this.key(key));
|
||||
renderer.on('resize', () => this.resize());
|
||||
renderer.on('destroy', () => this.close(false));
|
||||
this.unsubscribeInventory = inventory.subscribe(snapshot => {
|
||||
this.inventorySnapshot = snapshot;
|
||||
this.state.setHarnessRows([...snapshot.harnesses.map(harness => ({ id: `harness:${harness.id}`, kind: 'harness' as const })), { id: 'command:refresh', kind: 'command' }]);
|
||||
this.paint();
|
||||
});
|
||||
this.resize();
|
||||
}
|
||||
|
||||
private pane(region: Exclude<FocusRegion, 'header'>) {
|
||||
return new BoxRenderable(this.renderer, { height: '100%', backgroundColor: palette.surface,
|
||||
onMouseScroll: event => {
|
||||
if (region !== 'list' || this.state.activeSection !== 2 || !this.visible(region)) return;
|
||||
if (event.scroll?.direction === 'up') this.state.key('up');
|
||||
else if (event.scroll?.direction === 'down') this.state.key('down');
|
||||
this.paint();
|
||||
},
|
||||
onMouseDown: () => { if (!this.visible(region)) return; this.state.focus = region; this.paint(); } });
|
||||
}
|
||||
|
||||
@@ -118,15 +160,38 @@ export class OpenTuiShell {
|
||||
}
|
||||
|
||||
private key(key: KeyEvent) {
|
||||
const wasHarnesses = this.state.mode === 'section' && this.state.activeSection === 2;
|
||||
const effect = this.state.key(key.name, { ctrl: key.ctrl, shift: key.shift });
|
||||
if (wasHarnesses && !(this.state.mode === 'section' && this.state.activeSection === 2)) { this.screenGeneration++; this.inventory.cancel(); }
|
||||
if (effect !== 'none') key.preventDefault();
|
||||
if (effect === 'quit') this.close();
|
||||
else if (effect === 'activate') this.activateSelectedRow();
|
||||
else if (effect === 'render') this.paint();
|
||||
}
|
||||
|
||||
private activate(index: number) {
|
||||
if (this.state.belowMinimum || this.state.helpOpen) return;
|
||||
this.state.activateHeader(index); this.paint();
|
||||
const wasHarnesses = this.state.mode === 'section' && this.state.activeSection === 2;
|
||||
this.state.activateHeader(index);
|
||||
if (wasHarnesses && !(this.state.mode === 'section' && this.state.activeSection === 2)) { this.screenGeneration++; this.inventory.cancel(); }
|
||||
this.paint();
|
||||
}
|
||||
|
||||
private clickListSlot(slot: number) {
|
||||
if (this.closed || this.state.helpOpen || this.state.activeSection !== 2 || !this.visible('list')) return;
|
||||
const generation = this.screenGeneration;
|
||||
const row = this.state.harnessRows[this.state.listOffset + slot];
|
||||
if (!row || generation !== this.screenGeneration || !this.state.selectHarness(row.id)) return;
|
||||
this.state.focus = 'list';
|
||||
this.paint();
|
||||
}
|
||||
|
||||
private activateSelectedRow() {
|
||||
const row = this.state.selectedHarnessRow;
|
||||
if (this.state.activeSection !== 2 || row?.id !== 'command:refresh') { this.paint(); return; }
|
||||
void this.inventory.refresh();
|
||||
if (!this.state.splitPane) { this.state.focus = 'detail'; this.state.narrowPane = 'detail'; }
|
||||
this.paint();
|
||||
}
|
||||
|
||||
private resize() {
|
||||
@@ -161,6 +226,7 @@ export class OpenTuiShell {
|
||||
} else {
|
||||
this.listPane.width = '100%'; this.detailPane.width = '100%'; this.detailPane.flexGrow = 1; this.divider.visible = false;
|
||||
}
|
||||
this.paintHarnesses();
|
||||
const focusedPane = this.state.focus === 'list' || this.state.focus === 'detail' ? this.state.focus : undefined;
|
||||
this.focusPerimeter.visible = Boolean(focusedPane);
|
||||
if (focusedPane) {
|
||||
@@ -172,6 +238,41 @@ export class OpenTuiShell {
|
||||
this.syncHelp();
|
||||
}
|
||||
|
||||
private paintHarnesses() {
|
||||
const shown = this.state.mode === 'section' && this.state.activeSection === 2;
|
||||
this.listTitle.visible = shown; this.detailTitle.visible = shown; this.detailText.visible = shown; this.detailAction.visible = false;
|
||||
for (const row of this.listRows) row.visible = false;
|
||||
if (!shown) { this.detailText.content = ''; return; }
|
||||
const bodyHeight = Math.max(1, this.renderer.height - 5);
|
||||
const capacity = Math.max(1, Math.min(this.listRows.length, Math.floor((bodyHeight - 4) / 2)));
|
||||
this.state.setListCapacity(capacity);
|
||||
for (let slot = 0; slot < capacity; slot++) {
|
||||
const descriptor = this.state.harnessRows[this.state.listOffset + slot];
|
||||
const renderable = this.listRows[slot]!;
|
||||
if (!descriptor) continue;
|
||||
const selected = descriptor.id === this.state.harnessSelection;
|
||||
const harness = descriptor.kind === 'harness' ? this.inventorySnapshot.harnesses.find(value => `harness:${value.id}` === descriptor.id) : undefined;
|
||||
const status = harness ? harness.state === 'available' ? 'Available' : harness.state === 'not-installed' ? 'Not installed' : harness.state === 'cli-missing' ? 'CLI missing' : 'Incompatible' : this.inventorySnapshot.phase === 'refreshing' ? 'Refreshing…' : 'Refresh harnesses';
|
||||
renderable.content = descriptor.kind === 'command' ? `↻ ${status}\n Re-read and save local facts` : `${harness?.label ?? descriptor.id} · ${status}\n ${harness?.reason ?? 'Reason unavailable'}`;
|
||||
renderable.width = Math.max(1, (this.state.splitPane ? listWidth(this.renderer.width) : this.renderer.width) - 4);
|
||||
renderable.bg = selected ? palette.selected : palette.surface;
|
||||
renderable.fg = selected ? palette.selectedText : palette.text;
|
||||
renderable.visible = true;
|
||||
}
|
||||
const selected = this.state.selectedHarnessRow;
|
||||
if (selected?.kind === 'harness') {
|
||||
const harness = this.inventorySnapshot.harnesses.find(value => `harness:${value.id}` === selected.id);
|
||||
if (harness) {
|
||||
const saved = this.inventorySnapshot.savedConfigurations[harness.id] ?? 0;
|
||||
this.detailText.content = `${harness.label}\n\nState: ${harness.state}\nReason: ${harness.reason}\nVersion: ${harness.version ?? 'Not established'}\nExecutable: ${harness.executable ? 'Detected' : 'Not established'}\nUnderlying CLI: ${harness.cli ? 'Detected' : 'Not established'}\nProvider integrations: ${harness.providers.join(', ') || 'None'}\nSaved configurations: ${saved}\n\nThis inventory is local to this computer.`;
|
||||
} else this.detailText.content = 'This harness is no longer in the current inventory.';
|
||||
} else {
|
||||
this.detailText.content = `Refresh harnesses\n\nRe-read bounded local executable and version evidence, then save the complete harness inventory.\n\nStatus: ${this.inventorySnapshot.message}\n\nNo owner sign-in, relay, Host, agent, provider, browser, or credential operation is used.`;
|
||||
this.detailAction.content = this.inventorySnapshot.phase === 'refreshing' ? ' Refresh in progress ' : ' Refresh harnesses ';
|
||||
this.detailAction.visible = this.state.focus === 'detail' && this.visible('detail');
|
||||
}
|
||||
}
|
||||
|
||||
private syncHelp() {
|
||||
if (!this.state.helpOpen) {
|
||||
if (this.helpScrim) { this.help?.destroyRecursively(); this.helpScrim.destroyRecursively(); this.helpScrim = undefined; this.help = undefined; this.helpBody = undefined; this.helpBodyScrollable = false; this.helpTitle = undefined; this.helpActions = undefined; }
|
||||
@@ -214,6 +315,8 @@ export class OpenTuiShell {
|
||||
close(destroy = true) {
|
||||
if (this.closed) return;
|
||||
this.closed = true;
|
||||
this.unsubscribeInventory();
|
||||
this.inventory.dispose();
|
||||
if (destroy) this.renderer.destroy();
|
||||
this.finish();
|
||||
}
|
||||
|
||||
@@ -42,13 +42,14 @@ export function validateSettings(value: Settings): Settings {
|
||||
if (r.harness === 'codex' ? !r.cli || !isAbsolute(r.cli) || r.effort !== undefined || value.providers.find(p => p.id === r.providerId)?.type !== 'openai' : r.harness === 'pi' ? !['openai','databricks_v2'].includes(value.providers.find(p => p.id === r.providerId)!.type) || !r.cli || !isAbsolute(r.cli) || r.effort !== undefined && !piEfforts(value.providers.find(p => p.id === r.providerId)!.type, r.model).includes(r.effort) : r.cli !== undefined) throw Error('Unsupported runtime provider/CLI/effort combination');
|
||||
}
|
||||
for (const r of value.runtimes) validateRuntimeEffort(value.providers.find(p => p.id === r.providerId)!.type, r.model, r.effort);
|
||||
if (value.harnesses !== undefined && (!Array.isArray(value.harnesses) || value.harnesses.length > 32 || value.harnesses.some(h => typeof h.id !== 'string' || typeof h.label !== 'string' || typeof h.reason !== 'string' || !['available','not-installed','cli-missing','incompatible'].includes(h.state) || !Array.isArray(h.providers) || h.providers.some(p => typeof p !== 'string') || (h.executable !== undefined && !isAbsolute(h.executable)) || (h.cli !== undefined && !isAbsolute(h.cli))))) throw Error('Invalid harness inventory');
|
||||
if (value.harnesses !== undefined && (!Array.isArray(value.harnesses) || value.harnesses.length > 32 || value.harnesses.some(h => typeof h.id !== 'string' || typeof h.label !== 'string' || typeof h.reason !== 'string' || (h.version !== undefined && (typeof h.version !== 'string' || !/^\d+\.\d+\.\d+$/.test(h.version))) || !['available','not-installed','cli-missing','incompatible'].includes(h.state) || !Array.isArray(h.providers) || h.providers.some(p => typeof p !== 'string') || (h.executable !== undefined && !isAbsolute(h.executable)) || (h.cli !== undefined && !isAbsolute(h.cli))))) throw Error('Invalid harness inventory');
|
||||
// Reject unknown fields, including accidental secret-bearing input.
|
||||
const fields = (o: object, allowed: string[]) => { if (Object.keys(o).some(k => !allowed.includes(k))) throw Error('Unexpected settings field'); };
|
||||
fields(value, ['version','revision','agents','providers','runtimes','harnesses']);
|
||||
for (const a of value.agents) { fields(a, ['publicKey','key','profile','profileState','runtimeId']); if (a.profile) fields(a.profile, ['relay','name','picture','about']); }
|
||||
for (const p of value.providers) { fields(p, ['id','name','type','endpoint','key','wire']); fields(p.key, ['service','account']); }
|
||||
for (const r of value.runtimes) fields(r, ['id','name','harness','executable','providerId','model','effort','cli','environment']);
|
||||
for (const h of value.harnesses ?? []) fields(h, ['id','label','executable','cli','version','state','providers','reason']);
|
||||
return structuredClone(value);
|
||||
}
|
||||
/** Absent settings means an empty catalog, never reconstructed credentials. */
|
||||
|
||||
@@ -5,7 +5,8 @@ export const brand = '⬢ BEEHIVE';
|
||||
|
||||
export type FocusRegion = 'header' | 'list' | 'detail';
|
||||
export type ShellMode = 'section' | 'owner';
|
||||
export type ShellEffect = 'render' | 'quit' | 'none';
|
||||
export type ShellEffect = 'render' | 'activate' | 'quit' | 'none';
|
||||
export type HarnessListRow = Readonly<{ id: string; kind: 'harness' | 'command' }>;
|
||||
|
||||
/** Framework-independent state for the never-signed-in shell tracer. */
|
||||
export class ShellState {
|
||||
@@ -17,6 +18,10 @@ export class ShellState {
|
||||
width = 120;
|
||||
height = 40;
|
||||
narrowPane: 'list' | 'detail' = 'list';
|
||||
harnessRows: readonly HarnessListRow[] = [{ id: 'command:refresh', kind: 'command' }];
|
||||
harnessSelection = 'command:refresh';
|
||||
listOffset = 0;
|
||||
listCapacity = 1;
|
||||
|
||||
get belowMinimum() { return this.width < 50 || this.height < 20; }
|
||||
get protectedSection() { return this.activeSection < 2; }
|
||||
@@ -32,6 +37,43 @@ export class ShellState {
|
||||
if (this.belowMinimum) this.helpOpen = false;
|
||||
}
|
||||
|
||||
setHarnessRows(rows: readonly HarnessListRow[]) {
|
||||
const previous = this.harnessSelection;
|
||||
const previousHarnesses = this.harnessRows.filter(row => row.kind === 'harness');
|
||||
const previousHarnessIndex = previousHarnesses.findIndex(row => row.id === previous);
|
||||
const initialCommandOnly = this.harnessRows.length === 1 && this.harnessRows[0]?.id === 'command:refresh';
|
||||
this.harnessRows = rows.length ? rows : [{ id: 'command:refresh', kind: 'command' }];
|
||||
if (initialCommandOnly && this.harnessRows[0]?.kind === 'harness') this.harnessSelection = this.harnessRows[0].id;
|
||||
else if (this.harnessRows.some(row => row.id === previous)) this.harnessSelection = previous;
|
||||
else {
|
||||
const harnesses = this.harnessRows.filter(row => row.kind === 'harness');
|
||||
this.harnessSelection = harnesses[previousHarnessIndex]?.id
|
||||
?? harnesses[Math.min(previousHarnessIndex - 1, harnesses.length - 1)]?.id
|
||||
?? this.harnessRows.find(row => row.kind === 'command')?.id
|
||||
?? this.harnessRows[0]!.id;
|
||||
}
|
||||
this.revealHarnessSelection();
|
||||
}
|
||||
|
||||
setListCapacity(capacity: number) { this.listCapacity = Math.max(1, capacity); this.revealHarnessSelection(); }
|
||||
private revealHarnessSelection() {
|
||||
const index = Math.max(0, this.harnessRows.findIndex(row => row.id === this.harnessSelection));
|
||||
if (index < this.listOffset) this.listOffset = index;
|
||||
if (index >= this.listOffset + this.listCapacity) this.listOffset = index - this.listCapacity + 1;
|
||||
this.listOffset = Math.max(0, Math.min(this.listOffset, Math.max(0, this.harnessRows.length - this.listCapacity)));
|
||||
}
|
||||
selectHarness(id: string) {
|
||||
if (!this.harnessRows.some(row => row.id === id)) return false;
|
||||
this.harnessSelection = id; this.revealHarnessSelection(); return true;
|
||||
}
|
||||
private moveHarness(delta: number) {
|
||||
const index = Math.max(0, this.harnessRows.findIndex(row => row.id === this.harnessSelection));
|
||||
const next = Math.max(0, Math.min(this.harnessRows.length - 1, index + delta));
|
||||
if (next === index) return false;
|
||||
this.harnessSelection = this.harnessRows[next]!.id; this.revealHarnessSelection(); return true;
|
||||
}
|
||||
get selectedHarnessRow() { return this.harnessRows.find(row => row.id === this.harnessSelection); }
|
||||
|
||||
key(name: string, options: { ctrl?: boolean; shift?: boolean } = {}): ShellEffect {
|
||||
if (options.ctrl && name === 'q') return 'quit';
|
||||
if (this.belowMinimum) return 'none';
|
||||
@@ -66,7 +108,9 @@ export class ShellState {
|
||||
// active destination remain separate: only header activation changes the
|
||||
// destination, while these transitions only move the focus perimeter.
|
||||
if (this.collection && this.focus === 'list') {
|
||||
if (this.activeSection === 2 && name === 'down') return this.moveHarness(1) ? 'render' : 'none';
|
||||
if (name === 'up') {
|
||||
if (this.activeSection === 2 && this.moveHarness(-1)) return 'render';
|
||||
this.focus = 'header'; this.headerIndex = this.activeSection; return 'render';
|
||||
}
|
||||
if (name === 'right' || name === 'return') {
|
||||
@@ -76,6 +120,7 @@ export class ShellState {
|
||||
}
|
||||
}
|
||||
if (this.collection && this.focus === 'detail') {
|
||||
if (name === 'return' && this.activeSection === 2 && this.selectedHarnessRow?.kind === 'command') return 'activate';
|
||||
if (name === 'up') {
|
||||
this.focus = 'header'; this.headerIndex = this.activeSection; return 'render';
|
||||
}
|
||||
@@ -86,8 +131,9 @@ export class ShellState {
|
||||
}
|
||||
}
|
||||
if (name === 'tab') {
|
||||
const harnessAction = this.activeSection === 2 && this.selectedHarnessRow?.kind === 'command';
|
||||
const regions: FocusRegion[] = this.collection
|
||||
? this.splitPane ? ['header', 'list', 'detail'] : ['header', this.narrowPane]
|
||||
? this.splitPane ? harnessAction ? ['header', 'list', 'detail'] : ['header', 'list'] : this.narrowPane === 'list' ? ['header', 'list'] : harnessAction ? ['header', 'detail'] : ['header']
|
||||
: ['header', 'detail'];
|
||||
const current = Math.max(0, regions.indexOf(this.focus));
|
||||
this.focus = regions[(current + (options.shift ? regions.length - 1 : 1)) % regions.length]!;
|
||||
|
||||
123
beehive/test/harness-inventory.test.ts
Normal file
123
beehive/test/harness-inventory.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync, chmodSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { HarnessInventoryController, localHarnessInventory } from '../src/harness-inventory.ts';
|
||||
import type { DetectedHarness } from '../src/harness-discovery.ts';
|
||||
import type { Settings } from '../src/settings.ts';
|
||||
|
||||
const row = (id: string, state: DetectedHarness['state'] = 'available'): DetectedHarness => ({ id, label: id.toUpperCase(), state, providers: state === 'available' ? ['openai'] : [], reason: state === 'available' ? 'Detected; authorization unverified' : 'Not installed', ...(state === 'available' ? { executable: `/fixture/${id}` } : {}) });
|
||||
const fixture = (initial: DetectedHarness[] = []) => {
|
||||
let settings: Settings = { version: 1, revision: 4, agents: [], providers: [], runtimes: [], harnesses: initial };
|
||||
let saves = 0;
|
||||
const store = {
|
||||
read: () => structuredClone(settings),
|
||||
save: (candidate: Settings, expected: number) => { assert.equal(expected, settings.revision); saves++; settings = { ...structuredClone(candidate), revision: expected + 1 }; return structuredClone(settings); },
|
||||
};
|
||||
return { store, settings: () => settings, saves: () => saves };
|
||||
};
|
||||
|
||||
const deferred = <T>() => { let resolve!: (value: T) => void; let reject!: (error: unknown) => void; const promise = new Promise<T>((yes, no) => { resolve = yes; reject = no; }); return { promise, resolve, reject }; };
|
||||
|
||||
test('loads durable local harness facts and saved configuration counts without probing', () => {
|
||||
const data = fixture([row('codex'), row('pi', 'not-installed')]);
|
||||
const settings = data.settings();
|
||||
settings.providers = [{ id: 'provider', name: 'Provider', type: 'openai', endpoint: 'https://api.openai.com/v1', key: { service: 'beehive', account: 'provider' } }];
|
||||
settings.runtimes = [{ id: 'runtime', name: 'Saved', harness: 'codex', executable: '/fixture/codex', cli: '/fixture/codex-cli', providerId: 'provider', model: 'model' }];
|
||||
let probes = 0;
|
||||
const controller = new HarnessInventoryController(data.store, async () => { probes++; return []; });
|
||||
assert.equal(probes, 0);
|
||||
assert.deepEqual(controller.snapshot().harnesses.map(value => [value.id, value.state]), [['codex', 'available'], ['pi', 'not-installed']]);
|
||||
assert.equal(controller.snapshot().savedConfigurations.codex, 1);
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
test('refresh commits one complete inventory and rejects a duplicate in-flight request', async () => {
|
||||
const data = fixture([row('old')]);
|
||||
const work = deferred<DetectedHarness[]>();
|
||||
const controller = new HarnessInventoryController(data.store, () => work.promise);
|
||||
const states: string[] = [];
|
||||
controller.subscribe(snapshot => states.push(snapshot.phase));
|
||||
const first = controller.refresh();
|
||||
assert.equal(await controller.refresh(), false);
|
||||
assert.equal(controller.snapshot().phase, 'refreshing');
|
||||
work.resolve([row('codex'), row('pi', 'not-installed')]);
|
||||
assert.equal(await first, true);
|
||||
assert.equal(data.saves(), 1);
|
||||
assert.deepEqual(data.settings().harnesses?.map(value => value.id), ['codex', 'pi']);
|
||||
assert.deepEqual(states, ['idle', 'refreshing', 'idle']);
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
test('refresh failure keeps durable rows and exposes a retryable plain reason', async () => {
|
||||
const data = fixture([row('retained')]);
|
||||
const controller = new HarnessInventoryController(data.store, async () => { throw Error('/private/path/token=secret'); });
|
||||
assert.equal(await controller.refresh(), false);
|
||||
assert.equal(data.saves(), 0);
|
||||
assert.equal(controller.snapshot().harnesses[0]?.id, 'retained');
|
||||
assert.equal(controller.snapshot().phase, 'error');
|
||||
assert.doesNotMatch(controller.snapshot().message, /private|token|secret/);
|
||||
assert.match(controller.snapshot().message, /try again/i);
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
test('production adapter discovers and durably saves only an explicit isolated HOME/PATH fixture', async () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'beehive-harness-inventory-'));
|
||||
try {
|
||||
const bin = join(home, 'bin'); mkdirSync(bin);
|
||||
const executable = join(bin, 'buzz-agent'); writeFileSync(executable, '#!/bin/sh\necho "buzz-agent 2.3.4"\n'); chmodSync(executable, 0o700);
|
||||
const controller = localHarnessInventory({ BEEHIVE_HOME: home, BEEHIVE_HARNESS_PATH: bin, BEEHIVE_HARNESS_ISOLATED: '1', PATH: bin });
|
||||
assert.equal(await controller.refresh(), true);
|
||||
const buzz = controller.snapshot().harnesses.find(value => value.id === 'buzz-agent');
|
||||
assert.deepEqual({ state: buzz?.state, version: buzz?.version, executable: buzz?.executable, providers: buzz?.providers }, { state: 'available', version: '2.3.4', executable: realpathSync(executable), providers: ['openai', 'anthropic', 'openai-compat', 'openrouter', 'databricks_v2'] });
|
||||
assert.equal(controller.snapshot().harnesses.find(value => value.id === 'codex')?.state, 'not-installed');
|
||||
const persisted = JSON.parse(readFileSync(join(home, '.beehive', 'host', 'settings.json'), 'utf8'));
|
||||
assert.equal(persisted.harnesses.find((value: DetectedHarness) => value.id === 'buzz-agent').executable, realpathSync(executable));
|
||||
controller.dispose();
|
||||
|
||||
rmSync(executable);
|
||||
const relaunched = localHarnessInventory({ BEEHIVE_HOME: home, BEEHIVE_HARNESS_PATH: bin, BEEHIVE_HARNESS_ISOLATED: '1', PATH: '/ordinary/path-must-not-be-used' });
|
||||
assert.deepEqual({ state: relaunched.snapshot().harnesses.find(value => value.id === 'buzz-agent')?.state, version: relaunched.snapshot().harnesses.find(value => value.id === 'buzz-agent')?.version }, { state: 'available', version: '2.3.4' }, 'relaunch reads durable inventory without another probe');
|
||||
relaunched.dispose();
|
||||
} finally { rmSync(home, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('isolated production adapter fails closed unless both fixture root and executable path are explicit', () => {
|
||||
assert.throws(() => localHarnessInventory({ BEEHIVE_HARNESS_ISOLATED: '1', BEEHIVE_HARNESS_PATH: '/fixture/bin' }), /explicit BEEHIVE_HOME/);
|
||||
assert.throws(() => localHarnessInventory({ BEEHIVE_HARNESS_ISOLATED: '1', BEEHIVE_HOME: '/fixture/home' }), /explicit BEEHIVE_HOME/);
|
||||
});
|
||||
|
||||
test('a failed refresh and cancellation preserve the last trustworthy inventory across reconstruction', async () => {
|
||||
const data = fixture([row('trusted')]);
|
||||
const failed = new HarnessInventoryController(data.store, async () => { throw Error('probe failed'); });
|
||||
assert.equal(await failed.refresh(), false);
|
||||
failed.dispose();
|
||||
const reconstructed = new HarnessInventoryController(data.store, async () => [row('must-not-probe')]);
|
||||
assert.equal(reconstructed.snapshot().harnesses[0]?.id, 'trusted');
|
||||
const work = deferred<DetectedHarness[]>();
|
||||
const refresh = new HarnessInventoryController(data.store, () => work.promise);
|
||||
const pending = refresh.refresh(); refresh.cancel(); work.resolve([row('late')]);
|
||||
assert.equal(await pending, false);
|
||||
assert.equal(new HarnessInventoryController(data.store, async () => []).snapshot().harnesses[0]?.id, 'trusted');
|
||||
reconstructed.dispose(); refresh.dispose();
|
||||
});
|
||||
|
||||
test('cancel and disposal fence late discovery before persistence or presentation', async () => {
|
||||
for (const mode of ['cancel', 'dispose'] as const) {
|
||||
const data = fixture([row('retained')]);
|
||||
const work = deferred<DetectedHarness[]>();
|
||||
const controller = new HarnessInventoryController(data.store, () => work.promise);
|
||||
let notifications = 0;
|
||||
controller.subscribe(() => notifications++);
|
||||
const refresh = controller.refresh();
|
||||
if (mode === 'cancel') controller.cancel(); else controller.dispose();
|
||||
const beforeLate = notifications;
|
||||
work.resolve([row('late')]);
|
||||
assert.equal(await refresh, false);
|
||||
assert.equal(data.saves(), 0, `${mode} must fence durable commit`);
|
||||
assert.equal(notifications, beforeLate, `${mode} must fence late presentation`);
|
||||
assert.equal(data.settings().harnesses?.[0]?.id, 'retained');
|
||||
controller.dispose();
|
||||
}
|
||||
});
|
||||
@@ -2,7 +2,14 @@ import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createTestRenderer } from '@opentui/core/testing';
|
||||
import { OpenTuiShell, footerGuides, helpDialogGeometry, palette } from '../src/opentui-shell.ts';
|
||||
import { HarnessInventoryController } from '../src/harness-inventory.ts';
|
||||
import type { Settings } from '../src/settings.ts';
|
||||
|
||||
const inventoryWith = (harnesses: NonNullable<Settings['harnesses']>, discover: (signal: AbortSignal) => Promise<NonNullable<Settings['harnesses']>> = async () => []) => {
|
||||
let settings: Settings = { version: 1, revision: 0, agents: [], providers: [], runtimes: [], harnesses };
|
||||
return new HarnessInventoryController({ read: () => structuredClone(settings), save: (candidate, expected) => settings = { ...structuredClone(candidate), revision: expected + 1 } }, discover);
|
||||
};
|
||||
const inventory = () => inventoryWith([{ id: 'fixture', label: 'Fixture Harness', state: 'available', providers: [], reason: 'Synthetic test executable detected', executable: '/fixture/bin/harness' }]);
|
||||
const text = (frame: string) => frame.endsWith('\n') ? frame.slice(0, -1).split('\n') : frame.split('\n');
|
||||
const waitForEscape = () => new Promise(resolve => setTimeout(resolve, 50));
|
||||
const cell = (ui: Awaited<ReturnType<typeof createTestRenderer>>, x: number, y: number) => {
|
||||
@@ -17,7 +24,7 @@ const rgb = (value: any) => [value.buffer['0'], value.buffer['1'], value.buffer[
|
||||
|
||||
test('memory renderer draws the exact wide empty shell and focus perimeter', async () => {
|
||||
const ui = await createTestRenderer({ width: 120, height: 40, exitOnCtrlC: false });
|
||||
const shell = new OpenTuiShell(ui.renderer);
|
||||
const shell = new OpenTuiShell(ui.renderer, inventory());
|
||||
try {
|
||||
await ui.renderOnce(); let frame = ui.captureCharFrame();
|
||||
assert.ok(frame.includes('⬢ BEEHIVE')); assert.ok(frame.includes('HOST AGENTS HARNESSES PROVIDERS')); assert.ok(frame.includes('SIGNED OUT'));
|
||||
@@ -29,7 +36,8 @@ test('memory renderer draws the exact wide empty shell and focus perimeter', asy
|
||||
assert.equal(shell.state.headerIndex, 1); assert.equal(shell.state.activeSection, 0);
|
||||
ui.mockInput.pressArrow('right'); ui.mockInput.pressEnter(); await ui.renderOnce(); frame = ui.captureCharFrame();
|
||||
assert.equal(shell.state.activeSection, 2); assert.equal(shell.state.focus, 'list'); assert.ok(frame.includes('│'));
|
||||
assert.equal(text(frame).slice(3, 38).join('').replace(/[│─┌┐└┘ ]/g, ''), '');
|
||||
assert.ok(frame.includes('Fixture Harness · Available'));
|
||||
assert.ok(frame.includes('State: available'));
|
||||
const spans = JSON.stringify(ui.captureSpans());
|
||||
assert.ok(spans.includes('"0":255,"1":211,"2":78'), `focus color missing from ${spans.slice(0, 300)}`);
|
||||
} finally { shell.close(); }
|
||||
@@ -37,7 +45,7 @@ test('memory renderer draws the exact wide empty shell and focus perimeter', asy
|
||||
|
||||
test('memory renderer owns split geometry, flat hierarchy, and focus inset without width shifts', async () => {
|
||||
const ui = await createTestRenderer({ width: 120, height: 40, exitOnCtrlC: false });
|
||||
const shell = new OpenTuiShell(ui.renderer);
|
||||
const shell = new OpenTuiShell(ui.renderer, inventory());
|
||||
try {
|
||||
ui.mockInput.pressArrow('right'); ui.mockInput.pressArrow('right'); ui.mockInput.pressEnter(); await ui.renderOnce();
|
||||
let frame = text(ui.captureCharFrame());
|
||||
@@ -53,7 +61,7 @@ test('memory renderer owns split geometry, flat hierarchy, and focus inset witho
|
||||
ui.mockInput.pressArrow('left'); await ui.renderOnce();
|
||||
assert.equal(shell.state.focus, 'list'); assert.equal(rgb(cell(ui, 0, 3).fg), '255,211,78');
|
||||
ui.mockInput.pressTab(); await ui.renderOnce(); frame = text(ui.captureCharFrame());
|
||||
assert.equal(shell.state.focus, 'detail'); assert.equal(rgb(cell(ui, 36, 3).fg), '255,211,78');
|
||||
assert.equal(shell.state.focus, 'header', 'Tab skips informational Details that have no action');
|
||||
ui.mockInput.pressEscape(); await waitForEscape(); await ui.renderOnce(); frame = text(ui.captureCharFrame());
|
||||
assert.equal(frame[3]![35], '│'); assert.doesNotMatch(frame.slice(3, 38).join('\n'), /[┌┐└┘]/); // unfocused panes remain flat
|
||||
} finally { shell.close(); }
|
||||
@@ -61,7 +69,7 @@ test('memory renderer owns split geometry, flat hierarchy, and focus inset witho
|
||||
|
||||
test('memory renderer keeps keyboard, pointer, resize, and Help return focus visible', async () => {
|
||||
const ui = await createTestRenderer({ width: 120, height: 40, exitOnCtrlC: false });
|
||||
const shell = new OpenTuiShell(ui.renderer);
|
||||
const shell = new OpenTuiShell(ui.renderer, inventory());
|
||||
try {
|
||||
ui.mockInput.pressArrow('right'); ui.mockInput.pressArrow('right'); ui.mockInput.pressEnter(); await ui.renderOnce();
|
||||
await ui.mockMouse.click(60, 10); await ui.renderOnce();
|
||||
@@ -79,7 +87,7 @@ test('memory renderer keeps keyboard, pointer, resize, and Help return focus vis
|
||||
|
||||
test('memory renderer layers a full scrim and naturally sized help surface with constrained-body geometry', async () => {
|
||||
const ui = await createTestRenderer({ width: 120, height: 40, exitOnCtrlC: false });
|
||||
const shell = new OpenTuiShell(ui.renderer);
|
||||
const shell = new OpenTuiShell(ui.renderer, inventory());
|
||||
try {
|
||||
ui.mockInput.pressKey('?'); await ui.renderOnce();
|
||||
const geometry = helpDialogGeometry(120, 40);
|
||||
@@ -97,7 +105,7 @@ test('memory renderer layers a full scrim and naturally sized help surface with
|
||||
test('memory renderer reserves the overflow scrollbar cell outside help text and chrome', async () => {
|
||||
const content = Array.from({ length: 12 }, (_, row) => `overflow row ${row} ends here`).join('\n');
|
||||
const ui = await createTestRenderer({ width: 50, height: 20, exitOnCtrlC: false });
|
||||
const shell = new OpenTuiShell(ui.renderer, content);
|
||||
const shell = new OpenTuiShell(ui.renderer, inventory(), content);
|
||||
try {
|
||||
ui.mockInput.pressKey('?'); await ui.renderOnce();
|
||||
const frame = text(ui.captureCharFrame());
|
||||
@@ -113,9 +121,88 @@ test('memory renderer reserves the overflow scrollbar cell outside help text and
|
||||
} finally { shell.close(); }
|
||||
});
|
||||
|
||||
test('memory renderer binds refresh to its command row and applies real async results coherently', async () => {
|
||||
let complete!: (rows: NonNullable<Settings['harnesses']>) => void;
|
||||
const discovered = new Promise<NonNullable<Settings['harnesses']>>(resolve => { complete = resolve; });
|
||||
const ui = await createTestRenderer({ width: 120, height: 40, exitOnCtrlC: false });
|
||||
const controller = inventoryWith([], () => discovered);
|
||||
const shell = new OpenTuiShell(ui.renderer, controller);
|
||||
try {
|
||||
ui.mockInput.pressArrow('right'); ui.mockInput.pressArrow('right'); ui.mockInput.pressEnter(); await ui.renderOnce();
|
||||
assert.ok(ui.captureCharFrame().includes('Saved local harness inventory'));
|
||||
ui.mockInput.pressEnter(); await ui.renderOnce();
|
||||
assert.equal(shell.state.focus, 'detail', 'the command row opens its owned Details action without executing');
|
||||
assert.ok(ui.captureCharFrame().includes('Refresh harnesses'));
|
||||
ui.mockInput.pressEnter(); await ui.renderOnce();
|
||||
assert.ok(ui.captureCharFrame().includes('Refreshing local harness inventory'));
|
||||
complete([{ id: 'codex', label: 'Codex', state: 'incompatible', providers: [], reason: 'Codex ACP 1.10.0 or newer required; version unknown/outdated' }]);
|
||||
await new Promise(resolve => setTimeout(resolve, 0)); await ui.renderOnce();
|
||||
const frame = ui.captureCharFrame();
|
||||
assert.ok(frame.includes('Codex · Incompatible')); assert.ok(frame.includes('State: incompatible'));
|
||||
assert.equal(shell.state.harnessSelection, 'harness:codex', 'disappearing command selection uses deterministic first-row fallback');
|
||||
} finally { shell.close(); }
|
||||
});
|
||||
|
||||
test('renderer shows refresh failure, permits recovery, and fences completion after leaving Harnesses', async () => {
|
||||
let attempts = 0;
|
||||
let lateResolve!: (rows: NonNullable<Settings['harnesses']>) => void;
|
||||
const late = new Promise<NonNullable<Settings['harnesses']>>(resolve => { lateResolve = resolve; });
|
||||
const controller = inventoryWith([], async () => {
|
||||
attempts++;
|
||||
if (attempts === 1) throw Error('/private/diagnostic');
|
||||
if (attempts === 2) return [{ id: 'buzz-agent', label: 'Buzz Agent', state: 'available', providers: ['openai'], reason: 'Detected; provider/model access unverified', executable: '/fixture/buzz-agent' }];
|
||||
return late;
|
||||
});
|
||||
const ui = await createTestRenderer({ width: 120, height: 40, exitOnCtrlC: false });
|
||||
const shell = new OpenTuiShell(ui.renderer, controller);
|
||||
try {
|
||||
ui.mockInput.pressArrow('right'); ui.mockInput.pressArrow('right'); ui.mockInput.pressEnter(); await ui.renderOnce();
|
||||
ui.mockInput.pressEnter(); ui.mockInput.pressEnter(); await new Promise(resolve => setTimeout(resolve, 0)); await ui.renderOnce();
|
||||
assert.ok(ui.captureCharFrame().includes('Harness refresh failed'));
|
||||
assert.doesNotMatch(ui.captureCharFrame(), /private\/diagnostic/);
|
||||
ui.mockInput.pressEnter(); await new Promise(resolve => setTimeout(resolve, 0)); await ui.renderOnce();
|
||||
assert.ok(ui.captureCharFrame().includes('Buzz Agent · Available'));
|
||||
ui.mockInput.pressArrow('down'); ui.mockInput.pressEnter(); ui.mockInput.pressEnter(); await ui.renderOnce();
|
||||
ui.mockInput.pressArrow('up'); ui.mockInput.pressArrow('up'); ui.mockInput.pressArrow('right'); ui.mockInput.pressEnter(); await ui.renderOnce();
|
||||
assert.equal(shell.state.activeSection, 3);
|
||||
lateResolve([{ id: 'late', label: 'Late', state: 'available', providers: [], reason: 'late', executable: '/fixture/late' }]);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
assert.equal(controller.snapshot().harnesses.some(row => row.id === 'late'), false);
|
||||
} finally { shell.close(); }
|
||||
});
|
||||
|
||||
test('pointer selection does not execute a command and its Details action dispatches once', async () => {
|
||||
let attempts = 0;
|
||||
const ui = await createTestRenderer({ width: 120, height: 40, exitOnCtrlC: false });
|
||||
const shell = new OpenTuiShell(ui.renderer, inventoryWith([], async () => { attempts++; return []; }));
|
||||
try {
|
||||
ui.mockInput.pressArrow('right'); ui.mockInput.pressArrow('right'); ui.mockInput.pressEnter(); await ui.renderOnce();
|
||||
await ui.mockMouse.click(3, 6); await ui.renderOnce();
|
||||
assert.equal(attempts, 0, 'a List click only selects the command row');
|
||||
ui.mockInput.pressEnter(); await ui.renderOnce();
|
||||
await ui.mockMouse.click(40, 15); await new Promise(resolve => setTimeout(resolve, 0)); await ui.renderOnce();
|
||||
assert.equal(attempts, 1, 'mouse bubbling cannot double-dispatch the Details action');
|
||||
} finally { shell.close(); }
|
||||
});
|
||||
|
||||
test('virtual list click mapping targets the currently visible row after overflow scroll', async () => {
|
||||
const harnesses = Array.from({ length: 20 }, (_, index) => ({ id: `fixture-${index}`, label: `Harness ${index}`, state: 'available' as const, providers: [], reason: 'Synthetic test executable detected', executable: `/fixture/${index}` }));
|
||||
const ui = await createTestRenderer({ width: 50, height: 20, exitOnCtrlC: false });
|
||||
const shell = new OpenTuiShell(ui.renderer, inventoryWith(harnesses));
|
||||
try {
|
||||
ui.mockInput.pressArrow('right'); ui.mockInput.pressArrow('right'); ui.mockInput.pressEnter(); await ui.renderOnce();
|
||||
for (let index = 0; index < 14; index++) ui.mockInput.pressArrow('down');
|
||||
await ui.renderOnce();
|
||||
assert.ok(shell.state.listOffset > 0);
|
||||
const expected = shell.state.harnessRows[shell.state.listOffset]!.id;
|
||||
await ui.mockMouse.click(3, 6); await ui.renderOnce();
|
||||
assert.equal(shell.state.harnessSelection, expected, 'the slot maps through the current virtual offset, never a stale absolute index');
|
||||
} finally { shell.close(); }
|
||||
});
|
||||
|
||||
test('memory renderer preserves the 50x20 narrow shell, help, keyboard, and pointer activation', async () => {
|
||||
const ui = await createTestRenderer({ width: 50, height: 20, exitOnCtrlC: false });
|
||||
const shell = new OpenTuiShell(ui.renderer);
|
||||
const shell = new OpenTuiShell(ui.renderer, inventory());
|
||||
try {
|
||||
await ui.renderOnce(); let frame = ui.captureCharFrame();
|
||||
assert.equal(text(frame).length, 20); assert.ok(frame.includes(footerGuides.compact)); assert.ok(frame.includes('PROVIDERS'));
|
||||
|
||||
@@ -12,6 +12,7 @@ test('never-signed-in shell labels and initial state are exact', () => {
|
||||
|
||||
test('wide collection arrows traverse header, List and Details without activating a new destination', () => {
|
||||
const state = new ShellState();
|
||||
state.setHarnessRows([{ id: 'harness:fixture', kind: 'harness' }, { id: 'command:refresh', kind: 'command' }]);
|
||||
assert.equal(state.key('right'), 'render'); assert.equal(state.headerIndex, 1); assert.equal(state.activeSection, 0);
|
||||
state.key('right'); state.key('return');
|
||||
assert.deepEqual({ active: state.activeSection, header: state.headerIndex, focus: state.focus, split: state.splitPane }, { active: 2, header: 2, focus: 'list', split: true });
|
||||
@@ -26,6 +27,7 @@ test('wide collection arrows traverse header, List and Details without activatin
|
||||
|
||||
test('narrow pane transitions, resize, and Help preserve a visible valid focus target', () => {
|
||||
const state = new ShellState();
|
||||
state.setHarnessRows([{ id: 'harness:fixture', kind: 'harness' }, { id: 'command:refresh', kind: 'command' }]);
|
||||
state.activateHeader(2); state.key('right');
|
||||
assert.equal(state.focus, 'detail');
|
||||
state.resize(50, 20); assert.deepEqual({ focus: state.focus, pane: state.narrowPane, split: state.splitPane }, { focus: 'detail', pane: 'detail', split: false });
|
||||
@@ -40,6 +42,7 @@ test('narrow pane transitions, resize, and Help preserve a visible valid focus t
|
||||
|
||||
test('help, q semantics, minimum guard and narrow return are deterministic', () => {
|
||||
const state = new ShellState();
|
||||
state.setHarnessRows([{ id: 'harness:fixture', kind: 'harness' }, { id: 'command:refresh', kind: 'command' }]);
|
||||
assert.equal(state.key('?'), 'render'); assert.equal(state.helpOpen, true);
|
||||
assert.equal(state.key('q'), 'none'); assert.equal(state.helpOpen, true);
|
||||
assert.equal(state.key('escape'), 'render'); assert.equal(state.helpOpen, false);
|
||||
@@ -49,6 +52,26 @@ test('help, q semantics, minimum guard and narrow return are deterministic', ()
|
||||
state.key('return'); assert.equal(state.narrowPane, 'detail'); state.key('escape'); assert.equal(state.narrowPane, 'list'); assert.equal(state.focus, 'list');
|
||||
});
|
||||
|
||||
test('harness rows preserve stable identity, fall back deterministically, and virtualize overflow', () => {
|
||||
const state = new ShellState();
|
||||
const rows: Array<{ id: string; kind: 'harness' | 'command' }> = [...Array.from({ length: 20 }, (_, index) => ({ id: `harness:${index}`, kind: 'harness' as const })), { id: 'command:refresh', kind: 'command' }];
|
||||
state.setHarnessRows(rows); state.setListCapacity(5); state.activateHeader(2);
|
||||
for (let index = 0; index < 9; index++) assert.equal(state.key('down'), 'render');
|
||||
assert.equal(state.harnessSelection, 'harness:9'); assert.equal(state.listOffset, 5);
|
||||
state.setHarnessRows(rows.filter(row => row.id !== 'harness:3'));
|
||||
assert.equal(state.harnessSelection, 'harness:9', 'stable identity survives row reindexing');
|
||||
state.setHarnessRows([{ id: 'harness:replacement', kind: 'harness' }, { id: 'command:refresh', kind: 'command' }]);
|
||||
assert.equal(state.harnessSelection, 'harness:replacement', 'disappearing identity falls back to the next surviving harness');
|
||||
state.setHarnessRows([{ id: 'harness:a', kind: 'harness' }, { id: 'harness:b', kind: 'harness' }, { id: 'harness:c', kind: 'harness' }, { id: 'command:refresh', kind: 'command' }]);
|
||||
state.selectHarness('harness:b');
|
||||
state.setHarnessRows([{ id: 'harness:a', kind: 'harness' }, { id: 'harness:c', kind: 'harness' }, { id: 'command:refresh', kind: 'command' }]);
|
||||
assert.equal(state.harnessSelection, 'harness:c', 'removed row falls forward at the same harness position');
|
||||
state.setHarnessRows([{ id: 'harness:a', kind: 'harness' }, { id: 'command:refresh', kind: 'command' }]);
|
||||
assert.equal(state.harnessSelection, 'harness:a', 'then falls back to the previous surviving harness');
|
||||
state.key('down'); assert.equal(state.key('return'), 'render', 'the command row opens Details without executing');
|
||||
assert.equal(state.key('return'), 'activate', 'only the explicit command Details action activates');
|
||||
});
|
||||
|
||||
test('prototype breakpoint formulas are exact', () => {
|
||||
assert.equal(listWidth(120), 35); assert.equal(listWidth(90), 28); assert.equal(listWidth(72), 25); assert.equal(listWidth(50), 50);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user