From 78f9f3093ec2d6ce3ce1eb0bc41ce5fed9b8deab Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 20 Apr 2026 16:38:43 +0100 Subject: [PATCH] test: share codex app-server test helpers --- .../src/app-server/dynamic-tools.test.ts | 143 ++++++++---------- .../src/app-server/shared-client.test.ts | 44 +++--- 2 files changed, 80 insertions(+), 107 deletions(-) diff --git a/extensions/codex/src/app-server/dynamic-tools.test.ts b/extensions/codex/src/app-server/dynamic-tools.test.ts index fad1e80b919..35e4bfa5ead 100644 --- a/extensions/codex/src/app-server/dynamic-tools.test.ts +++ b/extensions/codex/src/app-server/dynamic-tools.test.ts @@ -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 { return { @@ -13,6 +14,50 @@ function createTool(overrides: Partial): AnyAgentTool { } as unknown as AnyAgentTool; } +function mediaResult(mediaUrl: string, audioAsVoice?: boolean): AgentToolResult { + return { + content: [{ type: "text", text: "Generated media reply." }], + details: { + media: { + mediaUrl, + ...(audioAsVoice === true ? { audioAsVoice: true } : {}), + }, + }, + }; +} + +function createBridgeWithToolResult(toolName: string, toolResult: AgentToolResult) { + 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, + 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; - 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; - 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({ diff --git a/extensions/codex/src/app-server/shared-client.test.ts b/extensions/codex/src/app-server/shared-client.test.ts index d59e1e9844a..54ef15aa6bd 100644 --- a/extensions/codex/src/app-server/shared-client.test.ts +++ b/extensions/codex/src/app-server/shared-client.test.ts @@ -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, + userAgent: string, +): Promise { + 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): Promise { + 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(