






















1+import fs from "node:fs/promises";
12/**
23 * Integration-style tests for before_tool_call behavior.
34 * Covers loop detection, diagnostics, plugin approval, and skill telemetry
@@ -27,6 +28,7 @@ import {
2728runBeforeToolCallHook,
2829wrapToolWithBeforeToolCallHook,
2930} from "./agent-tools.before-tool-call.js";
31+import { createOpenClawCodingTools } from "./agent-tools.js";
3032import { CRITICAL_THRESHOLD } from "./tool-loop-detection.js";
3133import type { AnyAgentTool } from "./tools/common.js";
3234import { callGatewayTool } from "./tools/gateway.js";
@@ -1844,6 +1846,106 @@ describe("before_tool_call requireApproval handling", () => {
1844184618451847expect(onResolution).toHaveBeenCalledWith("cancelled");
18461848});
1849+1850+it("forwards turn source routing fields from ctx to plugin.approval.request", async () => {
1851+hookRunner.runBeforeToolCall.mockResolvedValue({
1852+requireApproval: {
1853+title: "Channel-routed approval",
1854+description: "Must route to telegram",
1855+pluginId: "my-plugin",
1856+},
1857+});
1858+1859+mockCallGateway.mockResolvedValueOnce({ id: "route-id-1", status: "accepted" });
1860+mockCallGateway.mockResolvedValueOnce({ id: "route-id-1", decision: "allow-once" });
1861+1862+await runBeforeToolCallHook({
1863+toolName: "fetch",
1864+params: { url: "https://example.com" },
1865+ctx: {
1866+agentId: "main",
1867+sessionKey: "main",
1868+turnSourceChannel: "telegram",
1869+turnSourceTo: "-100123456789",
1870+turnSourceAccountId: "acct-42",
1871+turnSourceThreadId: 9001,
1872+},
1873+});
1874+1875+const requestCall = requireGatewayCall(0);
1876+expect(requestCall[0]).toBe("plugin.approval.request");
1877+const requestParams = requireRecord(requestCall[2], "approval request params");
1878+expect(requestParams.turnSourceChannel).toBe("telegram");
1879+expect(requestParams.turnSourceTo).toBe("-100123456789");
1880+expect(requestParams.turnSourceAccountId).toBe("acct-42");
1881+expect(requestParams.turnSourceThreadId).toBe(9001);
1882+});
1883+1884+it("uses the transport channel when tool policy provider differs", async () => {
1885+hookRunner.runBeforeToolCall.mockResolvedValue({
1886+requireApproval: {
1887+title: "Transport routed approval",
1888+description: "Must use the transport channel",
1889+pluginId: "my-plugin",
1890+},
1891+});
1892+1893+const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-hook-route-"));
1894+await fs.writeFile(path.join(tempDir, "note.txt"), "hello");
1895+mockCallGateway.mockResolvedValueOnce({ id: "transport-route-id", status: "accepted" });
1896+mockCallGateway.mockResolvedValueOnce({
1897+id: "transport-route-id",
1898+decision: "allow-once",
1899+});
1900+1901+const tools = createOpenClawCodingTools({
1902+workspaceDir: tempDir,
1903+messageProvider: "discord-voice",
1904+messageChannel: "discord",
1905+currentChannelId: "native-channel-1",
1906+currentMessagingTarget: "channel:deliverable-1",
1907+agentAccountId: "acct-1",
1908+currentThreadTs: "thread-1",
1909+});
1910+const readTool = tools.find((tool) => tool.name === "read");
1911+if (!readTool) {
1912+throw new Error("missing read tool");
1913+}
1914+await readTool.execute("tool-hook-route", { path: "note.txt" }, undefined, undefined);
1915+1916+const requestCall = requireGatewayCall(0);
1917+expect(requestCall[0]).toBe("plugin.approval.request");
1918+const requestParams = requireRecord(requestCall[2], "approval request params");
1919+expect(requestParams.turnSourceChannel).toBe("discord");
1920+expect(requestParams.turnSourceTo).toBe("channel:deliverable-1");
1921+expect(requestParams.turnSourceAccountId).toBe("acct-1");
1922+expect(requestParams.turnSourceThreadId).toBe("thread-1");
1923+});
1924+1925+it("omits turn source routing fields when ctx does not carry them", async () => {
1926+hookRunner.runBeforeToolCall.mockResolvedValue({
1927+requireApproval: {
1928+title: "No route ctx",
1929+description: "Local-only approval",
1930+},
1931+});
1932+1933+mockCallGateway.mockResolvedValueOnce({ id: "no-route-id", status: "accepted" });
1934+mockCallGateway.mockResolvedValueOnce({ id: "no-route-id", decision: "allow-once" });
1935+1936+await runBeforeToolCallHook({
1937+toolName: "bash",
1938+params: {},
1939+ctx: { agentId: "main", sessionKey: "main" },
1940+});
1941+1942+const requestCall = requireGatewayCall(0);
1943+const requestParams = requireRecord(requestCall[2], "approval request params");
1944+expect(requestParams.turnSourceChannel).toBeUndefined();
1945+expect(requestParams.turnSourceTo).toBeUndefined();
1946+expect(requestParams.turnSourceAccountId).toBeUndefined();
1947+expect(requestParams.turnSourceThreadId).toBeUndefined();
1948+});
18471949});
1848195018491951describe("before_tool_call tool content private-data capture", () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。