mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
chore: initialize my-pi extension bundle
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { cloneDefaultConfig, mock, runTest } from "./test-helpers.test.ts";
|
||||
|
||||
mock.module("@earendil-works/pi-coding-agent", {
|
||||
namedExports: {
|
||||
getAgentDir: () => "/tmp/.pi/agent",
|
||||
getSettingsListTheme: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const settingsListInputs: string[] = [];
|
||||
const settingsListUpdates: Array<{ id: string; value: string }> = [];
|
||||
|
||||
mock.module("@earendil-works/pi-tui", {
|
||||
namedExports: {
|
||||
Box: class {
|
||||
addChild(): void {}
|
||||
},
|
||||
Container: class {
|
||||
addChild(): void {}
|
||||
render(): string[] {
|
||||
return ["settings-content"];
|
||||
}
|
||||
invalidate(): void {}
|
||||
},
|
||||
SettingsList: class {
|
||||
handleInput(data: string): void {
|
||||
settingsListInputs.push(data);
|
||||
}
|
||||
updateValue(id: string, value: string): void {
|
||||
settingsListUpdates.push({ id, value });
|
||||
}
|
||||
},
|
||||
Spacer: class {},
|
||||
Text: class {},
|
||||
truncateToWidth: (text: string, width: number) => text.slice(0, width),
|
||||
visibleWidth: (text: string) => text.length,
|
||||
},
|
||||
});
|
||||
|
||||
function stripAnsi(text: string): string {
|
||||
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
||||
}
|
||||
|
||||
const { registerRtkIntegrationCommand } = await import("./command-register.ts");
|
||||
const { ZellijModal, ZellijSettingsModal } = await import("./zellij-modal.ts");
|
||||
const { getRtkArgumentCompletions } = await import("./command-completions.ts");
|
||||
|
||||
type Notification = { message: string; level: "info" | "warning" | "error" };
|
||||
|
||||
interface CommandContextStub {
|
||||
hasUI: boolean;
|
||||
ui: {
|
||||
notify(message: string, level: "info" | "warning" | "error"): void;
|
||||
custom<T>(): Promise<T>;
|
||||
};
|
||||
}
|
||||
|
||||
function createNotifyContext(hasUI: boolean): { ctx: CommandContextStub; notifications: Notification[] } {
|
||||
const notifications: Notification[] = [];
|
||||
return {
|
||||
ctx: {
|
||||
hasUI,
|
||||
ui: {
|
||||
notify(message: string, level: "info" | "warning" | "error") {
|
||||
notifications.push({ message, level });
|
||||
},
|
||||
async custom<T>(): Promise<T> {
|
||||
throw new Error("custom UI should not be invoked in config-modal tests");
|
||||
},
|
||||
},
|
||||
},
|
||||
notifications,
|
||||
};
|
||||
}
|
||||
|
||||
function lastNotification(notifications: Notification[]): Notification {
|
||||
return notifications[notifications.length - 1] as Notification;
|
||||
}
|
||||
|
||||
function createThemeStub(): { fg: (_name: string, text: string) => string; bold: (text: string) => string } {
|
||||
return {
|
||||
fg: (_name: string, text: string) => text,
|
||||
bold: (text: string) => text,
|
||||
};
|
||||
}
|
||||
|
||||
runTest("zellij settings modal renders overlay frame and delegates non-enter input", () => {
|
||||
settingsListInputs.length = 0;
|
||||
settingsListUpdates.length = 0;
|
||||
const settingsModal = new ZellijSettingsModal(
|
||||
{
|
||||
title: "RTK Integration Settings",
|
||||
settings: [
|
||||
{
|
||||
id: "enabled",
|
||||
label: "Enabled",
|
||||
description: "Enable integration",
|
||||
currentValue: "on",
|
||||
values: ["on", "off"],
|
||||
},
|
||||
],
|
||||
onChange: () => {},
|
||||
onClose: () => {},
|
||||
helpText: "Esc: close",
|
||||
},
|
||||
createThemeStub() as never,
|
||||
);
|
||||
const modal = new ZellijModal(settingsModal, {
|
||||
titleBar: {
|
||||
left: { text: "RTK Integration Settings", maxWidth: 30, color: "accent" },
|
||||
right: { text: "pi-rtk-optimizer", maxWidth: 20, color: "dim" },
|
||||
},
|
||||
helpUndertitle: { text: "Esc: close", color: "dim" },
|
||||
overlay: { anchor: "center", width: 86, maxHeight: "85%", margin: 1 },
|
||||
});
|
||||
|
||||
const rendered = modal.renderModal(86);
|
||||
settingsModal.handleInput("\r");
|
||||
settingsModal.handleInput("j");
|
||||
settingsModal.updateValue("enabled", "off");
|
||||
|
||||
assert.equal(rendered.visibleWidth, 86);
|
||||
assert.equal(rendered.contentWidth, 82);
|
||||
assert.ok(stripAnsi(rendered.lines[0] ?? "").includes("RTK Integration Settings"));
|
||||
assert.ok(stripAnsi(rendered.lines[rendered.lines.length - 1] ?? "").includes("Esc: close"));
|
||||
assert.deepEqual(modal.getOverlayOptions(), {
|
||||
overlay: true,
|
||||
overlayOptions: { anchor: "center", width: 86, maxHeight: "85%", margin: 1 },
|
||||
});
|
||||
assert.deepEqual(settingsListInputs, ["j"]);
|
||||
assert.deepEqual(settingsListUpdates, [{ id: "enabled", value: "off" }]);
|
||||
});
|
||||
|
||||
runTest("command completions return top-level and filtered RTK subcommands", () => {
|
||||
const topLevel = getRtkArgumentCompletions("");
|
||||
assert.ok(Array.isArray(topLevel));
|
||||
assert.ok(topLevel.some((item) => item.value === "show"));
|
||||
assert.ok(topLevel.some((item) => item.value === "clear-stats"));
|
||||
|
||||
const filtered = getRtkArgumentCompletions("st");
|
||||
assert.deepEqual(
|
||||
filtered?.map((item) => item.value),
|
||||
["stats"],
|
||||
);
|
||||
assert.equal(getRtkArgumentCompletions("show extra"), null);
|
||||
assert.equal(getRtkArgumentCompletions("zzz"), null);
|
||||
});
|
||||
|
||||
await runTest("config modal command handlers route RTK subcommands to controller actions", async () => {
|
||||
const config = cloneDefaultConfig();
|
||||
const controllerState = {
|
||||
config,
|
||||
cleared: 0,
|
||||
refreshed: 0,
|
||||
lastSavedMode: "",
|
||||
};
|
||||
|
||||
const controller = {
|
||||
getConfig: () => controllerState.config,
|
||||
setConfig: (next: typeof config, _ctx: unknown) => {
|
||||
controllerState.config = next;
|
||||
controllerState.lastSavedMode = next.mode;
|
||||
},
|
||||
getConfigPath: () => "C:/tmp/pi-rtk-optimizer/config.json",
|
||||
getRuntimeStatus: () => ({ rtkAvailable: false, lastError: "not found" }),
|
||||
refreshRuntimeStatus: async () => {
|
||||
controllerState.refreshed += 1;
|
||||
return { rtkAvailable: true, rtkExecutablePath: "C:/Tools/rtk.exe" };
|
||||
},
|
||||
getMetricsSummary: () => "metrics summary",
|
||||
clearMetrics: () => {
|
||||
controllerState.cleared += 1;
|
||||
},
|
||||
};
|
||||
|
||||
let registeredName = "";
|
||||
type RegisteredCommandDefinition = {
|
||||
description: string;
|
||||
getArgumentCompletions?: (argumentPrefix: string) => Array<{ value: string; label: string; description?: string }> | null;
|
||||
handler: (args: string, ctx: CommandContextStub) => Promise<void>;
|
||||
};
|
||||
let definition: RegisteredCommandDefinition | undefined;
|
||||
|
||||
registerRtkIntegrationCommand(
|
||||
{
|
||||
registerCommand(name: string, nextDefinition: RegisteredCommandDefinition) {
|
||||
registeredName = name;
|
||||
definition = nextDefinition;
|
||||
},
|
||||
} as never,
|
||||
controller as never,
|
||||
);
|
||||
|
||||
assert.equal(registeredName, "rtk");
|
||||
if (!definition) {
|
||||
throw new Error("Expected /rtk command definition to be registered");
|
||||
}
|
||||
assert.ok(definition.description.includes("Configure RTK rewrite"));
|
||||
assert.ok(typeof definition.getArgumentCompletions === "function");
|
||||
|
||||
const infoCtx = createNotifyContext(true);
|
||||
await definition.handler("help", infoCtx.ctx);
|
||||
assert.ok(lastNotification(infoCtx.notifications).message.includes("Usage: /rtk"));
|
||||
|
||||
await definition.handler("show", infoCtx.ctx);
|
||||
assert.ok(lastNotification(infoCtx.notifications).message.includes("mode=rewrite"));
|
||||
assert.ok(lastNotification(infoCtx.notifications).message.includes("rewriteSource=rtk"));
|
||||
assert.equal(lastNotification(infoCtx.notifications).message.includes("categories="), false);
|
||||
|
||||
await definition.handler("path", infoCtx.ctx);
|
||||
assert.equal(lastNotification(infoCtx.notifications).message, "rtk config: C:/tmp/pi-rtk-optimizer/config.json");
|
||||
|
||||
await definition.handler("verify", infoCtx.ctx);
|
||||
assert.equal(controllerState.refreshed, 1);
|
||||
assert.equal(lastNotification(infoCtx.notifications).level, "info");
|
||||
assert.ok(lastNotification(infoCtx.notifications).message.includes("available at C:/Tools/rtk.exe"));
|
||||
|
||||
await definition.handler("stats", infoCtx.ctx);
|
||||
assert.equal(lastNotification(infoCtx.notifications).message, "metrics summary");
|
||||
|
||||
await definition.handler("clear-stats", infoCtx.ctx);
|
||||
assert.equal(controllerState.cleared, 1);
|
||||
assert.equal(lastNotification(infoCtx.notifications).message, "RTK metrics cleared.");
|
||||
|
||||
await definition.handler("reset", infoCtx.ctx);
|
||||
assert.equal(controllerState.lastSavedMode, "rewrite");
|
||||
assert.equal(lastNotification(infoCtx.notifications).message, "RTK integration settings reset to defaults.");
|
||||
|
||||
await definition.handler("unknown", infoCtx.ctx);
|
||||
assert.equal(lastNotification(infoCtx.notifications).level, "warning");
|
||||
assert.ok(lastNotification(infoCtx.notifications).message.includes("Usage: /rtk"));
|
||||
|
||||
const headlessCtx = createNotifyContext(false);
|
||||
await definition.handler("", headlessCtx.ctx);
|
||||
assert.equal(lastNotification(headlessCtx.notifications).message, "/rtk requires interactive TUI mode.");
|
||||
});
|
||||
|
||||
console.log("All config-modal tests passed.");
|
||||
Reference in New Issue
Block a user