- 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.
236 lines
7.2 KiB
TypeScript
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");
|
|
});
|
|
});
|