Files
oh-my-pi/packages/coding-agent/test/model-selector-role-badge-thinking.test.ts
T
can1357 59c8749ad7 fix(tui): resolved tui status footer truncation using VT stripping
- Replaced ad-hoc ANSI/VT stripping regexes with `stripVTControlCharacters` in status text handling and related tests.
- Updated status footer rendering to truncate using `truncateToWidth` and visible width after VT stripping.
- Extended tui cursor handling and rendering to strip markers from all lines and fit repaint/append-tail lines to width.
- Expanded deterministic render tests with overlay-aware assertions and recorded the truncation/cursor-marker behavior in changelogs.
2026-05-31 07:54:23 +02:00

236 lines
7.2 KiB
TypeScript

import { beforeAll, describe, expect, test, vi } from "bun:test";
import { stripVTControlCharacters } from "node:util";
import { getBundledModel, type Model } from "@oh-my-pi/pi-ai";
import type { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { ModelSelectorComponent } from "@oh-my-pi/pi-coding-agent/modes/components/model-selector";
import { getThemeByName, setThemeInstance } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import type { TUI } from "@oh-my-pi/pi-tui";
function normalizeRenderedText(text: string): string {
return stripVTControlCharacters(text).replace(/\s+/g, " ").trim();
}
function createSelector(model: Model, settings: Settings): ModelSelectorComponent {
const modelRegistry = {
getAll: () => [model],
getDiscoverableProviders: () => [],
getCanonicalModels: () => [],
resolveCanonicalModel: () => undefined,
} as unknown as ModelRegistry;
const ui = {
requestRender: vi.fn(),
} as unknown as TUI;
return new ModelSelectorComponent(
ui,
model,
settings,
modelRegistry,
[{ model, thinkingLevel: "off" }],
() => {},
() => {},
);
}
function createOllamaCloudModel(id: string): Model {
return {
id,
name: "DeepSeek V4 Pro",
api: "ollama-chat",
provider: "ollama-cloud",
baseUrl: "https://ollama.com",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1_000_000,
maxTokens: 8192,
};
}
let testTheme = await getThemeByName("dark");
function installTestTheme(): void {
if (!testTheme) {
throw new Error("Failed to load dark theme for ModelSelector tests");
}
setThemeInstance(testTheme);
}
describe("ModelSelector role badge thinking display", () => {
beforeAll(async () => {
testTheme = await getThemeByName("dark");
if (!testTheme) {
throw new Error("Failed to load dark theme for ModelSelector tests");
}
});
test("shows custom roles from cycleOrder/modelRoles and honors built-in metadata overrides", async () => {
installTestTheme();
const model = getBundledModel("anthropic", "claude-sonnet-4-5");
if (!model) throw new Error("Expected bundled model anthropic/claude-sonnet-4-5");
const settings = Settings.isolated({
cycleOrder: ["smol", "custom-fast", "default"],
modelRoles: {
default: `${model.provider}/${model.id}`,
"custom-fast": `${model.provider}/${model.id}:low`,
smol: `${model.provider}/${model.id}`,
},
modelTags: {
smol: { name: "Quick", color: "error" },
},
});
const selector = createSelector(model, settings);
await Bun.sleep(0);
installTestTheme();
const rendered = normalizeRenderedText(selector.render(220).join("\n"));
expect(rendered).toContain("custom-fast (low)");
expect(rendered).toContain("SMOL (inherit)");
selector.handleInput("\n");
installTestTheme();
const menuRendered = normalizeRenderedText(selector.render(220).join("\n"));
expect(menuRendered).toContain("Set as custom-fast");
expect(menuRendered).toContain("Set as SMOL (Quick)");
});
test("refreshes Ollama Cloud using provider id instead of tab label", async () => {
installTestTheme();
const settings = Settings.isolated({});
const discoveredModel = createOllamaCloudModel("deepseek-v4-pro");
let availableModels: Model[] = [];
const refreshProvider = vi.fn(async (providerId: string) => {
if (providerId === "ollama-cloud") {
availableModels = [discoveredModel];
}
});
const modelRegistry = {
getAll: () => availableModels,
refresh: vi.fn(async () => {}),
refreshProvider,
getError: () => undefined,
getAvailable: () => availableModels,
getDiscoverableProviders: () => ["ollama-cloud"],
getCanonicalModels: () => [],
resolveCanonicalModel: () => undefined,
getProviderDiscoveryState: () => ({
provider: "ollama-cloud",
status: "idle",
optional: false,
stale: false,
models: [],
}),
} as unknown as ModelRegistry;
const ui = {
requestRender: vi.fn(),
} as unknown as TUI;
const selector = new ModelSelectorComponent(
ui,
undefined,
settings,
modelRegistry,
[],
() => {},
() => {},
);
await Bun.sleep(0);
installTestTheme();
const initialRendered = normalizeRenderedText(selector.render(220).join("\n"));
expect(initialRendered).toContain("OLLAMA CLOUD");
selector.handleInput("\t");
selector.handleInput("\t");
await Bun.sleep(125);
installTestTheme();
expect(refreshProvider).toHaveBeenCalledWith("ollama-cloud", "online");
expect(modelRegistry.refresh).toHaveBeenCalledTimes(1);
const rendered = normalizeRenderedText(selector.render(220).join("\n"));
expect(rendered).toContain("deepseek-v4-pro");
expect(rendered).not.toContain("Provider has not been refreshed yet");
});
test("switches provider tabs immediately and refreshes in background with spinner animation", async () => {
installTestTheme();
const settings = Settings.isolated({});
const discoveredModel = createOllamaCloudModel("deepseek-v4-pro");
let availableModels: Model[] = [];
let resolveRefresh: (() => void) | undefined;
const refreshProvider = vi.fn(
(_providerId: string, _strategy?: string) =>
new Promise<void>(resolve => {
resolveRefresh = () => {
availableModels = [discoveredModel];
resolve();
};
}),
);
const modelRegistry = {
getAll: () => availableModels,
refresh: vi.fn(async () => {}),
refreshProvider,
getError: () => undefined,
getAvailable: () => availableModels,
getDiscoverableProviders: () => ["ollama-cloud"],
getCanonicalModels: () => [],
resolveCanonicalModel: () => undefined,
getProviderDiscoveryState: () => ({
provider: "ollama-cloud",
status: "idle",
optional: false,
stale: false,
models: [],
}),
} as unknown as ModelRegistry;
const ui = {
requestRender: vi.fn(),
} as unknown as TUI;
const selector = new ModelSelectorComponent(
ui,
undefined,
settings,
modelRegistry,
[],
() => {},
() => {},
);
await Bun.sleep(0);
installTestTheme();
selector.handleInput("\t");
selector.handleInput("\t");
// Core regression: tab switch must not synchronously enter provider refresh.
expect(refreshProvider).not.toHaveBeenCalled();
const immediateRendered = normalizeRenderedText(selector.render(220).join("\n"));
expect(immediateRendered).toContain("Refreshing OLLAMA CLOUD in background");
await Bun.sleep(5);
expect(refreshProvider).not.toHaveBeenCalled();
await Bun.sleep(120);
expect(refreshProvider).toHaveBeenCalledWith("ollama-cloud", "online");
const spinnerFrame1 = selector.render(220).join("\n");
await Bun.sleep(100);
installTestTheme();
const spinnerFrame2 = selector.render(220).join("\n");
expect(normalizeRenderedText(spinnerFrame2)).toContain("Refreshing OLLAMA CLOUD in background");
expect(spinnerFrame2).not.toEqual(spinnerFrame1);
resolveRefresh?.();
await Bun.sleep(10);
installTestTheme();
expect(modelRegistry.refresh).toHaveBeenCalledTimes(1);
const finalRendered = normalizeRenderedText(selector.render(220).join("\n"));
expect(finalRendered).toContain("deepseek-v4-pro");
expect(finalRendered).not.toContain("Refreshing OLLAMA CLOUD in background");
});
});