




















@@ -18,6 +18,25 @@ type MockBeforeToolCallHookResult =
1818| { blocked: true; reason: string }
1919| { blocked: false; params: unknown };
202021+type ScopedToolsCall = {
22+sessionKey?: string;
23+accountId?: string;
24+messageProvider?: string;
25+senderIsOwner?: boolean;
26+surface?: string;
27+};
28+29+type BeforeToolCallHookInput = {
30+toolName?: string;
31+params?: unknown;
32+ctx?: {
33+agentId?: string;
34+config?: unknown;
35+sessionKey?: string;
36+};
37+signal?: unknown;
38+};
39+2140const runBeforeToolCallHookMock = vi.hoisted(() =>
2241vi.fn(
2342async (args: { params: unknown }): Promise<MockBeforeToolCallHookResult> => ({
@@ -88,6 +107,22 @@ async function sendRaw(params: {
88107});
89108}
90109110+function getScopedToolsCall(index: number): ScopedToolsCall {
111+const call = resolveGatewayScopedToolsMock.mock.calls[index]?.[0];
112+if (typeof call !== "object" || call === null) {
113+throw new Error(`Expected scoped tools call ${index} to receive an options object`);
114+}
115+return call as ScopedToolsCall;
116+}
117+118+function getBeforeToolCallHookInput(index: number): BeforeToolCallHookInput {
119+const call = runBeforeToolCallHookMock.mock.calls[index]?.[0];
120+if (typeof call !== "object" || call === null) {
121+throw new Error(`Expected before-tool-call hook ${index} to receive an input object`);
122+}
123+return call as BeforeToolCallHookInput;
124+}
125+91126beforeEach(() => {
92127resolveGatewayScopedToolsMock.mockClear();
93128runBeforeToolCallHookMock.mockClear();
@@ -139,15 +174,12 @@ describe("mcp loopback server", () => {
139174});
140175141176expect(response.status).toBe(200);
142-expect(resolveGatewayScopedToolsMock).toHaveBeenCalledWith(
143-expect.objectContaining({
144-sessionKey: "agent:main:telegram:group:chat123",
145-accountId: "work",
146-messageProvider: "telegram",
147-senderIsOwner: false,
148-surface: "loopback",
149-}),
150-);
177+const call = getScopedToolsCall(0);
178+expect(call.sessionKey).toBe("agent:main:telegram:group:chat123");
179+expect(call.accountId).toBe("work");
180+expect(call.messageProvider).toBe("telegram");
181+expect(call.senderIsOwner).toBe(false);
182+expect(call.surface).toBe("loopback");
151183});
152184153185it("adds empty properties for object schemas that omit properties", async () => {
@@ -210,24 +242,17 @@ describe("mcp loopback server", () => {
210242expect((await sendToolsList("false")).status).toBe(200);
211243212244expect(resolveGatewayScopedToolsMock).toHaveBeenCalledTimes(2);
213-expect(resolveGatewayScopedToolsMock).toHaveBeenNthCalledWith(
214-1,
215-expect.objectContaining({
216-sessionKey: "agent:main:matrix:dm:test",
217-messageProvider: "matrix",
218-senderIsOwner: true,
219-surface: "loopback",
220-}),
221-);
222-expect(resolveGatewayScopedToolsMock).toHaveBeenNthCalledWith(
223-2,
224-expect.objectContaining({
225-sessionKey: "agent:main:matrix:dm:test",
226-messageProvider: "matrix",
227-senderIsOwner: false,
228-surface: "loopback",
229-}),
230-);
245+const ownerCall = getScopedToolsCall(0);
246+expect(ownerCall.sessionKey).toBe("agent:main:matrix:dm:test");
247+expect(ownerCall.messageProvider).toBe("matrix");
248+expect(ownerCall.senderIsOwner).toBe(true);
249+expect(ownerCall.surface).toBe("loopback");
250+251+const nonOwnerCall = getScopedToolsCall(1);
252+expect(nonOwnerCall.sessionKey).toBe("agent:main:matrix:dm:test");
253+expect(nonOwnerCall.messageProvider).toBe("matrix");
254+expect(nonOwnerCall.senderIsOwner).toBe(false);
255+expect(nonOwnerCall.surface).toBe("loopback");
231256});
232257233258it("ignores spoofed owner headers when the bearer token is non-owner scoped", async () => {
@@ -247,14 +272,11 @@ describe("mcp loopback server", () => {
247272});
248273249274expect(response.status).toBe(200);
250-expect(resolveGatewayScopedToolsMock).toHaveBeenCalledWith(
251-expect.objectContaining({
252-sessionKey: "agent:main:matrix:dm:test",
253-messageProvider: "matrix",
254-senderIsOwner: false,
255-surface: "loopback",
256-}),
257-);
275+const call = getScopedToolsCall(0);
276+expect(call.sessionKey).toBe("agent:main:matrix:dm:test");
277+expect(call.messageProvider).toBe("matrix");
278+expect(call.senderIsOwner).toBe(false);
279+expect(call.surface).toBe("loopback");
258280});
259281260282it("filters owner-only tools from non-owner tool lists", async () => {
@@ -406,7 +428,7 @@ describe("mcp loopback server", () => {
406428});
407429408430it("honors before-tool-call hook blocks before loopback tool execution", async () => {
409-const execute = vi.fn(async () => ({
431+const execute = vi.fn<MockGatewayTool["execute"]>(async () => ({
410432content: [{ type: "text", text: "EXECUTED" }],
411433}));
412434runBeforeToolCallHookMock.mockResolvedValueOnce({
@@ -446,25 +468,20 @@ describe("mcp loopback server", () => {
446468};
447469448470expect(response.status).toBe(200);
449-expect(runBeforeToolCallHookMock).toHaveBeenCalledWith(
450-expect.objectContaining({
451-toolName: "message",
452-params: { body: "hello" },
453-ctx: expect.objectContaining({
454-agentId: "main",
455-config: { session: { mainKey: "main" } },
456-sessionKey: "agent:main:main",
457-}),
458-signal: expect.any(AbortSignal),
459-}),
460-);
471+const hookInput = getBeforeToolCallHookInput(0);
472+expect(hookInput.toolName).toBe("message");
473+expect(hookInput.params).toEqual({ body: "hello" });
474+expect(hookInput.ctx?.agentId).toBe("main");
475+expect(hookInput.ctx?.config).toEqual({ session: { mainKey: "main" } });
476+expect(hookInput.ctx?.sessionKey).toBe("agent:main:main");
477+expect(hookInput.signal).toBeInstanceOf(AbortSignal);
461478expect(execute).not.toHaveBeenCalled();
462479expect(payload.result?.isError).toBe(true);
463480expect(payload.result?.content?.[0]?.text).toBe("blocked by hook");
464481});
465482466483it("forwards the request abort signal to loopback tool execution", async () => {
467-const execute = vi.fn(async () => ({
484+const execute = vi.fn<MockGatewayTool["execute"]>(async () => ({
468485content: [{ type: "text", text: "EXECUTED" }],
469486}));
470487resolveGatewayScopedToolsMock.mockReturnValue({
@@ -501,11 +518,11 @@ describe("mcp loopback server", () => {
501518502519expect(response.status).toBe(200);
503520expect(payload.result?.isError).toBe(false);
504-expect(execute).toHaveBeenCalledWith(
505- expect.stringMatching(/^mcp-/),
506- { body: "hello" },
507- expect.any(AbortSignal),
508-);
521+expect(execute).toHaveBeenCalledTimes(1);
522+const [callId, params, signal] = execute.mock.calls[0] ?? [];
523+expect(callId).toMatch(/^mcp-/);
524+expect(params).toEqual({ body: "hello" });
525+expect(signal).toBeInstanceOf(AbortSignal);
509526});
510527511528it("tracks the active runtime only while the server is running", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。