import { randomUUID } from "node:crypto" ;
import {
assert ,
ClaudeChannelNotificationSchema,
ClaudePermissionNotificationSchema,
connectGateway,
connectMcpClient,
extractTextFromGatewayPayload,
type ClaudeChannelNotification,
maybeApprovePendingBridgePairing,
waitFor,
} from "./mcp-channels-harness.ts" ;
async function main() {
const gatewayUrl = process.env.GW_URL?.trim();
const gatewayToken = process.env.GW_TOKEN?.trim();
assert (gatewayUrl, "missing GW_URL" );
assert (gatewayToken, "missing GW_TOKEN" );
const gateway = await connectGateway({ url: gatewayUrl, token: gatewayToken });
let mcpHandle = await connectMcpClient({
gatewayUrl,
gatewayToken,
});
let mcp = mcpHandle.client;
try {
if (await maybeApprovePendingBridgePairing(gateway)) {
await Promise.allSettled([mcp.close(), mcpHandle.transport.close()]);
mcpHandle = await connectMcpClient({
gatewayUrl,
gatewayToken,
});
mcp = mcpHandle.client;
}
const conversation = await waitFor(
"seeded conversation in conversations_list" ,
async () => {
const listed = (await mcp.callTool({
name: "conversations_list" ,
arguments: {},
})) as {
structuredContent?: { conversations?: Array<Record<string, unknown>> };
};
return listed.structuredContent?.conversations?.find(
(entry) => entry.sessionKey === "agent:main:main" ,
);
},
240 _000 ,
);
assert (conversation.channel === "imessage" , "expected seeded channel" );
assert (conversation.to === "+15551234567" , "expected seeded target" );
const fetched = (await mcp.callTool({
name: "conversation_get" ,
arguments: { session_key: "agent:main:main" },
})) as {
structuredContent?: { conversation?: Record<string, unknown> };
isError?: boolean ;
};
assert (!fetched.isError, "conversation_get should succeed" );
assert (
fetched.structuredContent?.conversation?.sessionKey === "agent:main:main" ,
"conversation_get returned wrong session" ,
);
const messages = await waitFor(
"seeded transcript messages" ,
async () => {
const history = (await mcp.callTool({
name: "messages_read" ,
arguments: { session_key: "agent:main:main" , limit: 10 },
})) as {
structuredContent?: { messages?: Array<Record<string, unknown>> };
};
const currentMessages = history.structuredContent?.messages ?? [];
return currentMessages.length >= 2 ? currentMessages : undefined;
},
240 _000 ,
);
await waitFor(
"seeded attachment message" ,
() =>
messages.find((entry) => {
const raw = entry.__openclaw;
return (
raw && typeof raw === "object" && (raw as { id?: unknown }).id === "msg-attachment"
);
}),
240 _000 ,
);
const attachments = (await mcp.callTool({
name: "attachments_fetch" ,
arguments: { session_key: "agent:main:main" , message_id: "msg-attachment" },
})) as {
structuredContent?: { attachments?: Array<Record<string, unknown>> };
isError?: boolean ;
};
assert (!attachments.isError, "attachments_fetch should succeed" );
assert (
(attachments.structuredContent?.attachments?.length ?? 0 ) === 1 ,
"expected one seeded attachment" ,
);
const waited = (await Promise.all([
mcp.callTool({
name: "events_wait" ,
arguments: {
session_key: "agent:main:main" ,
after_cursor: 0 ,
timeout_ms: 10 _000 ,
},
}) as Promise<{
structuredContent?: { event?: Record<string, unknown> };
}>,
gateway.request("chat.inject" , {
sessionKey: "agent:main:main" ,
message: "assistant live event" ,
}),
]).then(([result]) => result)) as {
structuredContent?: { event?: Record<string, unknown> };
};
const assistantEvent = waited.structuredContent?.event;
assert (assistantEvent, "expected events_wait result" );
assert (assistantEvent.type === "message" , "expected message event" );
assert (assistantEvent.role === "assistant" , "expected assistant event role" );
assert (assistantEvent.text === "assistant live event" , "expected assistant event text" );
const assistantCursor = typeof assistantEvent.cursor === "number" ? assistantEvent.cursor : 0 ;
const polled = (await mcp.callTool({
name: "events_poll" ,
arguments: { session_key: "agent:main:main" , after_cursor: 0 , limit: 10 },
})) as {
structuredContent?: { events?: Array<Record<string, unknown>> };
};
assert (
(polled.structuredContent?.events ?? []).some(
(entry) => entry.text === "assistant live event" ,
),
"expected assistant event in events_poll" ,
);
const channelMessage = `hello from docker ${randomUUID()}`;
const userEvent = (await Promise.all([
mcp.callTool({
name: "events_wait" ,
arguments: {
session_key: "agent:main:main" ,
after_cursor: assistantCursor,
timeout_ms: 10 _000 ,
},
}) as Promise<{
structuredContent?: { event?: Record<string, unknown> };
}>,
gateway.request("chat.send" , {
sessionKey: "agent:main:main" ,
message: channelMessage,
idempotencyKey: randomUUID(),
}),
]).then(([result]) => result)) as {
structuredContent?: { event?: Record<string, unknown> };
};
const rawGatewayUserMessage = await waitFor("raw gateway user session.message" , () =>
gateway.events.find(
(entry) =>
entry.event === "session.message" &&
entry.payload.sessionKey === "agent:main:main" &&
extractTextFromGatewayPayload(entry.payload) === channelMessage,
),
);
if (userEvent.structuredContent?.event?.text !== channelMessage) {
throw new Error(
`expected user event after chat.send: ${JSON.stringify(
{
userEvent: userEvent.structuredContent?.event ?? null ,
rawGatewayUserMessage: rawGatewayUserMessage ?? null ,
recentGatewayEvents: gateway.events.slice(-10 ).map((entry) => ({
event: entry.event,
sessionKey: entry.payload.sessionKey,
text: extractTextFromGatewayPayload(entry.payload),
})),
},
null ,
2 ,
)}`,
);
}
assert (rawGatewayUserMessage, "expected raw gateway session.message after chat.send" );
let helpNotification: ClaudeChannelNotification;
try {
helpNotification = await waitFor(
"Claude channel notification" ,
() =>
mcpHandle.rawMessages
.map((entry) => ClaudeChannelNotificationSchema.safeParse(entry))
.find(
(entry) =>
entry.success &&
entry.data.params.meta.session_key === "agent:main:main" &&
entry.data.params.content === channelMessage,
)?.data.params,
);
} catch (error) {
throw new Error(
`timeout waiting for Claude channel notification: ${JSON.stringify(
{
rawMessages: mcpHandle.rawMessages.slice(-10 ),
},
null ,
2 ,
)}`,
{ cause: error },
);
}
assert (helpNotification.content === channelMessage, "expected Claude channel content" );
await mcp.notification({
method: "notifications/claude/channel/permission_request" ,
params: {
request_id: "abcde" ,
tool_name: "Bash" ,
description: "run npm test" ,
input_preview: '{"cmd":"npm test"}' ,
},
});
await gateway.request("chat.send" , {
sessionKey: "agent:main:main" ,
message: "yes abcde" ,
idempotencyKey: randomUUID(),
});
const permission = await waitFor(
"Claude permission notification" ,
() =>
mcpHandle.rawMessages
.map((entry) => ClaudePermissionNotificationSchema.safeParse(entry))
.find((entry) => entry.success && entry.data.params.request_id === "abcde" )?.data.params,
);
assert (permission.behavior === "allow" , "expected allow permission reply" );
process.stdout.write(
JSON.stringify(
{
ok: true ,
sessionKey: "agent:main:main" ,
rawNotifications: mcpHandle.rawMessages.filter(
(entry) =>
ClaudeChannelNotificationSchema.safeParse(entry).success ||
ClaudePermissionNotificationSchema.safeParse(entry).success,
).length,
},
null ,
2 ,
) + "\n" ,
);
} finally {
await Promise.allSettled([mcp.close(), mcpHandle.transport.close(), gateway.close()]);
}
}
await main();
Messung V0.5 in Prozent C=99 H=96 G=97
¤ Dauer der Verarbeitung: 0.4 Sekunden
¤
*© Formatika GbR, Deutschland