test: share codex app-server test helpers

This commit is contained in:
Peter Steinberger
2026-04-20 16:38:43 +01:00
parent c597db3fb8
commit 78f9f3093e
2 changed files with 80 additions and 107 deletions

View File

@@ -2,6 +2,7 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { AnyAgentTool } from "openclaw/plugin-sdk/agent-harness";
import { describe, expect, it, vi } from "vitest";
import { createCodexDynamicToolBridge } from "./dynamic-tools.js";
import type { JsonValue } from "./protocol.js";
function createTool(overrides: Partial<AnyAgentTool>): AnyAgentTool {
return {
@@ -13,6 +14,50 @@ function createTool(overrides: Partial<AnyAgentTool>): AnyAgentTool {
} as unknown as AnyAgentTool;
}
function mediaResult(mediaUrl: string, audioAsVoice?: boolean): AgentToolResult<unknown> {
return {
content: [{ type: "text", text: "Generated media reply." }],
details: {
media: {
mediaUrl,
...(audioAsVoice === true ? { audioAsVoice: true } : {}),
},
},
};
}
function createBridgeWithToolResult(toolName: string, toolResult: AgentToolResult<unknown>) {
return createCodexDynamicToolBridge({
tools: [
createTool({
name: toolName,
execute: vi.fn(async () => toolResult),
}),
],
signal: new AbortController().signal,
});
}
function expectInputText(text: string) {
return {
success: true,
contentItems: [{ type: "inputText", text }],
};
}
async function handleMessageToolCall(
bridge: ReturnType<typeof createCodexDynamicToolBridge>,
arguments_: JsonValue,
) {
return await bridge.handleToolCall({
threadId: "thread-1",
turnId: "turn-1",
callId: "call-1",
tool: "message",
arguments: arguments_,
});
}
describe("createCodexDynamicToolBridge", () => {
it.each([
{ toolName: "tts", mediaUrl: "/tmp/reply.opus", audioAsVoice: true },
@@ -22,23 +67,7 @@ describe("createCodexDynamicToolBridge", () => {
])(
"preserves structured media artifacts from $toolName tool results",
async ({ toolName, mediaUrl, audioAsVoice }) => {
const toolResult = {
content: [{ type: "text", text: "Generated media reply." }],
details: {
media: {
mediaUrl,
...(audioAsVoice === true ? { audioAsVoice: true } : {}),
},
},
} satisfies AgentToolResult<unknown>;
const tool = createTool({
name: toolName,
execute: vi.fn(async () => toolResult),
});
const bridge = createCodexDynamicToolBridge({
tools: [tool],
signal: new AbortController().signal,
});
const bridge = createBridgeWithToolResult(toolName, mediaResult(mediaUrl, audioAsVoice));
const result = await bridge.handleToolCall({
threadId: "thread-1",
@@ -48,49 +77,12 @@ describe("createCodexDynamicToolBridge", () => {
arguments: { prompt: "hello" },
});
expect(result).toEqual({
success: true,
contentItems: [{ type: "inputText", text: "Generated media reply." }],
});
expect(result).toEqual(expectInputText("Generated media reply."));
expect(bridge.telemetry.toolMediaUrls).toEqual([mediaUrl]);
expect(bridge.telemetry.toolAudioAsVoice).toBe(audioAsVoice === true);
},
);
it("preserves audio-as-voice metadata from tts results", async () => {
const toolResult = {
content: [{ type: "text", text: "Generated audio reply." }],
details: {
media: {
mediaUrl: "/tmp/reply.opus",
audioAsVoice: true,
},
},
} satisfies AgentToolResult<unknown>;
const tool = createTool({
execute: vi.fn(async () => toolResult),
});
const bridge = createCodexDynamicToolBridge({
tools: [tool],
signal: new AbortController().signal,
});
const result = await bridge.handleToolCall({
threadId: "thread-1",
turnId: "turn-1",
callId: "call-1",
tool: "tts",
arguments: { text: "hello" },
});
expect(result).toEqual({
success: true,
contentItems: [{ type: "inputText", text: "Generated audio reply." }],
});
expect(bridge.telemetry.toolMediaUrls).toEqual(["/tmp/reply.opus"]);
expect(bridge.telemetry.toolAudioAsVoice).toBe(true);
});
it("records messaging tool side effects while returning concise text to app-server", async () => {
const toolResult = {
content: [{ type: "text", text: "Sent." }],
@@ -105,25 +97,16 @@ describe("createCodexDynamicToolBridge", () => {
signal: new AbortController().signal,
});
const result = await bridge.handleToolCall({
threadId: "thread-1",
turnId: "turn-1",
callId: "call-1",
tool: "message",
arguments: {
action: "send",
text: "hello from Codex",
mediaUrl: "/tmp/reply.png",
provider: "telegram",
to: "chat-1",
threadId: "thread-ts-1",
},
const result = await handleMessageToolCall(bridge, {
action: "send",
text: "hello from Codex",
mediaUrl: "/tmp/reply.png",
provider: "telegram",
to: "chat-1",
threadId: "thread-ts-1",
});
expect(result).toEqual({
success: true,
contentItems: [{ type: "inputText", text: "Sent." }],
});
expect(result).toEqual(expectInputText("Sent."));
expect(bridge.telemetry).toMatchObject({
didSendViaMessagingTool: true,
messagingToolSentTexts: ["hello from Codex"],
@@ -151,17 +134,11 @@ describe("createCodexDynamicToolBridge", () => {
signal: new AbortController().signal,
});
const result = await bridge.handleToolCall({
threadId: "thread-1",
turnId: "turn-1",
callId: "call-1",
tool: "message",
arguments: {
action: "send",
text: "not delivered",
provider: "slack",
to: "C123",
},
const result = await handleMessageToolCall(bridge, {
action: "send",
text: "not delivered",
provider: "slack",
to: "C123",
});
expect(result).toEqual({

View File

@@ -18,6 +18,21 @@ vi.mock("openclaw/plugin-sdk/provider-auth", () => ({
let listCodexAppServerModels: typeof import("./models.js").listCodexAppServerModels;
let resetSharedCodexAppServerClientForTests: typeof import("./shared-client.js").resetSharedCodexAppServerClientForTests;
async function sendInitializeResult(
harness: ReturnType<typeof createClientHarness>,
userAgent: string,
): Promise<void> {
await vi.waitFor(() => expect(harness.writes.length).toBeGreaterThanOrEqual(1));
const initialize = JSON.parse(harness.writes[0] ?? "{}") as { id?: number };
harness.send({ id: initialize.id, result: { userAgent } });
}
async function sendEmptyModelList(harness: ReturnType<typeof createClientHarness>): Promise<void> {
await vi.waitFor(() => expect(harness.writes.length).toBeGreaterThanOrEqual(3));
const modelList = JSON.parse(harness.writes[2] ?? "{}") as { id?: number };
harness.send({ id: modelList.id, result: { data: [] } });
}
describe("shared Codex app-server client", () => {
beforeAll(async () => {
({ listCodexAppServerModels } = await import("./models.js"));
@@ -39,12 +54,7 @@ describe("shared Codex app-server client", () => {
// Model discovery uses the shared-client path, which owns child teardown
// when initialize discovers an unsupported app-server.
const listPromise = listCodexAppServerModels({ timeoutMs: 1000 });
await vi.waitFor(() => expect(harness.writes.length).toBeGreaterThanOrEqual(1));
const initialize = JSON.parse(harness.writes[0] ?? "{}") as { id?: number };
harness.send({
id: initialize.id,
result: { userAgent: "openclaw/0.117.9 (macOS; test)" },
});
await sendInitializeResult(harness, "openclaw/0.117.9 (macOS; test)");
await expect(listPromise).rejects.toThrow(
`Codex app-server ${MIN_CODEX_APP_SERVER_VERSION} or newer is required`,
@@ -67,15 +77,8 @@ describe("shared Codex app-server client", () => {
expect(first.process.kill).toHaveBeenCalledTimes(1);
const secondList = listCodexAppServerModels({ timeoutMs: 1000 });
await vi.waitFor(() => expect(second.writes.length).toBeGreaterThanOrEqual(1));
const initialize = JSON.parse(second.writes[0] ?? "{}") as { id?: number };
second.send({
id: initialize.id,
result: { userAgent: "openclaw/0.118.0 (macOS; test)" },
});
await vi.waitFor(() => expect(second.writes.length).toBeGreaterThanOrEqual(3));
const modelList = JSON.parse(second.writes[2] ?? "{}") as { id?: number };
second.send({ id: modelList.id, result: { data: [] } });
await sendInitializeResult(second, "openclaw/0.118.0 (macOS; test)");
await sendEmptyModelList(second);
await expect(secondList).resolves.toEqual({ models: [] });
expect(startSpy).toHaveBeenCalledTimes(2);
@@ -89,15 +92,8 @@ describe("shared Codex app-server client", () => {
timeoutMs: 1000,
authProfileId: "openai-codex:work",
});
await vi.waitFor(() => expect(harness.writes.length).toBeGreaterThanOrEqual(1));
const initialize = JSON.parse(harness.writes[0] ?? "{}") as { id?: number };
harness.send({
id: initialize.id,
result: { userAgent: "openclaw/0.118.0 (macOS; test)" },
});
await vi.waitFor(() => expect(harness.writes.length).toBeGreaterThanOrEqual(3));
const modelList = JSON.parse(harness.writes[2] ?? "{}") as { id?: number };
harness.send({ id: modelList.id, result: { data: [] } });
await sendInitializeResult(harness, "openclaw/0.118.0 (macOS; test)");
await sendEmptyModelList(harness);
await expect(listPromise).resolves.toEqual({ models: [] });
expect(mocks.bridgeCodexAppServerStartOptions).toHaveBeenCalledWith(