Skip to content

Commit 5e218b4

Browse files
committed
test(gateway): capture codex bind outbound replies
1 parent 53423a2 commit 5e218b4

1 file changed

Lines changed: 62 additions & 13 deletions

File tree

src/gateway/gateway-codex-bind.live.test.ts

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import os from "node:os";
44
import path from "node:path";
55
import { describe, it } from "vitest";
66
import { isLiveTestEnabled } from "../agents/live-test-helpers.js";
7+
import type { ChannelOutboundContext } from "../channels/plugins/types.public.js";
78
import { clearConfigCache, clearRuntimeConfigSnapshot } from "../config/config.js";
89
import type { OpenClawConfig } from "../config/types.openclaw.js";
910
import { isTruthyEnvValue } from "../infra/env.js";
@@ -31,7 +32,14 @@ const CODEX_BIND_TIMEOUT_MS = 10 * 60_000;
3132
const CODEX_BIND_REQUEST_TIMEOUT_MS = 180_000;
3233
const 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+
107157
function 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

Comments
 (0)