Files
oh-my-pi/packages/coding-agent/test/hook-selector-overflow.test.ts
T
2026-06-01 17:37:26 +08:00

125 lines
5.4 KiB
TypeScript

import { beforeAll, describe, expect, it } from "bun:test";
import { HookSelectorComponent } from "@oh-my-pi/pi-coding-agent/modes/components/hook-selector";
import { getThemeByName, setThemeInstance } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import { visibleWidth } from "@oh-my-pi/pi-tui";
beforeAll(async () => {
const theme = await getThemeByName("dark");
if (!theme) {
throw new Error("Failed to load dark theme for tests");
}
setThemeInstance(theme);
});
describe("HookSelectorComponent", () => {
it("keeps outlined options within render width", () => {
const options = [
"aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;b",
"bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;aaa;bbb;a",
"a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b;a;b",
];
const component = new HookSelectorComponent(
"Which pattern do you prefer?",
options,
() => {},
() => {},
{ outline: true, initialIndex: 0 },
);
const width = 80;
const lines = component.render(width);
for (const line of lines) {
expect(visibleWidth(Bun.stripANSI(line))).toBeLessThanOrEqual(width);
}
});
it("wraps outlined option text without omitting the tail", () => {
const options = [
"Option A: Move to OMP-native only by migrating reusable shared AI instructions into .omp/AGENTS.md, .omp/rules, .omp/skills, and .omp/agents while deliberately not creating a root .github directory.",
"Option B: Keep dual support by migrating canonical instructions into .omp while also maintaining a root .github/copilot-instructions.md compatibility bridge for editors that do not understand OMP resources yet.",
];
const component = new HookSelectorComponent(
"Which migration stance should be used?",
options,
() => {},
() => {},
{ outline: true, initialIndex: 0 },
);
const width = 72;
const lines = component.render(width);
const plain = lines.map(line => Bun.stripANSI(line)).join("\n");
const normalizedPlain = plain.replace(/[\u2500-\u257f]/g, " ").replace(/\s+/g, " ");
expect(normalizedPlain).toContain("not creating a root .github directory");
expect(normalizedPlain).toContain("do not understand OMP resources yet");
for (const line of lines) {
expect(visibleWidth(Bun.stripANSI(line))).toBeLessThanOrEqual(width);
}
});
it("renders option descriptions as separate wrapped rows", () => {
const options = [
{
label: "Use existing local credentials",
description:
"Authenticate via the provider keys and OAuth state already configured under ~/.omp without opening a new browser-based setup flow.",
},
{
label: "Set up Oh My Pi in terminal",
description:
"Launch the local terminal UI to add provider keys, select models, and keep the current editor session waiting for the configured credentials.",
},
];
const component = new HookSelectorComponent(
"How should authentication continue?",
options,
() => {},
() => {},
{ outline: true, initialIndex: 0 },
);
const width = 76;
const lines = component.render(width);
const plainLines = lines.map(line => Bun.stripANSI(line));
const normalizedPlain = plainLines
.join("\n")
.replace(/[\u2500-\u257f]/g, " ")
.replace(/\s+/g, " ");
const labelLineIndex = plainLines.findIndex(line => line.includes("Use existing local credentials"));
const descriptionLineIndex = plainLines.findIndex(line => line.includes("Authenticate via the provider keys"));
expect(labelLineIndex).toBeGreaterThanOrEqual(0);
expect(descriptionLineIndex).toBeGreaterThan(labelLineIndex);
expect(normalizedPlain).toContain("without opening a new browser-based setup flow");
expect(normalizedPlain).toContain("keep the current editor session waiting for the configured credentials");
for (const line of lines) {
expect(visibleWidth(Bun.stripANSI(line))).toBeLessThanOrEqual(width);
}
});
it("filters options by description text", () => {
const component = new HookSelectorComponent(
"Which setup path should be used?",
[
{ label: "Path A", description: "Reuse the credentials already available in the environment." },
{ label: "Path B", description: "Launch a browser flow to authorize a new provider account." },
{ label: "Path C", description: "Open the local settings file and edit provider keys manually." },
{ label: "Path D", description: "Skip provider setup and continue with offline-only tools." },
],
() => {},
() => {},
{ outline: true, maxVisible: 3 },
);
for (const key of "browser") {
component.handleInput(key);
}
const plain = component
.render(76)
.map(line => Bun.stripANSI(line))
.join("\n");
expect(plain).toContain("Path B");
expect(plain).toContain("Launch a browser flow");
expect(plain).not.toContain("Path A");
});
});