Files
openclaw/extensions/codex/src/app-server/protocol-validators.test.ts
stevenlee-oai a00438a867 fix(codex): restore connected apps for token-authenticated runs (#115075)
* fix(codex): restore connected apps for token-authenticated runs

* fix(codex): keep app inventory protocol types private

* fix(codex): align native runtime with Codex 0.146.0

Co-authored-by: Steven Lee <stevenlee@openai.com>

* fix(codex): clean up latest app-server integration

Co-authored-by: Steven Lee <stevenlee@openai.com>

* fix(codex): keep internal protocol types private

* fix(ci): repair current main Codex landing gates

* fix(ci): format inherited code mode matrix

* fix(codex): reconcile native app-server contracts with main

Prepare a verified GitHub-hosted mainline merge while preserving the reviewed Codex 0.146.0 fixes and canonical OpenAI authentication.

Co-authored-by: Steven Lee <stevenlee@openai.com>

* fix(codex): keep QA evidence in its owning plugin

Resolve the current-main Code Mode test rename without resurrecting the retired core test path.

Co-authored-by: Steven Lee <stevenlee@openai.com>

* fix(codex): enforce canonical OpenAI app-server auth

Reject retired provider aliases without runtime compatibility, direct operators to the doctor migration, and remove the redundant OpenAI API-key predicate.

Co-authored-by: Steven Lee <stevenlee@openai.com>

* chore(codex): reconcile latest main dependency graph

Preserve current main dependency changes while preparing the original Codex PR for an ancestry-preserving signed mainline merge.

Co-authored-by: Steven Lee <stevenlee@openai.com>

* fix(codex): unify bundled Codex 0.146 runtimes

Keep the ACP adapter on the same 0.146.0 Codex release as the managed runtime, remove obsolete 0.145.0 platform artifacts and unused semver compatibility, and preserve the latest main dependency upgrades.

Co-authored-by: Steven Lee <stevenlee@openai.com>

---------

Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-29 06:21:25 -04:00

170 lines
5.0 KiB
TypeScript

// Codex tests cover protocol validators plugin behavior.
import { describe, expect, it } from "vitest";
import {
assertCodexThreadForkParams,
readCodexModelListResponse,
readCodexTurn,
assertCodexThreadStartResponse,
assertCodexThreadResumeResponse,
} from "./protocol-validators.js";
import { CODEX_APP_SERVER_VERSION } from "./version.js";
function makeMinimalThread(overrides: Record<string, unknown> = {}) {
return {
id: "thread-1",
sessionId: "session-1",
cliVersion: CODEX_APP_SERVER_VERSION,
createdAt: 1715299200,
updatedAt: 1715299200,
cwd: "/tmp",
ephemeral: false,
modelProvider: "openai",
preview: "test thread",
source: "appServer",
status: { type: "notLoaded" },
turns: [],
...overrides,
};
}
function makeMinimalResponse(threadOverrides: Record<string, unknown> = {}) {
return {
approvalPolicy: "never",
approvalsReviewer: "user",
cwd: "/tmp",
model: "gpt-5.4",
modelProvider: "openai",
sandbox: { type: "dangerFullAccess" },
thread: makeMinimalThread(threadOverrides),
};
}
describe("Codex thread response validators", () => {
// The pinned Codex protocol requires both thread identities; never silently
// invent a session identity when a malformed response omits one.
it("rejects thread responses missing sessionId", () => {
for (const assertResponse of [
assertCodexThreadStartResponse,
assertCodexThreadResumeResponse,
]) {
const response = makeMinimalResponse({ sessionId: undefined });
delete (response.thread as Record<string, unknown>).sessionId;
expect(() => assertResponse(response)).toThrow("Invalid Codex app-server");
}
});
});
describe("assertCodexThreadForkParams", () => {
it("accepts the experimental beforeTurnId boundary", () => {
expect(
assertCodexThreadForkParams({
threadId: "thread-1",
beforeTurnId: "turn-2",
excludeTurns: true,
}),
).toMatchObject({ beforeTurnId: "turn-2" });
});
it("rejects a non-string beforeTurnId", () => {
expect(() => assertCodexThreadForkParams({ threadId: "thread-1", beforeTurnId: 2 })).toThrow(
"Invalid Codex app-server thread/fork params",
);
});
});
describe("assertCodexThreadStartResponse", () => {
it("accepts response with both id and sessionId", () => {
const response = makeMinimalResponse();
const result = assertCodexThreadStartResponse(response);
expect(result.thread.id).toBe("thread-1");
expect(result.thread.sessionId).toBe("session-1");
expect(result.thread.historyMode).toBe("legacy");
});
it("throws on invalid response", () => {
expect(() => assertCodexThreadStartResponse({})).toThrow("Invalid Codex app-server");
});
});
describe("assertCodexThreadResumeResponse", () => {
it("accepts the bounded initial turns page shipped by the managed Codex version", () => {
const result = assertCodexThreadResumeResponse({
...makeMinimalResponse(),
initialTurnsPage: {
data: [{ id: "turn-running", items: [], status: "inProgress" }],
nextCursor: null,
backwardsCursor: "resume-anchor",
},
});
expect(result.thread.turns).toEqual([]);
expect(result.initialTurnsPage?.data).toEqual([
{ id: "turn-running", items: [], status: "inProgress" },
]);
});
});
describe("readCodexModelListResponse", () => {
it("applies defaults from generated schemas behind local refs", () => {
const response = readCodexModelListResponse({
data: [
{
id: "gpt-test",
model: "gpt-test",
displayName: "GPT Test",
description: "test model",
hidden: false,
isDefault: false,
defaultReasoningEffort: "medium",
supportedReasoningEfforts: [],
},
],
});
const model = response?.data[0] as
| (NonNullable<ReturnType<typeof readCodexModelListResponse>>["data"][number] & {
serviceTiers?: unknown;
supportsPersonality?: unknown;
})
| undefined;
expect(model?.inputModalities).toEqual(["text", "image"]);
expect(model?.serviceTiers).toEqual([]);
expect(model?.supportsPersonality).toBe(false);
});
});
describe("readCodexTurn", () => {
it("does not merge defaults from unrelated thread item union branches", () => {
const turn = readCodexTurn({
id: "turn-1",
status: "completed",
items: [{ id: "item-1", type: "plan", text: "ship it" }],
});
expect(turn?.items[0]).toEqual({ id: "item-1", type: "plan", text: "ship it" });
});
it("accepts nullable arrays in generated dynamic tool call items", () => {
const turn = readCodexTurn({
id: "turn-1",
status: "completed",
items: [
{
arguments: {},
contentItems: null,
id: "item-1",
status: "completed",
tool: "render",
type: "dynamicToolCall",
},
],
});
expect(turn?.items[0]).toMatchObject({
contentItems: null,
id: "item-1",
type: "dynamicToolCall",
});
});
});