





















@@ -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";
333434-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[]) {
3543return createTestRegistry([
3644{
3745pluginId: "slack",
@@ -54,6 +62,18 @@ function createSlackCurrentConversationBindingRegistry() {
5462conversationBindings: {
5563supportsCurrentConversationBinding: 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+},
5777bindings: {
5878compileConfiguredBinding: () => null,
5979matchInboundConversation: () => null,
@@ -104,6 +124,36 @@ function formatAssistantTextPreview(texts: string[], maxChars = 800): string {
104124return combined.length <= maxChars ? combined : combined.slice(-maxChars);
105125}
106126127+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 {
108158if (value === undefined) {
109159delete process.env[name];
@@ -327,6 +377,7 @@ describeLive("gateway live (native Codex conversation binding)", () => {
327377const conversationId = `user:${slackUserId}`;
328378const bindModel =
329379process.env.OPENCLAW_LIVE_CODEX_BIND_MODEL?.trim() || DEFAULT_CODEX_BIND_MODEL;
380+const outboundReplies: CapturedOutboundReply[] = [];
330381331382await fs.mkdir(workspace, { recursive: true });
332383await fs.writeFile(
@@ -374,7 +425,7 @@ describeLive("gateway live (native Codex conversation binding)", () => {
374425requestTimeoutMs: CODEX_BIND_REQUEST_TIMEOUT_MS,
375426clientDisplayName: "vitest-codex-bind-live",
376427});
377-const channelRegistry = createSlackCurrentConversationBindingRegistry();
428+const channelRegistry = createSlackCurrentConversationBindingRegistry(outboundReplies);
378429pinActivePluginChannelRegistry(channelRegistry);
379430380431try {
@@ -394,9 +445,8 @@ describeLive("gateway live (native Codex conversation binding)", () => {
394445originatingTo: conversationId,
395446originatingAccountId: accountId,
396447});
397-const bindHistory = await waitForAssistantText({
398- client,
399- sessionKey,
448+const bindReply = await waitForOutboundText({
449+replies: outboundReplies,
400450contains: "Bound this conversation to Codex thread",
401451timeoutMs: 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;
409459410460const sendCodexCommand = async (message: string, contains: string, timeoutMs = 60_000) => {
411461await sendChatAndWait({
@@ -417,14 +467,13 @@ describeLive("gateway live (native Codex conversation binding)", () => {
417467originatingTo: conversationId,
418468originatingAccountId: 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;
428477return result;
429478};
430479@@ -442,9 +491,9 @@ describeLive("gateway live (native Codex conversation binding)", () => {
442491await sendCodexCommand("/codex stop", "No active Codex run to stop.");
443492444493const bindingStatus = await sendCodexCommand("/codex binding", "- Fast: on");
445-if (!bindingStatus.matchedAssistantText.includes("- Permissions: default")) {
494+if (!bindingStatus.matchedText.includes("- Permissions: default")) {
446495throw 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此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。