This commit is contained in:
2026-07-26 14:02:37 +07:00
parent bc56546b49
commit 367ebc1c7f
171 changed files with 4617 additions and 10402 deletions
@@ -239,4 +239,35 @@ describe("AgentSession bash and persistence characterization", () => {
expect(result.output).toContain("hello from custom ops");
expect(harness.session.messages[harness.session.messages.length - 1]?.role).toBe("bashExecution");
});
it("streams bash output to the callback and session events", async () => {
const harness = await createHarness();
harnesses.push(harness);
const callbackDeltas: string[] = [];
const eventUpdates: Array<{ id: string | undefined; delta: string }> = [];
const unsubscribe = harness.session.subscribe((event) => {
if (event.type === "bash_execution_update") {
eventUpdates.push({ id: event.id, delta: event.delta });
}
});
const operations: BashOperations = {
exec: async (_command, _cwd, options) => {
options.onData(Buffer.from("hello "));
options.onData(Buffer.from("world"));
return { exitCode: 0 };
},
};
await harness.session.executeBash("custom", (delta) => callbackDeltas.push(delta), {
id: "bash-1",
operations,
});
unsubscribe();
expect(callbackDeltas).toEqual(["hello ", "world"]);
expect(eventUpdates).toEqual([
{ id: "bash-1", delta: "hello " },
{ id: "bash-1", delta: "world" },
]);
});
});
@@ -175,6 +175,41 @@ describe("AgentSession compaction characterization", () => {
expect(getStreamCallCount()).toBe(1);
});
it("manually compacts with provider-resolved bearer auth", async () => {
const harness = await createHarness({ withConfiguredAuth: false });
harnesses.push(harness);
const model = harness.getModel();
harness.session.modelRuntime.registerNativeProvider({
id: model.provider,
name: "Faux bearer provider",
auth: {
apiKey: {
name: "Faux bearer token",
resolve: async () => ({
auth: { headers: { Authorization: "Bearer ambient-token" } },
source: "ambient bearer token",
}),
},
},
getModels: () => harness.models,
stream: () => createAssistantMessageEventStream(),
streamSimple: () => createAssistantMessageEventStream(),
});
seedCompactableSession(harness);
harness.setResponses([
(_context, options) => {
expect(options?.apiKey).toBeUndefined();
expect(options?.headers).toEqual({ Authorization: "Bearer ambient-token" });
return fauxAssistantMessage("summary with bearer auth");
},
]);
const result = await harness.session.compact();
expect(result.summary).toContain("summary with bearer auth");
expect(harness.faux.state.callCount).toBe(1);
});
it("persists usage from pi-generated manual compaction", async () => {
const harness = await createHarness({ withConfiguredAuth: false });
harnesses.push(harness);
+8 -3
View File
@@ -1,9 +1,9 @@
import { createInMemoryModelRegistry, getModelRuntime } from "../model-runtime-test-utils.ts";
import { createInMemoryModelRegistry, createModelRegistry, getModelRuntime } from "../model-runtime-test-utils.ts";
/**
* Local test harness for the new coding-agent test suite.
*/
import { existsSync, mkdirSync, rmSync } from "node:fs";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { AgentMessage, AgentTool } from "@earendil-works/pi-agent-core";
@@ -71,6 +71,7 @@ export interface HarnessOptions {
resourceLoader?: ResourceLoader;
extensionFactories?: Array<InlineExtension | CreateTestExtensionsResultInput>;
withConfiguredAuth?: boolean;
modelsJson?: Record<string, unknown>;
}
export interface Harness {
@@ -115,7 +116,11 @@ export async function createHarness(options: HarnessOptions = {}): Promise<Harne
if (withConfiguredAuth) {
await authStorage.modify(model.provider, async () => ({ type: "api_key", key: "faux-key" }));
}
const modelRegistry = await createInMemoryModelRegistry(authStorage);
const modelsPath = options.modelsJson === undefined ? undefined : join(tempDir, "models.json");
if (modelsPath) writeFileSync(modelsPath, JSON.stringify(options.modelsJson));
const modelRegistry = modelsPath
? await createModelRegistry(authStorage, modelsPath)
: await createInMemoryModelRegistry(authStorage);
if (withConfiguredAuth) {
modelRegistry.registerProvider(model.provider, {
baseUrl: model.baseUrl,
@@ -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();
});
});