refactor(server): exercise codex features through fake app-server (#887)

This commit is contained in:
Mohamed Boudra
2026-05-10 09:27:53 +00:00
committed by GitHub
parent 6220b47073
commit 17073fe8ff
2 changed files with 111 additions and 117 deletions
@@ -1,7 +1,11 @@
import { describe, expect, test, vi } from "vitest"; import { describe, expect, test } from "vitest";
import type { AgentSession, AgentSessionConfig } from "../agent-sdk-types.js"; import type { AgentSession, AgentSessionConfig } from "../agent-sdk-types.js";
import { __codexAppServerInternals } from "./codex-app-server-agent.js"; import { __codexAppServerInternals } from "./codex-app-server-agent.js";
import {
createFakeCodexAppServer,
type FakeCodexAppServer,
} from "./codex/test-utils/fake-app-server.js";
import { createTestLogger } from "../../../test-utils/test-logger.js"; import { createTestLogger } from "../../../test-utils/test-logger.js";
const CODEX_PROVIDER = "codex"; const CODEX_PROVIDER = "codex";
@@ -27,39 +31,7 @@ const TEST_COLLABORATION_MODES: CollaborationModeRecord[] = [
}, },
]; ];
interface CodexRequestFn { type CodexFeaturesTestSession = AgentSession;
(method: string, params?: unknown, timeoutMs?: number): Promise<unknown>;
}
interface CodexClientLike {
request: CodexRequestFn;
}
interface CodexSessionTestAccess {
client: CodexClientLike | null;
connected: boolean;
currentThreadId: string | null;
serviceTier: "fast" | null;
planModeEnabled: boolean;
cachedRuntimeInfo: unknown;
ensureThreadLoaded: () => Promise<void>;
ensureThread: () => Promise<void>;
buildUserInput: (...args: unknown[]) => Promise<unknown>;
resolveSlashCommandInvocation: (...args: unknown[]) => Promise<unknown>;
collaborationModes: CollaborationModeRecord[];
refreshResolvedCollaborationMode(): void;
}
type CodexFeaturesTestSession = AgentSession & {
connected: boolean;
currentThreadId: string | null;
collaborationModes: CollaborationModeRecord[];
refreshResolvedCollaborationMode(): void;
};
function asInternals(session: CodexFeaturesTestSession): CodexSessionTestAccess {
return session as unknown as CodexSessionTestAccess;
}
function createConfig(overrides: Partial<AgentSessionConfig> = {}): AgentSessionConfig { function createConfig(overrides: Partial<AgentSessionConfig> = {}): AgentSessionConfig {
return { return {
@@ -71,28 +43,36 @@ function createConfig(overrides: Partial<AgentSessionConfig> = {}): AgentSession
}; };
} }
function createSession( function createSessionHarness(configOverrides: Partial<AgentSessionConfig> = {}): {
configOverrides: Partial<AgentSessionConfig> = {}, session: CodexFeaturesTestSession;
): CodexFeaturesTestSession { appServer: FakeCodexAppServer;
} {
const config = createConfig(configOverrides); const config = createConfig(configOverrides);
const appServer = createFakeCodexAppServer({
"collaborationMode/list": () => ({ data: TEST_COLLABORATION_MODES }),
});
const session = new __codexAppServerInternals.CodexAppServerAgentSession( const session = new __codexAppServerInternals.CodexAppServerAgentSession(
{ ...config, provider: CODEX_PROVIDER }, { ...config, provider: CODEX_PROVIDER },
null, null,
createTestLogger(), createTestLogger(),
() => { async () => appServer.child,
throw new Error("Test session cannot spawn Codex app-server"); ) as CodexFeaturesTestSession;
}, return { session, appServer };
) as unknown as CodexFeaturesTestSession; }
session.connected = true;
session.currentThreadId = "test-thread"; async function createConnectedSession(configOverrides: Partial<AgentSessionConfig> = {}): Promise<{
session.collaborationModes = TEST_COLLABORATION_MODES; session: CodexFeaturesTestSession;
session.refreshResolvedCollaborationMode(); appServer: FakeCodexAppServer;
return session; }> {
const harness = createSessionHarness(configOverrides);
await harness.session.connect();
harness.appServer.assertNoErrors();
return harness;
} }
describe("Codex app-server provider features", () => { describe("Codex app-server provider features", () => {
test("features returns fast and plan toggles when supported", async () => { test("features returns fast and plan toggles when supported", async () => {
const session = createSession(); const { session } = await createConnectedSession();
expect(session.features).toEqual([ expect(session.features).toEqual([
{ {
@@ -140,8 +120,8 @@ describe("Codex app-server provider features", () => {
]); ]);
}); });
test("features returns only plan toggle when model does not support fast mode", () => { test("features returns only plan toggle when model does not support fast mode", async () => {
const session = createSession({ model: "gpt-3.5-turbo" }); const { session } = await createConnectedSession({ model: "gpt-3.5-turbo" });
expect(session.features).toEqual([ expect(session.features).toEqual([
{ {
@@ -157,49 +137,56 @@ describe("Codex app-server provider features", () => {
}); });
test("setFeature('fast_mode', true) sets serviceTier to fast", async () => { test("setFeature('fast_mode', true) sets serviceTier to fast", async () => {
const session = createSession(); const { session, appServer } = await createConnectedSession();
await session.setFeature?.("fast_mode", true); await session.setFeature?.("fast_mode", true);
await session.startTurn("hello");
expect(asInternals(session).serviceTier).toBe("fast"); await expect(appServer.waitForTurnStart()).resolves.toMatchObject({
serviceTier: "fast",
});
}); });
test("setFeature('fast_mode', false) clears serviceTier to null", async () => { test("setFeature('fast_mode', false) clears serviceTier to null", async () => {
const session = createSession({ const { session, appServer } = await createConnectedSession({
featureValues: { fast_mode: true }, featureValues: { fast_mode: true },
}); });
await session.setFeature?.("fast_mode", false); await session.setFeature?.("fast_mode", false);
await session.startTurn("hello");
expect(asInternals(session).serviceTier).toBeNull(); await expect(appServer.waitForTurnStart()).resolves.not.toMatchObject({
serviceTier: expect.anything(),
});
}); });
test("setFeature invalidates cachedRuntimeInfo", async () => { test("setFeature invalidates runtime info", async () => {
const session = createSession(); const { session } = await createConnectedSession();
await session.getRuntimeInfo(); await expect(session.getRuntimeInfo()).resolves.not.toMatchObject({
expect(asInternals(session).cachedRuntimeInfo).not.toBeNull(); extra: { collaborationMode: "Plan" },
});
await session.setFeature?.("fast_mode", true); await session.setFeature?.("plan_mode", true);
expect(asInternals(session).cachedRuntimeInfo).toBeNull(); await expect(session.getRuntimeInfo()).resolves.toMatchObject({
extra: { collaborationMode: "Plan" },
});
}); });
test("setFeature throws for unknown feature ids", async () => { test("setFeature throws for unknown feature ids", async () => {
const session = createSession(); const { session } = createSessionHarness();
await expect(session.setFeature?.("unknown_feature", true)).rejects.toThrow( await expect(session.setFeature?.("unknown_feature", true)).rejects.toThrow(
"Unknown Codex feature: unknown_feature", "Unknown Codex feature: unknown_feature",
); );
}); });
test("constructor restores feature flags from config.featureValues", () => { test("constructor restores feature flags from config.featureValues", async () => {
const session = createSession({ const { session, appServer } = await createConnectedSession({
featureValues: { fast_mode: true, plan_mode: true }, featureValues: { fast_mode: true, plan_mode: true },
}); });
expect(asInternals(session).serviceTier).toBe("fast");
expect(asInternals(session).planModeEnabled).toBe(true);
expect(session.features).toEqual([ expect(session.features).toEqual([
{ {
type: "toggle", type: "toggle",
@@ -220,41 +207,29 @@ describe("Codex app-server provider features", () => {
value: true, value: true,
}, },
]); ]);
await session.startTurn("hello");
await expect(appServer.waitForTurnStart()).resolves.toMatchObject({
serviceTier: "fast",
collaborationMode: expect.objectContaining({
mode: "plan",
}),
});
}); });
test("startTurn includes serviceTier when fast mode is enabled", async () => { test("startTurn includes serviceTier when fast mode is enabled", async () => {
const session = createSession(); const { session, appServer } = await createConnectedSession();
const request = vi.fn().mockResolvedValue(undefined);
asInternals(session).client = { request };
asInternals(session).connected = true;
asInternals(session).currentThreadId = "thread-123";
asInternals(session).ensureThreadLoaded = vi.fn().mockResolvedValue(undefined);
asInternals(session).ensureThread = vi.fn().mockResolvedValue(undefined);
asInternals(session).buildUserInput = vi.fn().mockResolvedValue([{ type: "text", text: "hi" }]);
asInternals(session).resolveSlashCommandInvocation = vi.fn().mockResolvedValue(null);
await session.setFeature?.("fast_mode", true); await session.setFeature?.("fast_mode", true);
await session.startTurn("hello"); await session.startTurn("hello");
expect(request).toHaveBeenCalledWith( await expect(appServer.waitForTurnStart()).resolves.toMatchObject({
"turn/start", serviceTier: "fast",
expect.objectContaining({ });
serviceTier: "fast",
}),
expect.any(Number),
);
}); });
test("setModel clears fast mode when switching to an unsupported model", async () => { test("setModel clears fast mode when switching to an unsupported model", async () => {
const session = createSession(); const { session, appServer } = await createConnectedSession();
const request = vi.fn().mockResolvedValue(undefined);
asInternals(session).client = { request };
asInternals(session).connected = true;
asInternals(session).currentThreadId = "thread-123";
asInternals(session).ensureThreadLoaded = vi.fn().mockResolvedValue(undefined);
asInternals(session).ensureThread = vi.fn().mockResolvedValue(undefined);
asInternals(session).buildUserInput = vi.fn().mockResolvedValue([{ type: "text", text: "hi" }]);
asInternals(session).resolveSlashCommandInvocation = vi.fn().mockResolvedValue(null);
await session.setFeature?.("fast_mode", true); await session.setFeature?.("fast_mode", true);
await session.setModel("gpt-3.5-turbo"); await session.setModel("gpt-3.5-turbo");
@@ -270,41 +245,23 @@ describe("Codex app-server provider features", () => {
value: false, value: false,
}, },
]); ]);
expect(asInternals(session).serviceTier).toBeNull();
await session.startTurn("hello"); await session.startTurn("hello");
expect(request).toHaveBeenCalledWith( await expect(appServer.waitForTurnStart()).resolves.not.toMatchObject({
"turn/start", serviceTier: expect.anything(),
expect.not.objectContaining({ });
serviceTier: expect.anything(),
}),
expect.any(Number),
);
}); });
test("startTurn switches collaboration mode when plan mode is enabled", async () => { test("startTurn switches collaboration mode when plan mode is enabled", async () => {
const session = createSession(); const { session, appServer } = await createConnectedSession();
const request = vi.fn().mockResolvedValue(undefined);
asInternals(session).client = { request };
asInternals(session).connected = true;
asInternals(session).currentThreadId = "thread-123";
asInternals(session).ensureThreadLoaded = vi.fn().mockResolvedValue(undefined);
asInternals(session).ensureThread = vi.fn().mockResolvedValue(undefined);
asInternals(session).buildUserInput = vi.fn().mockResolvedValue([{ type: "text", text: "hi" }]);
asInternals(session).resolveSlashCommandInvocation = vi.fn().mockResolvedValue(null);
await session.setFeature?.("plan_mode", true); await session.setFeature?.("plan_mode", true);
await session.startTurn("hello"); await session.startTurn("hello");
expect(request).toHaveBeenCalledWith( await expect(appServer.waitForTurnStart()).resolves.toMatchObject({
"turn/start", collaborationMode: expect.objectContaining({
expect.objectContaining({ mode: "plan",
collaborationMode: expect.objectContaining({
mode: "plan",
}),
}), }),
expect.any(Number), });
);
}); });
}); });
@@ -5,6 +5,7 @@ import { PassThrough } from "node:stream";
import type { AgentSession, AgentStreamEvent } from "../../../agent-sdk-types.js"; import type { AgentSession, AgentStreamEvent } from "../../../agent-sdk-types.js";
type JsonObject = Record<string, unknown>; type JsonObject = Record<string, unknown>;
type FakeCodexAppServerHandler = (params: unknown) => unknown;
type CodexAppServerChildProcess = ChildProcessWithoutNullStreams & { type CodexAppServerChildProcess = ChildProcessWithoutNullStreams & {
stdin: PassThrough; stdin: PassThrough;
stdout: PassThrough; stdout: PassThrough;
@@ -14,6 +15,7 @@ type CodexAppServerChildProcess = ChildProcessWithoutNullStreams & {
export interface FakeCodexAppServer { export interface FakeCodexAppServer {
readonly child: CodexAppServerChildProcess; readonly child: CodexAppServerChildProcess;
assertNoErrors(): void; assertNoErrors(): void;
waitForTurnStart(): Promise<JsonObject>;
requestCommandApproval(params: { requestCommandApproval(params: {
itemId: string; itemId: string;
threadId: string; threadId: string;
@@ -41,9 +43,30 @@ export function createCodexAppServerChildProcess(): CodexAppServerChildProcess {
} }
export function createFakeCodexAppServer( export function createFakeCodexAppServer(
handlers: Record<string, (params: unknown) => unknown>, handlers: Record<string, FakeCodexAppServerHandler> = {},
): FakeCodexAppServer { ): FakeCodexAppServer {
const child = createCodexAppServerChildProcess(); const child = createCodexAppServerChildProcess();
const responseHandlers: Record<string, FakeCodexAppServerHandler> = {
initialize: () => ({}),
"collaborationMode/list": () => ({ data: [] }),
"config/read": () => ({ config: {} }),
getUserSavedConfig: () => ({ config: {} }),
"model/list": () => ({
data: [
{
id: "gpt-5.4",
isDefault: true,
defaultReasoningEffort: "medium",
},
],
}),
"skills/list": () => ({ data: [] }),
"thread/start": () => ({ thread: { id: "thread-1" } }),
"thread/loaded/list": () => ({ data: [] }),
"thread/resume": () => ({}),
"turn/start": () => ({}),
...handlers,
};
const messages: JsonObject[] = []; const messages: JsonObject[] = [];
const errors: Error[] = []; const errors: Error[] = [];
const approvalRequestIds = new Map<string, number>(); const approvalRequestIds = new Map<string, number>();
@@ -67,7 +90,7 @@ export function createFakeCodexAppServer(
return; return;
} }
const handler = handlers[message.method]; const handler = responseHandlers[message.method];
if (!handler) { if (!handler) {
errors.push(new Error(`Unexpected Codex app-server request: ${message.method}`)); errors.push(new Error(`Unexpected Codex app-server request: ${message.method}`));
return; return;
@@ -143,6 +166,13 @@ export function createFakeCodexAppServer(
throw errors[0]; throw errors[0];
} }
}, },
async waitForTurnStart() {
const message = await waitForMessage(
(candidate) => candidate.method === "turn/start",
"turn start request",
);
return toJsonObject(message.params);
},
requestCommandApproval(params) { requestCommandApproval(params) {
const requestId = nextServerRequestId; const requestId = nextServerRequestId;
nextServerRequestId += 1; nextServerRequestId += 1;
@@ -171,6 +201,13 @@ export function createFakeCodexAppServer(
}; };
} }
function toJsonObject(value: unknown): JsonObject {
if (value && typeof value === "object" && !Array.isArray(value)) {
return value as JsonObject;
}
return {};
}
export function waitForNextPermission( export function waitForNextPermission(
session: AgentSession, session: AgentSession,
): Promise<Extract<AgentStreamEvent, { type: "permission_requested" }>> { ): Promise<Extract<AgentStreamEvent, { type: "permission_requested" }>> {