Files
openclaw/extensions/codex/src/app-server/protocol-validators.test.ts
Peter Steinberger 3548cff14b refactor: migrate validators to TypeBox (#86639)
* refactor: migrate validators to typebox

* fix: preserve json schema resource refs

* chore: clean schema preflight recursion

* refactor: remove lobster ajv shim

* fix: support schema array refs

* fix: validate schema dependencies

* fix: preserve schema contract checks

* fix: support same-document schema refs

* fix: preserve untyped map defaults

* fix: preserve schema default semantics

* test: avoid thenable schema literals

* test: build conditional schema key

* fix: defer resource id refs to typebox

* fix: reject invalid schema enum metadata

* fix: preserve default branch semantics

* fix: resolve schema resource refs

* fix: narrow conditional default fallback

* fix: preserve uri format validation

* fix: preserve validator compatibility

* test: avoid ajv cache lint violation

* fix: preserve typebox validation diagnostics

* fix: validate defaulted conditional schemas

* fix: normalize mcp draft schemas

* fix: preserve tuple schema defaults

* fix: resolve relative schema refs

* fix: scope typebox format semantics

* fix: align conditional format defaults

* fix: decode schema pointer refs

* fix: filter grouped secretref diagnostics

* fix: preserve default conditional compatibility

* fix: preserve nullable schema compatibility

* fix: settle defaults before conditionals

* fix: preserve default validation invariants

* fix: validate dynamic schema refs

* fix: reject malformed nullable schemas
2026-05-26 08:45:28 +01:00

142 lines
4.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
readCodexModelListResponse,
readCodexTurn,
assertCodexThreadStartResponse,
assertCodexThreadResumeResponse,
} from "./protocol-validators.js";
function makeMinimalThread(overrides: Record<string, unknown> = {}) {
return {
id: "thread-1",
sessionId: "session-1",
cliVersion: "0.129.0",
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("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");
});
it("normalizes missing sessionId from id", () => {
const response = makeMinimalResponse({ sessionId: undefined });
// Remove the sessionId key entirely
delete (response.thread as Record<string, unknown>).sessionId;
const result = assertCodexThreadStartResponse(response);
expect(result.thread.id).toBe("thread-1");
expect(result.thread.sessionId).toBe("thread-1");
});
it("normalizes missing id from sessionId", () => {
const response = makeMinimalResponse({ id: undefined, sessionId: "session-1" });
delete (response.thread as Record<string, unknown>).id;
const result = assertCodexThreadStartResponse(response);
expect(result.thread.id).toBe("session-1");
expect(result.thread.sessionId).toBe("session-1");
});
it("throws on invalid response", () => {
expect(() => assertCodexThreadStartResponse({})).toThrow("Invalid Codex app-server");
});
});
describe("assertCodexThreadResumeResponse", () => {
it("normalizes missing sessionId from id", () => {
const response = makeMinimalResponse({ sessionId: undefined });
delete (response.thread as Record<string, unknown>).sessionId;
const result = assertCodexThreadResumeResponse(response);
expect(result.thread.id).toBe("thread-1");
expect(result.thread.sessionId).toBe("thread-1");
});
});
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",
});
});
});