Files
elizaos--eliza/plugins/plugin-codex-cli/__tests__/codex-backend.test.ts
T
wehub-resource-sync 426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Waiting to run
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Waiting to run
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

299 lines
10 KiB
TypeScript

/**
* Unit coverage for the Codex provider: plugin model metadata, JWT expiry, the
* SSE parser, tool-schema translation, and CodexBackend request/stream handling
* driven by a fake fetch (no live model or network).
*/
import { afterEach, describe, expect, it } from "vitest";
import { __INTERNAL_buildCodexGenerateParams, codexCliPlugin } from "../index";
import {
__resetCodexAuthDeps,
__setCodexAuthDeps,
type CodexAuth,
isExpired,
} from "../src/codex-auth";
import { CodexBackend, translateMessagesToCodexInput } from "../src/codex-backend";
import { parseSSE } from "../src/sse-parser";
import { toOpenAITool } from "../src/tool-format-openai";
function jwtWithExp(exp: number): string {
const payload = Buffer.from(JSON.stringify({ exp })).toString("base64url");
return `x.${payload}.y`;
}
function sseResponse(events: string[]): Response {
return new Response(events.join(""), {
status: 200,
headers: { "content-type": "text/event-stream" },
});
}
const auth: CodexAuth = {
OPENAI_API_KEY: null,
auth_mode: "chatgpt",
last_refresh: "2026-01-01T00:00:00.000Z",
tokens: {
id_token: "id",
access_token: "access",
refresh_token: "refresh",
account_id: "acct_123",
},
};
describe("codex plugin metadata", () => {
it("declares CODEX_MODEL as display metadata for every model handler", () => {
const modelTypes = Object.keys(codexCliPlugin.models ?? {});
expect(modelTypes.length).toBeGreaterThan(0);
expect(Object.keys(codexCliPlugin.modelMetadata ?? {}).sort()).toEqual(modelTypes.sort());
for (const modelType of modelTypes) {
expect(codexCliPlugin.modelMetadata?.[modelType]).toEqual({
displayModelSetting: "CODEX_MODEL",
});
}
});
});
describe("codex auth helpers", () => {
afterEach(() => __resetCodexAuthDeps());
it("detects expired JWT access tokens with a buffer", () => {
__setCodexAuthDeps({ now: () => 1_000_000 });
expect(isExpired({ ...auth, tokens: { ...auth.tokens, access_token: jwtWithExp(900) } })).toBe(
true
);
expect(
isExpired({ ...auth, tokens: { ...auth.tokens, access_token: jwtWithExp(2_000) } })
).toBe(false);
});
});
describe("SSE parser", () => {
it("parses named events with multiline data", async () => {
const stream = new Response(
'event: response.output_text.delta\ndata: {"delta":"hel"}\ndata: {"delta":"lo"}\n\n'
).body;
expect(stream).toBeTruthy();
const events = [];
for await (const event of parseSSE(stream as ReadableStream<Uint8Array>)) events.push(event);
expect(events).toEqual([
{
event: "response.output_text.delta",
data: '{"delta":"hel"}\n{"delta":"lo"}',
},
]);
});
});
describe("tool translation", () => {
it("converts eliza tool definitions to OpenAI Responses function tools", () => {
expect(
toOpenAITool({
name: "search",
description: "Search the web",
parameters: { type: "object", properties: { q: { type: "string" } } },
})
).toEqual({
type: "function",
name: "search",
description: "Search the web",
parameters: { type: "object", properties: { q: { type: "string" } } },
strict: false,
});
});
it("normalizes strict tool schemas (codex backend 400s on partial required / missing additionalProperties)", () => {
const out = toOpenAITool({
name: "TASKS",
description: "spawn/list coding sub-agents",
strict: true,
parameters: {
type: "object",
properties: {
action: { type: "string", enum: ["create", "list"] },
prompt: { type: "string" },
},
required: ["action"],
},
});
expect(out.strict).toBe(true);
// required must list EVERY property, additionalProperties:false, and the
// originally-optional `prompt` is made nullable to keep its optional meaning.
expect(out.parameters).toEqual({
type: "object",
properties: {
action: { type: "string", enum: ["create", "list"] },
prompt: { type: ["string", "null"] },
},
required: ["action", "prompt"],
additionalProperties: false,
});
});
});
describe("CodexBackend", () => {
it("honors a per-call model override before Codex slot defaults", () => {
const runtime = {
getSetting(key: string) {
if (key === "CODEX_MODEL") return "gpt-5.5";
return undefined;
},
};
expect(
__INTERNAL_buildCodexGenerateParams(runtime as never, {
prompt: "hello",
model: " gpt-5-codex ",
}).model
).toBe("gpt-5-codex");
});
it("translates provider-neutral messages and tool calls", () => {
expect(
translateMessagesToCodexInput(
[
{ role: "system", content: "ignore me here" },
{ role: "user", content: "hello" },
{
role: "assistant",
content: "",
toolCalls: [{ id: "call_1", name: "lookup", arguments: { q: "x" } }],
},
{ role: "tool", toolCallId: "call_1", content: "result" },
],
"fallback"
)
).toEqual([
{ type: "message", role: "user", content: [{ type: "input_text", text: "hello" }] },
{ type: "function_call", call_id: "call_1", name: "lookup", arguments: '{"q":"x"}' },
{ type: "function_call_output", call_id: "call_1", output: "result" },
{ type: "message", role: "user", content: [{ type: "input_text", text: "fallback" }] },
]);
});
it("preserves tool-call and tool-result CONTENT PARTS (the planner's transcript format)", () => {
// The planner renders prior steps as `tool-call` / `tool-result` content
// parts, NOT the `toolCalls` field + plain-text tool message. Dropping
// these made codex blind to its own fetches ("I don't have the fetched
// contents visible here") and re-issue the same call in a loop.
expect(
translateMessagesToCodexInput(
[
{ role: "user", content: "newest nodejs version?" },
{
role: "assistant",
content: [
{ type: "text", text: "Fetching the dist index." },
{
type: "tool-call",
toolCallId: "call_1",
toolName: "WEB_FETCH",
input: { url: "https://nodejs.org/dist/index.json" },
},
],
},
{
role: "tool",
content: [
{
type: "tool-result",
toolCallId: "call_1",
toolName: "WEB_FETCH",
output: { type: "text", value: 'text: [{"version":"v26.3.1"}]' },
},
],
},
],
"newest nodejs version?"
)
).toEqual([
{
type: "message",
role: "user",
content: [{ type: "input_text", text: "newest nodejs version?" }],
},
{
type: "message",
role: "assistant",
content: [{ type: "output_text", text: "Fetching the dist index." }],
},
{
type: "function_call",
call_id: "call_1",
name: "WEB_FETCH",
arguments: '{"url":"https://nodejs.org/dist/index.json"}',
},
{ type: "function_call_output", call_id: "call_1", output: 'text: [{"version":"v26.3.1"}]' },
{
type: "message",
role: "user",
content: [{ type: "input_text", text: "newest nodejs version?" }],
},
]);
});
it("posts codex headers, parses text and function calls", async () => {
const bodies: unknown[] = [];
const backend = new CodexBackend({
authPath: "/tmp/auth.json",
jitterMaxMs: 0,
loadAuth: async () => auth,
fetchImpl: (async (_url: string | URL | Request, init?: RequestInit) => {
bodies.push(JSON.parse(String(init?.body)));
expect((init?.headers as Record<string, string>).Authorization).toBe("Bearer access");
expect((init?.headers as Record<string, string>)["chatgpt-account-id"]).toBe("acct_123");
expect((init?.headers as Record<string, string>).originator).toBe("codex_cli_rs");
return sseResponse([
'event: response.output_text.delta\ndata: {"delta":"hi"}\n\n',
'event: response.output_item.added\ndata: {"item":{"id":"item_1","type":"function_call","call_id":"call_1","name":"lookup"}}\n\n',
'event: response.function_call_arguments.delta\ndata: {"item_id":"item_1","delta":"{\\"q\\":"}\n\n',
'event: response.output_item.done\ndata: {"item":{"id":"item_1","type":"function_call","call_id":"call_1","name":"lookup","arguments":"{\\"q\\":\\"x\\"}"}}\n\n',
'event: response.completed\ndata: {"response":{"stop_reason":"tool_calls","usage":{"input_tokens":3,"output_tokens":4}}}\n\n',
]);
}) as typeof fetch,
});
const result = await backend.generate({
prompt: "hello",
tools: [{ name: "lookup", parameters: { type: "object" } }],
toolChoice: { name: "lookup" },
});
expect(result).toEqual({
text: "hi",
toolCalls: [{ id: "call_1", name: "lookup", arguments: { q: "x" }, type: "function" }],
finishReason: "tool_calls",
usage: { inputTokens: 3, outputTokens: 4, totalTokens: 7 },
});
expect(bodies[0]).toMatchObject({
model: "gpt-5.5",
input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hello" }] }],
tools: [{ type: "function", name: "lookup" }],
tool_choice: { type: "function", name: "lookup" },
});
});
it("never forwards temperature or max_output_tokens (the codex backend 400s on them)", async () => {
const bodies: unknown[] = [];
const backend = new CodexBackend({
authPath: "/tmp/auth.json",
jitterMaxMs: 0,
loadAuth: async () => auth,
fetchImpl: (async (_url: string | URL | Request, init?: RequestInit) => {
bodies.push(JSON.parse(String(init?.body)));
return sseResponse([
'event: response.output_text.delta\ndata: {"delta":"ok"}\n\n',
'event: response.completed\ndata: {"response":{"stop_reason":"stop","usage":{"input_tokens":1,"output_tokens":1}}}\n\n',
]);
}) as typeof fetch,
});
// The runtime's planner/response-handler calls always pass maxTokens (and
// often temperature); the ChatGPT codex backend rejects both with a 400
// "Unsupported parameter", which previously emptied every codex turn.
await backend.generate({ prompt: "hi", temperature: 0.7, maxTokens: 2048 });
const body = bodies[0] as Record<string, unknown>;
expect(body).not.toHaveProperty("temperature");
expect(body).not.toHaveProperty("max_output_tokens");
});
});