test(codex): share run-attempt app-server harness

This commit is contained in:
Vincent Koc
2026-04-12 09:21:10 +01:00
parent c902d20eb7
commit b5dfeaab4c

View File

@@ -44,6 +44,73 @@ function createParams(sessionFile: string, workspaceDir: string): EmbeddedRunAtt
} as EmbeddedRunAttemptParams; } as EmbeddedRunAttemptParams;
} }
function createAppServerHarness(
requestImpl: (method: string, params: unknown) => Promise<unknown>,
) {
const requests: Array<{ method: string; params: unknown }> = [];
let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
const request = vi.fn(async (method: string, params?: unknown) => {
requests.push({ method, params });
return requestImpl(method, params);
});
__testing.setCodexAppServerClientFactoryForTests(
async () =>
({
request,
addNotificationHandler: (handler: typeof notify) => {
notify = handler;
return () => undefined;
},
addRequestHandler: () => () => undefined,
}) as never,
);
return {
request,
requests,
async waitForMethod(method: string) {
await vi.waitFor(() => expect(requests.some((entry) => entry.method === method)).toBe(true));
},
async completeTurn(params: { threadId: string; turnId: string }) {
await notify({
method: "turn/completed",
params: {
threadId: params.threadId,
turnId: params.turnId,
turn: { id: params.turnId, status: "completed" },
},
});
},
};
}
function expectResumeRequest(
requests: Array<{ method: string; params: unknown }>,
params: Record<string, unknown>,
) {
expect(requests).toEqual(
expect.arrayContaining([
{
method: "thread/resume",
params,
},
]),
);
}
function createResumeHarness() {
return createAppServerHarness(async (method) => {
if (method === "thread/resume") {
return { thread: { id: "thread-existing" }, modelProvider: "openai" };
}
if (method === "turn/start") {
return { turn: { id: "turn-1", status: "inProgress" } };
}
return {};
});
}
describe("runCodexAppServerAttempt", () => { describe("runCodexAppServerAttempt", () => {
beforeEach(async () => { beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-run-")); tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-run-"));
@@ -56,9 +123,7 @@ describe("runCodexAppServerAttempt", () => {
}); });
it("forwards queued user input and aborts the active app-server turn", async () => { it("forwards queued user input and aborts the active app-server turn", async () => {
const requests: Array<{ method: string; params: unknown }> = []; const { requests, waitForMethod } = createAppServerHarness(async (method, _params) => {
const request = vi.fn(async (method: string, params?: unknown) => {
requests.push({ method, params });
if (method === "thread/start") { if (method === "thread/start") {
return { thread: { id: "thread-1" }, model: "gpt-5.4-codex", modelProvider: "openai" }; return { thread: { id: "thread-1" }, model: "gpt-5.4-codex", modelProvider: "openai" };
} }
@@ -67,21 +132,11 @@ describe("runCodexAppServerAttempt", () => {
} }
return {}; return {};
}); });
__testing.setCodexAppServerClientFactoryForTests(
async () =>
({
request,
addNotificationHandler: () => () => undefined,
addRequestHandler: () => () => undefined,
}) as never,
);
const run = runCodexAppServerAttempt( const run = runCodexAppServerAttempt(
createParams(path.join(tempDir, "session.jsonl"), path.join(tempDir, "workspace")), createParams(path.join(tempDir, "session.jsonl"), path.join(tempDir, "workspace")),
); );
await vi.waitFor(() => await waitForMethod("turn/start");
expect(requests.some((entry) => entry.method === "turn/start")).toBe(true),
);
expect(queueAgentHarnessMessage("session-1", "more context")).toBe(true); expect(queueAgentHarnessMessage("session-1", "more context")).toBe(true);
await vi.waitFor(() => await vi.waitFor(() =>
@@ -126,9 +181,7 @@ describe("runCodexAppServerAttempt", () => {
}; };
process.on("unhandledRejection", onUnhandledRejection); process.on("unhandledRejection", onUnhandledRejection);
try { try {
const requests: Array<{ method: string; params: unknown }> = []; const { waitForMethod } = createAppServerHarness(async (method, _params) => {
const request = vi.fn(async (method: string, params?: unknown) => {
requests.push({ method, params });
if (method === "thread/start") { if (method === "thread/start") {
return { thread: { id: "thread-1" }, model: "gpt-5.4-codex", modelProvider: "openai" }; return { thread: { id: "thread-1" }, model: "gpt-5.4-codex", modelProvider: "openai" };
} }
@@ -140,14 +193,6 @@ describe("runCodexAppServerAttempt", () => {
} }
return {}; return {};
}); });
__testing.setCodexAppServerClientFactoryForTests(
async () =>
({
request,
addNotificationHandler: () => () => undefined,
addRequestHandler: () => () => undefined,
}) as never,
);
const abortController = new AbortController(); const abortController = new AbortController();
const params = createParams( const params = createParams(
path.join(tempDir, "session.jsonl"), path.join(tempDir, "session.jsonl"),
@@ -156,9 +201,7 @@ describe("runCodexAppServerAttempt", () => {
params.abortSignal = abortController.signal; params.abortSignal = abortController.signal;
const run = runCodexAppServerAttempt(params); const run = runCodexAppServerAttempt(params);
await vi.waitFor(() => await waitForMethod("turn/start");
expect(requests.some((entry) => entry.method === "turn/start")).toBe(true),
);
abortController.abort("shutdown"); abortController.abort("shutdown");
await expect(run).resolves.toMatchObject({ aborted: true }); await expect(run).resolves.toMatchObject({ aborted: true });
@@ -170,10 +213,7 @@ describe("runCodexAppServerAttempt", () => {
}); });
it("forwards image attachments to the app-server turn input", async () => { it("forwards image attachments to the app-server turn input", async () => {
const requests: Array<{ method: string; params: unknown }> = []; const { requests, waitForMethod, completeTurn } = createAppServerHarness(async (method) => {
let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
const request = vi.fn(async (method: string, params?: unknown) => {
requests.push({ method, params });
if (method === "thread/start") { if (method === "thread/start") {
return { thread: { id: "thread-1" }, model: "gpt-5.4-codex", modelProvider: "openai" }; return { thread: { id: "thread-1" }, model: "gpt-5.4-codex", modelProvider: "openai" };
} }
@@ -182,17 +222,6 @@ describe("runCodexAppServerAttempt", () => {
} }
return {}; return {};
}); });
__testing.setCodexAppServerClientFactoryForTests(
async () =>
({
request,
addNotificationHandler: (handler: typeof notify) => {
notify = handler;
return () => undefined;
},
addRequestHandler: () => () => undefined,
}) as never,
);
const params = createParams( const params = createParams(
path.join(tempDir, "session.jsonl"), path.join(tempDir, "session.jsonl"),
path.join(tempDir, "workspace"), path.join(tempDir, "workspace"),
@@ -210,17 +239,8 @@ describe("runCodexAppServerAttempt", () => {
]; ];
const run = runCodexAppServerAttempt(params); const run = runCodexAppServerAttempt(params);
await vi.waitFor(() => await waitForMethod("turn/start");
expect(requests.some((entry) => entry.method === "turn/start")).toBe(true), await completeTurn({ threadId: "thread-1", turnId: "turn-1" });
);
await notify({
method: "turn/completed",
params: {
threadId: "thread-1",
turnId: "turn-1",
turn: { id: "turn-1", status: "completed" },
},
});
await run; await run;
expect(requests).toEqual( expect(requests).toEqual(
@@ -338,60 +358,22 @@ describe("runCodexAppServerAttempt", () => {
modelProvider: "openai", modelProvider: "openai",
dynamicToolsFingerprint: "[]", dynamicToolsFingerprint: "[]",
}); });
const requests: Array<{ method: string; params: unknown }> = []; const { requests, waitForMethod, completeTurn } = createResumeHarness();
let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
const request = vi.fn(async (method: string, params?: unknown) => {
requests.push({ method, params });
if (method === "thread/resume") {
return { thread: { id: "thread-existing" }, modelProvider: "openai" };
}
if (method === "turn/start") {
return { turn: { id: "turn-1", status: "inProgress" } };
}
return {};
});
__testing.setCodexAppServerClientFactoryForTests(
async () =>
({
request,
addNotificationHandler: (handler: typeof notify) => {
notify = handler;
return () => undefined;
},
addRequestHandler: () => () => undefined,
}) as never,
);
const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir)); const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir));
await vi.waitFor(() => await waitForMethod("turn/start");
expect(requests.some((entry) => entry.method === "turn/start")).toBe(true), await completeTurn({ threadId: "thread-existing", turnId: "turn-1" });
);
await notify({
method: "turn/completed",
params: {
threadId: "thread-existing",
turnId: "turn-1",
turn: { id: "turn-1", status: "completed" },
},
});
await run; await run;
expect(requests).toEqual( expectResumeRequest(requests, {
expect.arrayContaining([ threadId: "thread-existing",
{ model: "gpt-5.4-codex",
method: "thread/resume", modelProvider: "openai",
params: { approvalPolicy: "never",
threadId: "thread-existing", approvalsReviewer: "user",
model: "gpt-5.4-codex", sandbox: "workspace-write",
modelProvider: "openai", persistExtendedHistory: true,
approvalPolicy: "never", });
approvalsReviewer: "user",
sandbox: "workspace-write",
persistExtendedHistory: true,
},
},
]),
);
}); });
it("passes configured app-server policy, sandbox, service tier, and model on resume", async () => { it("passes configured app-server policy, sandbox, service tier, and model on resume", async () => {
@@ -403,29 +385,7 @@ describe("runCodexAppServerAttempt", () => {
model: "gpt-5.2", model: "gpt-5.2",
modelProvider: "openai", modelProvider: "openai",
}); });
const requests: Array<{ method: string; params: unknown }> = []; const { requests, waitForMethod, completeTurn } = createResumeHarness();
let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
const request = vi.fn(async (method: string, params?: unknown) => {
requests.push({ method, params });
if (method === "thread/resume") {
return { thread: { id: "thread-existing" }, modelProvider: "openai" };
}
if (method === "turn/start") {
return { turn: { id: "turn-1", status: "inProgress" } };
}
return {};
});
__testing.setCodexAppServerClientFactoryForTests(
async () =>
({
request,
addNotificationHandler: (handler: typeof notify) => {
notify = handler;
return () => undefined;
},
addRequestHandler: () => () => undefined,
}) as never,
);
const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir), { const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir), {
pluginConfig: { pluginConfig: {
@@ -437,34 +397,22 @@ describe("runCodexAppServerAttempt", () => {
}, },
}, },
}); });
await vi.waitFor(() => await waitForMethod("turn/start");
expect(requests.some((entry) => entry.method === "turn/start")).toBe(true), await completeTurn({ threadId: "thread-existing", turnId: "turn-1" });
);
await notify({
method: "turn/completed",
params: {
threadId: "thread-existing",
turnId: "turn-1",
turn: { id: "turn-1", status: "completed" },
},
});
await run; await run;
expectResumeRequest(requests, {
threadId: "thread-existing",
model: "gpt-5.4-codex",
modelProvider: "openai",
approvalPolicy: "on-request",
approvalsReviewer: "guardian_subagent",
sandbox: "danger-full-access",
serviceTier: "priority",
persistExtendedHistory: true,
});
expect(requests).toEqual( expect(requests).toEqual(
expect.arrayContaining([ expect.arrayContaining([
{
method: "thread/resume",
params: {
threadId: "thread-existing",
model: "gpt-5.4-codex",
modelProvider: "openai",
approvalPolicy: "on-request",
approvalsReviewer: "guardian_subagent",
sandbox: "danger-full-access",
serviceTier: "priority",
persistExtendedHistory: true,
},
},
{ {
method: "turn/start", method: "turn/start",
params: expect.objectContaining({ params: expect.objectContaining({