Files
openclaw/extensions/codex/src/app-server/thread-lifecycle.test.ts
2026-05-17 06:42:21 +01:00

288 lines
9.2 KiB
TypeScript

import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness-runtime";
import { describe, expect, it } from "vitest";
import {
buildTurnStartParams,
buildThreadResumeParams,
buildThreadStartParams,
resolveReasoningEffort,
} from "./thread-lifecycle.js";
function createAttemptParams(params: {
provider: string;
authProfileId?: string;
authProfileProvider?: string;
authProfileProviders?: Record<string, string>;
bootstrapContextMode?: "full" | "lightweight";
bootstrapContextRunKind?: "default" | "heartbeat" | "cron";
images?: EmbeddedRunAttemptParams["images"];
}): EmbeddedRunAttemptParams {
const authProfileProviders =
params.authProfileProviders ??
(params.authProfileId
? { [params.authProfileId]: params.authProfileProvider ?? "openai-codex" }
: {});
return {
provider: params.provider,
modelId: "gpt-5.4",
prompt: "test prompt",
authProfileId: params.authProfileId,
...(params.bootstrapContextMode ? { bootstrapContextMode: params.bootstrapContextMode } : {}),
...(params.bootstrapContextRunKind
? { bootstrapContextRunKind: params.bootstrapContextRunKind }
: {}),
...(params.images ? { images: params.images } : {}),
authProfileStore: {
version: 1,
profiles: Object.fromEntries(
Object.entries(authProfileProviders).map(([profileId, provider]) => [
profileId,
{
type: "oauth" as const,
provider,
access: "access-token",
refresh: "refresh-token",
expires: Date.now() + 60_000,
},
]),
),
},
} as EmbeddedRunAttemptParams;
}
function createAppServerOptions() {
return {
approvalPolicy: "on-request",
approvalsReviewer: "user",
sandbox: "workspace-write",
} as const;
}
describe("Codex app-server native code mode config", () => {
it("enables Codex code-mode-only on thread/start without clobbering other config", () => {
const request = buildThreadStartParams(createAttemptParams({ provider: "openai" }), {
cwd: "/repo",
dynamicTools: [],
appServer: createAppServerOptions() as never,
developerInstructions: "test instructions",
config: {
"features.hooks": true,
apps: { _default: { enabled: false } },
},
});
expect(request.config).toEqual({
"features.hooks": true,
apps: { _default: { enabled: false } },
"features.code_mode": true,
"features.code_mode_only": true,
});
});
it("enables Codex code-mode-only on thread/resume", () => {
const request = buildThreadResumeParams(createAttemptParams({ provider: "openai" }), {
threadId: "thread-1",
appServer: createAppServerOptions() as never,
developerInstructions: "test instructions",
});
expect(request.config).toEqual({
"features.code_mode": true,
"features.code_mode_only": true,
});
});
it("disables native Codex project docs for lightweight context threads", () => {
const request = buildThreadStartParams(
createAttemptParams({
provider: "openai",
bootstrapContextMode: "lightweight",
bootstrapContextRunKind: "cron",
}),
{
cwd: "/repo",
dynamicTools: [],
appServer: createAppServerOptions() as never,
developerInstructions: "test instructions",
config: {
project_doc_max_bytes: 64_000,
"features.hooks": true,
},
},
);
expect(request.config).toEqual({
project_doc_max_bytes: 0,
"features.hooks": true,
"features.code_mode": true,
"features.code_mode_only": true,
});
});
it("keeps native Codex project docs enabled when context is not lightweight", () => {
const request = buildThreadResumeParams(
createAttemptParams({ provider: "openai", bootstrapContextRunKind: "cron" }),
{
threadId: "thread-1",
appServer: createAppServerOptions() as never,
developerInstructions: "test instructions",
config: {
project_doc_max_bytes: 64_000,
},
},
);
expect(request.config).toEqual({
project_doc_max_bytes: 64_000,
"features.code_mode": true,
"features.code_mode_only": true,
});
});
});
describe("Codex app-server turn input image sanitizing", () => {
it("replaces malformed inline images before turn/start", () => {
const request = buildTurnStartParams(
createAttemptParams({
provider: "openai",
images: [{ type: "image", mimeType: "image/jpeg", data: "not base64!" }] as never,
}),
{
threadId: "thread-1",
cwd: "/repo",
appServer: createAppServerOptions() as never,
},
);
expect(request.input).toEqual([
{ type: "text", text: "test prompt", text_elements: [] },
{
type: "text",
text: "[codex user input] omitted image payload: invalid inline image data",
text_elements: [],
},
]);
});
});
describe("Codex app-server model provider selection", () => {
it.each(["openai", "openai-codex"])(
"omits public %s modelProvider when forwarding native Codex auth on thread/start",
(provider) => {
const request = buildThreadStartParams(
createAttemptParams({ provider, authProfileId: "work" }),
{
cwd: "/repo",
dynamicTools: [],
appServer: createAppServerOptions() as never,
developerInstructions: "test instructions",
},
);
expect(request).not.toHaveProperty("modelProvider");
},
);
it("uses the bound native Codex auth profile when deciding thread/resume modelProvider", () => {
const request = buildThreadResumeParams(
createAttemptParams({
provider: "openai",
authProfileProviders: { bound: "openai-codex" },
}),
{
threadId: "thread-1",
authProfileId: "bound",
appServer: createAppServerOptions() as never,
developerInstructions: "test instructions",
},
);
expect(request).not.toHaveProperty("modelProvider");
});
it("does not infer native Codex auth from the profile id prefix", () => {
const request = buildThreadStartParams(
createAttemptParams({
provider: "openai",
authProfileId: "openai-codex:work",
authProfileProvider: "openai",
}),
{
cwd: "/repo",
dynamicTools: [],
appServer: createAppServerOptions() as never,
developerInstructions: "test instructions",
},
);
expect(request.modelProvider).toBe("openai");
});
it("keeps public OpenAI modelProvider when no native Codex auth profile is selected", () => {
const request = buildThreadStartParams(createAttemptParams({ provider: "openai" }), {
cwd: "/repo",
dynamicTools: [],
appServer: createAppServerOptions() as never,
developerInstructions: "test instructions",
});
expect(request.modelProvider).toBe("openai");
});
});
describe("resolveReasoningEffort (#71946)", () => {
describe("modern Codex models (none/low/medium/high/xhigh enum)", () => {
it.each(["gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.2"] as const)(
"translates 'minimal' -> 'low' for %s so the first request is accepted",
(modelId) => {
expect(resolveReasoningEffort("minimal", modelId)).toBe("low");
},
);
it.each(["gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.2"] as const)(
"passes 'low' / 'medium' / 'high' / 'xhigh' through unchanged for %s",
(modelId) => {
expect(resolveReasoningEffort("low", modelId)).toBe("low");
expect(resolveReasoningEffort("medium", modelId)).toBe("medium");
expect(resolveReasoningEffort("high", modelId)).toBe("high");
expect(resolveReasoningEffort("xhigh", modelId)).toBe("xhigh");
},
);
it("normalizes case-variant model ids", () => {
expect(resolveReasoningEffort("minimal", "GPT-5.5")).toBe("low");
expect(resolveReasoningEffort("minimal", " gpt-5.4-mini ")).toBe("low");
});
});
describe("legacy / non-modern Codex models", () => {
it.each(["gpt-5", "gpt-4o", "o3-mini", "codex-mini-latest"] as const)(
"preserves 'minimal' for %s — pre-modern enum still supports it",
(modelId) => {
expect(resolveReasoningEffort("minimal", modelId)).toBe("minimal");
},
);
it("preserves 'minimal' for empty / unknown model ids (conservative default)", () => {
expect(resolveReasoningEffort("minimal", "")).toBe("minimal");
expect(resolveReasoningEffort("minimal", "unknown-model-xyz")).toBe("minimal");
});
});
describe("non-effort thinkLevel values", () => {
it("returns null for 'off'", () => {
expect(resolveReasoningEffort("off", "gpt-5.5")).toBeNull();
expect(resolveReasoningEffort("off", "gpt-4o")).toBeNull();
});
it("returns null for 'adaptive' (non-effort enum value)", () => {
expect(resolveReasoningEffort("adaptive", "gpt-5.5")).toBeNull();
expect(resolveReasoningEffort("adaptive", "gpt-4o")).toBeNull();
});
it("returns null for 'max' (non-effort enum value)", () => {
expect(resolveReasoningEffort("max", "gpt-5.5")).toBeNull();
expect(resolveReasoningEffort("max", "gpt-4o")).toBeNull();
});
});
});