Files
oh-my-pi/packages/coding-agent/test/sdk-model-selection.test.ts
T
can1357 7f88a0c24c refactor: restructured logger.time() API to accept function references with wall-clock markers
- Refactored logger.time() API to accept function references and arguments separately instead of wrapped callbacks.
- Replaced RingBuffer-based timing with wall-clock markers and async span tracking for improved timing accuracy.
- Updated 20+ call sites across executor, kernel, main, and tools modules to use new logger.time() signature.
- Removed parsePath() helper function and inlined path.split() calls in settings module.
- Added test case verifying model selection without auth validation during startup.
2026-04-08 08:47:32 +02:00

153 lines
4.9 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, test, vi } from "bun:test";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { getBundledModel } from "@oh-my-pi/pi-ai";
import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
import { createAgentSession, type ExtensionFactory } from "@oh-my-pi/pi-coding-agent/sdk";
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
import { Snowflake } from "@oh-my-pi/pi-utils";
describe("createAgentSession deferred model pattern resolution", () => {
let tempDir: string;
beforeEach(() => {
tempDir = path.join(os.tmpdir(), `pi-sdk-model-selection-${Snowflake.next()}`);
fs.mkdirSync(tempDir, { recursive: true });
});
afterEach(() => {
if (tempDir && fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
const providerExtension: ExtensionFactory = pi => {
pi.registerProvider("runtime-provider", {
baseUrl: "https://runtime.example.com/v1",
apiKey: "RUNTIME_KEY",
api: "openai-completions",
models: [
{
id: "runtime-model",
name: "Runtime Model",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 8192,
},
{
id: "runtime-reasoning-model",
name: "Runtime Reasoning Model",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 8192,
},
],
});
};
function buildSessionOptions(modelPattern: string) {
return {
cwd: tempDir,
agentDir: tempDir,
sessionManager: SessionManager.inMemory(),
disableExtensionDiscovery: true,
extensions: [providerExtension],
skills: [],
contextFiles: [],
promptTemplates: [],
slashCommands: [],
enableMCP: false,
enableLsp: false,
modelPattern,
};
}
test("resolves explicit modelPattern after extension providers register", async () => {
const { session, modelFallbackMessage } = await createAgentSession(
buildSessionOptions("runtime-provider/runtime-model"),
);
expect(session.model).toBeDefined();
expect(session.model?.provider).toBe("runtime-provider");
expect(session.model?.id).toBe("runtime-model");
expect(modelFallbackMessage).toBeUndefined();
});
test("does not silently fallback when explicit modelPattern is unresolved", async () => {
const { session, modelFallbackMessage } = await createAgentSession(
buildSessionOptions("missing-provider/missing-model"),
);
expect(session.model).toBeUndefined();
expect(modelFallbackMessage).toBe('Model "missing-provider/missing-model" not found');
});
test("does not apply default role thinking override when modelPattern is explicit", async () => {
const settings = Settings.isolated({ defaultThinkingLevel: "off" });
settings.setModelRole("smol", "runtime-provider/runtime-reasoning-model");
settings.setModelRole("default", "pi/smol:high");
const { session } = await createAgentSession({
...buildSessionOptions("runtime-provider/runtime-reasoning-model"),
settings,
});
expect(session.model?.provider).toBe("runtime-provider");
expect(session.model?.id).toBe("runtime-reasoning-model");
expect(session.thinkingLevel).toBe("off");
});
test("selects the settings default model without synchronously validating auth", async () => {
const defaultModel = getBundledModel("anthropic", "claude-sonnet-4-5");
if (!defaultModel) {
throw new Error("Expected bundled anthropic default model");
}
const authStorage = await AuthStorage.create(path.join(tempDir, "testauth.db"));
authStorage.setRuntimeApiKey(defaultModel.provider, "test-key");
const modelRegistry = new ModelRegistry(authStorage, path.join(tempDir, "models.yml"));
const settings = Settings.isolated();
settings.setModelRole("default", `${defaultModel.provider}/${defaultModel.id}`);
const getApiKeySpy = vi
.spyOn(modelRegistry, "getApiKey")
.mockRejectedValue(new Error("settings default model should not validate auth during startup"));
try {
const { session } = await createAgentSession({
cwd: tempDir,
agentDir: tempDir,
authStorage,
modelRegistry,
settings,
sessionManager: SessionManager.inMemory(),
disableExtensionDiscovery: true,
skills: [],
contextFiles: [],
promptTemplates: [],
slashCommands: [],
enableMCP: false,
enableLsp: false,
});
try {
expect(session.model?.provider).toBe(defaultModel.provider);
expect(session.model?.id).toBe(defaultModel.id);
expect(getApiKeySpy).not.toHaveBeenCalled();
} finally {
await session.dispose();
}
} finally {
getApiKeySpy.mockRestore();
authStorage.close();
}
});
});