mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:30:43 +00:00
test: share codex app-server test helpers
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user