
























@@ -23,6 +23,7 @@ type MockBeforeToolCallHookResult =
23232424type ScopedToolsCall = {
2525sessionId?: string;
26+onYield?: (message: string) => Promise<void> | void;
2627sessionKey?: string;
2728accountId?: string;
2829messageProvider?: string;
@@ -81,8 +82,10 @@ const resolveGatewayScopedToolsMock = vi.hoisted(() =>
8182})),
8283);
838485+const runtimeConfigMock = vi.hoisted(() => ({ session: { mainKey: "main" } }));
86+8487vi.mock("../config/io.js", () => ({
85-getRuntimeConfig: () => ({ session: { mainKey: "main" } }),
88+getRuntimeConfig: () => runtimeConfigMock,
8689}));
87908891vi.mock("../config/sessions.js", () => ({
@@ -106,6 +109,10 @@ import {
106109ensureMcpLoopbackServer,
107110startMcpLoopbackServer,
108111} from "./mcp-http.js";
112+import {
113+clearMcpLoopbackYieldContext,
114+registerMcpLoopbackYieldContext,
115+} from "./mcp-http.loopback-runtime.js";
109116import { McpLoopbackToolCache } from "./mcp-http.runtime.js";
110117111118let server: Awaited<ReturnType<typeof startMcpLoopbackServer>> | undefined;
@@ -729,6 +736,71 @@ describe("mcp loopback server", () => {
729736expect(getScopedToolsCall(1).sessionId).toBe("run-session-b");
730737});
731738739+it("refreshes cached yield tools for consecutive runs with the same session id", async () => {
740+const { runtime } = await startLoopbackServerForTest();
741+resolveGatewayScopedToolsMock.mockImplementation((params): MockGatewayScopedTools => {
742+const call = params as ScopedToolsCall;
743+return {
744+agentId: "main",
745+tools: [
746+{
747+name: "sessions_yield",
748+description: "yield current run",
749+parameters: { type: "object", properties: {} },
750+execute: async (_toolCallId, args) => {
751+const message = (args as { message?: string }).message ?? "yielded";
752+await Promise.resolve(call.onYield?.(message));
753+return {
754+content: [{ type: "text", text: JSON.stringify({ status: "yielded", message }) }],
755+};
756+},
757+},
758+],
759+};
760+});
761+const headers = {
762+"x-session-key": "agent:main:telegram:group:chat123",
763+"x-openclaw-session-id": "run-session-reused",
764+"x-openclaw-message-channel": "telegram",
765+};
766+767+const firstContext = registerMcpLoopbackYieldContext("run-session-reused");
768+try {
769+expect(
770+(
771+await sendLoopbackToolCall({
772+token: runtime?.ownerToken,
773+name: "sessions_yield",
774+args: { message: "first yield" },
775+ headers,
776+})
777+).status,
778+).toBe(200);
779+expect(firstContext).toMatchObject({ yielded: true, message: "first yield" });
780+} finally {
781+clearMcpLoopbackYieldContext("run-session-reused", firstContext);
782+}
783+784+const secondContext = registerMcpLoopbackYieldContext("run-session-reused");
785+try {
786+expect(
787+(
788+await sendLoopbackToolCall({
789+token: runtime?.ownerToken,
790+name: "sessions_yield",
791+args: { message: "second yield" },
792+ headers,
793+})
794+).status,
795+).toBe(200);
796+expect(secondContext).toMatchObject({ yielded: true, message: "second yield" });
797+} finally {
798+clearMcpLoopbackYieldContext("run-session-reused", secondContext);
799+}
800+801+expect(resolveGatewayScopedToolsMock).toHaveBeenCalledTimes(2);
802+});
803+732804it("keeps loopback tool cache entries separate by inbound event kind, delivery mode, and inbound audio", async () => {
733805const { runtime } = await startLoopbackServerForTest();
734806const sendToolsList = async (
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。