Files
pi/packages/coding-agent/test/agent-session-dynamic-provider.test.ts
Mario Zechner 8a0903ebf2 feat(ai): compat entrypoint, core-only root barrel (phase 5)
The root barrel is now core-only and side-effect free: types,
createModels/createProvider, auth substrate, lazyStream/lazyApi, faux,
utils. Generated catalogs, api-registry, env-api-keys, images, global
stream functions, and per-API lazy wrappers leave the root.

New @earendil-works/pi-ai/compat preserves the old surface verbatim as
a strict superset of the root: api-dispatch stream/complete with env
key injection, the builtin registration side effect (skip-if-present so
it cannot clobber earlier overrides), deprecated getModel/getModels/
getProviders aliases of the new getBuiltin* reads in providers/all,
lazy api wrappers + setBedrockProviderModule, and image generation.
Compat dies with the coding-agent ModelManager migration.

Packaging: exports map gains ./compat, ./providers/*, ./api/*;
sideEffects array lists only the effectful modules.

Old-global imports across agent/coding-agent/examples and pi-ai tests
switch to /compat (path-only; compat is a superset). The coding-agent
extension loader resolves the pi-ai ROOT specifier to compat, so
existing user extensions using the old global API keep working at
runtime until compat is removed. vitest configs alias /compat to src;
browser smoke imports old globals from /compat.
2026-06-10 21:17:12 +02:00

118 lines
3.6 KiB
TypeScript

import { existsSync, mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { getModel } from "@earendil-works/pi-ai/compat";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { AuthStorage } from "../src/core/auth-storage.ts";
import { DefaultResourceLoader } from "../src/core/resource-loader.ts";
import type { ExtensionFactory } from "../src/core/sdk.ts";
import { createAgentSession } from "../src/core/sdk.ts";
import { SessionManager } from "../src/core/session-manager.ts";
import { SettingsManager } from "../src/core/settings-manager.ts";
describe("AgentSession dynamic provider registration", () => {
let tempDir: string;
let agentDir: string;
beforeEach(() => {
tempDir = join(tmpdir(), `pi-dynamic-provider-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
agentDir = join(tempDir, "agent");
mkdirSync(agentDir, { recursive: true });
});
afterEach(() => {
if (tempDir && existsSync(tempDir)) {
rmSync(tempDir, { recursive: true, force: true });
}
});
async function createSession(extensionFactories: ExtensionFactory[]) {
const settingsManager = SettingsManager.create(tempDir, agentDir);
const sessionManager = SessionManager.inMemory();
const authStorage = AuthStorage.create(join(agentDir, "auth.json"));
authStorage.setRuntimeApiKey("anthropic", "test-key");
const resourceLoader = new DefaultResourceLoader({
cwd: tempDir,
agentDir,
settingsManager,
extensionFactories,
});
await resourceLoader.reload();
const { session } = await createAgentSession({
cwd: tempDir,
agentDir,
model: getModel("anthropic", "claude-sonnet-4-5")!,
settingsManager,
sessionManager,
authStorage,
resourceLoader,
});
return session;
}
async function capturePromptBaseUrl(
session: Awaited<ReturnType<typeof createSession>>,
): Promise<string | undefined> {
let baseUrl: string | undefined;
session.agent.streamFn = async (model) => {
baseUrl = model.baseUrl;
throw new Error("stop");
};
await session.prompt("hello");
return baseUrl;
}
it("applies top-level registerProvider overrides to the active model", async () => {
const session = await createSession([
(pi) => {
pi.registerProvider("anthropic", { baseUrl: "http://localhost:8080/top-level" });
},
]);
expect(session.model?.baseUrl).toBe("http://localhost:8080/top-level");
expect(await capturePromptBaseUrl(session)).toBe("http://localhost:8080/top-level");
session.dispose();
});
it("applies session_start registerProvider overrides to the active model", async () => {
const session = await createSession([
(pi) => {
pi.on("session_start", () => {
pi.registerProvider("anthropic", { baseUrl: "http://localhost:8080/session-start" });
});
},
]);
await session.bindExtensions({});
expect(session.model?.baseUrl).toBe("http://localhost:8080/session-start");
expect(await capturePromptBaseUrl(session)).toBe("http://localhost:8080/session-start");
session.dispose();
});
it("applies command-time registerProvider overrides without reload", async () => {
const session = await createSession([
(pi) => {
pi.registerCommand("use-proxy", {
description: "Use proxy",
handler: async () => {
pi.registerProvider("anthropic", { baseUrl: "http://localhost:8080/command" });
},
});
},
]);
await session.bindExtensions({});
await session.prompt("/use-proxy");
expect(session.model?.baseUrl).toBe("http://localhost:8080/command");
expect(await capturePromptBaseUrl(session)).toBe("http://localhost:8080/command");
session.dispose();
});
});