Files
fusion/packages/engine/src/__tests__/agent-session-helpers.test.ts
Fusion bccc39f741 feat(FN-3961): propagate taskEnv into agent subprocess sessions
Propagates `taskEnv` (task-scoped environment variables) through the agent session creation pipeline, including executor sessions, spawned child agents, and workflow sessions. The engine's session factory (`agent-session-helpers.ts`), executor, PI agent creation (`pi.ts`), and step session executor

Fusion-Task-Id: FN-3961
2026-05-10 23:36:10 -07:00

219 lines
6.0 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import {
extractRuntimeHint,
resolveHeartbeatSessionModels,
resolveMergerSessionModel,
} from "../agent-session-helpers.js";
const { resolveRuntimeMock } = vi.hoisted(() => ({
resolveRuntimeMock: vi.fn(),
}));
vi.mock("../runtime-resolution.js", async () => {
const actual = await vi.importActual<typeof import("../runtime-resolution.js")>("../runtime-resolution.js");
return {
...actual,
resolveRuntime: resolveRuntimeMock,
};
});
describe("extractRuntimeHint", () => {
it("returns undefined for undefined config", () => {
expect(extractRuntimeHint(undefined)).toBeUndefined();
});
it("returns undefined when runtimeHint key is missing", () => {
expect(extractRuntimeHint({})).toBeUndefined();
});
it("returns normalized runtime hint when configured", () => {
expect(extractRuntimeHint({ runtimeHint: " openclaw " })).toBe("openclaw");
});
it("returns undefined for whitespace-only runtimeHint", () => {
expect(extractRuntimeHint({ runtimeHint: " " })).toBeUndefined();
});
it("returns undefined for non-string runtimeHint", () => {
expect(extractRuntimeHint({ runtimeHint: 42 })).toBeUndefined();
});
});
describe("resolveHeartbeatSessionModels", () => {
it("uses agent runtime model as primary and execution settings as fallback", () => {
expect(resolveHeartbeatSessionModels(
{
executionProvider: "openai",
executionModelId: "gpt-4.1",
},
{ model: "anthropic/claude-sonnet-4-5" },
)).toEqual({
defaultProvider: "anthropic",
defaultModelId: "claude-sonnet-4-5",
fallbackProvider: "openai",
fallbackModelId: "gpt-4.1",
});
});
it("uses execution settings model when runtime override is missing", () => {
expect(resolveHeartbeatSessionModels(
{
executionProvider: "openai",
executionModelId: "gpt-4.1",
},
{},
)).toEqual({
defaultProvider: "openai",
defaultModelId: "gpt-4.1",
fallbackProvider: undefined,
fallbackModelId: undefined,
});
});
it("does not duplicate fallback when runtime and execution model are the same", () => {
expect(resolveHeartbeatSessionModels(
{
executionProvider: "openai",
executionModelId: "gpt-4.1",
},
{ modelProvider: "openai", modelId: "gpt-4.1" },
)).toEqual({
defaultProvider: "openai",
defaultModelId: "gpt-4.1",
fallbackProvider: undefined,
fallbackModelId: undefined,
});
});
});
describe("createResolvedAgentSession", () => {
beforeEach(() => {
resolveRuntimeMock.mockReset();
});
it("forwards taskEnv unchanged to runtime session factory", async () => {
const mockSession = { prompt: vi.fn() } as any;
const createSessionMock = vi.fn().mockResolvedValue({
session: mockSession,
sessionFile: "session.json",
});
resolveRuntimeMock.mockResolvedValue({
runtime: {
id: "pi",
name: "Default PI Runtime",
createSession: createSessionMock,
promptWithFallback: vi.fn(),
describeModel: vi.fn(() => "mock/model"),
},
runtimeId: "pi",
wasConfigured: false,
});
const { createResolvedAgentSession } = await import("../agent-session-helpers.js");
const taskEnv = { PATH: "/tmp/bin", FUSION_TEST_VAR: "value" };
await createResolvedAgentSession({
sessionPurpose: "executor",
pluginRunner: undefined,
cwd: "/tmp/project",
systemPrompt: "system",
taskEnv,
});
expect(createSessionMock).toHaveBeenCalledWith(
expect.objectContaining({
taskEnv,
}),
);
});
});
describe("resolveMergerSessionModel", () => {
it("uses assigned agent runtime model when both provider and modelId are present", () => {
expect(
resolveMergerSessionModel(
{
defaultProviderOverride: "openai",
defaultModelIdOverride: "gpt-4.1",
defaultProvider: "anthropic",
defaultModelId: "claude-3-5-sonnet",
},
{ model: " anthropic/claude-3-5-sonnet-20241022 " },
),
).toEqual({
provider: "anthropic",
modelId: "claude-3-5-sonnet-20241022",
});
});
it("falls back to default override pair when runtime model is not fully specified", () => {
expect(
resolveMergerSessionModel(
{
defaultProviderOverride: "openai",
defaultModelIdOverride: "gpt-4.1",
defaultProvider: "anthropic",
defaultModelId: "claude-3-5-sonnet",
},
{ modelProvider: "anthropic" },
),
).toEqual({
provider: "openai",
modelId: "gpt-4.1",
});
});
it("falls back to global defaults when no override pair is configured", () => {
expect(
resolveMergerSessionModel(
{
defaultProvider: "anthropic",
defaultModelId: "claude-3-5-sonnet",
},
{ modelId: "claude-3-opus" },
),
).toEqual({
provider: "anthropic",
modelId: "claude-3-5-sonnet",
});
});
it("ignores partial override pairs and falls back to global defaults", () => {
expect(
resolveMergerSessionModel({
defaultProviderOverride: "openai",
defaultProvider: "anthropic",
defaultModelId: "claude-3-5-sonnet",
}),
).toEqual({
provider: "anthropic",
modelId: "claude-3-5-sonnet",
});
expect(
resolveMergerSessionModel({
defaultModelIdOverride: "gpt-4.1",
defaultProvider: "anthropic",
defaultModelId: "claude-3-5-sonnet",
}),
).toEqual({
provider: "anthropic",
modelId: "claude-3-5-sonnet",
});
});
it("works when assignedAgentRuntimeConfig is undefined", () => {
expect(
resolveMergerSessionModel({
defaultProviderOverride: "openai",
defaultModelIdOverride: "gpt-4.1",
defaultProvider: "anthropic",
defaultModelId: "claude-3-5-sonnet",
}),
).toEqual({
provider: "openai",
modelId: "gpt-4.1",
});
});
});