From 5e218b402f832c4c12b772fb1cb2c2b96e1d161f Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 6 May 2026 01:28:56 -0700 Subject: [PATCH] test(gateway): capture codex bind outbound replies --- src/gateway/gateway-codex-bind.live.test.ts | 75 +++++++++++++++++---- 1 file changed, 62 insertions(+), 13 deletions(-) diff --git a/src/gateway/gateway-codex-bind.live.test.ts b/src/gateway/gateway-codex-bind.live.test.ts index e668ccdecd0..2acedde5d56 100644 --- a/src/gateway/gateway-codex-bind.live.test.ts +++ b/src/gateway/gateway-codex-bind.live.test.ts @@ -4,6 +4,7 @@ import os from "node:os"; import path from "node:path"; import { describe, it } from "vitest"; import { isLiveTestEnabled } from "../agents/live-test-helpers.js"; +import type { ChannelOutboundContext } from "../channels/plugins/types.public.js"; import { clearConfigCache, clearRuntimeConfigSnapshot } from "../config/config.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { isTruthyEnvValue } from "../infra/env.js"; @@ -31,7 +32,14 @@ const CODEX_BIND_TIMEOUT_MS = 10 * 60_000; const CODEX_BIND_REQUEST_TIMEOUT_MS = 180_000; const DEFAULT_CODEX_BIND_MODEL = "gpt-5.4"; -function createSlackCurrentConversationBindingRegistry() { +type CapturedOutboundReply = { + accountId?: string; + text: string; + threadId?: string | number; + to: string; +}; + +function createSlackCurrentConversationBindingRegistry(outboundReplies: CapturedOutboundReply[]) { return createTestRegistry([ { pluginId: "slack", @@ -54,6 +62,18 @@ function createSlackCurrentConversationBindingRegistry() { conversationBindings: { supportsCurrentConversationBinding: true, }, + outbound: { + deliveryMode: "direct", + sendText: async ({ accountId, text, threadId, to }: ChannelOutboundContext) => { + outboundReplies.push({ + ...(accountId ? { accountId } : {}), + text, + ...(threadId != null ? { threadId } : {}), + to, + }); + return { channel: "slack", messageId: `slack-${outboundReplies.length}` }; + }, + }, bindings: { compileConfiguredBinding: () => null, matchInboundConversation: () => null, @@ -104,6 +124,36 @@ function formatAssistantTextPreview(texts: string[], maxChars = 800): string { return combined.length <= maxChars ? combined : combined.slice(-maxChars); } +async function waitForOutboundText(params: { + replies: CapturedOutboundReply[]; + contains: string; + minReplyCount?: number; + timeoutMs?: number; +}): Promise<{ outboundTexts: string[]; matchedText: string }> { + const timeoutMs = params.timeoutMs ?? 60_000; + const startedAt = Date.now(); + + while (Date.now() - startedAt < timeoutMs) { + const outboundTexts = params.replies + .map((reply) => reply.text) + .filter((value) => value.trim().length > 0); + const minReplyCount = params.minReplyCount ?? 1; + const matchedText = outboundTexts + .slice(Math.max(0, minReplyCount - 1)) + .find((text) => text.includes(params.contains)); + if (outboundTexts.length >= minReplyCount && matchedText) { + return { outboundTexts, matchedText }; + } + await sleep(500); + } + + throw new Error( + `timed out waiting for outbound text containing ${params.contains}: ${formatAssistantTextPreview( + params.replies.map((reply) => reply.text), + )}`, + ); +} + function restoreEnvVar(name: string, value: string | undefined): void { if (value === undefined) { delete process.env[name]; @@ -327,6 +377,7 @@ describeLive("gateway live (native Codex conversation binding)", () => { const conversationId = `user:${slackUserId}`; const bindModel = process.env.OPENCLAW_LIVE_CODEX_BIND_MODEL?.trim() || DEFAULT_CODEX_BIND_MODEL; + const outboundReplies: CapturedOutboundReply[] = []; await fs.mkdir(workspace, { recursive: true }); await fs.writeFile( @@ -374,7 +425,7 @@ describeLive("gateway live (native Codex conversation binding)", () => { requestTimeoutMs: CODEX_BIND_REQUEST_TIMEOUT_MS, clientDisplayName: "vitest-codex-bind-live", }); - const channelRegistry = createSlackCurrentConversationBindingRegistry(); + const channelRegistry = createSlackCurrentConversationBindingRegistry(outboundReplies); pinActivePluginChannelRegistry(channelRegistry); try { @@ -394,9 +445,8 @@ describeLive("gateway live (native Codex conversation binding)", () => { originatingTo: conversationId, originatingAccountId: accountId, }); - const bindHistory = await waitForAssistantText({ - client, - sessionKey, + const bindReply = await waitForOutboundText({ + replies: outboundReplies, contains: "Bound this conversation to Codex thread", timeoutMs: CODEX_BIND_REQUEST_TIMEOUT_MS, }); @@ -405,7 +455,7 @@ describeLive("gateway live (native Codex conversation binding)", () => { accountId, conversationId, }); - let commandAssistantCount = bindHistory.assistantTexts.length; + let commandReplyCount = bindReply.outboundTexts.length; const sendCodexCommand = async (message: string, contains: string, timeoutMs = 60_000) => { await sendChatAndWait({ @@ -417,14 +467,13 @@ describeLive("gateway live (native Codex conversation binding)", () => { originatingTo: conversationId, originatingAccountId: accountId, }); - const result = await waitForAssistantText({ - client, - sessionKey, + const result = await waitForOutboundText({ + replies: outboundReplies, contains, - minAssistantCount: commandAssistantCount + 1, + minReplyCount: commandReplyCount + 1, timeoutMs, }); - commandAssistantCount = result.assistantTexts.length; + commandReplyCount = result.outboundTexts.length; return result; }; @@ -442,9 +491,9 @@ describeLive("gateway live (native Codex conversation binding)", () => { await sendCodexCommand("/codex stop", "No active Codex run to stop."); const bindingStatus = await sendCodexCommand("/codex binding", "- Fast: on"); - if (!bindingStatus.matchedAssistantText.includes("- Permissions: default")) { + if (!bindingStatus.matchedText.includes("- Permissions: default")) { throw new Error( - `binding status did not include default permissions: ${bindingStatus.matchedAssistantText}`, + `binding status did not include default permissions: ${bindingStatus.matchedText}`, ); }