@@ -4,6 +4,7 @@ import os from "node:os";
44import path from "node:path" ;
55import { describe , it } from "vitest" ;
66import { isLiveTestEnabled } from "../agents/live-test-helpers.js" ;
7+ import type { ChannelOutboundContext } from "../channels/plugins/types.public.js" ;
78import { clearConfigCache , clearRuntimeConfigSnapshot } from "../config/config.js" ;
89import type { OpenClawConfig } from "../config/types.openclaw.js" ;
910import { isTruthyEnvValue } from "../infra/env.js" ;
@@ -31,7 +32,14 @@ const CODEX_BIND_TIMEOUT_MS = 10 * 60_000;
3132const CODEX_BIND_REQUEST_TIMEOUT_MS = 180_000 ;
3233const DEFAULT_CODEX_BIND_MODEL = "gpt-5.4" ;
3334
34- function createSlackCurrentConversationBindingRegistry ( ) {
35+ type CapturedOutboundReply = {
36+ accountId ?: string ;
37+ text : string ;
38+ threadId ?: string | number ;
39+ to : string ;
40+ } ;
41+
42+ function createSlackCurrentConversationBindingRegistry ( outboundReplies : CapturedOutboundReply [ ] ) {
3543 return createTestRegistry ( [
3644 {
3745 pluginId : "slack" ,
@@ -54,6 +62,18 @@ function createSlackCurrentConversationBindingRegistry() {
5462 conversationBindings : {
5563 supportsCurrentConversationBinding : true ,
5664 } ,
65+ outbound : {
66+ deliveryMode : "direct" ,
67+ sendText : async ( { accountId, text, threadId, to } : ChannelOutboundContext ) => {
68+ outboundReplies . push ( {
69+ ...( accountId ? { accountId } : { } ) ,
70+ text,
71+ ...( threadId != null ? { threadId } : { } ) ,
72+ to,
73+ } ) ;
74+ return { channel : "slack" , messageId : `slack-${ outboundReplies . length } ` } ;
75+ } ,
76+ } ,
5777 bindings : {
5878 compileConfiguredBinding : ( ) => null ,
5979 matchInboundConversation : ( ) => null ,
@@ -104,6 +124,36 @@ function formatAssistantTextPreview(texts: string[], maxChars = 800): string {
104124 return combined . length <= maxChars ? combined : combined . slice ( - maxChars ) ;
105125}
106126
127+ async function waitForOutboundText ( params : {
128+ replies : CapturedOutboundReply [ ] ;
129+ contains : string ;
130+ minReplyCount ?: number ;
131+ timeoutMs ?: number ;
132+ } ) : Promise < { outboundTexts : string [ ] ; matchedText : string } > {
133+ const timeoutMs = params . timeoutMs ?? 60_000 ;
134+ const startedAt = Date . now ( ) ;
135+
136+ while ( Date . now ( ) - startedAt < timeoutMs ) {
137+ const outboundTexts = params . replies
138+ . map ( ( reply ) => reply . text )
139+ . filter ( ( value ) => value . trim ( ) . length > 0 ) ;
140+ const minReplyCount = params . minReplyCount ?? 1 ;
141+ const matchedText = outboundTexts
142+ . slice ( Math . max ( 0 , minReplyCount - 1 ) )
143+ . find ( ( text ) => text . includes ( params . contains ) ) ;
144+ if ( outboundTexts . length >= minReplyCount && matchedText ) {
145+ return { outboundTexts, matchedText } ;
146+ }
147+ await sleep ( 500 ) ;
148+ }
149+
150+ throw new Error (
151+ `timed out waiting for outbound text containing ${ params . contains } : ${ formatAssistantTextPreview (
152+ params . replies . map ( ( reply ) => reply . text ) ,
153+ ) } `,
154+ ) ;
155+ }
156+
107157function restoreEnvVar ( name : string , value : string | undefined ) : void {
108158 if ( value === undefined ) {
109159 delete process . env [ name ] ;
@@ -327,6 +377,7 @@ describeLive("gateway live (native Codex conversation binding)", () => {
327377 const conversationId = `user:${ slackUserId } ` ;
328378 const bindModel =
329379 process . env . OPENCLAW_LIVE_CODEX_BIND_MODEL ?. trim ( ) || DEFAULT_CODEX_BIND_MODEL ;
380+ const outboundReplies : CapturedOutboundReply [ ] = [ ] ;
330381
331382 await fs . mkdir ( workspace , { recursive : true } ) ;
332383 await fs . writeFile (
@@ -374,7 +425,7 @@ describeLive("gateway live (native Codex conversation binding)", () => {
374425 requestTimeoutMs : CODEX_BIND_REQUEST_TIMEOUT_MS ,
375426 clientDisplayName : "vitest-codex-bind-live" ,
376427 } ) ;
377- const channelRegistry = createSlackCurrentConversationBindingRegistry ( ) ;
428+ const channelRegistry = createSlackCurrentConversationBindingRegistry ( outboundReplies ) ;
378429 pinActivePluginChannelRegistry ( channelRegistry ) ;
379430
380431 try {
@@ -394,9 +445,8 @@ describeLive("gateway live (native Codex conversation binding)", () => {
394445 originatingTo : conversationId ,
395446 originatingAccountId : accountId ,
396447 } ) ;
397- const bindHistory = await waitForAssistantText ( {
398- client,
399- sessionKey,
448+ const bindReply = await waitForOutboundText ( {
449+ replies : outboundReplies ,
400450 contains : "Bound this conversation to Codex thread" ,
401451 timeoutMs : CODEX_BIND_REQUEST_TIMEOUT_MS ,
402452 } ) ;
@@ -405,7 +455,7 @@ describeLive("gateway live (native Codex conversation binding)", () => {
405455 accountId,
406456 conversationId,
407457 } ) ;
408- let commandAssistantCount = bindHistory . assistantTexts . length ;
458+ let commandReplyCount = bindReply . outboundTexts . length ;
409459
410460 const sendCodexCommand = async ( message : string , contains : string , timeoutMs = 60_000 ) => {
411461 await sendChatAndWait ( {
@@ -417,14 +467,13 @@ describeLive("gateway live (native Codex conversation binding)", () => {
417467 originatingTo : conversationId ,
418468 originatingAccountId : accountId ,
419469 } ) ;
420- const result = await waitForAssistantText ( {
421- client,
422- sessionKey,
470+ const result = await waitForOutboundText ( {
471+ replies : outboundReplies ,
423472 contains,
424- minAssistantCount : commandAssistantCount + 1 ,
473+ minReplyCount : commandReplyCount + 1 ,
425474 timeoutMs,
426475 } ) ;
427- commandAssistantCount = result . assistantTexts . length ;
476+ commandReplyCount = result . outboundTexts . length ;
428477 return result ;
429478 } ;
430479
@@ -442,9 +491,9 @@ describeLive("gateway live (native Codex conversation binding)", () => {
442491 await sendCodexCommand ( "/codex stop" , "No active Codex run to stop." ) ;
443492
444493 const bindingStatus = await sendCodexCommand ( "/codex binding" , "- Fast: on" ) ;
445- if ( ! bindingStatus . matchedAssistantText . includes ( "- Permissions: default" ) ) {
494+ if ( ! bindingStatus . matchedText . includes ( "- Permissions: default" ) ) {
446495 throw new Error (
447- `binding status did not include default permissions: ${ bindingStatus . matchedAssistantText } ` ,
496+ `binding status did not include default permissions: ${ bindingStatus . matchedText } ` ,
448497 ) ;
449498 }
450499
0 commit comments