openclaw / test /cli-message-authority.integration.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
205131a verified
Raw History Blame Contribute Delete
29.3 kB
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { discordPlugin } from "../extensions/discord/api.js";
import { slackPlugin } from "../extensions/slack/api.js";
import {
createOperationalRunInstanceRef,
prepareAgentRunAdmission,
} from "../src/agents/admitted-run-context.js";
import { buildPreparedCliRunContext } from "../src/agents/cli-runner.test-helpers.js";
import { runPlugin, SUCCESS_RESULT } from "../src/agents/cli-runner/execute-plugin.test-support.js";
import { createCliToolTracking } from "../src/agents/cli-runner/execute-tool-tracking.js";
import { buildCliMcpGrantContext } from "../src/agents/cli-runner/mcp-grant-context.js";
import type { RunCliAgentParams } from "../src/agents/cli-runner/types.js";
import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../src/config/config.js";
import type { OpenClawConfig } from "../src/config/types.openclaw.js";
import { createAgentRuntimeApprovalAuthorityValidator } from "../src/gateway/agent-runtime-identity-token.js";
import {
activateMcpLoopbackClientGrantCapture,
deactivateMcpLoopbackClientGrantCapture,
mintMcpLoopbackClientGrant,
resolveMcpLoopbackClientGrant,
revokeMcpLoopbackClientGrant,
} from "../src/gateway/mcp-grant-store.js";
import { closeMcpLoopbackServer, ensureMcpLoopbackServer } from "../src/gateway/mcp-http.js";
import {
beginMcpLoopbackToolCallCapture,
clearMcpLoopbackToolCallCapture,
getActiveMcpLoopbackRuntime,
} from "../src/gateway/mcp-http.loopback-runtime.js";
import {
mintMessageActionTurnCapability,
revokeMessageActionTurnCapability,
} from "../src/gateway/message-action-turn-capability.js";
import { createRequestGatewayMethodRegistry } from "../src/gateway/server-methods.js";
import type { GatewayRequestContext } from "../src/gateway/server-methods/types.js";
import { createPluginRegistry } from "../src/plugins/registry.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../src/plugins/runtime.js";
import {
bindGatewayContextResolver,
clearGatewayContextResolver,
} from "../src/plugins/runtime/gateway-request-scope.js";
import type { PluginRuntime } from "../src/plugins/runtime/types.js";
import { createPluginRecord } from "../src/plugins/status.test-fixtures.js";
import { trackAsyncWork } from "../src/shared/async-work-scope.js";
import type { Deferred } from "../src/shared/deferred.js";
import { closeOpenClawStateDatabaseForTest } from "../src/state/openclaw-state-db.js";
import { createDeferred, withTestTimeout } from "./helpers/promise.js";
// Device inventory is unrelated to the real grant, message tool, and provider boundary.
vi.mock("../src/agents/node-exec-availability.js", () => ({
loadNodeExecAvailability: async () => ({ cacheKey: "no-nodes", isAvailable: () => false }),
}));
const channels = {
discord: { current: "100000000000000003", sender: "100000000000000009" },
slack: { current: "C0123456789", sender: "U0123456789" },
};
const discordSibling = "100000000000000004";
const discordDm = "100000000000000005";
const discordGuild = "100000000000000001";
const discordManagementRole = "100000000000000006";
const discordMessage = "100000000000000020";
const discordChannelPath = `/api/v10/channels/${channels.discord.current}`;
const react = { action: "react", channel: "discord", messageId: discordMessage, emoji: "✅" };
type McpResponse = {
result?: {
tools?: Array<{ name: string }>;
content?: Array<{ type: string; text?: string }>;
isError?: boolean;
};
error?: unknown;
};
function expectSuccess(response: McpResponse) {
expect(response.error).toBeUndefined();
expect(response.result?.isError, JSON.stringify(response)).not.toBe(true);
const text = response.result?.content?.find((item) => item.type === "text")?.text;
expect(JSON.parse(text ?? "null")).toMatchObject({ ok: true });
}
function expectDenied(response: McpResponse, reason: RegExp) {
expect(response.error).toBeUndefined();
expect(response.result?.isError).toBe(true);
expect(response.result?.content?.map((item) => item.text).join("\n")).toMatch(reason);
}
describe("CLI message authority integration", () => {
const requests: Array<{
method: string;
path: string;
fields: Record<string, string>;
body: string;
}> = [];
const cleanupTurns: Array<() => void> = [];
const providerWork = new Set<Promise<void>>();
const providerErrors: unknown[] = [];
let provider: Server;
let providerOrigin: string;
let mcpOrigin: string;
let cfg: OpenClawConfig;
let workspaceDir: string;
let sequence = 0;
let realFetch: typeof fetch;
let gatewaySend = false;
let heldRequest: { method: "GET" | "POST"; entered: Deferred; release: Deferred } | undefined;
async function handleProviderRequest(req: IncomingMessage, res: ServerResponse) {
const url = new URL(req.url ?? "/", "http://fixture.invalid");
const chunks: Buffer[] = [];
for await (const chunk of req) {
chunks.push(Buffer.from(chunk));
}
const rawBody = Buffer.concat(chunks).toString();
const fields = Object.fromEntries(new URLSearchParams(rawBody));
requests.push({ method: req.method ?? "", path: url.pathname, fields, body: rawBody });
const gate = heldRequest;
if (
gate &&
req.method === gate.method &&
url.pathname === `${discordChannelPath}${gate.method === "POST" ? "/messages" : ""}`
) {
gate.entered.resolve();
await gate.release.promise;
if (heldRequest === gate) {
heldRequest = undefined;
}
}
let body: unknown;
if (req.method === "GET" && /^\/api\/v10\/channels\/\d+$/.test(url.pathname)) {
const channelId = url.pathname.split("/").at(-1);
body =
channelId === discordDm
? { id: discordDm, type: 1, recipients: [{ id: channels.discord.sender }] }
: { id: channelId, type: 0, guild_id: discordGuild, name: "allowed" };
} else if (req.method === "GET" && /\/channels\/\d+\/messages$/.test(url.pathname)) {
body = [];
} else if (req.method === "POST" && url.pathname === "/api/v10/users/@me/channels") {
expect(JSON.parse(rawBody)).toEqual({ recipient_id: channels.discord.sender });
body = { id: discordDm, type: 1, recipients: [{ id: channels.discord.sender }] };
} else if (req.method === "POST" && url.pathname === `${discordChannelPath}/messages`) {
body = {
id: discordMessage,
channel_id: channels.discord.current,
content: "accepted message",
};
} else if (req.method === "PUT" && /\/reactions\/[^/]+\/@me$/.test(url.pathname)) {
res.writeHead(204).end();
return;
} else if (
(req.method === "PUT" && url.pathname === `${discordChannelPath}/pins/${discordMessage}`) ||
(req.method === "DELETE" &&
url.pathname === `${discordChannelPath}/messages/${discordMessage}`)
) {
res.writeHead(204).end();
return;
} else if (req.method === "GET" && url.pathname === `/api/v10/guilds/${discordGuild}`) {
body = {
id: discordGuild,
owner_id: "100000000000000008",
roles: [
{ id: discordGuild, permissions: "0" },
{ id: discordManagementRole, permissions: "16" }, // Discord MANAGE_CHANNELS.
],
};
} else if (
req.method === "GET" &&
url.pathname === `/api/v10/guilds/${discordGuild}/members/${channels.discord.sender}`
) {
body = { user: { id: channels.discord.sender }, roles: [discordManagementRole] };
} else if (req.method === "PATCH" && url.pathname === discordChannelPath) {
body = {
id: channels.discord.current,
type: 0,
guild_id: discordGuild,
name: "allowed",
topic: "A permitted channel topic",
};
} else if (url.pathname === "/api/conversations.info") {
body = { ok: true, channel: { id: fields.channel, is_channel: true, name: "allowed" } };
} else if (url.pathname === "/api/conversations.history") {
body = { ok: true, messages: [], has_more: false };
} else {
throw new Error(`Unexpected provider request: ${req.method} ${url.pathname}`);
}
res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(body));
}
beforeAll(async () => {
const isolatedHome = process.env.OPENCLAW_TEST_HOME;
if (!isolatedHome) {
throw new Error("CLI message integration requires the shared isolated test HOME");
}
workspaceDir = path.join(isolatedHome, "workspace");
provider = createServer((req, res) => {
const work = handleProviderRequest(req, res).catch((error: unknown) => {
providerErrors.push(error);
res.destroy();
});
providerWork.add(work);
void work.finally(() => providerWork.delete(work));
});
await new Promise<void>((resolve) => {
provider.listen(0, "127.0.0.1", resolve);
});
const address = provider.address();
if (!address || typeof address === "string") {
throw new Error("Expected provider TCP address");
}
providerOrigin = `http://127.0.0.1:${address.port}`;
realFetch = globalThis.fetch.bind(globalThis);
cfg = {
agents: { defaults: { workspace: workspaceDir } },
tools: { allow: ["message"] },
channels: {
discord: {
enabled: true,
token: "synthetic-message-provider-token",
groupPolicy: "allowlist",
guilds: { [discordGuild]: { channels: { "*": { enabled: true } } } },
},
slack: {
enabled: true,
botToken: "synthetic-message-provider-token",
groupPolicy: "open",
dm: { groupEnabled: true },
},
},
};
setRuntimeConfigSnapshot(cfg, cfg);
await ensureMcpLoopbackServer(0);
const runtime = expectDefined(getActiveMcpLoopbackRuntime(), "task-owned MCP runtime");
mcpOrigin = `http://127.0.0.1:${runtime.port}`;
});
beforeEach(() => {
gatewaySend = false;
requests.length = 0;
providerErrors.length = 0;
vi.stubGlobal("fetch", (input: RequestInfo | URL, init?: RequestInit) => {
const url = new URL(input instanceof Request ? input.url : String(input));
if (url.origin === "https://discord.com" && url.pathname.startsWith("/api/v10/")) {
return realFetch(new URL(`${url.pathname}${url.search}`, providerOrigin), init);
}
if (url.origin !== providerOrigin && url.origin !== mcpOrigin) {
throw new Error(`Unexpected fixture network destination: ${url.origin}`);
}
return realFetch(input, init);
});
vi.stubEnv("SLACK_API_URL", `${providerOrigin}/api/`);
for (const key of [
"HTTP_PROXY",
"HTTPS_PROXY",
"ALL_PROXY",
"http_proxy",
"https_proxy",
"all_proxy",
]) {
vi.stubEnv(key, undefined);
}
const owner = createPluginRegistry({
logger: { info() {}, warn() {}, error() {}, debug() {} },
runtime: {} as PluginRuntime,
activateGlobalSideEffects: false,
});
// Installer provenance has its own coverage; the official adapters and gates are real.
for (const plugin of [discordPlugin, slackPlugin]) {
const record = createPluginRecord({
id: plugin.id,
origin: "global",
trustedOfficialInstall: true,
});
owner.registry.plugins.push(record);
owner.createApi(record, { config: cfg, registrationMode: "full" }).registerChannel({
plugin: {
...plugin,
status: undefined,
...(plugin.actions
? {
actions: {
...plugin.actions,
// Only the accepted-send case selects the supported Gateway-owned dispatch branch.
resolveExecutionMode: (params) =>
gatewaySend && params.action === "send"
? "gateway"
: (plugin.actions?.resolveExecutionMode?.(params) ?? "local"),
},
}
: {}),
},
});
}
setActivePluginRegistry(owner.registry);
setRuntimeConfigSnapshot(cfg, cfg);
});
afterEach(async () => {
heldRequest?.release.resolve();
heldRequest = undefined;
for (const cleanup of cleanupTurns.splice(0)) {
cleanup();
}
await Promise.all(providerWork);
if (providerErrors.length) {
throw new AggregateError(providerErrors, "Provider fixture failed");
}
});
afterAll(async () => {
await closeMcpLoopbackServer();
provider?.closeAllConnections();
if (provider?.listening) {
await new Promise<void>((resolve, reject) => {
provider.close((error) => (error ? reject(error) : resolve()));
});
}
closeOpenClawStateDatabaseForTest();
resetPluginRuntimeStateForTest();
clearRuntimeConfigSnapshot();
vi.unstubAllGlobals();
vi.unstubAllEnvs();
});
function holdProviderRequest(method: "GET" | "POST") {
const gate = { method, entered: createDeferred(), release: createDeferred() };
heldRequest = gate;
return {
entered: () =>
withTestTimeout(gate.entered.promise, 10_000, "Expected provider request was not reached"),
release: () => gate.release.resolve(),
};
}
async function createTurn(
channel: keyof typeof channels,
options: {
splitSession?: boolean;
bindCapability?: boolean;
gatewaySend?: boolean;
discordDm?: boolean;
} = {},
) {
gatewaySend = options.gatewaySend === true;
const runtime = expectDefined(getActiveMcpLoopbackRuntime(), "active MCP runtime");
const source = new AbortController();
const runId = `cli-message-${channel}-${++sequence}`;
const direct = channel === "discord" && options.discordDm === true;
const policySessionKey = direct
? `agent:main:discord:default:direct:${channels.discord.sender}`
: `agent:main:${channel}:channel:${channels[channel].current}`;
const splitSession = options.splitSession || direct;
const run = {
sessionId: `session-${runId}`,
sessionKey: splitSession ? "agent:main:main" : policySessionKey,
sessionFile: path.join(workspaceDir, `${runId}.jsonl`),
runId,
workspaceDir,
...(splitSession ? { runtimePolicySessionKey: policySessionKey } : {}),
provider: "claude-cli" as const,
model: "test-model",
prompt: "Inspect the current conversation.",
timeoutMs: 60_000,
messageProvider: channel,
messageChannel: channel,
// Discord CLI ingress receives the user target; the capability keeps the native DM id.
currentChannelId: direct ? `user:${channels.discord.sender}` : channels[channel].current,
...(direct ? { chatType: "direct" as const, currentMessageId: discordMessage } : {}),
agentAccountId: "default",
senderId: channels[channel].sender,
senderIsOwner: false,
cliToolAvailability: { native: [], openClaw: ["message"] },
} satisfies RunCliAgentParams;
const admission = prepareAgentRunAdmission({
cfg,
facts: {
runId,
agentId: "main",
ingress: { kind: "system", boundary: "cli-message-test", state: "present" },
},
operationalRunInstance: createOperationalRunInstanceRef(runId),
});
const admitted = await admission.admit("gateway", runId);
const directToolContext = direct
? expectDefined(
discordPlugin.threading?.buildToolContext?.({
cfg,
accountId: "default",
context: {
Channel: "discord",
From: `discord:${run.senderId}`,
To: run.currentChannelId,
ChatType: "direct",
NativeChannelId: discordDm,
CurrentMessageId: discordMessage,
},
hasRepliedRef: undefined,
}),
"Discord DM threading context",
)
: undefined;
const capability = mintMessageActionTurnCapability({
agentId: "main",
runId,
sessionKey: policySessionKey,
sourceReplySessionKey: run.sessionKey,
sessionId: run.sessionId,
requesterAccountId: "default",
requesterSenderId: run.senderId,
toolContext: {
currentChannelProvider: channel,
...(directToolContext ?? {
currentChannelId: run.currentChannelId,
currentChatType: "channel" as const,
}),
},
});
const grant = mintMcpLoopbackClientGrant({
context: buildCliMcpGrantContext({
run,
config: cfg,
requireExplicitMessageTarget: false,
agentId: "main",
modelProvider: "anthropic",
modelId: "test-model",
toolsAllow: ["message"],
}),
runtimeOwnerToken: runtime.ownerToken,
admittedRunContext: admitted,
messageActionTurnCapability: options.bindCapability === false ? undefined : capability,
abortSignal: source.signal,
});
const capture = {
token: grant.token,
runtimeOwnerToken: runtime.ownerToken,
captureKey: `capture-${runId}`,
};
const gatewayContext = {
trackExecution: trackAsyncWork,
getRuntimeConfig: () => cfg,
dedupe: new Map(),
getGatewayMethodRegistry: () => createRequestGatewayMethodRegistry(),
validateAgentRuntimeApprovalAuthority: createAgentRuntimeApprovalAuthorityValidator(),
} as GatewayRequestContext;
bindGatewayContextResolver(admitted, () => gatewayContext);
cleanupTurns.push(() => {
source.abort();
revokeMcpLoopbackClientGrant(grant.token);
revokeMessageActionTurnCapability(capability);
clearMcpLoopbackToolCallCapture(capture.captureKey);
clearGatewayContextResolver(admitted);
admission.close();
});
expect(activateMcpLoopbackClientGrantCapture(capture)).not.toBe(false);
beginMcpLoopbackToolCallCapture({ captureKey: capture.captureKey, onToolCallResult() {} });
const rpc = async (
method: string,
params?: Record<string, unknown>,
headers?: Record<string, string>,
): Promise<McpResponse> => {
const response = await realFetch(`${mcpOrigin}/mcp`, {
method: "POST",
headers: {
...headers,
authorization: `Bearer ${grant.token}`,
"content-type": "application/json",
"x-openclaw-cli-capture-key": capture.captureKey,
},
body: JSON.stringify({
jsonrpc: "2.0",
id: ++sequence,
method,
...(params ? { params } : {}),
}),
signal: AbortSignal.timeout(30_000),
});
const payload = (await response.json()) as McpResponse;
expect(response.status, JSON.stringify(payload)).toBe(200);
return payload;
};
// Warm discovery so later calls also exercise the grant-owned tool cache.
expect((await rpc("tools/list")).result?.tools?.some((tool) => tool.name === "message")).toBe(
true,
);
return {
capability,
source,
capture,
runParams: {
...run,
agentId: "main",
config: cfg,
admittedRunContext: admitted,
abortSignal: source.signal,
messageActionTurnCapability: options.bindCapability === false ? undefined : capability,
},
isCurrent: () => resolveMcpLoopbackClientGrant(capture)?.isCurrent() === true,
call: (args: Record<string, unknown>, headers?: Record<string, string>) =>
rpc("tools/call", { name: "message", arguments: args }, headers),
};
}
it.each(["discord", "slack"] as const)(
"reads through an official %s CLI grant",
async (channel) => {
const turn = await createTurn(channel, { splitSession: channel === "discord" });
const target = channel === "discord" ? discordSibling : channels.slack.current;
if (channel === "discord") {
expect(turn.runParams.sessionKey).not.toBe(turn.runParams.runtimePolicySessionKey);
}
expectSuccess(
await turn.call({ action: "read", channel, target: `channel:${target}`, limit: 1 }),
);
expect(requests).toContainEqual(
expect.objectContaining(
channel === "discord"
? { method: "GET", path: `/api/v10/channels/${target}/messages` }
: {
method: "POST",
path: "/api/conversations.history",
fields: expect.objectContaining({ channel: target }),
},
),
);
},
);
it("reacts with bare, prefixed, and default channel targets while rejecting another conversation", async () => {
const turn = await createTurn("discord");
for (const target of [
`channel:${channels.discord.current}`,
channels.discord.current,
undefined,
]) {
expectSuccess(await turn.call({ ...react, ...(target ? { target } : {}) }));
}
expect(requests.filter((request) => request.method === "PUT")).toEqual([
expect.objectContaining({
path: `${discordChannelPath}/messages/${discordMessage}/reactions/%E2%9C%85/@me`,
}),
expect.objectContaining({
path: `${discordChannelPath}/messages/${discordMessage}/reactions/%E2%9C%85/@me`,
}),
expect.objectContaining({
path: `${discordChannelPath}/messages/${discordMessage}/reactions/%E2%9C%85/@me`,
}),
]);
const before = requests.length;
expectDenied(
await turn.call(
{ ...react, target: `channel:${discordSibling}` },
{ "x-openclaw-current-channel-id": discordSibling },
),
/exact current conversation and account/,
);
expect(requests).toHaveLength(before);
});
it.each([
{ selection: "a prefixed native target", target: `channel:${discordDm}` },
{ selection: "a bare native target", target: discordDm },
{ selection: "an implicit target", target: undefined },
])("reacts in a Discord DM with $selection and an implicit account", async ({ target }) => {
const turn = await createTurn("discord", { discordDm: true });
expectSuccess(await turn.call({ ...react, ...(target ? { target } : {}) }));
expect(requests.filter((request) => request.method === "PUT")).toEqual([
expect.objectContaining({
path: `/api/v10/channels/${discordDm}/messages/${discordMessage}/reactions/%E2%9C%85/@me`,
}),
]);
});
it("pins the current Discord message without an explicit target", async () => {
const turn = await createTurn("discord");
expectSuccess(
await turn.call({ action: "pin", channel: "discord", messageId: discordMessage }),
);
expect(requests.filter((request) => request.method === "PUT")).toEqual([
expect.objectContaining({ path: `${discordChannelPath}/pins/${discordMessage}` }),
]);
});
it("deletes an explicit message in the current Discord channel", async () => {
const turn = await createTurn("discord");
expectSuccess(
await turn.call({
action: "delete",
channel: "discord",
target: `channel:${channels.discord.current}`,
messageId: discordMessage,
}),
);
expect(requests.filter((request) => request.method === "DELETE")).toEqual([
expect.objectContaining({ path: `${discordChannelPath}/messages/${discordMessage}` }),
]);
});
it("edits the current Discord channel with the admitted sender's permission", async () => {
const turn = await createTurn("discord");
expectSuccess(
await turn.call({
action: "channel-edit",
channel: "discord",
target: `channel:${channels.discord.current}`,
topic: "A permitted channel topic",
}),
);
expect(requests).toContainEqual(
expect.objectContaining({
method: "GET",
path: `/api/v10/guilds/${discordGuild}/members/${channels.discord.sender}`,
}),
);
const edits = requests.filter((request) => request.method === "PATCH");
expect(edits).toHaveLength(1);
expect(edits[0]?.path).toBe(discordChannelPath);
expect(JSON.parse(edits[0]?.body ?? "null")).toEqual({ topic: "A permitted channel topic" });
});
it("does not accept a private capability supplied through child arguments or headers", async () => {
const turn = await createTurn("discord", { bindCapability: false });
expect(turn.isCurrent()).toBe(true);
expectDenied(
await turn.call(
{
action: "read",
channel: "discord",
target: `channel:${channels.discord.current}`,
messageActionTurnCapability: turn.capability,
conversationReadOrigin: "direct-operator",
},
{ "x-openclaw-message-action-turn-capability": turn.capability },
),
/current provider and account context/,
);
expect(requests).toEqual([]);
});
it("rejects a revoked capability after warming the MCP tool cache", async () => {
const turn = await createTurn("discord");
revokeMessageActionTurnCapability(turn.capability);
expect(turn.isCurrent()).toBe(true);
expectDenied(await turn.call(react), /turn capability.*no longer active/);
expect(requests).toEqual([]);
});
it("cancels a provider lookup before a reaction can be written", async () => {
const turn = await createTurn("discord");
const lookup = holdProviderRequest("GET");
const response = turn.call({ ...react, target: `channel:${channels.discord.current}` });
try {
await lookup.entered();
turn.source.abort();
} finally {
lookup.release();
}
expectDenied(await response, /abort|cancel|no longer active/i);
expect(requests).toContainEqual(
expect.objectContaining({ method: "GET", path: discordChannelPath }),
);
expect(requests.every((request) => request.method === "GET")).toBe(true);
});
it("retains an accepted Gateway send result after normal CLI completion", async () => {
const turn = await createTurn("discord", { gatewaySend: true, bindCapability: false });
const heldSend = holdProviderRequest("POST");
const context = buildPreparedCliRunContext({
...turn.runParams,
backend: { command: "/bin/sh", args: [] },
});
context.params = turn.runParams;
context.preparedBackend.mcpClientGrantCapture = {
transportToken: turn.capture.token,
adoptProcessToken: vi.fn(),
revokeProcessToken: vi.fn(),
activate: (captureKey, assertCurrent) => {
activateMcpLoopbackClientGrantCapture({ ...turn.capture, captureKey, assertCurrent });
},
deactivate: (captureKey) => {
deactivateMcpLoopbackClientGrantCapture({ ...turn.capture, captureKey });
},
};
const tracking = createCliToolTracking(context);
const toolCallId = "accepted-gateway-send";
const args = {
action: "send",
channel: "discord",
target: `channel:${channels.discord.current}`,
message: "Preserve the accepted message.",
};
let pendingSend: ReturnType<typeof turn.call> | undefined;
let nativeSignal: AbortSignal | undefined;
let drain: Promise<void> | undefined;
const recordRunError = vi.fn();
try {
await expect(
runPlugin(
context,
async function* (execution) {
nativeSignal = execution.abortSignal;
tracking.handleCliToolUseStart({
toolCallId,
name: "mcp__openclaw__message",
kind: "mcp_tool_use",
args,
});
pendingSend = turn.call(args);
void pendingSend.catch(() => undefined);
await heldSend.entered();
yield { ...SUCCESS_RESULT, session_id: turn.runParams.sessionId };
},
{
sessionId: turn.runParams.sessionId,
activeToolCount: () => (pendingSend ? 1 : 0),
mcpCapture: {
captureKey: turn.capture.captureKey,
beginCapture: tracking.beginGatewayCapture,
},
},
),
).resolves.toMatchObject({ reason: "exit", exitCode: 0 });
expect(nativeSignal?.aborted).toBe(true);
drain = tracking.finishDeliveryTracking({
useManagedClaudeLiveSession: false,
recordRunError,
});
heldSend.release();
const response = await expectDefined(pendingSend, "accepted MCP message request");
await drain;
expect(response).toMatchObject({ result: { isError: false } });
expect(JSON.stringify(response)).toContain(discordMessage);
expect(tracking.resolveCliLoopbackTerminalOutcome(toolCallId)).toEqual({
outcome: "completed",
});
expect(tracking.withExecutionEvidence({ text: "completed" }).didSendViaMessagingTool).toBe(
true,
);
expect(recordRunError).not.toHaveBeenCalled();
expect(requests.filter((request) => request.method === "POST")).toHaveLength(1);
} finally {
heldSend.release();
await pendingSend?.catch(() => undefined);
await drain;
tracking.finalizeCapture(() => {});
}
expect(turn.isCurrent()).toBe(false);
});
});