




























@@ -40,6 +40,8 @@ let resetPluginRuntimeStateForTest: typeof import("./runtime.js").resetPluginRun
4040let setActivePluginRegistry: typeof import("./runtime.js").setActivePluginRegistry;
4141let clearCurrentPluginMetadataSnapshot: typeof import("./current-plugin-metadata-snapshot.js").clearCurrentPluginMetadataSnapshot;
4242let setCurrentPluginMetadataSnapshot: typeof import("./current-plugin-metadata-snapshot.js").setCurrentPluginMetadataSnapshot;
43+let getPluginRuntimeGatewayRequestScope: typeof import("./runtime/gateway-request-scope.js").getPluginRuntimeGatewayRequestScope;
44+let withPluginRuntimeGatewayRequestScope: typeof import("./runtime/gateway-request-scope.js").withPluginRuntimeGatewayRequestScope;
43454446function makeTool(name: string) {
4547return {
@@ -480,6 +482,8 @@ describe("resolvePluginTools optional tools", () => {
480482 resetPluginRuntimeStateForTest,
481483 setActivePluginRegistry,
482484} = await import("./runtime.js"));
485+({ getPluginRuntimeGatewayRequestScope, withPluginRuntimeGatewayRequestScope } =
486+await import("./runtime/gateway-request-scope.js"));
483487({ clearCurrentPluginMetadataSnapshot, setCurrentPluginMetadataSnapshot } =
484488await import("./current-plugin-metadata-snapshot.js"));
485489});
@@ -510,6 +514,155 @@ describe("resolvePluginTools optional tools", () => {
510514vi.useRealTimers();
511515});
512516517+it("runs plugin tool factories, prepare callbacks, and execute callbacks under the owning plugin scope", async () => {
518+const context = createContext();
519+const observed: Array<{
520+phase: "factory" | "prepare" | "execute";
521+pluginId?: string;
522+pluginSource?: string;
523+}> = [];
524+525+setRegistry(
526+["multi", "optional-demo"].map((pluginId) => ({
527+ pluginId,
528+optional: false,
529+source: `/tmp/${pluginId}.js`,
530+names: [`${pluginId}_tool`],
531+factory: () => {
532+const scope = getPluginRuntimeGatewayRequestScope();
533+observed.push({
534+phase: "factory",
535+pluginId: scope?.pluginId,
536+pluginSource: scope?.pluginSource,
537+});
538+return {
539+name: `${pluginId}_tool`,
540+description: `${pluginId} tool`,
541+parameters: { type: "object", properties: {} },
542+prepareArguments(args: unknown) {
543+const prepareScope = getPluginRuntimeGatewayRequestScope();
544+observed.push({
545+phase: "prepare",
546+pluginId: prepareScope?.pluginId,
547+pluginSource: prepareScope?.pluginSource,
548+});
549+return args;
550+},
551+async execute() {
552+const executeScope = getPluginRuntimeGatewayRequestScope();
553+observed.push({
554+phase: "execute",
555+pluginId: executeScope?.pluginId,
556+pluginSource: executeScope?.pluginSource,
557+});
558+return { content: [{ type: "text", text: pluginId }] };
559+},
560+};
561+},
562+})),
563+);
564+565+await withPluginRuntimeGatewayRequestScope(
566+{
567+pluginId: "outer",
568+pluginSource: "/tmp/outer.js",
569+isWebchatConnect: () => false,
570+},
571+async () => {
572+const tools = resolvePluginTools(createResolveToolsParams({ context }));
573+expect(tools.map((tool) => tool.name)).toEqual(["multi_tool", "optional-demo_tool"]);
574+for (const tool of tools) {
575+await tool.execute(`call-${tool.name}`, tool.prepareArguments?.({}) ?? {}, undefined);
576+expect(getPluginRuntimeGatewayRequestScope()).toMatchObject({
577+pluginId: "outer",
578+pluginSource: "/tmp/outer.js",
579+});
580+}
581+},
582+);
583+584+expect(getPluginRuntimeGatewayRequestScope()).toBeUndefined();
585+expect(observed).toEqual([
586+{ phase: "factory", pluginId: "multi", pluginSource: "/tmp/multi.js" },
587+{
588+phase: "factory",
589+pluginId: "optional-demo",
590+pluginSource: "/tmp/optional-demo.js",
591+},
592+{ phase: "prepare", pluginId: "multi", pluginSource: "/tmp/multi.js" },
593+{ phase: "execute", pluginId: "multi", pluginSource: "/tmp/multi.js" },
594+{
595+phase: "prepare",
596+pluginId: "optional-demo",
597+pluginSource: "/tmp/optional-demo.js",
598+},
599+{
600+phase: "execute",
601+pluginId: "optional-demo",
602+pluginSource: "/tmp/optional-demo.js",
603+},
604+]);
605+});
606+607+it("wraps every array tool callback and restores caller scope after errors", async () => {
608+const context = createContext();
609+const observed: Array<{ name: string; pluginId?: string; pluginSource?: string }> = [];
610+setRegistry([
611+{
612+pluginId: "multi",
613+optional: false,
614+source: "/tmp/multi.js",
615+names: ["array_first", "array_second"],
616+factory: () =>
617+["array_first", "array_second"].map((name) => ({
618+ name,
619+description: `${name} tool`,
620+parameters: { type: "object", properties: {} },
621+prepareArguments() {
622+const scope = getPluginRuntimeGatewayRequestScope();
623+observed.push({ name: `${name}:prepare`, pluginId: scope?.pluginId });
624+if (name === "array_second") {
625+throw new Error("bad args");
626+}
627+return {};
628+},
629+async execute() {
630+const scope = getPluginRuntimeGatewayRequestScope();
631+observed.push({
632+ name,
633+pluginId: scope?.pluginId,
634+pluginSource: scope?.pluginSource,
635+});
636+return { content: [{ type: "text", text: name }] };
637+},
638+})),
639+},
640+]);
641+642+await withPluginRuntimeGatewayRequestScope(
643+{
644+pluginId: "outer",
645+pluginSource: "/tmp/outer.js",
646+isWebchatConnect: () => false,
647+},
648+async () => {
649+const tools = resolvePluginTools(createResolveToolsParams({ context }));
650+await tools[0]?.execute("call-first", tools[0].prepareArguments?.({}) ?? {}, undefined);
651+expect(() => tools[1]?.prepareArguments?.({})).toThrow("bad args");
652+expect(getPluginRuntimeGatewayRequestScope()).toMatchObject({
653+pluginId: "outer",
654+pluginSource: "/tmp/outer.js",
655+});
656+},
657+);
658+659+expect(observed).toEqual([
660+{ name: "array_first:prepare", pluginId: "multi" },
661+{ name: "array_first", pluginId: "multi", pluginSource: "/tmp/multi.js" },
662+{ name: "array_second:prepare", pluginId: "multi" },
663+]);
664+});
665+513666it("does not load plugin-owned tools whose manifest metadata has no available signal", () => {
514667const config = createContext().config;
515668installToolManifestSnapshot({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。