






























@@ -26,6 +26,7 @@ vi.mock("../config/plugin-auto-enable.js", () => ({
26262727let resolvePluginTools: typeof import("./tools.js").resolvePluginTools;
2828let buildPluginToolMetadataKey: typeof import("./tools.js").buildPluginToolMetadataKey;
29+let resetPluginToolFactoryCache: typeof import("./tools.js").resetPluginToolFactoryCache;
2930let pinActivePluginChannelRegistry: typeof import("./runtime.js").pinActivePluginChannelRegistry;
3031let resetPluginRuntimeStateForTest: typeof import("./runtime.js").resetPluginRuntimeStateForTest;
3132let setActivePluginRegistry: typeof import("./runtime.js").setActivePluginRegistry;
@@ -58,14 +59,15 @@ function createContext() {
5859}
59606061function createResolveToolsParams(params?: {
62+context?: ReturnType<typeof createContext> & Record<string, unknown>;
6163toolAllowlist?: readonly string[];
6264existingToolNames?: Set<string>;
6365env?: NodeJS.ProcessEnv;
6466suppressNameConflicts?: boolean;
6567allowGatewaySubagentBinding?: boolean;
6668}) {
6769return {
68-context: createContext() as never,
70+context: (params?.context ?? createContext()) as never,
6971 ...(params?.toolAllowlist ? { toolAllowlist: [...params.toolAllowlist] } : {}),
7072 ...(params?.existingToolNames ? { existingToolNames: params.existingToolNames } : {}),
7173 ...(params?.env ? { env: params.env } : {}),
@@ -360,7 +362,8 @@ function expectConflictingCoreNameResolution(params: {
360362361363describe("resolvePluginTools optional tools", () => {
362364beforeAll(async () => {
363-({ buildPluginToolMetadataKey, resolvePluginTools } = await import("./tools.js"));
365+({ buildPluginToolMetadataKey, resetPluginToolFactoryCache, resolvePluginTools } =
366+await import("./tools.js"));
364367({ pinActivePluginChannelRegistry, resetPluginRuntimeStateForTest, setActivePluginRegistry } =
365368await import("./runtime.js"));
366369({ clearCurrentPluginMetadataSnapshot, setCurrentPluginMetadataSnapshot } =
@@ -380,11 +383,13 @@ describe("resolvePluginTools optional tools", () => {
380383}));
381384resetPluginRuntimeStateForTest?.();
382385clearCurrentPluginMetadataSnapshot?.();
386+resetPluginToolFactoryCache?.();
383387});
384388385389afterEach(() => {
386390resetPluginRuntimeStateForTest?.();
387391clearCurrentPluginMetadataSnapshot?.();
392+resetPluginToolFactoryCache?.();
388393setLoggerOverride(null);
389394loggingState.rawConsole = null;
390395resetLogger();
@@ -812,6 +817,163 @@ describe("resolvePluginTools optional tools", () => {
812817expect(warnSpy).not.toHaveBeenCalled();
813818});
814819820+it("caches plugin tool factory results for equivalent request context", () => {
821+const factory = vi.fn(() => makeTool("cached_tool"));
822+setRegistry([
823+{
824+pluginId: "cache-test",
825+optional: false,
826+source: "/tmp/cache-test.js",
827+names: ["cached_tool"],
828+ factory,
829+},
830+]);
831+832+const first = resolvePluginTools(createResolveToolsParams({ context: createContext() }));
833+const second = resolvePluginTools(createResolveToolsParams({ context: createContext() }));
834+835+expectResolvedToolNames(first, ["cached_tool"]);
836+expectResolvedToolNames(second, ["cached_tool"]);
837+expect(factory).toHaveBeenCalledTimes(1);
838+expect(second[0]).toBe(first[0]);
839+});
840+841+it("does not reuse plugin tool factory results across sandbox context changes", () => {
842+const factory = vi.fn((rawCtx: unknown) => {
843+const ctx = rawCtx as { sandboxed?: boolean };
844+return ctx.sandboxed ? null : makeTool("sandbox_sensitive_tool");
845+});
846+setRegistry([
847+{
848+pluginId: "sandbox-sensitive",
849+optional: false,
850+source: "/tmp/sandbox-sensitive.js",
851+names: ["sandbox_sensitive_tool"],
852+ factory,
853+},
854+]);
855+856+const hostTools = resolvePluginTools(
857+createResolveToolsParams({
858+context: { ...createContext(), sandboxed: false },
859+}),
860+);
861+const sandboxedTools = resolvePluginTools(
862+createResolveToolsParams({
863+context: { ...createContext(), sandboxed: true },
864+}),
865+);
866+867+expectResolvedToolNames(hostTools, ["sandbox_sensitive_tool"]);
868+expect(sandboxedTools).toEqual([]);
869+expect(factory).toHaveBeenCalledTimes(2);
870+});
871+872+it("does not reuse plugin tool factory results across runtime config changes", () => {
873+const firstRuntimeConfig = {
874+ ...createContext().config,
875+plugins: { ...createContext().config.plugins, allow: ["runtime_sensitive_tool"] },
876+};
877+const secondRuntimeConfig = {
878+ ...createContext().config,
879+plugins: { ...createContext().config.plugins, allow: ["runtime_sensitive_next_tool"] },
880+};
881+const factory = vi.fn((rawCtx: unknown) => {
882+const ctx = rawCtx as { runtimeConfig?: { plugins?: { allow?: string[] } } };
883+return makeTool(ctx.runtimeConfig?.plugins?.allow?.[0] ?? "runtime_missing_tool");
884+});
885+setRegistry([
886+{
887+pluginId: "runtime-sensitive",
888+optional: false,
889+source: "/tmp/runtime-sensitive.js",
890+names: ["runtime_sensitive_tool", "runtime_sensitive_next_tool"],
891+ factory,
892+},
893+]);
894+895+const first = resolvePluginTools(
896+createResolveToolsParams({
897+context: { ...createContext(), runtimeConfig: firstRuntimeConfig as never },
898+}),
899+);
900+const second = resolvePluginTools(
901+createResolveToolsParams({
902+context: { ...createContext(), runtimeConfig: secondRuntimeConfig as never },
903+}),
904+);
905+906+expectResolvedToolNames(first, ["runtime_sensitive_tool"]);
907+expectResolvedToolNames(second, ["runtime_sensitive_next_tool"]);
908+expect(factory).toHaveBeenCalledTimes(2);
909+});
910+911+it("reuses plugin tool factory results when only runtime config getter identity changes", () => {
912+const runtimeConfig = {
913+ ...createContext().config,
914+plugins: { ...createContext().config.plugins, allow: ["getter_sensitive_tool"] },
915+};
916+const factory = vi.fn((rawCtx: unknown) => {
917+const ctx = rawCtx as { getRuntimeConfig?: () => { plugins?: { allow?: string[] } } };
918+return makeTool(ctx.getRuntimeConfig?.()?.plugins?.allow?.[0] ?? "getter_missing_tool");
919+});
920+setRegistry([
921+{
922+pluginId: "getter-sensitive",
923+optional: false,
924+source: "/tmp/getter-sensitive.js",
925+names: ["getter_sensitive_tool"],
926+ factory,
927+},
928+]);
929+930+const context = createContext();
931+const first = resolvePluginTools(
932+createResolveToolsParams({
933+context: { ...context, getRuntimeConfig: () => runtimeConfig as never },
934+}),
935+);
936+const second = resolvePluginTools(
937+createResolveToolsParams({
938+context: { ...context, getRuntimeConfig: () => runtimeConfig as never },
939+}),
940+);
941+942+expectResolvedToolNames(first, ["getter_sensitive_tool"]);
943+expectResolvedToolNames(second, ["getter_sensitive_tool"]);
944+expect(factory).toHaveBeenCalledTimes(1);
945+});
946+947+it("reads live runtime config once per plugin tool resolution for cache keys", () => {
948+const runtimeConfig = createContext().config;
949+const getRuntimeConfig = vi.fn(() => runtimeConfig);
950+setRegistry([
951+{
952+pluginId: "getter-a",
953+optional: false,
954+source: "/tmp/getter-a.js",
955+names: ["getter_a_tool"],
956+factory: () => makeTool("getter_a_tool"),
957+},
958+{
959+pluginId: "getter-b",
960+optional: false,
961+source: "/tmp/getter-b.js",
962+names: ["getter_b_tool"],
963+factory: () => makeTool("getter_b_tool"),
964+},
965+]);
966+967+const tools = resolvePluginTools(
968+createResolveToolsParams({
969+context: { ...createContext(), getRuntimeConfig: getRuntimeConfig as never },
970+}),
971+);
972+973+expectResolvedToolNames(tools, ["getter_a_tool", "getter_b_tool"]);
974+expect(getRuntimeConfig).toHaveBeenCalledTimes(1);
975+});
976+815977it("skips factory-returned tools outside the manifest tool contract", () => {
816978const registry = setRegistry([
817979{
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。