new_pull
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import type { Api, Model } from "@earendil-works/pi-ai";
|
||||
import { setKeybindings } from "@earendil-works/pi-tui";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { KeybindingsManager } from "../../../src/core/keybindings.ts";
|
||||
import { ScopedModelsSelectorComponent } from "../../../src/modes/interactive/components/scoped-models-selector.ts";
|
||||
import { InteractiveMode } from "../../../src/modes/interactive/interactive-mode.ts";
|
||||
import { initTheme } from "../../../src/modes/interactive/theme/theme.ts";
|
||||
import { stripAnsi } from "../../../src/utils/ansi.ts";
|
||||
import { createHarness, type Harness } from "../harness.ts";
|
||||
|
||||
function createInteractiveContext(options: {
|
||||
allModels: Model<Api>[];
|
||||
enabledModelIds: string[];
|
||||
scopedModels?: Array<{ model: Model<Api> }>;
|
||||
}) {
|
||||
let selector: ScopedModelsSelectorComponent | undefined;
|
||||
const setScopedModels = vi.fn();
|
||||
const getAvailable = vi.fn().mockResolvedValue(options.allModels);
|
||||
const context = {
|
||||
session: {
|
||||
modelRuntime: {
|
||||
refresh: vi.fn(),
|
||||
getAvailable,
|
||||
},
|
||||
scopedModels: options.scopedModels ?? [],
|
||||
setScopedModels,
|
||||
},
|
||||
settingsManager: {
|
||||
getEnabledModels: () => options.enabledModelIds,
|
||||
setEnabledModels: vi.fn(),
|
||||
},
|
||||
showStatus: vi.fn(),
|
||||
showSelector: (factory: (done: () => void) => { component: ScopedModelsSelectorComponent }) => {
|
||||
selector = factory(() => {}).component;
|
||||
},
|
||||
updateAvailableProviderCount: vi.fn(),
|
||||
ui: { requestRender: vi.fn() },
|
||||
};
|
||||
return { context, getAvailable, getSelector: () => selector, setScopedModels };
|
||||
}
|
||||
|
||||
async function showModelsSelector(context: object): Promise<void> {
|
||||
const show = Reflect.get(InteractiveMode.prototype, "showModelsSelector") as (this: object) => Promise<void>;
|
||||
await show.call(context);
|
||||
}
|
||||
|
||||
describe("issue #6949 unavailable scoped models", () => {
|
||||
const harnesses: Harness[] = [];
|
||||
|
||||
beforeAll(() => {
|
||||
initTheme("dark");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
setKeybindings(new KeybindingsManager());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
while (harnesses.length > 0) harnesses.pop()?.cleanup();
|
||||
});
|
||||
|
||||
it("shows and removes an enabled model without a catalog entry", async () => {
|
||||
const harness = await createHarness({ models: [{ id: "available", name: "Available" }] });
|
||||
harnesses.push(harness);
|
||||
const availableId = `${harness.models[0].provider}/${harness.models[0].id}`;
|
||||
const unavailableId = `${harness.models[0].provider}/unavailable`;
|
||||
const changes: Array<string[] | null> = [];
|
||||
const persisted: Array<string[] | null> = [];
|
||||
const selector = new ScopedModelsSelectorComponent(
|
||||
{
|
||||
allModels: [...harness.models],
|
||||
enabledModelIds: [unavailableId, availableId],
|
||||
},
|
||||
{
|
||||
onChange: (enabledIds) => {
|
||||
changes.push(enabledIds);
|
||||
},
|
||||
onPersist: (enabledIds) => {
|
||||
persisted.push(enabledIds);
|
||||
},
|
||||
onCancel: () => {},
|
||||
},
|
||||
);
|
||||
|
||||
expect(stripAnsi(selector.render(100).join("\n"))).toContain(`${unavailableId} [unavailable] ✗`);
|
||||
selector.handleInput("\r");
|
||||
expect(changes).toEqual([[availableId]]);
|
||||
selector.handleInput("\x13");
|
||||
expect(persisted).toEqual([[availableId]]);
|
||||
});
|
||||
|
||||
it("passes unmatched settings patterns to the selector with one combined resolution", async () => {
|
||||
const harness = await createHarness({ models: [{ id: "available", name: "Available" }] });
|
||||
harnesses.push(harness);
|
||||
const unavailableIds = ["unavailable-one", "unavailable-two"].map((id) => `${harness.models[0].provider}/${id}`);
|
||||
const { context, getAvailable, getSelector } = createInteractiveContext({
|
||||
allModels: [],
|
||||
enabledModelIds: unavailableIds,
|
||||
});
|
||||
|
||||
await showModelsSelector(context);
|
||||
|
||||
const selector = getSelector();
|
||||
if (!selector) throw new Error("Expected scoped-model selector to open");
|
||||
const rendered = stripAnsi(selector.render(100).join("\n"));
|
||||
for (const unavailableId of unavailableIds) {
|
||||
expect(rendered).toContain(`${unavailableId} [unavailable] ✗`);
|
||||
}
|
||||
expect(getAvailable).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("opens when only a session-scoped model is unavailable", async () => {
|
||||
const harness = await createHarness({ models: [{ id: "unavailable", name: "Unavailable" }] });
|
||||
harnesses.push(harness);
|
||||
const model = harness.models[0];
|
||||
const fullId = `${model.provider}/${model.id}`;
|
||||
const { context, getSelector } = createInteractiveContext({
|
||||
allModels: [],
|
||||
enabledModelIds: [],
|
||||
scopedModels: [{ model }],
|
||||
});
|
||||
|
||||
await showModelsSelector(context);
|
||||
|
||||
const selector = getSelector();
|
||||
if (!selector) throw new Error("Expected scoped-model selector to open");
|
||||
expect(stripAnsi(selector.render(100).join("\n"))).toContain(`${fullId} [unavailable] ✗`);
|
||||
});
|
||||
|
||||
it("does not clear a partial scope when an enabled model is unavailable", async () => {
|
||||
const harness = await createHarness({
|
||||
models: [
|
||||
{ id: "one", name: "One" },
|
||||
{ id: "two", name: "Two" },
|
||||
{ id: "three", name: "Three" },
|
||||
],
|
||||
});
|
||||
harnesses.push(harness);
|
||||
const [one, two] = harness.models;
|
||||
const enabledIds = [one, two].map((model) => `${model.provider}/${model.id}`);
|
||||
const unavailableId = `${one.provider}/unavailable`;
|
||||
const { context, getSelector, setScopedModels } = createInteractiveContext({
|
||||
allModels: [...harness.models],
|
||||
enabledModelIds: [...enabledIds, unavailableId],
|
||||
scopedModels: [{ model: one }, { model: two }],
|
||||
});
|
||||
|
||||
await showModelsSelector(context);
|
||||
const selector = getSelector();
|
||||
if (!selector) throw new Error("Expected scoped-model selector to open");
|
||||
selector.handleInput("\x1b[1;3B");
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(setScopedModels).toHaveBeenLastCalledWith([
|
||||
{ model: two, thinkingLevel: undefined },
|
||||
{ model: one, thinkingLevel: undefined },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { setKeybindings, type TUI } from "@earendil-works/pi-tui";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { KeybindingsManager } from "../../../src/core/keybindings.ts";
|
||||
import { ModelSelectorComponent } from "../../../src/modes/interactive/components/model-selector.ts";
|
||||
import { initTheme } from "../../../src/modes/interactive/theme/theme.ts";
|
||||
import { stripAnsi } from "../../../src/utils/ansi.ts";
|
||||
import { createHarness, type Harness } from "../harness.ts";
|
||||
|
||||
function createFakeTui(): TUI {
|
||||
return {
|
||||
requestRender: () => {},
|
||||
} as unknown as TUI;
|
||||
}
|
||||
|
||||
function modelsJson(provider: string, model: string): Record<string, unknown> {
|
||||
return {
|
||||
providers: {
|
||||
[provider]: {
|
||||
baseUrl: "https://example.test/v1",
|
||||
api: "openai-completions",
|
||||
apiKey: "test-key",
|
||||
models: [{ id: model }],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("issue #6999 models.json hot reload", () => {
|
||||
let harness: Harness | undefined;
|
||||
|
||||
beforeAll(() => {
|
||||
initTheme("dark");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
setKeybindings(new KeybindingsManager());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
harness?.cleanup();
|
||||
harness = undefined;
|
||||
});
|
||||
|
||||
it("reloads models.json when opening /model", async () => {
|
||||
harness = await createHarness({ modelsJson: modelsJson("old-provider", "old-model") });
|
||||
expect(harness.session.modelRuntime.getModel("old-provider", "old-model")).toBeDefined();
|
||||
|
||||
writeFileSync(join(harness.tempDir, "models.json"), JSON.stringify(modelsJson("new-provider", "new-model")));
|
||||
const selector = new ModelSelectorComponent(
|
||||
createFakeTui(),
|
||||
harness.getModel(),
|
||||
harness.settingsManager,
|
||||
harness.session.modelRuntime,
|
||||
[],
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const rendered = stripAnsi(selector.render(120).join("\n"));
|
||||
expect(rendered).toContain("new-model [new-provider]");
|
||||
expect(rendered).toContain("Model catalogs refreshed.");
|
||||
});
|
||||
expect(harness.session.modelRuntime.getModel("old-provider", "old-model")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user