Files
letta-code/src/tests/agent/subagent-model-resolution.test.ts
2026-02-16 01:11:41 -08:00

210 lines
6.8 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import {
resolveSubagentLauncher,
resolveSubagentModel,
} from "../../agent/subagents/manager";
describe("resolveSubagentLauncher", () => {
test("explicit launcher takes precedence over .ts script autodetection", () => {
const launcher = resolveSubagentLauncher(["-p", "hi"], {
env: {
LETTA_CODE_BIN: "custom-bun",
LETTA_CODE_BIN_ARGS_JSON: JSON.stringify(["run", "src/index.ts"]),
} as NodeJS.ProcessEnv,
argv: ["bun", "/tmp/dev-entry.ts"],
execPath: "/opt/homebrew/bin/bun",
platform: "darwin",
});
expect(launcher).toEqual({
command: "custom-bun",
args: ["run", "src/index.ts", "-p", "hi"],
});
});
test("explicit launcher takes precedence over .js script autodetection", () => {
const launcher = resolveSubagentLauncher(["-p", "hi"], {
env: {
LETTA_CODE_BIN: "custom-node",
} as NodeJS.ProcessEnv,
argv: ["node", "/tmp/letta.js"],
execPath: "/usr/local/bin/node",
platform: "win32",
});
expect(launcher).toEqual({
command: "custom-node",
args: ["-p", "hi"],
});
});
test("preserves existing .ts dev behavior for any ts entrypoint", () => {
const launcher = resolveSubagentLauncher(
["--output-format", "stream-json"],
{
env: {} as NodeJS.ProcessEnv,
argv: ["bun", "/tmp/custom-runner.ts"],
execPath: "/opt/homebrew/bin/bun",
platform: "darwin",
},
);
expect(launcher).toEqual({
command: "/opt/homebrew/bin/bun",
args: ["/tmp/custom-runner.ts", "--output-format", "stream-json"],
});
});
test("uses node runtime for bundled js on win32", () => {
const launcher = resolveSubagentLauncher(["-p", "prompt"], {
env: {} as NodeJS.ProcessEnv,
argv: ["node", "C:\\Program Files\\Letta\\letta.js"],
execPath: "C:\\Program Files\\nodejs\\node.exe",
platform: "win32",
});
expect(launcher).toEqual({
command: "C:\\Program Files\\nodejs\\node.exe",
args: ["C:\\Program Files\\Letta\\letta.js", "-p", "prompt"],
});
});
test("keeps direct js spawn behavior on non-win32", () => {
const launcher = resolveSubagentLauncher(["-p", "prompt"], {
env: {} as NodeJS.ProcessEnv,
argv: ["node", "/usr/local/lib/letta.js"],
execPath: "/usr/local/bin/node",
platform: "linux",
});
expect(launcher).toEqual({
command: "/usr/local/lib/letta.js",
args: ["-p", "prompt"],
});
});
test("falls back to global letta when no launcher hints available", () => {
const launcher = resolveSubagentLauncher(["-p", "prompt"], {
env: {} as NodeJS.ProcessEnv,
argv: ["node", ""],
execPath: "/usr/local/bin/node",
platform: "linux",
});
expect(launcher).toEqual({
command: "letta",
args: ["-p", "prompt"],
});
});
test("keeps explicit launcher with spaces as a single command token", () => {
const launcher = resolveSubagentLauncher(
["--output-format", "stream-json"],
{
env: {
LETTA_CODE_BIN:
'"C:\\Users\\Example User\\AppData\\Roaming\\npm\\letta.cmd"',
} as NodeJS.ProcessEnv,
argv: ["node", "C:\\Program Files\\Letta\\letta.js"],
execPath: "C:\\Program Files\\nodejs\\node.exe",
platform: "win32",
},
);
expect(launcher).toEqual({
command: "C:\\Users\\Example User\\AppData\\Roaming\\npm\\letta.cmd",
args: ["--output-format", "stream-json"],
});
});
});
describe("resolveSubagentModel", () => {
test("prefers BYOK-swapped handle when available", async () => {
const cases = [
{ parentProvider: "lc-anthropic", baseProvider: "anthropic" },
{ parentProvider: "lc-openai", baseProvider: "openai" },
{ parentProvider: "lc-zai", baseProvider: "zai" },
{ parentProvider: "lc-gemini", baseProvider: "google_ai" },
{ parentProvider: "lc-openrouter", baseProvider: "openrouter" },
{ parentProvider: "lc-minimax", baseProvider: "minimax" },
{ parentProvider: "lc-bedrock", baseProvider: "bedrock" },
{ parentProvider: "chatgpt-plus-pro", baseProvider: "chatgpt-plus-pro" },
];
for (const { parentProvider, baseProvider } of cases) {
const recommendedHandle = `${baseProvider}/test-model`;
const swappedHandle = `${parentProvider}/test-model`;
const parentHandle = `${parentProvider}/parent-model`;
const result = await resolveSubagentModel({
recommendedModel: recommendedHandle,
parentModelHandle: parentHandle,
availableHandles: new Set([recommendedHandle, swappedHandle]),
});
expect(result).toBe(swappedHandle);
}
});
test("falls back to parent model when recommended is unavailable", async () => {
const result = await resolveSubagentModel({
recommendedModel: "anthropic/test-model",
parentModelHandle: "lc-anthropic/parent-model",
availableHandles: new Set(),
});
expect(result).toBe("lc-anthropic/parent-model");
});
test("BYOK parent ignores base-provider recommended when swap is unavailable", async () => {
const result = await resolveSubagentModel({
recommendedModel: "anthropic/test-model",
parentModelHandle: "lc-anthropic/parent-model",
availableHandles: new Set(["anthropic/test-model"]),
});
expect(result).toBe("lc-anthropic/parent-model");
});
test("BYOK parent accepts recommended handle when already using same BYOK prefix", async () => {
const result = await resolveSubagentModel({
recommendedModel: "lc-anthropic/test-model",
parentModelHandle: "lc-anthropic/parent-model",
availableHandles: new Set(["lc-anthropic/test-model"]),
});
expect(result).toBe("lc-anthropic/test-model");
});
test("uses recommended model when parent is not BYOK and model is available", async () => {
const result = await resolveSubagentModel({
recommendedModel: "anthropic/test-model",
parentModelHandle: "anthropic/parent-model",
availableHandles: new Set(["anthropic/test-model"]),
});
expect(result).toBe("anthropic/test-model");
});
test("explicit user model overrides all other resolution", async () => {
const result = await resolveSubagentModel({
userModel: "lc-openrouter/custom-model",
recommendedModel: "anthropic/test-model",
parentModelHandle: "lc-anthropic/parent-model",
availableHandles: new Set(["lc-anthropic/test-model"]),
});
expect(result).toBe("lc-openrouter/custom-model");
});
test("inherits parent when recommended is inherit", async () => {
const result = await resolveSubagentModel({
recommendedModel: "inherit",
parentModelHandle: "lc-anthropic/parent-model",
availableHandles: new Set(["lc-anthropic/parent-model"]),
});
expect(result).toBe("lc-anthropic/parent-model");
});
});