mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 07:23:06 +00:00
241 lines
7.9 KiB
TypeScript
241 lines
7.9 KiB
TypeScript
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.");
|