Files
oh-my-pi/packages/coding-agent/test/selector-settings-side-effects.test.ts
T

128 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test";
import { stripVTControlCharacters } from "node:util";
import { buildModel } from "@oh-my-pi/pi-catalog/build";
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { SelectorController } from "@oh-my-pi/pi-coding-agent/modes/controllers/selector-controller";
import { getThemeByName, setThemeInstance } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import { beginSettingsTest, restoreSettingsTestState, type SettingsTestState } from "./helpers/settings-test-state";
let settingsState: SettingsTestState | undefined;
beforeEach(async () => {
settingsState = beginSettingsTest();
await Settings.init({ inMemory: true });
});
afterEach(() => {
restoreSettingsTestState(settingsState);
settingsState = undefined;
});
describe("selector setting side effects", () => {
it("refreshes the status line when git integration changes at runtime", () => {
const updateSettings = vi.fn();
const requestRender = vi.fn();
const controller = new SelectorController({
statusLine: { updateSettings },
ui: { requestRender },
} as unknown as ConstructorParameters<typeof SelectorController>[0]);
Settings.instance.override("git.enabled", false);
controller.handleSettingChange("git.enabled", false);
expect(updateSettings).toHaveBeenCalledWith(
expect.objectContaining({
preset: Settings.instance.get("statusLine.preset"),
leftSegments: Settings.instance.get("statusLine.leftSegments"),
rightSegments: Settings.instance.get("statusLine.rightSegments"),
}),
);
// The setting-change side effect is a single render request — the lazy
// top-border provider rebuilds during paint (#4145).
expect(requestRender).toHaveBeenCalledTimes(1);
});
it("invalidates the UI and requests a repaint when tui.tight changes", () => {
const invalidate = vi.fn();
const requestRender = vi.fn();
const controller = new SelectorController({
ui: { invalidate, requestRender },
} as unknown as ConstructorParameters<typeof SelectorController>[0]);
controller.handleSettingChange("tui.tight", true);
expect(invalidate).toHaveBeenCalledTimes(1);
expect(requestRender).toHaveBeenCalledTimes(1);
});
it("replaces malformed default retry fallback chains from the model selector action", async () => {
const testTheme = await getThemeByName("dark");
if (!testTheme) throw new Error("Failed to load dark theme for model selector test");
setThemeInstance(testTheme);
const settings = Settings.isolated({});
settings.set("retry.fallbackChains", { default: "not-an-array" } as unknown as Record<string, string[]>);
const fallback = buildModel({
id: "retry-fallback-model",
name: "retry-fallback-model",
api: "ollama-chat",
baseUrl: "https://example.com",
reasoning: false,
provider: "test",
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128_000,
maxTokens: 1024,
});
const showStatus = vi.fn();
const showError = vi.fn();
const controller = new SelectorController({
ui: { requestRender: vi.fn(), setFocus: vi.fn() },
editorContainer: { clear: vi.fn(), addChild: vi.fn() },
editor: {},
settings,
session: {
model: undefined,
modelRegistry: {
getAll: () => [fallback],
getDiscoverableProviders: () => [],
},
scopedModels: [{ model: fallback }],
getContextUsage: () => undefined,
},
statusLine: { invalidate: vi.fn() },
updateEditorBorderColor: vi.fn(),
keybindings: { getKeys: () => [] },
showStatus,
showError,
} as unknown as ConstructorParameters<typeof SelectorController>[0]);
let selector: { handleInput(input: string): void; render(width: number): string[] } | undefined;
controller.showSelector = create => {
const result = create(() => {});
selector = result.component as typeof selector;
};
controller.showModelSelector();
if (!selector) throw new Error("Expected model selector to be shown");
selector.handleInput("\n");
for (let attempt = 0; attempt < 20; attempt++) {
const selectedLine = stripVTControlCharacters(selector.render(220).join("\n"))
.split("\n")
.find(line => {
if (!line.includes("Set as DEFAULT retry fallback")) return false;
const trimmed = line.trimStart();
return trimmed.startsWith("❯") || trimmed.startsWith("▸") || trimmed.startsWith(">");
});
if (selectedLine) break;
selector.handleInput("\x1b[B");
if (attempt === 19) throw new Error("Default retry fallback action was not selectable");
}
selector.handleInput("\n");
await Promise.resolve();
expect(showError).not.toHaveBeenCalled();
expect(settings.get("retry.fallbackChains")).toEqual({ default: ["test/retry-fallback-model"] });
expect(showStatus).toHaveBeenCalledWith("Default fallback model: test/retry-fallback-model");
});
});