import fs from "node:fs";
import path from "node:path";
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import { listAgentHarnessIds } from "../agents/harness/registry.js";
import { resolveConfigEnvVars } from "../config/env-substitution.js";
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
import {
  clearRuntimeConfigSnapshot,
  setRuntimeConfigSnapshot,
} from "../config/runtime-snapshot.js";
import { getContextEngineFactory, listContextEngineIds } from "../context-engine/registry.js";
import {
  clearInternalHooks,
  createInternalHookEvent,
  getRegisteredEventKeys,
  triggerInternalHook,
} from "../hooks/internal-hooks.js";
import {
  emitDiagnosticEvent,
  resetDiagnosticEventsForTest,
  waitForDiagnosticEventsDrained,
} from "../infra/diagnostic-events.js";
import {
  clearDetachedTaskLifecycleRuntimeRegistration,
  getDetachedTaskLifecycleRuntimeRegistration,
  registerDetachedTaskLifecycleRuntime,
  type DetachedTaskLifecycleRuntime,
} from "../tasks/detached-task-runtime-state.js";
import { withEnv } from "../test-utils/env.js";
import { buildPluginApi } from "./api-builder.js";
import { clearPluginCommands } from "./command-registry-state.js";
import { getPluginCommandSpecs } from "./command-specs.js";
import { listCompactionProviderIds } from "./compaction-provider.js";
import {
  getEmbeddingProvider,
  listEmbeddingProviders,
  registerEmbeddingProvider,
} from "./embedding-providers.js";
import {
  getGlobalHookRunner,
  getGlobalPluginRegistry,
  resetGlobalHookRunner,
} from "./hook-runner-global.js";
import { createHookRunner } from "./hooks.js";
import { writePersistedInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-records.js";
import {
  clearPluginInteractiveHandlerRegistrations,
  clearPluginInteractiveHandlers,
  resolvePluginInteractiveNamespaceMatch,
} from "./interactive-registry.js";
import {
  claimPluginInteractiveCallbackDedupe,
  commitPluginInteractiveCallbackDedupe,
} from "./interactive-state.js";
import {
  testing,
  clearPluginLoaderCache,
  loadOpenClawPlugins,
  type PluginLoadOptions,
  PluginLoadReentryError,
  resolveRuntimePluginRegistry,
} from "./loader.js";
import {
  cleanupPluginLoaderFixturesForTest,
  EMPTY_PLUGIN_SCHEMA,
  makeTempDir,
  mkdirSafe,
  type PluginLoadConfig,
  type PluginRegistry,
  resetPluginLoaderTestStateForTest,
  type TempPlugin,
  useNoBundledPlugins,
  writePlugin,
} from "./loader.test-fixtures.js";
import { loadPluginManifestRegistry } from "./manifest-registry.js";
import {
  listMemoryEmbeddingProviders,
  registerMemoryEmbeddingProvider,
} from "./memory-embedding-providers.js";
import {
  buildMemoryPromptSection,
  clearMemoryPluginState,
  getMemoryCapabilityRegistration,
  getMemoryRuntime,
  listActiveMemoryPublicArtifacts,
  listMemoryCorpusSupplements,
  listMemoryPromptSupplements,
  registerMemoryCapability,
  registerMemoryCorpusSupplement,
  registerMemoryPromptSupplement,
  resolveMemoryFlushPlan,
} from "./memory-state.js";
import { ensureOpenClawPluginSdkAlias } from "./plugin-sdk-dist-alias.js";
import { createEmptyPluginRegistry } from "./registry.js";
import {
  getActivePluginRegistry,
  getActivePluginRegistryKey,
  listImportedRuntimePluginIds,
  setActivePluginRegistry,
} from "./runtime.js";
import {
  testing as runtimeRegistryLoaderTesting,
  ensurePluginRegistryLoaded,
} from "./runtime/runtime-registry-loader.js";
import type { PluginSdkResolutionPreference } from "./sdk-alias.js";
let cachedBundledTelegramDir = "";
let cachedBundledMemoryDir = "";

type GlobalHookRunner = NonNullable<ReturnType<typeof getGlobalHookRunner>>;
type PluginStartupTraceDetail = {
  name: string;
  metrics: ReadonlyArray<readonly [string, number | string]>;
};

function countMatching<T>(items: readonly T[], predicate: (item: T) => boolean): number {
  let count = 0;
  for (const item of items) {
    if (predicate(item)) {
      count += 1;
    }
  }
  return count;
}

function expectGlobalHookRunner(runner: ReturnType<typeof getGlobalHookRunner>): GlobalHookRunner {
  if (runner === null) {
    throw new Error("Expected global hook runner");
  }
  expect(typeof runner.hasHooks).toBe("function");
  return runner;
}

function createDetachedTaskRuntimeStub(id: string): DetachedTaskLifecycleRuntime {
  const fail = (name: string): never => {
    throw new Error(`detached runtime ${id} should not execute ${name} in this test`);
  };
  return {
    createQueuedTaskRun: () => fail("createQueuedTaskRun"),
    createRunningTaskRun: () => fail("createRunningTaskRun"),
    startTaskRunByRunId: () => fail("startTaskRunByRunId"),
    recordTaskRunProgressByRunId: () => fail("recordTaskRunProgressByRunId"),
    finalizeTaskRunByRunId: () => fail("finalizeTaskRunByRunId"),
    completeTaskRunByRunId: () => fail("completeTaskRunByRunId"),
    failTaskRunByRunId: () => fail("failTaskRunByRunId"),
    setDetachedTaskDeliveryStatusByRunId: () => fail("setDetachedTaskDeliveryStatusByRunId"),
    cancelDetachedTaskRunById: async () => ({
      found: true,
      cancelled: true,
    }),
  };
}

const BUNDLED_TELEGRAM_PLUGIN_BODY = `module.exports = {
  id: "telegram",
  register(api) {
    api.registerChannel({
      plugin: {
        id: "telegram",
        meta: {
          id: "telegram",
          label: "Telegram",
          selectionLabel: "Telegram",
          docsPath: "/channels/telegram",
          blurb: "telegram channel",
        },
        capabilities: { chatTypes: ["direct"] },
        config: {
          listAccountIds: () => [],
          resolveAccount: () => ({ accountId: "default" }),
        },
        outbound: { deliveryMode: "direct" },
      },
    });
  },
};`;

function simplePluginBody(id: string) {
  return `module.exports = { id: ${JSON.stringify(id)}, register() {} };`;
}

function updatePluginManifest(plugin: Pick<TempPlugin, "dir">, patch: Record<string, unknown>) {
  const manifestPath = path.join(plugin.dir, "openclaw.plugin.json");
  const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as Record<string, unknown>;
  fs.writeFileSync(manifestPath, JSON.stringify({ ...raw, ...patch }, null, 2), "utf-8");
}

function memoryPluginBody(id: string) {
  return `module.exports = { id: ${JSON.stringify(id)}, kind: "memory", register() {} };`;
}

const RESERVED_ADMIN_PLUGIN_METHOD = "config.plugin.inspect";
const RESERVED_ADMIN_SCOPE_WARNING =
  "gateway method scope coerced to operator.admin for reserved core namespace";

function writeBundledPlugin(params: {
  id: string;
  body?: string;
  filename?: string;
  bundledDir?: string;
}) {
  const bundledDir = params.bundledDir ?? makeTempDir();
  const plugin = writePlugin({
    id: params.id,
    dir: bundledDir,
    filename: params.filename ?? "index.cjs",
    body: params.body ?? simplePluginBody(params.id),
  });
  delete process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
  process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir;
  return { bundledDir, plugin };
}

function writeWorkspacePlugin(params: {
  id: string;
  body?: string;
  filename?: string;
  workspaceDir?: string;
}) {
  const workspaceDir = params.workspaceDir ?? makeTempDir();
  const workspacePluginDir = path.join(workspaceDir, ".openclaw", "extensions", params.id);
  mkdirSafe(workspacePluginDir);
  const plugin = writePlugin({
    id: params.id,
    dir: workspacePluginDir,
    filename: params.filename ?? "index.cjs",
    body: params.body ?? simplePluginBody(params.id),
  });
  return { workspaceDir, workspacePluginDir, plugin };
}

function withStateDir<T>(run: (stateDir: string) => T) {
  const stateDir = makeTempDir();
  return withEnv({ OPENCLAW_STATE_DIR: stateDir }, () => run(stateDir));
}

function loadBundledMemoryPluginRegistry(options?: {
  packageMeta?: { name: string; version: string; description?: string };
  pluginBody?: string;
  pluginFilename?: string;
}) {
  if (!options && cachedBundledMemoryDir) {
    process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = cachedBundledMemoryDir;
    return loadOpenClawPlugins({
      cache: false,
      workspaceDir: cachedBundledMemoryDir,
      config: {
        plugins: {
          slots: {
            memory: "memory-core",
          },
        },
      },
    });
  }

  const bundledDir = makeTempDir();
  let pluginDir = bundledDir;
  let pluginFilename = options?.pluginFilename ?? "memory-core.cjs";

  if (options?.packageMeta) {
    pluginDir = path.join(bundledDir, "memory-core");
    pluginFilename = options.pluginFilename ?? "index.js";
    mkdirSafe(pluginDir);
    fs.writeFileSync(
      path.join(pluginDir, "package.json"),
      JSON.stringify(
        {
          name: options.packageMeta.name,
          version: options.packageMeta.version,
          description: options.packageMeta.description,
          openclaw: { extensions: [`./${pluginFilename}`] },
        },
        null,
        2,
      ),
      "utf-8",
    );
  }

  writePlugin({
    id: "memory-core",
    body:
      options?.pluginBody ??
      `module.exports = { id: "memory-core", kind: "memory", register() {} };`,
    dir: pluginDir,
    filename: pluginFilename,
  });
  if (!options) {
    cachedBundledMemoryDir = bundledDir;
  }
  process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir;

  return loadOpenClawPlugins({
    cache: false,
    workspaceDir: bundledDir,
    config: {
      plugins: {
        slots: {
          memory: "memory-core",
        },
      },
    },
  });
}

function setupBundledTelegramPlugin() {
  if (!cachedBundledTelegramDir) {
    cachedBundledTelegramDir = makeTempDir();
    writePlugin({
      id: "telegram",
      body: BUNDLED_TELEGRAM_PLUGIN_BODY,
      dir: cachedBundledTelegramDir,
      filename: "telegram.cjs",
    });
  }
  process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = cachedBundledTelegramDir;
}

function expectTelegramLoaded(registry: ReturnType<typeof loadOpenClawPlugins>) {
  const telegram = registry.plugins.find((entry) => entry.id === "telegram");
  expect(telegram?.status).toBe("loaded");
  expect(registry.channels.map((entry) => entry.plugin.id)).toContain("telegram");
}

function loadRegistryFromSinglePlugin(params: {
  plugin: TempPlugin;
  pluginConfig?: Record<string, unknown>;
  includeWorkspaceDir?: boolean;
  options?: Omit<Parameters<typeof loadOpenClawPlugins>[0], "cache" | "workspaceDir" | "config">;
}) {
  const pluginConfig = params.pluginConfig ?? {};
  return loadOpenClawPlugins({
    cache: false,
    ...(params.includeWorkspaceDir === false ? {} : { workspaceDir: params.plugin.dir }),
    ...params.options,
    config: {
      plugins: {
        load: { paths: [params.plugin.file] },
        ...pluginConfig,
      },
    },
  });
}

function loadRegistryFromAllowedPlugins(
  plugins: TempPlugin[],
  options?: Omit<Parameters<typeof loadOpenClawPlugins>[0], "cache" | "config">,
) {
  return loadOpenClawPlugins({
    cache: false,
    ...options,
    config: {
      plugins: {
        load: { paths: plugins.map((plugin) => plugin.file) },
        allow: plugins.map((plugin) => plugin.id),
      },
    },
  });
}

function runRegistryScenarios<
  T extends { assert: (registry: PluginRegistry, scenario: T) => void },
>(scenarios: readonly T[], loadRegistry: (scenario: T) => PluginRegistry) {
  for (const scenario of scenarios) {
    scenario.assert(loadRegistry(scenario), scenario);
  }
}

function runScenarioCases<T>(scenarios: readonly T[], run: (scenario: T) => void) {
  for (const scenario of scenarios) {
    run(scenario);
  }
}

function runSinglePluginRegistryScenarios<
  T extends {
    pluginId: string;
    body: string;
    assert: (registry: PluginRegistry, scenario: T) => void;
  },
>(scenarios: readonly T[], resolvePluginConfig?: (scenario: T) => Record<string, unknown>) {
  runRegistryScenarios(scenarios, (scenario) => {
    const plugin = writePlugin({
      id: scenario.pluginId,
      filename: `${scenario.pluginId}.cjs`,
      body: scenario.body,
    });
    return loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: resolvePluginConfig?.(scenario) ?? { allow: [scenario.pluginId] },
    });
  });
}

function loadRegistryFromScenarioPlugins(plugins: readonly TempPlugin[]) {
  return plugins.length === 1
    ? loadRegistryFromSinglePlugin({
        plugin: plugins[0],
        pluginConfig: {
          allow: [plugins[0].id],
        },
      })
    : loadRegistryFromAllowedPlugins([...plugins]);
}

function expectOpenAllowWarnings(params: {
  warnings: string[];
  pluginId: string;
  expectedWarnings: number;
  label: string;
}) {
  const openAllowWarnings = params.warnings.filter((msg) => msg.includes("plugins.allow is empty"));
  expect(openAllowWarnings, params.label).toHaveLength(params.expectedWarnings);
  if (params.expectedWarnings > 0) {
    expect(
      openAllowWarnings.some((msg) => msg.includes(params.pluginId)),
      params.label,
    ).toBe(true);
  }
}

function expectLoadedPluginProvenance(params: {
  scenario: { label: string };
  registry: PluginRegistry;
  warnings: string[];
  pluginId: string;
  expectWarning: boolean;
  expectedSource?: string;
}) {
  const plugin = params.registry.plugins.find((entry) => entry.id === params.pluginId);
  expect(plugin?.status, params.scenario.label).toBe("loaded");
  if (params.expectedSource) {
    expect(plugin?.source, params.scenario.label).toBe(params.expectedSource);
  }
  expect(
    params.warnings.some(
      (msg) =>
        msg.includes(params.pluginId) &&
        msg.includes("loaded without install/load-path provenance"),
    ),
    params.scenario.label,
  ).toBe(params.expectWarning);
}

function expectRegisteredHttpRoute(
  registry: PluginRegistry,
  scenario: {
    pluginId: string;
    expectedPath: string;
    expectedAuth: string;
    expectedMatch: string;
    label: string;
  },
) {
  const route = registry.httpRoutes.find((entry) => entry.pluginId === scenario.pluginId);
  if (!route) {
    throw new Error(`expected http route for ${scenario.label}`);
  }
  expect(route.path, scenario.label).toBe(scenario.expectedPath);
  expect(route.auth, scenario.label).toBe(scenario.expectedAuth);
  expect(route.match, scenario.label).toBe(scenario.expectedMatch);
  const httpPlugin = registry.plugins.find((entry) => entry.id === scenario.pluginId);
  expect(httpPlugin?.httpRoutes, scenario.label).toBe(1);
}

function expectDuplicateRegistrationResult(
  registry: PluginRegistry,
  scenario: {
    selectCount: (registry: PluginRegistry) => number;
    ownerB: string;
    duplicateMessage: string;
    label: string;
    assertPrimaryOwner?: (registry: PluginRegistry) => void;
  },
) {
  expect(scenario.selectCount(registry), scenario.label).toBe(1);
  scenario.assertPrimaryOwner?.(registry);
  expect(
    registry.diagnostics.some(
      (diag) =>
        diag.level === "error" &&
        diag.pluginId === scenario.ownerB &&
        diag.message === scenario.duplicateMessage,
    ),
    scenario.label,
  ).toBe(true);
}

function expectPluginSourcePrecedence(
  registry: PluginRegistry,
  scenario: {
    pluginId: string;
    expectedLoadedOrigin: string;
    expectedDisabledOrigin: string;
    label: string;
    expectedDisabledError?: string;
    expectDuplicateWarning?: boolean;
  },
) {
  const entries = registry.plugins.filter((entry) => entry.id === scenario.pluginId);
  expect(entries, scenario.label).toHaveLength(1);
  const loaded = entries[0];
  expect(loaded?.origin, scenario.label).toBe(scenario.expectedLoadedOrigin);
  expect(loaded?.status, scenario.label).toBe("loaded");
  const expectedWarning =
    scenario.expectedDisabledError ??
    `${scenario.expectedDisabledOrigin} plugin will be overridden by ${scenario.expectedLoadedOrigin} plugin`;
  const hasDuplicateWarning = registry.diagnostics.some(
    (diag) =>
      diag.level === "warn" &&
      diag.pluginId === scenario.pluginId &&
      diag.message.includes(expectedWarning),
  );
  expect(hasDuplicateWarning, scenario.label).toBe(scenario.expectDuplicateWarning ?? true);
}

function expectPluginOriginAndStatus(params: {
  registry: PluginRegistry;
  pluginId: string;
  origin: string;
  status: string;
  label: string;
  errorIncludes?: string;
}) {
  const plugin = params.registry.plugins.find((entry) => entry.id === params.pluginId);
  expect(plugin?.origin, params.label).toBe(params.origin);
  expect(plugin?.status, params.label).toBe(params.status);
  if (params.errorIncludes) {
    expect(plugin?.error, params.label).toContain(params.errorIncludes);
  }
}

function expectRegistryErrorDiagnostic(params: {
  registry: PluginRegistry;
  pluginId: string;
  message: string;
}) {
  const diagnostic = params.registry.diagnostics.find(
    (entry) =>
      entry.level === "error" &&
      entry.pluginId === params.pluginId &&
      entry.message === params.message,
  );
  if (!diagnostic) {
    throw new Error(`Expected registry error diagnostic: ${params.message}`);
  }
}

function expectDiagnosticContaining(params: {
  registry: PluginRegistry;
  message: string;
  level?: string;
  pluginId?: string;
}) {
  const diagnostic = params.registry.diagnostics.find(
    (entry) =>
      (!params.level || entry.level === params.level) &&
      (!params.pluginId || entry.pluginId === params.pluginId) &&
      entry.message.includes(params.message),
  );
  if (!diagnostic) {
    throw new Error(`Expected diagnostic containing: ${params.message}`);
  }
}

function expectNoDiagnosticContaining(params: {
  registry: PluginRegistry;
  message: string;
  level?: string;
  pluginId?: string;
}) {
  const diagnostic = params.registry.diagnostics.find(
    (entry) =>
      (!params.level || entry.level === params.level) &&
      (!params.pluginId || entry.pluginId === params.pluginId) &&
      entry.message.includes(params.message),
  );
  expect(diagnostic, params.message).toBeUndefined();
}

function createWarningLogger(warnings: string[]) {
  return {
    info: () => {},
    warn: (msg: string) => warnings.push(msg),
    error: () => {},
  };
}

function createErrorLogger(errors: string[]) {
  return {
    info: () => {},
    warn: () => {},
    error: (msg: string) => errors.push(msg),
    debug: () => {},
  };
}

function createEscapingEntryFixture(params: { id: string; sourceBody: string }) {
  const pluginDir = makeTempDir();
  const outsideDir = makeTempDir();
  const outsideEntry = path.join(outsideDir, "outside.cjs");
  const linkedEntry = path.join(pluginDir, "entry.cjs");
  fs.writeFileSync(outsideEntry, params.sourceBody, "utf-8");
  fs.writeFileSync(
    path.join(pluginDir, "openclaw.plugin.json"),
    JSON.stringify(
      {
        id: params.id,
        configSchema: EMPTY_PLUGIN_SCHEMA,
      },
      null,
      2,
    ),
    "utf-8",
  );
  return { pluginDir, outsideEntry, linkedEntry };
}

function resolveLoadedPluginSource(
  registry: ReturnType<typeof loadOpenClawPlugins>,
  pluginId: string,
) {
  return fs.realpathSync(registry.plugins.find((entry) => entry.id === pluginId)?.source ?? "");
}

function expectCachePartitionByPluginSource(params: {
  pluginId: string;
  loadFirst: () => ReturnType<typeof loadOpenClawPlugins>;
  loadSecond: () => ReturnType<typeof loadOpenClawPlugins>;
  expectedFirstSource: string;
  expectedSecondSource: string;
}) {
  const first = params.loadFirst();
  const second = params.loadSecond();

  expect(second).not.toBe(first);
  expect(resolveLoadedPluginSource(first, params.pluginId)).toBe(
    fs.realpathSync(params.expectedFirstSource),
  );
  expect(resolveLoadedPluginSource(second, params.pluginId)).toBe(
    fs.realpathSync(params.expectedSecondSource),
  );
}

function expectCacheMissThenHit(params: {
  loadFirst: () => ReturnType<typeof loadOpenClawPlugins>;
  loadVariant: () => ReturnType<typeof loadOpenClawPlugins>;
}) {
  const first = params.loadFirst();
  const second = params.loadVariant();
  const third = params.loadVariant();

  expect(second).not.toBe(first);
  expect(third).toBe(second);
}

function createSetupEntryChannelPluginFixture(params: {
  id: string;
  label: string;
  packageName: string;
  fullBlurb: string;
  setupBlurb: string;
  configured: boolean;
  startupDeferConfiguredChannelFullLoadUntilAfterListen?: boolean;
  useBundledFullEntryContract?: boolean;
  bundledFullEntryId?: string;
  useBundledSetupEntryContract?: boolean;
  bundledSetupEntryId?: string;
  splitBundledSetupSecrets?: boolean;
  bundledSetupRuntimeMarker?: string;
  bundledSetupRuntimeRoutePath?: string;
  bundledSetupRuntimeRegisterError?: string;
  bundledSetupRuntimeLateRoutePath?: string;
  bundledSetupRuntimeError?: string;
  bundledFullRuntimeMarker?: string;
  requireBundledFullRuntimeBeforeLoad?: boolean;
}) {
  useNoBundledPlugins();
  const pluginDir = makeTempDir();
  const fullMarker = path.join(pluginDir, "full-loaded.txt");
  const setupMarker = path.join(pluginDir, "setup-loaded.txt");
  const listAccountIds = params.configured ? '["default"]' : "[]";
  const resolveAccount = params.configured
    ? '({ accountId: "default", token: "configured" })'
    : '({ accountId: "default" })';

  fs.writeFileSync(
    path.join(pluginDir, "package.json"),
    JSON.stringify(
      {
        name: params.packageName,
        openclaw: {
          extensions: ["./index.cjs"],
          setupEntry: "./setup-entry.cjs",
          ...(params.startupDeferConfiguredChannelFullLoadUntilAfterListen
            ? {
                startup: {
                  deferConfiguredChannelFullLoadUntilAfterListen: true,
                },
              }
            : {}),
        },
      },
      null,
      2,
    ),
    "utf-8",
  );
  fs.writeFileSync(
    path.join(pluginDir, "openclaw.plugin.json"),
    JSON.stringify(
      {
        id: params.id,
        configSchema: EMPTY_PLUGIN_SCHEMA,
        channels: [params.id],
      },
      null,
      2,
    ),
    "utf-8",
  );
  fs.writeFileSync(
    path.join(pluginDir, "index.cjs"),
    params.useBundledFullEntryContract
      ? `require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
module.exports = {
  kind: "bundled-channel-entry",
  id: ${JSON.stringify(params.bundledFullEntryId ?? params.id)},
  name: ${JSON.stringify(params.label)},
  description: ${JSON.stringify(params.fullBlurb)},
  loadChannelPlugin: () => {
    ${
      params.requireBundledFullRuntimeBeforeLoad && params.bundledFullRuntimeMarker
        ? `if (!require("node:fs").existsSync(${JSON.stringify(params.bundledFullRuntimeMarker)})) {
      throw new Error("bundled runtime not initialized");
    }`
        : ""
    }
    return {
      id: ${JSON.stringify(params.bundledFullEntryId ?? params.id)},
      meta: {
        id: ${JSON.stringify(params.bundledFullEntryId ?? params.id)},
        label: ${JSON.stringify(params.label)},
        selectionLabel: ${JSON.stringify(params.label)},
        docsPath: ${JSON.stringify(`/channels/${params.bundledFullEntryId ?? params.id}`)},
        blurb: ${JSON.stringify(params.fullBlurb)},
      },
      capabilities: { chatTypes: ["direct"] },
      config: {
        listAccountIds: () => ${listAccountIds},
        resolveAccount: () => ${resolveAccount},
      },
      outbound: { deliveryMode: "direct" },
    };
  },
  ${
    params.bundledFullRuntimeMarker
      ? `setChannelRuntime: () => {
    require("node:fs").writeFileSync(${JSON.stringify(params.bundledFullRuntimeMarker)}, "loaded", "utf-8");
  },`
      : ""
  }
  register() {},
};`
      : `require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
module.exports = {
  id: ${JSON.stringify(params.id)},
  register(api) {
    api.registerChannel({
      plugin: {
        id: ${JSON.stringify(params.id)},
        meta: {
          id: ${JSON.stringify(params.id)},
          label: ${JSON.stringify(params.label)},
          selectionLabel: ${JSON.stringify(params.label)},
          docsPath: ${JSON.stringify(`/channels/${params.id}`)},
          blurb: ${JSON.stringify(params.fullBlurb)},
        },
        capabilities: { chatTypes: ["direct"] },
        config: {
          listAccountIds: () => ${listAccountIds},
          resolveAccount: () => ${resolveAccount},
        },
        outbound: { deliveryMode: "direct" },
      },
    });
  },
};`,
    "utf-8",
  );
  fs.writeFileSync(
    path.join(pluginDir, "setup-entry.cjs"),
    params.useBundledSetupEntryContract
      ? `require("node:fs").writeFileSync(${JSON.stringify(setupMarker)}, "loaded", "utf-8");
module.exports = {
  kind: "bundled-channel-setup-entry",
  loadSetupPlugin: () => ({
    id: ${JSON.stringify(params.bundledSetupEntryId ?? params.id)},
    meta: {
      id: ${JSON.stringify(params.bundledSetupEntryId ?? params.id)},
      label: ${JSON.stringify(params.label)},
      selectionLabel: ${JSON.stringify(params.label)},
      docsPath: ${JSON.stringify(`/channels/${params.bundledSetupEntryId ?? params.id}`)},
      blurb: ${JSON.stringify(params.setupBlurb)},
    },
    capabilities: { chatTypes: ["direct"] },
    config: {
      listAccountIds: () => ${listAccountIds},
      resolveAccount: () => ${resolveAccount},
    },
    outbound: { deliveryMode: "direct" },
  }),
  ${
    params.splitBundledSetupSecrets
      ? `loadSetupSecrets: () => ({
    secretTargetRegistryEntries: [
      {
        id: ${JSON.stringify(`channels.${params.id}.setup-token`)},
        targetType: "channel",
      },
    ],
  }),`
      : ""
  }
  ${
    params.bundledSetupRuntimeError
      ? `setChannelRuntime: () => {
    throw new Error(${JSON.stringify(params.bundledSetupRuntimeError)});
  },`
      : params.bundledSetupRuntimeMarker
        ? `setChannelRuntime: () => {
    require("node:fs").writeFileSync(${JSON.stringify(params.bundledSetupRuntimeMarker)}, "loaded", "utf-8");
  },`
        : ""
  }
	  ${
      params.bundledSetupRuntimeRoutePath
        ? `registerSetupRuntime: (api) => {
	    api.registerHttpRoute({
	      path: ${JSON.stringify(params.bundledSetupRuntimeRoutePath)},
	      auth: "plugin",
	      handler: async () => true,
	    });
	    ${
        params.bundledSetupRuntimeRegisterError
          ? `throw new Error(${JSON.stringify(params.bundledSetupRuntimeRegisterError)});`
          : ""
      }
	    ${
        params.bundledSetupRuntimeLateRoutePath
          ? `queueMicrotask(() => {
	      api.registerHttpRoute({
	        path: ${JSON.stringify(params.bundledSetupRuntimeLateRoutePath)},
	        auth: "plugin",
	        handler: async () => true,
	      });
	    });`
          : ""
      }
	  },`
        : ""
    }
	};`
      : `require("node:fs").writeFileSync(${JSON.stringify(setupMarker)}, "loaded", "utf-8");
module.exports = {
  plugin: {
    id: ${JSON.stringify(params.id)},
    meta: {
      id: ${JSON.stringify(params.id)},
      label: ${JSON.stringify(params.label)},
      selectionLabel: ${JSON.stringify(params.label)},
      docsPath: ${JSON.stringify(`/channels/${params.id}`)},
      blurb: ${JSON.stringify(params.setupBlurb)},
    },
    capabilities: { chatTypes: ["direct"] },
    config: {
      listAccountIds: () => ${listAccountIds},
      resolveAccount: () => ${resolveAccount},
    },
    outbound: { deliveryMode: "direct" },
  },
  ${
    params.bundledSetupRuntimeMarker
      ? `setChannelRuntime: () => {
    require("node:fs").writeFileSync(${JSON.stringify(params.bundledSetupRuntimeMarker)}, "loaded", "utf-8");
  },`
      : ""
  }
};`,
    "utf-8",
  );

  return { pluginDir, fullMarker, setupMarker };
}

function createEnvResolvedPluginFixture(pluginId: string) {
  useNoBundledPlugins();
  const openclawHome = makeTempDir();
  const ignoredHome = makeTempDir();
  const stateDir = makeTempDir();
  const pluginDir = path.join(openclawHome, "plugins", pluginId);
  mkdirSafe(pluginDir);
  const plugin = writePlugin({
    id: pluginId,
    dir: pluginDir,
    filename: "index.cjs",
    body: `module.exports = { id: ${JSON.stringify(pluginId)}, register() {} };`,
  });
  const env = {
    ...process.env,
    OPENCLAW_HOME: openclawHome,
    HOME: ignoredHome,
    OPENCLAW_STATE_DIR: stateDir,
    OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins",
  };
  return { plugin, env };
}

function expectEscapingEntryRejected(params: {
  id: string;
  linkKind: "symlink" | "hardlink";
  sourceBody: string;
}) {
  useNoBundledPlugins();
  const { outsideEntry, linkedEntry } = createEscapingEntryFixture({
    id: params.id,
    sourceBody: params.sourceBody,
  });
  try {
    if (params.linkKind === "symlink") {
      fs.symlinkSync(outsideEntry, linkedEntry);
    } else {
      fs.linkSync(outsideEntry, linkedEntry);
    }
  } catch (err) {
    if (params.linkKind === "hardlink" && (err as NodeJS.ErrnoException).code === "EXDEV") {
      return undefined;
    }
    if (params.linkKind === "symlink") {
      return undefined;
    }
    throw err;
  }

  const registry = loadOpenClawPlugins({
    cache: false,
    config: {
      plugins: {
        load: { paths: [linkedEntry] },
        allow: [params.id],
      },
    },
  });

  const record = registry.plugins.find((entry) => entry.id === params.id);
  expect(record?.status).not.toBe("loaded");
  expectDiagnosticContaining({ registry, message: "escapes" });
  return registry;
}

function createStartupTraceRecorder(): {
  details: PluginStartupTraceDetail[];
  startupTrace: NonNullable<PluginLoadOptions["startupTrace"]>;
} {
  const details: PluginStartupTraceDetail[] = [];
  return {
    details,
    startupTrace: {
      detail: (name, metrics) => {
        details.push({ name, metrics });
      },
    },
  };
}

function collectStartupTraceMetrics(
  details: readonly PluginStartupTraceDetail[],
  name: string,
): Record<string, number | string> {
  const matched = details.filter((entry) => entry.name === name);
  expect(matched.length).toBeGreaterThan(0);
  const metrics: Record<string, number | string> = {};
  for (const entry of matched) {
    for (const [key, value] of entry.metrics) {
      metrics[key] = value;
    }
  }
  return metrics;
}

afterEach(() => {
  resetDiagnosticEventsForTest();
  clearRuntimeConfigSnapshot();
  runtimeRegistryLoaderTesting.resetPluginRegistryLoadedForTests();
  resetPluginLoaderTestStateForTest();
});

afterAll(() => {
  cleanupPluginLoaderFixturesForTest();
  cachedBundledTelegramDir = "";
  cachedBundledMemoryDir = "";
});

describe("loadOpenClawPlugins", () => {
  it("emits loader startup trace timings for normal plugin load and register", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "trace-plugin",
      filename: "trace-plugin.cjs",
      body: `module.exports = { id: "trace-plugin", register() {} };`,
    });
    const { details, startupTrace } = createStartupTraceRecorder();

    loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        allow: ["trace-plugin"],
      },
      options: {
        startupTrace,
      },
    });

    const metrics = collectStartupTraceMetrics(details, "plugins.gateway-load.plugin.trace-plugin");
    expect(metrics.loadMs).toEqual(expect.any(Number));
    expect(metrics.loadFailedCount).toBe(0);
    expect(metrics.registerMs).toEqual(expect.any(Number));
    expect(metrics.registerFailedCount).toBe(0);
    expect(metrics.loadAndRegisterMs).toEqual(expect.any(Number));
  });

  it("resolves ${ENV_VAR} references in plugin config before handing config to the plugin", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "env-config-probe",
      filename: "env-config-probe.cjs",
      // Schema must permit apiKey, otherwise the empty-schema guard rejects the
      // config before register() runs and the env substitution is never exercised.
      configSchema: {
        type: "object",
        additionalProperties: false,
        properties: { apiKey: { type: "string" } },
      },
      body: `module.exports = {
  id: "env-config-probe",
  register(api) {
    globalThis.envConfigProbeResult = api.pluginConfig;
  },
};`,
    });
    const probe = globalThis as unknown as Record<string, unknown>;
    const entries = {
      "env-config-probe": { config: { apiKey: "${ENV_CONFIG_PROBE_SECRET}" } },
    };

    // Case 1: the referenced variable is present in process.env.
    delete probe.envConfigProbeResult;
    withEnv({ ENV_CONFIG_PROBE_SECRET: "process-env-secret" }, () => {
      loadRegistryFromSinglePlugin({
        plugin,
        pluginConfig: { allow: ["env-config-probe"], entries },
        options: { resolveRawConfigEnvVars: true },
      });
    });
    // Before the fix, the plugin received the literal "${ENV_CONFIG_PROBE_SECRET}".
    expect(probe.envConfigProbeResult).toMatchObject({ apiKey: "process-env-secret" });

    // Case 2: the referenced variable lives only in the loader's explicit env,
    // not in process.env — proving the substitution honors the per-load env.
    delete probe.envConfigProbeResult;
    expect(process.env.ENV_CONFIG_PROBE_SECRET).toBeUndefined();
    loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: { allow: ["env-config-probe"], entries },
      options: {
        env: { ...process.env, ENV_CONFIG_PROBE_SECRET: "explicit-env-secret" },
        resolveRawConfigEnvVars: true,
      },
    });
    expect(probe.envConfigProbeResult).toMatchObject({ apiKey: "explicit-env-secret" });

    // Case 3: config.env.vars participates in the same effective env as config IO.
    delete probe.envConfigProbeResult;
    withEnv({ ENV_CONFIG_PROBE_SECRET: undefined }, () => {
      loadOpenClawPlugins({
        cache: false,
        workspaceDir: plugin.dir,
        config: {
          env: {
            vars: {
              ENV_CONFIG_PROBE_PLUGIN_FILE: plugin.file,
              ENV_CONFIG_PROBE_SECRET: "config-env-secret",
            },
          },
          plugins: {
            load: { paths: ["${ENV_CONFIG_PROBE_PLUGIN_FILE}"] },
            allow: ["env-config-probe"],
            entries,
          },
        },
        resolveRawConfigEnvVars: true,
      });
    });
    expect(probe.envConfigProbeResult).toMatchObject({ apiKey: "config-env-secret" });

    // Case 4: config that already went through read-time substitution must not
    // be processed again. Escaped placeholders intentionally become literals.
    delete probe.envConfigProbeResult;
    const resolvedEscapedEntries = resolveConfigEnvVars(
      {
        "env-config-probe": { config: { apiKey: "$${ENV_CONFIG_PROBE_SECRET}" } },
      },
      { ENV_CONFIG_PROBE_SECRET: "should-not-leak" } as NodeJS.ProcessEnv,
    ) as typeof entries;
    withEnv({ ENV_CONFIG_PROBE_SECRET: "process-env-secret" }, () => {
      loadRegistryFromSinglePlugin({
        plugin,
        pluginConfig: {
          allow: ["env-config-probe"],
          entries: structuredClone(resolvedEscapedEntries),
        },
      });
    });
    expect(probe.envConfigProbeResult).toMatchObject({
      apiKey: "${ENV_CONFIG_PROBE_SECRET}",
    });
  });

  it("emits loader startup trace failure counts for load and register failures", () => {
    useNoBundledPlugins();
    const loadFailPlugin = writePlugin({
      id: "trace-load-fail",
      filename: "trace-load-fail.cjs",
      body: `throw new Error("load boom");`,
    });
    const registerFailPlugin = writePlugin({
      id: "trace-register-fail",
      filename: "trace-register-fail.cjs",
      body: `module.exports = { id: "trace-register-fail", register() { throw new Error("register boom"); } };`,
    });
    const { details, startupTrace } = createStartupTraceRecorder();

    loadOpenClawPlugins({
      cache: false,
      config: {
        plugins: {
          load: { paths: [loadFailPlugin.file, registerFailPlugin.file] },
          allow: ["trace-load-fail", "trace-register-fail"],
        },
      },
      startupTrace,
    });

    const loadFailMetrics = collectStartupTraceMetrics(
      details,
      "plugins.gateway-load.plugin.trace-load-fail",
    );
    expect(loadFailMetrics.loadMs).toEqual(expect.any(Number));
    expect(loadFailMetrics.loadFailedCount).toBe(1);
    expect(loadFailMetrics.registerMs).toBeUndefined();

    const registerFailMetrics = collectStartupTraceMetrics(
      details,
      "plugins.gateway-load.plugin.trace-register-fail",
    );
    expect(registerFailMetrics.loadFailedCount).toBe(0);
    expect(registerFailMetrics.registerMs).toEqual(expect.any(Number));
    expect(registerFailMetrics.registerFailedCount).toBe(1);
    expect(registerFailMetrics.loadAndRegisterMs).toEqual(expect.any(Number));
  });

  it("can load scoped plugins from a supplied manifest registry without rereading manifests", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "supplied-manifest",
      body: `module.exports = { id: "supplied-manifest", register() {} };`,
    });
    const config = {
      plugins: {
        load: { paths: [plugin.file] },
        allow: [plugin.id],
      },
    };
    const manifestRegistry = loadPluginManifestRegistry({ config });
    fs.rmSync(path.join(plugin.dir, "openclaw.plugin.json"));

    const registry = loadOpenClawPlugins({
      cache: false,
      config,
      manifestRegistry,
      onlyPluginIds: [plugin.id],
    });

    expect(registry.plugins.find((entry) => entry.id === plugin.id)?.status).toBe("loaded");
  });

  it("loads installed plugin packages discovered from persisted install records", () => {
    useNoBundledPlugins();
    const stateDir = makeTempDir();
    const plugin = writePlugin({
      id: "installed-record-plugin",
      body: `module.exports = { id: "installed-record-plugin", register() {} };`,
    });
    writePersistedInstalledPluginIndexInstallRecordsSync(
      {
        [plugin.id]: {
          source: "git",
          spec: "git:file:///tmp/installed-record-plugin.git@abc123",
          installPath: plugin.dir,
          gitUrl: "file:///tmp/installed-record-plugin.git",
          gitCommit: "abc123",
        },
      },
      { stateDir },
    );

    const registry = withEnv({ OPENCLAW_STATE_DIR: stateDir }, () =>
      loadOpenClawPlugins({
        cache: false,
        config: {
          plugins: {
            entries: {
              [plugin.id]: { enabled: true },
            },
          },
        },
      }),
    );

    const record = registry.plugins.find((entry) => entry.id === plugin.id);
    expect(record?.id).toBe(plugin.id);
    expect(record?.status).toBe("loaded");
    expect(record?.rootDir).toBe(fs.realpathSync.native(plugin.dir));
  });

  it("refreshes bundled plugin-sdk aliases without deleting the shared alias directory", () => {
    const distRoot = makeTempDir();
    const pluginSdkDir = path.join(distRoot, "plugin-sdk");
    const aliasDir = path.join(distRoot, "extensions", "node_modules", "openclaw", "plugin-sdk");
    mkdirSafe(pluginSdkDir);
    mkdirSafe(aliasDir);
    fs.writeFileSync(path.join(pluginSdkDir, "index.js"), "export const value = 1;\n", "utf8");
    fs.writeFileSync(path.join(pluginSdkDir, "core.js"), "export const core = 1;\n", "utf8");
    fs.writeFileSync(path.join(aliasDir, "sentinel.txt"), "keep\n", "utf8");

    ensureOpenClawPluginSdkAlias(distRoot);
    fs.writeFileSync(path.join(pluginSdkDir, "core.js"), "export const core = 2;\n", "utf8");
    ensureOpenClawPluginSdkAlias(distRoot);

    expect(fs.existsSync(path.join(aliasDir, "sentinel.txt"))).toBe(true);
    expect(fs.readFileSync(path.join(aliasDir, "core.js"), "utf8")).toContain("core.js");
  });

  it("keeps private local-only plugin-sdk artifacts out of package dist aliases", () => {
    const packageRoot = makeTempDir();
    const distRoot = path.join(packageRoot, "dist");
    const pluginSdkDir = path.join(distRoot, "plugin-sdk");
    const aliasRoot = path.join(distRoot, "extensions", "node_modules", "openclaw");
    const aliasDir = path.join(aliasRoot, "plugin-sdk");
    mkdirSafe(pluginSdkDir);
    mkdirSafe(aliasDir);
    fs.writeFileSync(
      path.join(packageRoot, "package.json"),
      JSON.stringify(
        {
          name: "openclaw",
          version: "2026.4.22",
          type: "module",
          exports: {
            "./plugin-sdk": "./dist/plugin-sdk/index.js",
            "./plugin-sdk/string-coerce-runtime": "./dist/plugin-sdk/string-coerce-runtime.js",
          },
        },
        null,
        2,
      ),
      "utf8",
    );
    fs.writeFileSync(path.join(pluginSdkDir, "index.js"), "export const root = true;\n", "utf8");
    fs.writeFileSync(
      path.join(pluginSdkDir, "string-coerce-runtime.js"),
      "export const publicRuntime = true;\n",
      "utf8",
    );
    fs.writeFileSync(
      path.join(pluginSdkDir, "ssrf-runtime-internal.js"),
      "export const internal = true;\n",
      "utf8",
    );
    fs.writeFileSync(
      path.join(aliasDir, "ssrf-runtime-internal.js"),
      "export const staleInternal = true;\n",
      "utf8",
    );

    ensureOpenClawPluginSdkAlias(distRoot);

    const aliasPackage = JSON.parse(
      fs.readFileSync(path.join(aliasRoot, "package.json"), "utf8"),
    ) as { exports?: Record<string, string> };
    expect(aliasPackage.exports).toEqual({
      "./plugin-sdk": "./plugin-sdk/index.js",
      "./plugin-sdk/string-coerce-runtime": "./plugin-sdk/string-coerce-runtime.js",
    });
    expect(fs.existsSync(path.join(aliasDir, "index.js"))).toBe(true);
    expect(fs.existsSync(path.join(aliasDir, "string-coerce-runtime.js"))).toBe(true);
    expect(fs.existsSync(path.join(aliasDir, "ssrf-runtime-internal.js"))).toBe(false);
  });

  it("keeps private local-only plugin-sdk artifacts out of legacy package dist aliases", () => {
    const packageRoot = makeTempDir();
    const distRoot = path.join(packageRoot, "dist");
    const pluginSdkDir = path.join(distRoot, "plugin-sdk");
    const aliasRoot = path.join(distRoot, "extensions", "node_modules", "openclaw");
    const aliasDir = path.join(aliasRoot, "plugin-sdk");
    mkdirSafe(pluginSdkDir);
    mkdirSafe(aliasDir);
    fs.writeFileSync(
      path.join(packageRoot, "package.json"),
      JSON.stringify({ name: "openclaw", version: "2026.4.22", type: "module" }, null, 2),
      "utf8",
    );
    fs.writeFileSync(path.join(pluginSdkDir, "index.js"), "export const root = true;\n", "utf8");
    fs.writeFileSync(
      path.join(pluginSdkDir, "string-coerce-runtime.js"),
      "export const publicRuntime = true;\n",
      "utf8",
    );
    fs.writeFileSync(
      path.join(pluginSdkDir, "ssrf-runtime-internal.js"),
      "export const internal = true;\n",
      "utf8",
    );
    fs.writeFileSync(
      path.join(aliasDir, "ssrf-runtime-internal.js"),
      "export const staleInternal = true;\n",
      "utf8",
    );

    ensureOpenClawPluginSdkAlias(distRoot);

    const aliasPackage = JSON.parse(
      fs.readFileSync(path.join(aliasRoot, "package.json"), "utf8"),
    ) as { exports?: Record<string, string> };
    expect(aliasPackage.exports).toEqual({
      "./plugin-sdk": "./plugin-sdk/index.js",
      "./plugin-sdk/string-coerce-runtime": "./plugin-sdk/string-coerce-runtime.js",
    });
    expect(fs.existsSync(path.join(aliasDir, "index.js"))).toBe(true);
    expect(fs.existsSync(path.join(aliasDir, "string-coerce-runtime.js"))).toBe(true);
    expect(fs.existsSync(path.join(aliasDir, "ssrf-runtime-internal.js"))).toBe(false);
  });

  it("keeps private QA plugin-sdk filenames out of bundled source markers", () => {
    const source = fs.readFileSync(new URL("./plugin-sdk-dist-alias.ts", import.meta.url), "utf8");

    expect(source).not.toContain("qa-channel.js");
    expect(source).not.toContain("qa-channel-protocol.js");
    expect(source).not.toContain("qa-lab.js");
    expect(source).not.toContain("qa-runtime.js");
  });

  it("disables bundled plugins by default", () => {
    const bundledDir = makeTempDir();
    writePlugin({
      id: "bundled",
      body: `module.exports = { id: "bundled", register() {} };`,
      dir: bundledDir,
      filename: "bundled.cjs",
    });
    process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir;

    const registry = loadOpenClawPlugins({
      cache: false,
      config: {
        plugins: {
          allow: ["bundled"],
        },
      },
    });

    const bundled = registry.plugins.find((entry) => entry.id === "bundled");
    expect(bundled?.status).toBe("disabled");
  });
  it("loads bundled plugins with plugin-sdk imports from a package dist root", () => {
    const packageRoot = makeTempDir();
    const bundledDir = path.join(packageRoot, "dist", "extensions");
    const pluginRoot = path.join(bundledDir, "discord");
    fs.mkdirSync(path.join(packageRoot, "dist", "plugin-sdk"), { recursive: true });
    fs.mkdirSync(pluginRoot, { recursive: true });
    fs.writeFileSync(
      path.join(packageRoot, "package.json"),
      JSON.stringify({ name: "openclaw", version: "2026.4.22", type: "module" }),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(packageRoot, "dist", "plugin-sdk", "string-coerce-runtime.js"),
      "export const normalizeLowercaseStringOrEmpty = (value) => String(value).toLowerCase();\n",
      "utf-8",
    );
    ensureOpenClawPluginSdkAlias(path.join(packageRoot, "dist"));
    fs.writeFileSync(
      path.join(pluginRoot, "index.js"),
      [
        `import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";`,
        `export default {`,
        `  id: "discord",`,
        `  register(api) {`,
        `    api.registerCommand({ name: normalizeLowercaseStringOrEmpty("DISCORD"), handler: () => "ok" });`,
        `  },`,
        `};`,
        "",
      ].join("\n"),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginRoot, "package.json"),
      JSON.stringify(
        {
          name: "@openclaw/discord",
          version: "1.0.0",
          type: "module",
          openclaw: { extensions: ["./index.js"] },
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginRoot, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "discord",
          enabledByDefault: true,
          configSchema: EMPTY_PLUGIN_SCHEMA,
        },
        null,
        2,
      ),
      "utf-8",
    );
    process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir;

    const registry = loadOpenClawPlugins({
      cache: false,
      config: {
        plugins: {
          enabled: true,
        },
      },
    });

    const record = registry.plugins.find((entry) => entry.id === "discord");
    expect(
      record?.status,
      JSON.stringify({ error: record?.error, diagnostics: registry.diagnostics }, null, 2),
    ).toBe("loaded");
  });
  it("registers standalone text transforms", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "text-shim",
      filename: "text-shim.cjs",
      body: `module.exports = {
        id: "text-shim",
        register(api) {
          api.registerTextTransforms({
            input: [{ from: /red basket/g, to: "blue basket" }],
            output: [{ from: /blue basket/g, to: "red basket" }],
          });
        },
      };`,
    });

    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: { allow: ["text-shim"] },
    });

    expect(registry.textTransforms).toHaveLength(1);
    const transformRegistration = registry.textTransforms[0];
    expect(transformRegistration?.pluginId).toBe("text-shim");
    expect(transformRegistration?.transforms.input).toEqual([
      { from: /red basket/g, to: "blue basket" },
    ]);
    expect(transformRegistration?.transforms.output).toEqual([
      { from: /blue basket/g, to: "red basket" },
    ]);
  });

  it.each([
    {
      name: "loads bundled telegram plugin when enabled",
      config: {
        plugins: {
          allow: ["telegram"],
          entries: {
            telegram: { enabled: true },
          },
        },
      } satisfies PluginLoadConfig,
      assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
        expectTelegramLoaded(registry);
      },
    },
    {
      name: "loads bundled channel plugins when channels.<id>.enabled=true",
      config: {
        channels: {
          telegram: {
            enabled: true,
          },
        },
        plugins: {
          enabled: true,
        },
      } satisfies PluginLoadConfig,
      assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
        expectTelegramLoaded(registry);
      },
    },
    {
      name: "lets explicit bundled channel enablement bypass restrictive allowlists",
      config: {
        channels: {
          telegram: {
            enabled: true,
          },
        },
        plugins: {
          allow: ["browser"],
        },
      } satisfies PluginLoadConfig,
      assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
        const telegram = registry.plugins.find((entry) => entry.id === "telegram");
        expect(telegram?.status).toBe("loaded");
        expect(telegram?.error).toBeUndefined();
        expect(telegram?.explicitlyEnabled).toBe(true);
      },
    },
    {
      name: "still respects explicit disable via plugins.entries for bundled channels",
      config: {
        channels: {
          telegram: {
            enabled: true,
          },
        },
        plugins: {
          entries: {
            telegram: { enabled: false },
          },
        },
      } satisfies PluginLoadConfig,
      assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
        const telegram = registry.plugins.find((entry) => entry.id === "telegram");
        expect(telegram?.status).toBe("disabled");
        expect(telegram?.error).toBe("disabled in config");
      },
    },
  ] as const)(
    "handles bundled telegram plugin enablement and override rules: $name",
    ({ config, assert }) => {
      setupBundledTelegramPlugin();
      const registry = loadOpenClawPlugins({
        cache: false,
        workspaceDir: cachedBundledTelegramDir,
        config,
      });
      assert(registry);
    },
  );

  it("marks auto-enabled bundled channels as activated but not explicitly enabled", () => {
    setupBundledTelegramPlugin();
    const rawConfig = {
      channels: {
        telegram: {
          botToken: "x",
        },
      },
      plugins: {
        enabled: true,
      },
    } satisfies PluginLoadConfig;
    const autoEnabled = applyPluginAutoEnable({
      config: rawConfig,
      env: {},
    });

    const registry = loadOpenClawPlugins({
      cache: false,
      workspaceDir: cachedBundledTelegramDir,
      config: autoEnabled.config,
      activationSourceConfig: rawConfig,
      autoEnabledReasons: autoEnabled.autoEnabledReasons,
    });

    const telegram = registry.plugins.find((entry) => entry.id === "telegram");
    expect(telegram?.explicitlyEnabled).toBe(false);
    expect(telegram?.activated).toBe(true);
    expect(telegram?.activationSource).toBe("auto");
    expect(telegram?.activationReason).toBe("telegram configured");
  });

  it("materializes auto-enabled bundled channels into restrictive allowlists", () => {
    setupBundledTelegramPlugin();
    const rawConfig = {
      channels: {
        telegram: {
          botToken: "x",
        },
      },
      plugins: {
        allow: ["browser"],
      },
    } satisfies PluginLoadConfig;
    const autoEnabled = applyPluginAutoEnable({
      config: rawConfig,
      env: {},
    });

    const registry = loadOpenClawPlugins({
      cache: false,
      workspaceDir: cachedBundledTelegramDir,
      config: autoEnabled.config,
      activationSourceConfig: rawConfig,
      autoEnabledReasons: autoEnabled.autoEnabledReasons,
    });

    const telegram = registry.plugins.find((entry) => entry.id === "telegram");
    expect(autoEnabled.config.plugins?.allow).toEqual(["browser", "telegram"]);
    expect(telegram?.status).toBe("loaded");
    expect(telegram?.error).toBeUndefined();
    expect(telegram?.explicitlyEnabled).toBe(false);
    expect(telegram?.activated).toBe(true);
    expect(telegram?.activationSource).toBe("auto");
    expect(telegram?.activationReason).toBe("telegram configured");
  });

  it("preserves all auto-enable reasons in activation metadata", () => {
    setupBundledTelegramPlugin();
    const rawConfig = {
      channels: {
        telegram: {
          botToken: "x",
        },
      },
      plugins: {
        enabled: true,
      },
    } satisfies PluginLoadConfig;

    const registry = loadOpenClawPlugins({
      cache: false,
      workspaceDir: cachedBundledTelegramDir,
      config: {
        ...rawConfig,
        plugins: {
          enabled: true,
          entries: {
            telegram: {
              enabled: true,
            },
          },
        },
      },
      activationSourceConfig: rawConfig,
      autoEnabledReasons: {
        telegram: ["telegram configured", "telegram selected for startup"],
      },
    });

    const telegram = registry.plugins.find((entry) => entry.id === "telegram");
    expect(telegram?.explicitlyEnabled).toBe(false);
    expect(telegram?.activated).toBe(true);
    expect(telegram?.activationSource).toBe("auto");
    expect(telegram?.activationReason).toBe("telegram configured; telegram selected for startup");
  });

  it("keeps explicit plugin enablement distinct from derived activation", () => {
    const { bundledDir } = writeBundledPlugin({
      id: "demo",
    });
    const config = {
      plugins: {
        entries: {
          demo: {
            enabled: true,
          },
        },
      },
    } satisfies PluginLoadConfig;

    const registry = loadOpenClawPlugins({
      cache: false,
      workspaceDir: bundledDir,
      config,
      activationSourceConfig: config,
    });

    const demo = registry.plugins.find((entry) => entry.id === "demo");
    expect(demo?.explicitlyEnabled).toBe(true);
    expect(demo?.activated).toBe(true);
    expect(demo?.activationSource).toBe("explicit");
    expect(demo?.activationReason).toBe("enabled in config");
  });

  it("preserves package.json metadata for bundled memory plugins", () => {
    const registry = loadBundledMemoryPluginRegistry({
      packageMeta: {
        name: "@openclaw/memory-core",
        version: "1.2.3",
        description: "Memory plugin package",
      },
      pluginBody:
        'module.exports = { id: "memory-core", kind: "memory", name: "Memory (Core)", register() {} };',
    });

    const memory = registry.plugins.find((entry) => entry.id === "memory-core");
    expect(memory?.status).toBe("loaded");
    expect(memory?.origin).toBe("bundled");
    expect(memory?.name).toBe("Memory (Core)");
    expect(memory?.version).toBe("1.2.3");
  });
  it.each([
    {
      label: "loads plugins from config paths",
      run: () => {
        useNoBundledPlugins();
        const plugin = writePlugin({
          id: "allowed-config-path",
          filename: "allowed-config-path.cjs",
          body: `module.exports = {
  id: "allowed-config-path",
  register(api) {
    api.registerGatewayMethod("allowed-config-path.ping", ({ respond }) => respond(true, { ok: true }));
  },
};`,
        });

        const registry = loadOpenClawPlugins({
          cache: false,
          workspaceDir: plugin.dir,
          config: {
            plugins: {
              load: { paths: [plugin.file] },
              allow: ["allowed-config-path"],
            },
          },
        });

        const loaded = registry.plugins.find((entry) => entry.id === "allowed-config-path");
        expect(loaded?.status).toBe("loaded");
        expect(Object.keys(registry.gatewayHandlers)).toContain("allowed-config-path.ping");
        expect(registry.gatewayMethodDescriptors).toMatchObject([
          {
            name: "allowed-config-path.ping",
            owner: { kind: "plugin", pluginId: "allowed-config-path" },
          },
        ]);
      },
    },
    {
      label: "adapts returned plugin gateway method values into responses",
      run: async () => {
        useNoBundledPlugins();
        const plugin = writePlugin({
          id: "returned-gateway-method",
          filename: "returned-gateway-method.cjs",
          body: `module.exports = {
  id: "returned-gateway-method",
  register(api) {
    api.registerGatewayMethod("returned-gateway-method.status", async () => ({ ok: true, status: "ready" }));
  },
};`,
        });

        const registry = loadOpenClawPlugins({
          cache: false,
          workspaceDir: plugin.dir,
          config: {
            plugins: {
              load: { paths: [plugin.file] },
              allow: ["returned-gateway-method"],
            },
          },
        });

        const responses: unknown[] = [];
        await registry.gatewayHandlers["returned-gateway-method.status"]?.({
          params: {},
          respond: (ok: boolean, payload: unknown, error?: unknown) =>
            responses.push({ ok, payload, error }),
        } as never);

        expect(responses).toEqual([
          { ok: true, payload: { ok: true, status: "ready" }, error: undefined },
        ]);
      },
    },
    {
      label: "keeps explicit plugin gateway responses authoritative",
      run: async () => {
        useNoBundledPlugins();
        const plugin = writePlugin({
          id: "explicit-gateway-method",
          filename: "explicit-gateway-method.cjs",
          body: `module.exports = {
  id: "explicit-gateway-method",
  register(api) {
    api.registerGatewayMethod("explicit-gateway-method.status", ({ respond }) => {
      respond(true, { status: "responded" });
      return { status: "returned" };
    });
  },
};`,
        });

        const registry = loadOpenClawPlugins({
          cache: false,
          workspaceDir: plugin.dir,
          config: {
            plugins: {
              load: { paths: [plugin.file] },
              allow: ["explicit-gateway-method"],
            },
          },
        });

        const responses: unknown[] = [];
        await registry.gatewayHandlers["explicit-gateway-method.status"]?.({
          params: {},
          respond: (ok: boolean, payload: unknown, error?: unknown) =>
            responses.push({ ok, payload, error }),
        } as never);

        expect(responses).toEqual([
          { ok: true, payload: { status: "responded" }, error: undefined },
        ]);
      },
    },
    {
      label: "coerces reserved gateway method namespaces to operator.admin",
      run: () => {
        useNoBundledPlugins();
        const plugin = writePlugin({
          id: "reserved-gateway-scope",
          filename: "reserved-gateway-scope.cjs",
          body: `module.exports = {
  id: "reserved-gateway-scope",
  register(api) {
    api.registerGatewayMethod(
      ${JSON.stringify(RESERVED_ADMIN_PLUGIN_METHOD)},
      ({ respond }) => respond(true, { ok: true }),
      { scope: "operator.read" },
    );
  },
};`,
        });

        const registry = loadOpenClawPlugins({
          cache: false,
          workspaceDir: plugin.dir,
          config: {
            plugins: {
              load: { paths: [plugin.file] },
              allow: ["reserved-gateway-scope"],
            },
          },
        });

        expect(Object.keys(registry.gatewayHandlers)).toContain(RESERVED_ADMIN_PLUGIN_METHOD);
        expect(registry.gatewayMethodDescriptors[0]?.scope).toBe("operator.admin");
        expectDiagnosticContaining({
          registry,
          message: `${RESERVED_ADMIN_SCOPE_WARNING}: ${RESERVED_ADMIN_PLUGIN_METHOD}`,
        });
      },
    },
    {
      label: "rejects gateway methods that collide with hidden core methods",
      run: () => {
        useNoBundledPlugins();
        const plugin = writePlugin({
          id: "hidden-core-collision",
          filename: "hidden-core-collision.cjs",
          body: `module.exports = {
  id: "hidden-core-collision",
  register(api) {
    api.registerGatewayMethod("config.openFile", ({ respond }) => respond(true, { ok: true }));
  },
};`,
        });

        const registry = loadOpenClawPlugins({
          cache: false,
          workspaceDir: plugin.dir,
          coreGatewayMethodNames: ["config.openFile"],
          config: {
            plugins: {
              load: { paths: [plugin.file] },
              allow: ["hidden-core-collision"],
            },
          },
        });

        expect(Object.keys(registry.gatewayHandlers)).not.toContain("config.openFile");
        expectDiagnosticContaining({
          registry,
          level: "error",
          pluginId: "hidden-core-collision",
          message: "gateway method already registered: config.openFile",
        });
      },
    },
    {
      label: "rejects async register functions instead of silently loading them",
      run: () => {
        useNoBundledPlugins();
        const plugin = writePlugin({
          id: "async-register",
          filename: "async-register.cjs",
          body: `module.exports = {
  id: "async-register",
  async register(api) {
    await Promise.resolve();
    api.registerGatewayMethod("async-register.ping", ({ respond }) => respond(true, { ok: true }));
  },
};`,
        });

        const registry = loadOpenClawPlugins({
          cache: false,
          config: {
            plugins: {
              load: { paths: [plugin.file] },
              allow: ["async-register"],
            },
          },
        });

        const loaded = registry.plugins.find((entry) => entry.id === "async-register");
        expect(loaded?.status).toBe("error");
        expect(loaded?.failurePhase).toBe("register");
        expect(loaded?.error).toContain("plugin register must be synchronous");
        expect(Object.keys(registry.gatewayHandlers)).not.toContain("async-register.ping");
      },
    },
    {
      label:
        "keeps sendSessionAttachment callable after register closes while blocking registration-only APIs",
      run: () => {
        const registerGatewayMethod = vi.fn();
        const registerSessionExtension = vi.fn();
        const sendSessionAttachment = vi.fn(async () => ({
          ok: true as const,
          channel: "proofchat",
          deliveredTo: "12345",
          count: 1,
        }));
        const emitAgentEvent = vi.fn(() => ({
          emitted: true as const,
          stream: "late-attachment-plugin.workflow",
        }));
        const api = buildPluginApi({
          id: "late-attachment-plugin",
          name: "Late Attachment Plugin",
          source: "/tmp/late-attachment-plugin/index.cjs",
          registrationMode: "full",
          config: {},
          runtime: {} as never,
          logger: {
            info() {},
            warn() {},
            error() {},
            debug() {},
          },
          resolvePath: (input) => input,
          handlers: {
            emitAgentEvent,
            registerGatewayMethod,
            registerSessionExtension,
            sendSessionAttachment,
          },
        });
        let capturedApi: typeof api | undefined;

        testing.runPluginRegisterSync((guardedApi) => {
          capturedApi = guardedApi;
          // Host-hook delivery remains callable after registration closes; only registration-only APIs lock.
          guardedApi.registerGatewayMethod("proofchat.ping", vi.fn() as never);
        }, api);

        expect(registerGatewayMethod).toHaveBeenCalledTimes(1);
        expect(
          capturedApi?.registerGatewayMethod("proofchat.late-ping", vi.fn() as never),
        ).toBeUndefined();
        expect(registerGatewayMethod).toHaveBeenCalledTimes(1);

        const attachmentParams = {
          sessionKey: "agent:main:main",
          files: [{ path: "./proof-report.txt" }],
          text: "attachment ready",
        };
        const lateResult = capturedApi?.sendSessionAttachment(attachmentParams);
        const lateWorkflowResult =
          capturedApi?.session?.workflow.sendSessionAttachment(attachmentParams);
        const eventParams = {
          runId: "run-late",
          stream: "late-attachment-plugin.workflow",
          data: { phase: "done" },
        };
        const lateEventResult = capturedApi?.emitAgentEvent(eventParams);
        const lateNamespacedEventResult = capturedApi?.agent?.events.emitAgentEvent(eventParams);
        capturedApi?.session?.state.registerSessionExtension({
          namespace: "late",
          description: "late extension should stay blocked",
        });

        expect(lateResult).toBe(sendSessionAttachment.mock.results[0]?.value);
        expect(lateWorkflowResult).toBe(sendSessionAttachment.mock.results[1]?.value);
        expect(sendSessionAttachment).toHaveBeenCalledWith({
          sessionKey: "agent:main:main",
          files: [{ path: "./proof-report.txt" }],
          text: "attachment ready",
        });
        expect(sendSessionAttachment).toHaveBeenCalledTimes(2);
        expect(lateEventResult).toEqual({
          emitted: true,
          stream: "late-attachment-plugin.workflow",
        });
        expect(lateNamespacedEventResult).toEqual({
          emitted: true,
          stream: "late-attachment-plugin.workflow",
        });
        expect(emitAgentEvent).toHaveBeenCalledTimes(2);
        expect(emitAgentEvent).toHaveBeenCalledWith(eventParams);
        expect(registerSessionExtension).not.toHaveBeenCalled();
      },
    },
    {
      label: "limits imports to the requested plugin ids",
      run: () => {
        useNoBundledPlugins();
        const allowed = writePlugin({
          id: "allowed-scoped-only",
          filename: "allowed-scoped-only.cjs",
          body: `module.exports = { id: "allowed-scoped-only", register() {} };`,
        });
        const skippedMarker = path.join(makeTempDir(), "skipped-loaded.txt");
        const skipped = writePlugin({
          id: "skipped-scoped-only",
          filename: "skipped-scoped-only.cjs",
          body: `require("node:fs").writeFileSync(${JSON.stringify(skippedMarker)}, "loaded", "utf-8");
module.exports = { id: "skipped-scoped-only", register() { throw new Error("skipped plugin should not load"); } };`,
        });

        const registry = loadOpenClawPlugins({
          cache: false,
          config: {
            plugins: {
              load: { paths: [allowed.file, skipped.file] },
              allow: ["allowed-scoped-only", "skipped-scoped-only"],
            },
          },
          onlyPluginIds: ["allowed-scoped-only"],
        });

        expect(registry.plugins.map((entry) => entry.id)).toEqual(["allowed-scoped-only"]);
        expect(fs.existsSync(skippedMarker)).toBe(false);
      },
    },
    {
      label: "can build a manifest-only snapshot without importing plugin modules",
      run: () => {
        useNoBundledPlugins();
        const importedMarker = path.join(makeTempDir(), "manifest-only-imported.txt");
        const plugin = writePlugin({
          id: "manifest-only-plugin",
          filename: "manifest-only-plugin.cjs",
          body: `require("node:fs").writeFileSync(${JSON.stringify(importedMarker)}, "loaded", "utf-8");
module.exports = { id: "manifest-only-plugin", register() { throw new Error("manifest-only snapshot should not register"); } };`,
        });

        const registry = loadOpenClawPlugins({
          cache: false,
          activate: false,
          loadModules: false,
          config: {
            plugins: {
              load: { paths: [plugin.file] },
              allow: ["manifest-only-plugin"],
              entries: {
                "manifest-only-plugin": { enabled: true },
              },
            },
          },
        });

        expect(fs.existsSync(importedMarker)).toBe(false);
        const record = registry.plugins.find((entry) => entry.id === "manifest-only-plugin");
        expect(record?.status).toBe("loaded");
      },
    },
    {
      label: "includes manifest-owned surfaces in manifest-only snapshots",
      run: () => {
        useNoBundledPlugins();
        const importedMarker = path.join(makeTempDir(), "manifest-surfaces-imported.txt");
        const plugin = writePlugin({
          id: "manifest-surfaces-plugin",
          filename: "manifest-surfaces-plugin.cjs",
          body: `require("node:fs").writeFileSync(${JSON.stringify(importedMarker)}, "loaded", "utf-8");
module.exports = { id: "manifest-surfaces-plugin", register() { throw new Error("manifest-only snapshot should not register"); } };`,
        });
        fs.writeFileSync(
          path.join(plugin.dir, "openclaw.plugin.json"),
          JSON.stringify(
            {
              id: "manifest-surfaces-plugin",
              configSchema: EMPTY_PLUGIN_SCHEMA,
              channels: ["manifest-surfaces-channel"],
              providers: ["manifest-surfaces-provider"],
              cliBackends: ["manifest-surfaces-cli"],
              setup: { cliBackends: ["manifest-surfaces-setup-cli"] },
              commandAliases: [{ name: "manifest-surfaces-command" }],
            },
            null,
            2,
          ),
          "utf-8",
        );

        const registry = loadOpenClawPlugins({
          cache: false,
          activate: false,
          loadModules: false,
          config: {
            plugins: {
              load: { paths: [plugin.file] },
              allow: ["manifest-surfaces-plugin"],
              entries: {
                "manifest-surfaces-plugin": { enabled: true },
              },
            },
          },
        });

        const record = registry.plugins.find((entry) => entry.id === "manifest-surfaces-plugin");
        expect(fs.existsSync(importedMarker)).toBe(false);
        expect(record?.channelIds).toEqual(["manifest-surfaces-channel"]);
        expect(record?.providerIds).toEqual(["manifest-surfaces-provider"]);
        expect(record?.cliBackendIds).toEqual([
          "manifest-surfaces-cli",
          "manifest-surfaces-setup-cli",
        ]);
        expect(record?.commands).toEqual(["manifest-surfaces-command"]);
      },
    },
    {
      label: "marks a selected memory slot as matched during manifest-only snapshots",
      run: () => {
        useNoBundledPlugins();
        const memoryPlugin = writePlugin({
          id: "memory-demo",
          filename: "memory-demo.cjs",
          body: `module.exports = {
  id: "memory-demo",
  kind: "memory",
  register() {},
};`,
        });
        fs.writeFileSync(
          path.join(memoryPlugin.dir, "openclaw.plugin.json"),
          JSON.stringify(
            {
              id: "memory-demo",
              kind: "memory",
              configSchema: EMPTY_PLUGIN_SCHEMA,
            },
            null,
            2,
          ),
          "utf-8",
        );

        const registry = loadOpenClawPlugins({
          cache: false,
          activate: false,
          loadModules: false,
          config: {
            plugins: {
              load: { paths: [memoryPlugin.file] },
              allow: ["memory-demo"],
              slots: { memory: "memory-demo" },
              entries: {
                "memory-demo": { enabled: true },
              },
            },
          },
        });

        expectNoDiagnosticContaining({
          registry,
          message: "memory slot plugin not found or not marked as memory: memory-demo",
        });
        const record = registry.plugins.find((entry) => entry.id === "memory-demo");
        expect(record?.memorySlotSelected).toBe(true);
      },
    },
    {
      label: "tracks plugins as imported when module evaluation throws after top-level execution",
      run: () => {
        useNoBundledPlugins();
        const importMarker = "__openclaw_loader_import_throw_marker";
        Reflect.deleteProperty(globalThis, importMarker);

        const plugin = writePlugin({
          id: "throws-after-import",
          filename: "throws-after-import.cjs",
          body: `globalThis.${importMarker} = (globalThis.${importMarker} ?? 0) + 1;
throw new Error("boom after import");
module.exports = { id: "throws-after-import", register() {} };`,
        });

        const registry = loadOpenClawPlugins({
          cache: false,
          activate: false,
          config: {
            plugins: {
              load: { paths: [plugin.file] },
              allow: ["throws-after-import"],
            },
          },
        });

        try {
          const record = registry.plugins.find((entry) => entry.id === "throws-after-import");
          expect(record?.status).toBe("error");
          expect(listImportedRuntimePluginIds()).toContain("throws-after-import");
          expect(Number(Reflect.get(globalThis, importMarker) ?? 0)).toBeGreaterThan(0);
        } finally {
          Reflect.deleteProperty(globalThis, importMarker);
        }
      },
    },
    {
      label: "fails loudly when a plugin reenters the same snapshot load during register",
      run: () => {
        useNoBundledPlugins();
        const marker = "__openclaw_loader_reentry_error";
        const reenterFnMarker = "__openclaw_loader_reentry_fn";
        Reflect.deleteProperty(globalThis, marker);
        Reflect.set(
          globalThis,
          reenterFnMarker,
          (options: Parameters<typeof loadOpenClawPlugins>[0]) => loadOpenClawPlugins(options),
        );
        const pluginDir = makeTempDir();
        const pluginFile = path.join(pluginDir, "reentrant-snapshot.cjs");
        const nestedOptions = {
          cache: false,
          activate: false,
          workspaceDir: pluginDir,
          config: {
            plugins: {
              load: { paths: [pluginFile] },
              allow: ["reentrant-snapshot"],
            },
          },
        } satisfies Parameters<typeof loadOpenClawPlugins>[0];
        writePlugin({
          id: "reentrant-snapshot",
          dir: pluginDir,
          filename: "reentrant-snapshot.cjs",
          body: `module.exports = {
  id: "reentrant-snapshot",
  register() {
    try {
      globalThis.${reenterFnMarker}(${JSON.stringify(nestedOptions)});
    } catch (error) {
      globalThis.${marker} = {
        name: error?.name,
        message: String(error?.message ?? error),
      };
      throw error;
    }
  },
};`,
        });

        const registry = loadOpenClawPlugins(nestedOptions);

        try {
          const reentryError = Reflect.get(globalThis, marker) as
            | { name?: unknown; message?: unknown }
            | undefined;
          expect(reentryError?.name).toBe(PluginLoadReentryError.name);
          expect(String(reentryError?.message)).toContain("plugin load reentry detected");
          const record = registry.plugins.find((entry) => entry.id === "reentrant-snapshot");
          expect(record?.status).toBe("error");
          expect(record?.error).toContain("plugin load reentry detected");
          expect(record?.failurePhase).toBe("register");
        } finally {
          Reflect.deleteProperty(globalThis, marker);
          Reflect.deleteProperty(globalThis, reenterFnMarker);
        }
      },
    },
    {
      label: "lets resolveRuntimePluginRegistry short-circuit during same snapshot load",
      run: () => {
        useNoBundledPlugins();
        const marker = "__openclaw_runtime_registry_reentry_marker";
        const resolverMarker = "__openclaw_runtime_registry_reentry_fn";
        Reflect.deleteProperty(globalThis, marker);
        Reflect.set(
          globalThis,
          resolverMarker,
          (options: Parameters<typeof resolveRuntimePluginRegistry>[0]) =>
            resolveRuntimePluginRegistry(options),
        );
        const pluginDir = makeTempDir();
        const pluginFile = path.join(pluginDir, "runtime-registry-reentry.cjs");
        const nestedOptions = {
          cache: false,
          activate: false,
          workspaceDir: pluginDir,
          config: {
            plugins: {
              load: { paths: [pluginFile] },
              allow: ["runtime-registry-reentry"],
            },
          },
        } satisfies Parameters<typeof loadOpenClawPlugins>[0];
        writePlugin({
          id: "runtime-registry-reentry",
          dir: pluginDir,
          filename: "runtime-registry-reentry.cjs",
          body: `module.exports = {
  id: "runtime-registry-reentry",
  register() {
    const registry = globalThis.${resolverMarker}(${JSON.stringify(nestedOptions)});
    globalThis.${marker} = registry === undefined ? "undefined" : "loaded";
  },
};`,
        });

        const registry = loadOpenClawPlugins(nestedOptions);

        try {
          expect(Reflect.get(globalThis, marker)).toBe("undefined");
          const record = registry.plugins.find((entry) => entry.id === "runtime-registry-reentry");
          expect(record?.status).toBe("loaded");
        } finally {
          Reflect.deleteProperty(globalThis, marker);
          Reflect.deleteProperty(globalThis, resolverMarker);
        }
      },
    },
    {
      label: "keeps scoped plugin loads in a separate cache entry",
      run: () => {
        useNoBundledPlugins();
        const allowed = writePlugin({
          id: "allowed-cache-scope",
          filename: "allowed-cache-scope.cjs",
          body: `module.exports = { id: "allowed-cache-scope", register() {} };`,
        });
        const extra = writePlugin({
          id: "extra-cache-scope",
          filename: "extra-cache-scope.cjs",
          body: `module.exports = { id: "extra-cache-scope", register() {} };`,
        });
        const options = {
          config: {
            plugins: {
              load: { paths: [allowed.file, extra.file] },
              allow: ["allowed-cache-scope", "extra-cache-scope"],
            },
          },
        };

        const full = loadOpenClawPlugins(options);
        const scoped = loadOpenClawPlugins({
          ...options,
          onlyPluginIds: ["allowed-cache-scope"],
        });
        const scopedAgain = loadOpenClawPlugins({
          ...options,
          onlyPluginIds: ["allowed-cache-scope"],
        });

        expect(full.plugins.map((entry) => entry.id).toSorted()).toEqual([
          "allowed-cache-scope",
          "extra-cache-scope",
        ]);
        expect(scoped).not.toBe(full);
        expect(scoped.plugins.map((entry) => entry.id)).toEqual(["allowed-cache-scope"]);
        expect(scopedAgain).toBe(scoped);
      },
    },
    {
      label: "can load a scoped registry without replacing the active global registry",
      run: () => {
        useNoBundledPlugins();
        const plugin = writePlugin({
          id: "allowed-nonactivating-scope",
          filename: "allowed-nonactivating-scope.cjs",
          body: `module.exports = { id: "allowed-nonactivating-scope", register() {} };`,
        });
        const previousRegistry = createEmptyPluginRegistry();
        setActivePluginRegistry(previousRegistry, "existing-registry");
        resetGlobalHookRunner();

        const scoped = loadOpenClawPlugins({
          cache: false,
          activate: false,
          workspaceDir: plugin.dir,
          config: {
            plugins: {
              load: { paths: [plugin.file] },
              allow: ["allowed-nonactivating-scope"],
            },
          },
          onlyPluginIds: ["allowed-nonactivating-scope"],
        });

        expect(scoped.plugins.map((entry) => entry.id)).toEqual(["allowed-nonactivating-scope"]);
        expect(getActivePluginRegistry()).toBe(previousRegistry);
        expect(getActivePluginRegistryKey()).toBe("existing-registry");
        expect(getGlobalHookRunner()).toBeNull();
      },
    },
  ] as const)("handles config-path and scoped plugin loads: $label", async ({ run }) => {
    await run();
  });

  it("treats an explicit empty plugin scope as scoped-empty instead of unscoped", () => {
    useNoBundledPlugins();
    const allowed = writePlugin({
      id: "allowed-empty-scope",
      filename: "allowed-empty-scope.cjs",
      body: `module.exports = { id: "allowed-empty-scope", register() {} };`,
    });
    const extra = writePlugin({
      id: "extra-empty-scope",
      filename: "extra-empty-scope.cjs",
      body: `module.exports = { id: "extra-empty-scope", register() {} };`,
    });

    const registry = loadOpenClawPlugins({
      cache: false,
      activate: false,
      config: {
        plugins: {
          load: { paths: [allowed.file, extra.file] },
          allow: ["allowed-empty-scope", "extra-empty-scope"],
        },
      },
      onlyPluginIds: [],
    });

    expect(registry.plugins).toStrictEqual([]);
  });

  it("skips discovery and manifest registry loading entirely when onlyPluginIds is an explicit empty array", async () => {
    useNoBundledPlugins();
    const allowed = writePlugin({
      id: "allowed-empty-scope",
      filename: "allowed-empty-scope.cjs",
      body: `module.exports = { id: "allowed-empty-scope", register() {} };`,
    });

    const discovery = await import("./discovery.js");
    const manifestRegistry = await import("./manifest-registry.js");
    const discoverySpy = vi.spyOn(discovery, "discoverOpenClawPlugins");
    const manifestSpy = vi.spyOn(manifestRegistry, "loadPluginManifestRegistry");

    const registry = loadOpenClawPlugins({
      cache: false,
      activate: false,
      config: {
        plugins: {
          load: { paths: [allowed.file] },
          allow: ["allowed-empty-scope"],
        },
      },
      onlyPluginIds: [],
    });

    expect(registry.plugins).toStrictEqual([]);
    expect(discoverySpy).not.toHaveBeenCalled();
    expect(manifestSpy).not.toHaveBeenCalled();

    discoverySpy.mockRestore();
    manifestSpy.mockRestore();
  });

  it("only publishes plugin commands to the global registry during activating loads", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "command-plugin",
      filename: "command-plugin.cjs",
      body: `module.exports = {
        id: "command-plugin",
        register(api) {
          api.registerCommand({
            name: "pair",
            description: "Pair device",
            acceptsArgs: true,
            handler: async ({ args }) => ({ text: \`paired:\${args ?? ""}\` }),
          });
        },
      };`,
    });
    clearPluginCommands();

    const scoped = loadOpenClawPlugins({
      cache: false,
      activate: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["command-plugin"],
        },
      },
      onlyPluginIds: ["command-plugin"],
    });

    expect(scoped.plugins.find((entry) => entry.id === "command-plugin")?.status).toBe("loaded");
    expect(scoped.commands.map((entry) => entry.command.name)).toEqual(["pair"]);
    expect(getPluginCommandSpecs("telegram")).toStrictEqual([]);

    const active = loadOpenClawPlugins({
      cache: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["command-plugin"],
        },
      },
      onlyPluginIds: ["command-plugin"],
    });

    expect(active.plugins.find((entry) => entry.id === "command-plugin")?.status).toBe("loaded");
    expect(getPluginCommandSpecs()).toEqual([
      {
        name: "pair",
        description: "Pair device",
        acceptsArgs: true,
      },
    ]);

    clearPluginCommands();
  });

  it("clears plugin agent harnesses during activating reloads", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "codex-harness",
      filename: "codex-harness.cjs",
      body: `module.exports = {
        id: "codex-harness",
        register(api) {
          api.registerAgentHarness({
            id: "codex",
            label: "Codex",
            supports: () => ({ supported: true }),
            runAttempt: async () => ({ ok: false, error: "unused" }),
          });
        },
      };`,
    });

    loadOpenClawPlugins({
      cache: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["codex-harness"],
        },
      },
      onlyPluginIds: ["codex-harness"],
    });
    expect(listAgentHarnessIds()).toEqual(["codex"]);

    loadOpenClawPlugins({
      cache: false,
      workspaceDir: makeTempDir(),
      config: {
        plugins: {
          allow: [],
        },
      },
    });
    expect(listAgentHarnessIds()).toStrictEqual([]);
  });

  it("rejects malformed plugin agent harness registrations", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "bad-harness",
      filename: "bad-harness.cjs",
      body: `module.exports = {
        id: "bad-harness",
        register(api) {
          api.registerAgentHarness({
            id: "broken",
            label: "Broken",
          });
        },
      };`,
    });

    const registry = loadOpenClawPlugins({
      cache: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["bad-harness"],
        },
      },
      onlyPluginIds: ["bad-harness"],
    });

    expect(listAgentHarnessIds()).toStrictEqual([]);
    const diagnostic = registry.diagnostics.find(
      (entry) =>
        entry.level === "error" &&
        entry.pluginId === "bad-harness" &&
        entry.message === 'agent harness "broken" registration missing required runtime methods',
    );
    if (!diagnostic) {
      throw new Error("Expected bad-harness runtime methods diagnostic");
    }
  });

  it("does not register internal hooks globally during non-activating loads", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "internal-hook-snapshot",
      filename: "internal-hook-snapshot.cjs",
      body: `module.exports = {
        id: "internal-hook-snapshot",
        register(api) {
          api.registerHook("gateway:startup", () => {}, { name: "snapshot-hook" });
        },
      };`,
    });

    clearInternalHooks();
    const scoped = loadOpenClawPlugins({
      cache: false,
      activate: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["internal-hook-snapshot"],
        },
      },
      onlyPluginIds: ["internal-hook-snapshot"],
    });

    expect(scoped.plugins.find((entry) => entry.id === "internal-hook-snapshot")?.status).toBe(
      "loaded",
    );
    expect(scoped.hooks.map((entry) => entry.entry.hook.name)).toEqual(["snapshot-hook"]);
    expect(getRegisteredEventKeys()).toStrictEqual([]);

    clearInternalHooks();
  });

  it("replaces prior plugin hook registrations on activating reloads", async () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "internal-hook-reload",
      filename: "internal-hook-reload.cjs",
      body: `module.exports = {
        id: "internal-hook-reload",
        register(api) {
          api.registerHook(
            "gateway:startup",
            (event) => {
              event.messages.push("reload-hook-fired");
            },
            { name: "reload-hook" },
          );
        },
      };`,
    });

    clearInternalHooks();

    const loadOptions = {
      cache: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["internal-hook-reload"],
        },
      },
      onlyPluginIds: ["internal-hook-reload"],
    };

    loadOpenClawPlugins(loadOptions);
    loadOpenClawPlugins(loadOptions);

    const event = createInternalHookEvent("gateway", "startup", "gateway:startup");
    await triggerInternalHook(event);
    expect(countMatching(event.messages, (message) => message === "reload-hook-fired")).toBe(1);

    clearInternalHooks();
  });

  it("injects plugin config into internal hook event context", async () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "hook-config-context",
      filename: "hook-config-context.cjs",
      body: `module.exports = {
        id: "hook-config-context",
        register(api) {
          api.registerHook(
            "gateway:startup",
            (event) => {
              event.messages.push(event.context.pluginConfig?.marker);
            },
            { name: "hook-config-context" },
          );
        },
      };`,
    });
    fs.writeFileSync(
      path.join(plugin.dir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "hook-config-context",
          configSchema: { type: "object" },
        },
        null,
        2,
      ),
      "utf-8",
    );

    clearInternalHooks();

    loadOpenClawPlugins({
      cache: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["hook-config-context"],
          entries: {
            "hook-config-context": {
              config: {
                marker: "plugin-config-visible",
              },
            },
          },
        },
      },
      onlyPluginIds: ["hook-config-context"],
    });

    const event = createInternalHookEvent("gateway", "startup", "gateway:startup");
    await triggerInternalHook(event);
    expect(event.messages).toEqual(["plugin-config-visible"]);
    expect(event.context).toStrictEqual({});

    clearInternalHooks();
  });

  it("rolls back global side effects when registration fails", async () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "failing-side-effects",
      filename: "failing-side-effects.cjs",
      body: `module.exports = {
        id: "failing-side-effects",
        register(api) {
          api.registerHook(
            "gateway:startup",
            (event) => {
              event.messages.push("should-not-run");
            },
            { name: "failing-side-effects-hook" },
          );
          api.registerCommand({
            name: "failme",
            description: "Fail me",
            handler: async () => ({ text: "nope" }),
          });
          api.registerReload({
            onConfigReload: async () => {},
          });
          api.registerNodeHostCommand({
            command: "failme",
            description: "failme",
            run: async () => ({ ok: true }),
          });
          api.registerNodeInvokePolicy({
            commands: ["failme.node"],
            handle: async () => ({ ok: true }),
          });
          api.registerSecurityAuditCollector({
            id: "failme",
            collect: async () => [],
          });
          api.registerInteractiveHandler({
            channel: "slack",
            namespace: "failme",
            handle: async () => ({ handled: true }),
          });
          api.registerContextEngine("failme-context", () => ({
            info: { id: "failme-context", name: "Failme Context" },
            ingest: async () => {},
            assemble: async () => ({ messages: [] }),
          }));
          throw new Error("boom");
        },
      };`,
    });

    clearInternalHooks();
    clearPluginCommands();
    clearPluginInteractiveHandlers();

    const registry = loadOpenClawPlugins({
      cache: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["failing-side-effects"],
        },
      },
      onlyPluginIds: ["failing-side-effects"],
    });

    expect(registry.plugins.find((entry) => entry.id === "failing-side-effects")?.status).toBe(
      "error",
    );
    expect(getRegisteredEventKeys()).toStrictEqual([]);
    expect(getPluginCommandSpecs()).toStrictEqual([]);
    expect(registry.reloads).toStrictEqual([]);
    expect(registry.nodeHostCommands).toStrictEqual([]);
    expect(registry.nodeInvokePolicies).toStrictEqual([]);
    expect(registry.securityAuditCollectors).toStrictEqual([]);
    expect(resolvePluginInteractiveNamespaceMatch("slack", "failme:payload")).toBeNull();
    expect(getContextEngineFactory("failme-context")).toBeUndefined();
    expect(listContextEngineIds()).not.toContain("failme-context");

    const event = createInternalHookEvent("gateway", "startup", "gateway:startup");
    await triggerInternalHook(event);
    expect(event.messages).toStrictEqual([]);

    clearInternalHooks();
    clearPluginCommands();
    clearPluginInteractiveHandlers();
  });

  it("fails plugin registration when a hook is missing its required name", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "nameless-hook",
      filename: "nameless-hook.cjs",
      body: `module.exports = {
        id: "nameless-hook",
        register(api) {
          api.registerHook("gateway:startup", () => {});
        },
      };`,
    });

    clearInternalHooks();

    const registry = loadOpenClawPlugins({
      cache: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["nameless-hook"],
        },
      },
      onlyPluginIds: ["nameless-hook"],
    });

    const record = registry.plugins.find((entry) => entry.id === "nameless-hook");
    expect(record?.status).toBe("error");
    expect(record?.failurePhase).toBe("register");
    expect(record?.error).toContain("hook registration missing name");
    expect(registry.hooks).toStrictEqual([]);
    expect(getRegisteredEventKeys()).toStrictEqual([]);
    expectDiagnosticContaining({
      registry,
      level: "error",
      pluginId: "nameless-hook",
      message: "hook registration missing name",
    });

    clearInternalHooks();
  });

  it("fails plugin registration when a non-memory plugin registers a memory capability", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "invalid-memory-capability",
      filename: "invalid-memory-capability.cjs",
      body: `module.exports = {
        id: "invalid-memory-capability",
        register(api) {
          api.registerMemoryCapability({
            promptBuilder: () => ["should not register"],
          });
        },
      };`,
    });

    const registry = loadOpenClawPlugins({
      cache: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["invalid-memory-capability"],
        },
      },
      onlyPluginIds: ["invalid-memory-capability"],
    });

    const record = registry.plugins.find((entry) => entry.id === "invalid-memory-capability");
    expect(record?.status).toBe("error");
    expect(record?.failurePhase).toBe("register");
    expect(record?.error).toContain("only memory plugins can register a memory capability");
    expect(getMemoryCapabilityRegistration()).toBeUndefined();
    expectDiagnosticContaining({
      registry,
      level: "error",
      pluginId: "invalid-memory-capability",
      message: "only memory plugins can register a memory capability",
    });
  });

  it("can scope bundled provider loads without hanging", () => {
    const bundledDir = makeTempDir();
    const scopedDir = path.join(bundledDir, "scoped-provider");
    mkdirSafe(scopedDir);
    fs.writeFileSync(
      path.join(scopedDir, "package.json"),
      JSON.stringify({
        name: "@openclaw/scoped-provider",
        openclaw: { extensions: ["./index.cjs"] },
      }),
      "utf-8",
    );
    const plugin = writePlugin({
      id: "scoped-provider",
      dir: scopedDir,
      filename: "index.cjs",
      body: `module.exports = {
        id: "scoped-provider",
        register(api) {
          api.registerProvider({
            id: "scoped-provider",
            label: "Scoped Provider",
            auth: [],
          });
        },
      };`,
    });
    updatePluginManifest(plugin, { enabledByDefault: true, providers: ["scoped-provider"] });

    const unscopedDir = path.join(bundledDir, "unscoped-provider");
    mkdirSafe(unscopedDir);
    fs.writeFileSync(
      path.join(unscopedDir, "package.json"),
      JSON.stringify({
        name: "@openclaw/unscoped-provider",
        openclaw: { extensions: ["./index.cjs"] },
      }),
      "utf-8",
    );
    const unscoped = writePlugin({
      id: "unscoped-provider",
      dir: unscopedDir,
      filename: "index.cjs",
      body: `module.exports = {
        id: "unscoped-provider",
        register() {
          throw new Error("unscoped provider should not load");
        },
      };`,
    });
    updatePluginManifest(unscoped, {
      enabledByDefault: true,
      providers: ["unscoped-provider"],
    });
    process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir;
    delete process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;

    const scoped = loadOpenClawPlugins({
      cache: false,
      activate: false,
      config: {
        plugins: {
          enabled: true,
          allow: ["scoped-provider", "unscoped-provider"],
        },
      },
      onlyPluginIds: ["scoped-provider"],
    });

    expect(scoped.plugins.map((entry) => entry.id)).toEqual(["scoped-provider"]);
    expect(scoped.plugins[0]?.status).toBe("loaded");
    expect(scoped.providers.map((entry) => entry.provider.id)).toEqual(["scoped-provider"]);
  });

  it("does not replace active memory plugin registries during non-activating loads", () => {
    useNoBundledPlugins();
    registerMemoryEmbeddingProvider({
      id: "active",
      create: async () => ({ provider: null }),
    });
    registerMemoryCorpusSupplement("memory-wiki", {
      search: async () => [],
      get: async () => null,
    });
    registerMemoryPromptSupplement("memory-wiki", () => ["active wiki supplement"]);
    const activeRuntime = {
      async getMemorySearchManager() {
        return { manager: null, error: "active" };
      },
      resolveMemoryBackendConfig() {
        return { backend: "builtin" as const };
      },
    };
    registerMemoryCapability("memory-core", {
      promptBuilder: () => ["active memory section"],
      flushPlanResolver: () => ({
        softThresholdTokens: 1,
        forceFlushTranscriptBytes: 2,
        reserveTokensFloor: 3,
        prompt: "active",
        systemPrompt: "active",
        relativePath: "memory/active.md",
      }),
      runtime: activeRuntime,
    });
    const plugin = writePlugin({
      id: "snapshot-memory",
      filename: "snapshot-memory.cjs",
      body: `module.exports = {
        id: "snapshot-memory",
        kind: "memory",
        register(api) {
          api.registerMemoryEmbeddingProvider({
            id: "snapshot",
            create: async () => ({ provider: null }),
          });
          api.registerMemoryPromptSection(() => ["snapshot memory section"]);
          api.registerMemoryFlushPlan(() => ({
            softThresholdTokens: 10,
            forceFlushTranscriptBytes: 20,
            reserveTokensFloor: 30,
            prompt: "snapshot",
            systemPrompt: "snapshot",
            relativePath: "memory/snapshot.md",
          }));
          api.registerMemoryRuntime({
            async getMemorySearchManager() {
              return { manager: null, error: "snapshot" };
            },
            resolveMemoryBackendConfig() {
              return { backend: "qmd", qmd: {} };
            },
          });
        },
      };`,
    });

    const scoped = loadOpenClawPlugins({
      cache: false,
      activate: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["snapshot-memory"],
          slots: { memory: "snapshot-memory" },
        },
      },
      onlyPluginIds: ["snapshot-memory"],
    });

    expect(scoped.plugins.find((entry) => entry.id === "snapshot-memory")?.status).toBe("loaded");
    expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual([
      "active memory section",
      "active wiki supplement",
    ]);
    expect(listMemoryCorpusSupplements()).toHaveLength(1);
    expect(resolveMemoryFlushPlan({})?.relativePath).toBe("memory/active.md");
    expect(getMemoryRuntime()).toBe(activeRuntime);
    expect(listMemoryEmbeddingProviders().map((adapter) => adapter.id)).toEqual(["active"]);
  });

  it("does not replace active embedding providers during non-activating loads", () => {
    useNoBundledPlugins();
    registerEmbeddingProvider({
      id: "active",
      create: async () => ({ provider: null }),
    });
    const plugin = writePlugin({
      id: "snapshot-embedding",
      filename: "snapshot-embedding.cjs",
      body: `module.exports = {
        id: "snapshot-embedding",
        register(api) {
          api.registerEmbeddingProvider({
            id: "snapshot",
            create: async () => ({ provider: null }),
          });
        },
      };`,
    });
    updatePluginManifest(plugin, {
      contracts: { embeddingProviders: ["snapshot"] },
    });

    const scoped = loadOpenClawPlugins({
      cache: false,
      activate: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["snapshot-embedding"],
        },
      },
      onlyPluginIds: ["snapshot-embedding"],
    });

    expect(scoped.plugins.find((entry) => entry.id === "snapshot-embedding")?.status).toBe(
      "loaded",
    );
    expect(scoped.embeddingProviders.map((entry) => entry.provider.id)).toEqual(["snapshot"]);
    expect(listEmbeddingProviders().map((adapter) => adapter.id)).toEqual([
      "openai-compatible",
      "active",
    ]);
    expect(getEmbeddingProvider("snapshot")).toBeUndefined();
  });

  it("allows non-activating embedding provider snapshots to reuse active ids", () => {
    useNoBundledPlugins();
    registerEmbeddingProvider({
      id: "shared",
      create: async () => ({ provider: null }),
    });
    const plugin = writePlugin({
      id: "snapshot-shared-embedding",
      filename: "snapshot-shared-embedding.cjs",
      body: `module.exports = {
        id: "snapshot-shared-embedding",
        register(api) {
          api.registerEmbeddingProvider({
            id: "shared",
            create: async () => ({ provider: null }),
          });
        },
      };`,
    });
    updatePluginManifest(plugin, {
      contracts: { embeddingProviders: ["shared"] },
    });

    const scoped = loadOpenClawPlugins({
      cache: false,
      activate: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["snapshot-shared-embedding"],
        },
      },
      onlyPluginIds: ["snapshot-shared-embedding"],
    });

    expect(scoped.plugins.find((entry) => entry.id === "snapshot-shared-embedding")?.status).toBe(
      "loaded",
    );
    expect(scoped.embeddingProviders.map((entry) => entry.provider.id)).toEqual(["shared"]);
    expect(listEmbeddingProviders().map((adapter) => adapter.id)).toEqual([
      "openai-compatible",
      "shared",
    ]);
    expect(getEmbeddingProvider("shared")?.id).toBe("shared");
  });

  it("clears newly-registered embedding providers when plugin register fails", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "failing-embedding",
      filename: "failing-embedding.cjs",
      body: `module.exports = {
        id: "failing-embedding",
        register(api) {
          api.registerEmbeddingProvider({
            id: "failed",
            create: async () => ({ provider: null }),
          });
          throw new Error("embedding register failed");
        },
      };`,
    });
    updatePluginManifest(plugin, {
      contracts: { embeddingProviders: ["failed"] },
    });

    const registry = loadOpenClawPlugins({
      cache: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["failing-embedding"],
        },
      },
      onlyPluginIds: ["failing-embedding"],
    });

    expect(registry.plugins.find((entry) => entry.id === "failing-embedding")?.status).toBe(
      "error",
    );
    expect(listEmbeddingProviders().map((adapter) => adapter.id)).toStrictEqual([
      "openai-compatible",
    ]);
  });

  it("clears newly-registered memory plugin registries when plugin register fails", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "failing-memory",
      filename: "failing-memory.cjs",
      body: `module.exports = {
        id: "failing-memory",
        kind: "memory",
        register(api) {
          api.registerMemoryEmbeddingProvider({
            id: "failed",
            create: async () => ({ provider: null }),
          });
          api.registerMemoryPromptSection(() => ["stale failure section"]);
          api.registerMemoryPromptSupplement(() => ["stale failure supplement"]);
          api.registerMemoryCorpusSupplement({
            search: async () => [],
            get: async () => null,
          });
          api.registerMemoryFlushPlan(() => ({
            softThresholdTokens: 10,
            forceFlushTranscriptBytes: 20,
            reserveTokensFloor: 30,
            prompt: "failed",
            systemPrompt: "failed",
            relativePath: "memory/failed.md",
          }));
          api.registerMemoryRuntime({
            async getMemorySearchManager() {
              return { manager: null, error: "failed" };
            },
            resolveMemoryBackendConfig() {
              return { backend: "builtin" };
            },
          });
          throw new Error("memory register failed");
        },
      };`,
    });

    const registry = loadOpenClawPlugins({
      cache: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["failing-memory"],
          slots: { memory: "failing-memory" },
        },
      },
      onlyPluginIds: ["failing-memory"],
    });

    expect(registry.plugins.find((entry) => entry.id === "failing-memory")?.status).toBe("error");
    expect(buildMemoryPromptSection({ availableTools: new Set() })).toStrictEqual([]);
    expect(listMemoryCorpusSupplements()).toStrictEqual([]);
    expect(resolveMemoryFlushPlan({})).toBeNull();
    expect(getMemoryRuntime()).toBeUndefined();
    expect(listMemoryEmbeddingProviders()).toStrictEqual([]);
  });

  it("does not replace the active detached task runtime during non-activating loads", () => {
    useNoBundledPlugins();
    const activeRuntime = createDetachedTaskRuntimeStub("active");
    registerDetachedTaskLifecycleRuntime("active-runtime", activeRuntime);

    const plugin = writePlugin({
      id: "snapshot-detached-runtime",
      filename: "snapshot-detached-runtime.cjs",
      body: `module.exports = {
        id: "snapshot-detached-runtime",
        register(api) {
          api.registerDetachedTaskRuntime({
            createQueuedTaskRun() { throw new Error("snapshot createQueuedTaskRun should not run"); },
            createRunningTaskRun() { throw new Error("snapshot createRunningTaskRun should not run"); },
            startTaskRunByRunId() { throw new Error("snapshot startTaskRunByRunId should not run"); },
            recordTaskRunProgressByRunId() { throw new Error("snapshot recordTaskRunProgressByRunId should not run"); },
            finalizeTaskRunByRunId() { throw new Error("snapshot finalizeTaskRunByRunId should not run"); },
            completeTaskRunByRunId() { throw new Error("snapshot completeTaskRunByRunId should not run"); },
            failTaskRunByRunId() { throw new Error("snapshot failTaskRunByRunId should not run"); },
            setDetachedTaskDeliveryStatusByRunId() { throw new Error("snapshot setDetachedTaskDeliveryStatusByRunId should not run"); },
            async cancelDetachedTaskRunById() { return { found: true, cancelled: true }; },
          });
        },
      };`,
    });

    const scoped = loadOpenClawPlugins({
      cache: false,
      activate: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["snapshot-detached-runtime"],
        },
      },
      onlyPluginIds: ["snapshot-detached-runtime"],
    });

    expect(scoped.plugins.find((entry) => entry.id === "snapshot-detached-runtime")?.status).toBe(
      "loaded",
    );
    const runtimeRegistration = getDetachedTaskLifecycleRuntimeRegistration();
    expect(runtimeRegistration?.pluginId).toBe("active-runtime");
    expect(runtimeRegistration?.runtime).toBe(activeRuntime);
  });

  it("clears newly-registered detached task runtimes when plugin register fails", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "failing-detached-runtime",
      filename: "failing-detached-runtime.cjs",
      body: `module.exports = {
        id: "failing-detached-runtime",
        register(api) {
          api.registerDetachedTaskRuntime({
            createQueuedTaskRun() { throw new Error("failing createQueuedTaskRun should not run"); },
            createRunningTaskRun() { throw new Error("failing createRunningTaskRun should not run"); },
            startTaskRunByRunId() { throw new Error("failing startTaskRunByRunId should not run"); },
            recordTaskRunProgressByRunId() { throw new Error("failing recordTaskRunProgressByRunId should not run"); },
            finalizeTaskRunByRunId() { throw new Error("failing finalizeTaskRunByRunId should not run"); },
            completeTaskRunByRunId() { throw new Error("failing completeTaskRunByRunId should not run"); },
            failTaskRunByRunId() { throw new Error("failing failTaskRunByRunId should not run"); },
            setDetachedTaskDeliveryStatusByRunId() { throw new Error("failing setDetachedTaskDeliveryStatusByRunId should not run"); },
            async cancelDetachedTaskRunById() { return { found: true, cancelled: true }; },
          });
          throw new Error("detached runtime register failed");
        },
      };`,
    });

    const registry = loadOpenClawPlugins({
      cache: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["failing-detached-runtime"],
        },
      },
      onlyPluginIds: ["failing-detached-runtime"],
    });

    expect(registry.plugins.find((entry) => entry.id === "failing-detached-runtime")?.status).toBe(
      "error",
    );
    expect(getDetachedTaskLifecycleRuntimeRegistration()).toBeUndefined();
  });

  it("restores cached detached task runtime registrations on cache hits", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "cached-detached-runtime",
      filename: "cached-detached-runtime.cjs",
      body: `module.exports = {
        id: "cached-detached-runtime",
        register(api) {
          api.registerDetachedTaskRuntime({
            createQueuedTaskRun() { throw new Error("cached createQueuedTaskRun should not run"); },
            createRunningTaskRun() { throw new Error("cached createRunningTaskRun should not run"); },
            startTaskRunByRunId() { throw new Error("cached startTaskRunByRunId should not run"); },
            recordTaskRunProgressByRunId() { throw new Error("cached recordTaskRunProgressByRunId should not run"); },
            finalizeTaskRunByRunId() { throw new Error("cached finalizeTaskRunByRunId should not run"); },
            completeTaskRunByRunId() { throw new Error("cached completeTaskRunByRunId should not run"); },
            failTaskRunByRunId() { throw new Error("cached failTaskRunByRunId should not run"); },
            setDetachedTaskDeliveryStatusByRunId() { throw new Error("cached setDetachedTaskDeliveryStatusByRunId should not run"); },
            async cancelDetachedTaskRunById() { return { found: true, cancelled: true }; },
          });
        },
      };`,
    });

    const loadOptions = {
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["cached-detached-runtime"],
        },
      },
      onlyPluginIds: ["cached-detached-runtime"],
    } satisfies Parameters<typeof loadOpenClawPlugins>[0];

    loadOpenClawPlugins(loadOptions);
    expect(getDetachedTaskLifecycleRuntimeRegistration()?.pluginId).toBe("cached-detached-runtime");

    clearDetachedTaskLifecycleRuntimeRegistration();
    expect(getDetachedTaskLifecycleRuntimeRegistration()).toBeUndefined();

    loadOpenClawPlugins(loadOptions);

    expect(getDetachedTaskLifecycleRuntimeRegistration()?.pluginId).toBe("cached-detached-runtime");
  });

  it("restores cached command and interactive handler registrations on cache hits", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "cached-command-interactive",
      filename: "cached-command-interactive.cjs",
      body: `module.exports = {
        id: "cached-command-interactive",
        register(api) {
          api.registerCommand({
            name: "hue",
            description: "Control Hue lights",
            handler: async () => ({ text: "ok" }),
          });
          api.registerInteractiveHandler({
            channel: "telegram",
            namespace: "hue",
            handle: async () => ({ handled: true }),
          });
        },
      };`,
    });

    const loadOptions = {
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["cached-command-interactive"],
        },
      },
      onlyPluginIds: ["cached-command-interactive"],
    } satisfies Parameters<typeof loadOpenClawPlugins>[0];

    loadOpenClawPlugins(loadOptions);
    expect(getPluginCommandSpecs()).toEqual([
      { name: "hue", description: "Control Hue lights", acceptsArgs: false },
    ]);
    const match = resolvePluginInteractiveNamespaceMatch("telegram", "hue:on");
    expect(match?.namespace).toBe("hue");
    expect(match?.payload).toBe("on");

    const dedupeKey = "telegram:hue:callback-1";
    expect(claimPluginInteractiveCallbackDedupe(dedupeKey, 1_000)).toBe(true);
    commitPluginInteractiveCallbackDedupe(dedupeKey, 1_000);
    expect(claimPluginInteractiveCallbackDedupe(dedupeKey, 1_001)).toBe(false);

    loadOpenClawPlugins(loadOptions);
    expect(claimPluginInteractiveCallbackDedupe(dedupeKey, 1_002)).toBe(false);

    clearPluginCommands();
    clearPluginInteractiveHandlerRegistrations();
    expect(getPluginCommandSpecs()).toStrictEqual([]);
    expect(resolvePluginInteractiveNamespaceMatch("telegram", "hue:on")).toBeNull();

    loadOpenClawPlugins(loadOptions);

    expect(getPluginCommandSpecs()).toEqual([
      { name: "hue", description: "Control Hue lights", acceptsArgs: false },
    ]);
    const registration = resolvePluginInteractiveNamespaceMatch("telegram", "hue:on")?.registration;
    expect(registration?.pluginId).toBe("cached-command-interactive");
    expect(registration?.namespace).toBe("hue");
    expect(registration?.channel).toBe("telegram");
    expect(claimPluginInteractiveCallbackDedupe(dedupeKey, 1_003)).toBe(false);
  });

  it("clears stale detached task runtime registrations on active reloads when no plugin re-registers one", () => {
    useNoBundledPlugins();
    registerDetachedTaskLifecycleRuntime("stale-runtime", createDetachedTaskRuntimeStub("stale"));

    loadOpenClawPlugins({
      cache: false,
      config: {
        plugins: {
          load: { paths: [] },
          allow: [],
        },
      },
    });

    expect(getDetachedTaskLifecycleRuntimeRegistration()).toBeUndefined();
  });

  it("restores cached memory capability public artifacts on cache hits", async () => {
    useNoBundledPlugins();
    const workspaceDir = makeTempDir();
    const absolutePath = path.join(workspaceDir, "MEMORY.md");
    fs.writeFileSync(absolutePath, "# Memory\n");
    const plugin = writePlugin({
      id: "cached-memory-capability",
      filename: "cached-memory-capability.cjs",
      body: `module.exports = {
        id: "cached-memory-capability",
        kind: "memory",
        register(api) {
          api.registerMemoryCapability({
            publicArtifacts: {
              async listArtifacts() {
                return [{
                  kind: "memory-root",
                  workspaceDir: ${JSON.stringify(workspaceDir)},
                  relativePath: "MEMORY.md",
                  absolutePath: ${JSON.stringify(absolutePath)},
                  agentIds: ["main"],
                  contentType: "markdown",
                }];
              },
            },
          });
        },
      };`,
    });

    const options = {
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["cached-memory-capability"],
          slots: { memory: "cached-memory-capability" },
        },
      },
      onlyPluginIds: ["cached-memory-capability"],
    };

    const expectedArtifacts = [
      {
        kind: "memory-root",
        workspaceDir,
        relativePath: "MEMORY.md",
        absolutePath,
        agentIds: ["main"],
        contentType: "markdown" as const,
      },
    ];

    const first = loadOpenClawPlugins(options);
    await expect(listActiveMemoryPublicArtifacts({ cfg: {} as never })).resolves.toEqual(
      expectedArtifacts,
    );

    clearMemoryPluginState();

    const second = loadOpenClawPlugins(options);
    expect(second).toBe(first);
    await expect(listActiveMemoryPublicArtifacts({ cfg: {} as never })).resolves.toEqual(
      expectedArtifacts,
    );
  });

  it("preserves previously registered memory capability across activate:false snapshot loads", async () => {
    useNoBundledPlugins();
    const workspaceDir = makeTempDir();
    const absolutePath = path.join(workspaceDir, "MEMORY.md");
    fs.writeFileSync(absolutePath, "# Memory\n");
    const memoryPlugin = writePlugin({
      id: "capability-survives-memory",
      filename: "capability-survives-memory.cjs",
      body: `module.exports = {
        id: "capability-survives-memory",
        kind: "memory",
        register(api) {
          api.registerMemoryCapability({
            publicArtifacts: {
              async listArtifacts() {
                return [{
                  kind: "memory-root",
                  workspaceDir: ${JSON.stringify(workspaceDir)},
                  relativePath: "MEMORY.md",
                  absolutePath: ${JSON.stringify(absolutePath)},
                  agentIds: ["main"],
                  contentType: "markdown",
                }];
              },
            },
          });
        },
      };`,
    });
    const sidecarPlugin = writePlugin({
      id: "capability-survives-sidecar",
      filename: "capability-survives-sidecar.cjs",
      body: `module.exports = {
        id: "capability-survives-sidecar",
        register() {},
      };`,
    });

    const activateConfig = {
      plugins: {
        load: { paths: [memoryPlugin.file, sidecarPlugin.file] },
        allow: ["capability-survives-memory", "capability-survives-sidecar"],
        slots: { memory: "capability-survives-memory" },
      },
    };
    loadOpenClawPlugins({
      cache: false,
      workspaceDir: memoryPlugin.dir,
      config: activateConfig,
    });

    const expectedArtifacts = [
      {
        kind: "memory-root",
        workspaceDir,
        relativePath: "MEMORY.md",
        absolutePath,
        agentIds: ["main"],
        contentType: "markdown" as const,
      },
    ];

    await expect(listActiveMemoryPublicArtifacts({ cfg: {} as never })).resolves.toEqual(
      expectedArtifacts,
    );

    // Simulate what resolvePluginWebSearchProviders and similar read-only paths do:
    // load plugins again with activate:false. Each per-plugin snapshot/rollback must
    // preserve the previously registered memory capability.
    loadOpenClawPlugins({
      cache: false,
      activate: false,
      workspaceDir: memoryPlugin.dir,
      config: activateConfig,
    });

    await expect(listActiveMemoryPublicArtifacts({ cfg: {} as never })).resolves.toEqual(
      expectedArtifacts,
    );
  });

  it("uses discovery registration mode for non-activating loads", () => {
    useNoBundledPlugins();
    const marker = "__openclawDiscoveryModeTest";
    const plugin = writePlugin({
      id: "discovery-mode-test",
      filename: "discovery-mode-test.cjs",
      body: `module.exports = {
        id: "discovery-mode-test",
        register(api) {
          globalThis.${marker} = globalThis.${marker} || [];
          globalThis.${marker}.push(api.registrationMode);
          api.registerProvider({ id: "discovery-provider", label: "Discovery Provider", auth: [] });
          api.registerTool({
            name: "discovery_tool",
            description: "Discovery tool",
            parameters: {},
            execute: async () => ({ content: [{ type: "text", text: "ok" }] }),
          });
        },
      };`,
    });
    updatePluginManifest(plugin, { contracts: { tools: ["discovery_tool"] } });
    const config = {
      plugins: {
        load: { paths: [plugin.file] },
        allow: ["discovery-mode-test"],
      },
    };

    const snapshot = loadOpenClawPlugins({
      activate: false,
      cache: false,
      workspaceDir: plugin.dir,
      config,
    });
    expect((globalThis as Record<string, unknown>)[marker]).toEqual(["discovery"]);
    expect(snapshot.providers.map((entry) => entry.provider.id)).toEqual(["discovery-provider"]);
    expect(snapshot.tools.flatMap((entry) => entry.names)).toContain("discovery_tool");

    loadOpenClawPlugins({
      cache: false,
      workspaceDir: plugin.dir,
      config,
    });
    expect((globalThis as Record<string, unknown>)[marker]).toEqual(["discovery", "full"]);
    delete (globalThis as Record<string, unknown>)[marker];
  });

  it("rejects plugin tool registration without manifest tool ownership", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "undeclared-tool-owner",
      filename: "undeclared-tool-owner.cjs",
      body: `module.exports = {
        id: "undeclared-tool-owner",
        register(api) {
          api.registerTool({
            name: "undeclared_tool",
            description: "Undeclared tool",
            parameters: {},
            execute: async () => ({ content: [{ type: "text", text: "ok" }] }),
          });
        },
      };`,
    });

    const registry = loadOpenClawPlugins({
      activate: false,
      cache: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["undeclared-tool-owner"],
        },
      },
    });

    expect(registry.tools).toStrictEqual([]);
    expect(
      registry.diagnostics.some(
        (entry) =>
          entry.pluginId === "undeclared-tool-owner" &&
          entry.message === "plugin must declare contracts.tools before registering agent tools",
      ),
    ).toBe(true);
  });

  it("rejects plugin tool names outside the manifest tool contract", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "wrong-tool-owner",
      filename: "wrong-tool-owner.cjs",
      body: `module.exports = {
        id: "wrong-tool-owner",
        register(api) {
          api.registerTool({
            name: "runtime_tool",
            description: "Runtime tool",
            parameters: {},
            execute: async () => ({ content: [{ type: "text", text: "ok" }] }),
          });
        },
      };`,
    });
    updatePluginManifest(plugin, { contracts: { tools: ["manifest_tool"] } });

    const registry = loadOpenClawPlugins({
      activate: false,
      cache: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["wrong-tool-owner"],
        },
      },
    });

    expect(registry.tools).toStrictEqual([]);
    expect(
      registry.diagnostics.some(
        (entry) =>
          entry.pluginId === "wrong-tool-owner" &&
          entry.message === "plugin must declare contracts.tools for: runtime_tool",
      ),
    ).toBe(true);
  });

  it("caches non-activating snapshots without restoring global side effects", () => {
    useNoBundledPlugins();
    clearPluginCommands();
    const marker = "__openclawSnapshotCacheRegisterCount";
    const plugin = writePlugin({
      id: "snapshot-cache",
      filename: "snapshot-cache.cjs",
      body: `module.exports = {
        id: "snapshot-cache",
        register(api) {
          globalThis.${marker} = (globalThis.${marker} || 0) + 1;
          api.registerCommand({
            name: "snapshot-command",
            description: "Snapshot command",
            handler: async () => ({ text: "ok" }),
          });
        },
      };`,
    });
    const options = {
      activate: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["snapshot-cache"],
        },
      },
      onlyPluginIds: ["snapshot-cache"],
    };

    const first = loadOpenClawPlugins(options);
    const second = loadOpenClawPlugins(options);

    expect(second).toBe(first);
    expect((globalThis as Record<string, unknown>)[marker]).toBe(1);
    expect(first.commands.map((entry) => entry.command.name)).toEqual(["snapshot-command"]);
    expect(getPluginCommandSpecs()).toStrictEqual([]);

    const active = loadOpenClawPlugins({
      workspaceDir: plugin.dir,
      config: options.config,
      onlyPluginIds: ["snapshot-cache"],
    });
    expect(active).not.toBe(first);
    expect((globalThis as Record<string, unknown>)[marker]).toBe(2);
    expect(getPluginCommandSpecs()).toEqual([
      {
        name: "snapshot-command",
        description: "Snapshot command",
        acceptsArgs: false,
      },
    ]);
    delete (globalThis as Record<string, unknown>)[marker];
  });

  it("does not re-register non-bundled plugins after gateway-bindable boot loads", () => {
    useNoBundledPlugins();
    const marker = "__openclawGatewayBootRegisterCount";
    const plugin = writePlugin({
      id: "costclaw-boot-cache",
      filename: "costclaw-boot-cache.cjs",
      body: `module.exports = {
        id: "costclaw-boot-cache",
        register() {
          globalThis.${marker} = (globalThis.${marker} || 0) + 1;
        },
      };`,
    });
    const config = {
      plugins: {
        load: { paths: [plugin.file] },
        allow: ["costclaw-boot-cache"],
        entries: {
          "costclaw-boot-cache": { enabled: true },
        },
      },
    };

    loadOpenClawPlugins({
      workspaceDir: plugin.dir,
      config,
      runtimeOptions: {
        allowGatewaySubagentBinding: true,
      },
    });
    ensurePluginRegistryLoaded({
      scope: "all",
      workspaceDir: plugin.dir,
      config,
    });

    expect((globalThis as Record<string, unknown>)[marker]).toBe(1);
    delete (globalThis as Record<string, unknown>)[marker];
  });

  it("reuses a gateway-bindable cache entry for later default-mode loads", () => {
    useNoBundledPlugins();
    const marker = "__openclawGatewayBindableCacheRegisterCount";
    const plugin = writePlugin({
      id: "gateway-bindable-cache",
      filename: "gateway-bindable-cache.cjs",
      body: `module.exports = {
        id: "gateway-bindable-cache",
        register() {
          globalThis.${marker} = (globalThis.${marker} || 0) + 1;
        },
      };`,
    });
    const options = {
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["gateway-bindable-cache"],
          entries: {
            "gateway-bindable-cache": { enabled: true },
          },
        },
      },
    };

    const gatewayBindable = loadOpenClawPlugins({
      ...options,
      runtimeOptions: {
        allowGatewaySubagentBinding: true,
      },
    });
    const defaultMode = loadOpenClawPlugins(options);

    expect(defaultMode).toBe(gatewayBindable);
    expect((globalThis as Record<string, unknown>)[marker]).toBe(1);
    delete (globalThis as Record<string, unknown>)[marker];
  });

  it("re-initializes global hook runner when serving registry from cache", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "cache-hook-runner",
      filename: "cache-hook-runner.cjs",
      body: `module.exports = { id: "cache-hook-runner", register() {} };`,
    });

    const options = {
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["cache-hook-runner"],
        },
      },
    };

    const first = loadOpenClawPlugins(options);
    expectGlobalHookRunner(getGlobalHookRunner());

    resetGlobalHookRunner();
    expect(getGlobalHookRunner()).toBeNull();

    const second = loadOpenClawPlugins(options);
    expect(second).toBe(first);
    expectGlobalHookRunner(getGlobalHookRunner());

    resetGlobalHookRunner();
  });

  it("preserves the gateway-bindable hook runner across later default-mode activating loads", () => {
    useNoBundledPlugins();
    const gatewayPlugin = writePlugin({
      id: "gateway-hook-surface",
      filename: "gateway-hook-surface.cjs",
      body: `module.exports = { id: "gateway-hook-surface", register(api) {
        api.on("subagent_ended", () => undefined);
      } };`,
    });
    const defaultPlugin = writePlugin({
      id: "default-hook-surface",
      filename: "default-hook-surface.cjs",
      body: `module.exports = { id: "default-hook-surface", register(api) {
        api.on("message_sent", () => undefined);
      } };`,
    });

    const gatewayRegistry = loadOpenClawPlugins({
      workspaceDir: gatewayPlugin.dir,
      config: {
        plugins: {
          load: { paths: [gatewayPlugin.file] },
          allow: ["gateway-hook-surface"],
          entries: {
            "gateway-hook-surface": {
              enabled: true,
              hooks: { allowConversationAccess: true },
            },
          },
        },
      },
      runtimeOptions: {
        allowGatewaySubagentBinding: true,
      },
    });
    expect(getGlobalPluginRegistry()).toBe(gatewayRegistry);
    expect(expectGlobalHookRunner(getGlobalHookRunner()).hasHooks("subagent_ended")).toBe(true);

    const defaultRegistry = loadOpenClawPlugins({
      workspaceDir: defaultPlugin.dir,
      config: {
        plugins: {
          load: { paths: [defaultPlugin.file] },
          allow: ["default-hook-surface"],
          entries: {
            "default-hook-surface": {
              enabled: true,
              hooks: { allowConversationAccess: true },
            },
          },
        },
      },
    });

    expect(getActivePluginRegistry()).toBe(defaultRegistry);
    expect(getGlobalPluginRegistry()).toBe(gatewayRegistry);
    const globalHookRunner = expectGlobalHookRunner(getGlobalHookRunner());
    expect(globalHookRunner.hasHooks("subagent_ended")).toBe(true);
    expect(globalHookRunner.hasHooks("message_sent")).toBe(false);
  });

  it.each([
    {
      name: "does not reuse cached bundled plugin registries across env changes",
      pluginId: "cache-root",
      setup: () => {
        const bundledA = makeTempDir();
        const bundledB = makeTempDir();
        const pluginA = writePlugin({
          id: "cache-root",
          dir: path.join(bundledA, "cache-root"),
          filename: "index.cjs",
          body: `module.exports = { id: "cache-root", register() {} };`,
        });
        const pluginB = writePlugin({
          id: "cache-root",
          dir: path.join(bundledB, "cache-root"),
          filename: "index.cjs",
          body: `module.exports = { id: "cache-root", register() {} };`,
        });

        const options = {
          config: {
            plugins: {
              allow: ["cache-root"],
              entries: {
                "cache-root": { enabled: true },
              },
            },
          },
        };

        return {
          expectedFirstSource: pluginA.file,
          expectedSecondSource: pluginB.file,
          loadFirst: () =>
            loadOpenClawPlugins({
              ...options,
              env: {
                ...process.env,
                OPENCLAW_BUNDLED_PLUGINS_DIR: bundledA,
              },
            }),
          loadSecond: () =>
            loadOpenClawPlugins({
              ...options,
              env: {
                ...process.env,
                OPENCLAW_BUNDLED_PLUGINS_DIR: bundledB,
              },
            }),
        };
      },
    },
    {
      name: "does not reuse cached load-path plugin registries across env home changes",
      pluginId: "demo",
      setup: () => {
        const homeA = makeTempDir();
        const homeB = makeTempDir();
        const stateDir = makeTempDir();
        const bundledDir = makeTempDir();
        const pluginA = writePlugin({
          id: "demo",
          dir: path.join(homeA, "plugins", "demo"),
          filename: "index.cjs",
          body: `module.exports = { id: "demo", register() {} };`,
        });
        const pluginB = writePlugin({
          id: "demo",
          dir: path.join(homeB, "plugins", "demo"),
          filename: "index.cjs",
          body: `module.exports = { id: "demo", register() {} };`,
        });

        const options = {
          config: {
            plugins: {
              allow: ["demo"],
              entries: {
                demo: { enabled: true },
              },
              load: {
                paths: ["~/plugins/demo"],
              },
            },
          },
        };

        return {
          expectedFirstSource: pluginA.file,
          expectedSecondSource: pluginB.file,
          loadFirst: () =>
            loadOpenClawPlugins({
              ...options,
              env: {
                ...process.env,
                HOME: homeA,
                OPENCLAW_HOME: undefined,
                OPENCLAW_STATE_DIR: stateDir,
                OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir,
              },
            }),
          loadSecond: () =>
            loadOpenClawPlugins({
              ...options,
              env: {
                ...process.env,
                HOME: homeB,
                OPENCLAW_HOME: undefined,
                OPENCLAW_STATE_DIR: stateDir,
                OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir,
              },
            }),
        };
      },
    },
  ])("$name", ({ pluginId, setup }) => {
    const { expectedFirstSource, expectedSecondSource, loadFirst, loadSecond } = setup();
    expectCachePartitionByPluginSource({
      pluginId,
      loadFirst,
      loadSecond,
      expectedFirstSource,
      expectedSecondSource,
    });
  });

  it.each([
    {
      name: "does not reuse cached registries when env-resolved install paths change",
      setup: () => {
        useNoBundledPlugins();
        const openclawHome = makeTempDir();
        const ignoredHome = makeTempDir();
        const stateDir = makeTempDir();
        const pluginDir = path.join(openclawHome, "plugins", "tracked-install-cache");
        mkdirSafe(pluginDir);
        const plugin = writePlugin({
          id: "tracked-install-cache",
          dir: pluginDir,
          filename: "index.cjs",
          body: `module.exports = { id: "tracked-install-cache", register() {} };`,
        });

        writePersistedInstalledPluginIndexInstallRecordsSync(
          {
            "tracked-install-cache": {
              source: "path" as const,
              installPath: "~/plugins/tracked-install-cache",
              sourcePath: "~/plugins/tracked-install-cache",
            },
          },
          { stateDir },
        );

        const options = {
          config: {
            plugins: {
              load: { paths: [plugin.file] },
              allow: ["tracked-install-cache"],
            },
          },
        };

        const secondHome = makeTempDir();
        return {
          loadFirst: () =>
            loadOpenClawPlugins({
              ...options,
              env: {
                ...process.env,
                OPENCLAW_HOME: openclawHome,
                HOME: ignoredHome,
                OPENCLAW_STATE_DIR: stateDir,
                OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins",
              },
            }),
          loadVariant: () =>
            loadOpenClawPlugins({
              ...options,
              env: {
                ...process.env,
                OPENCLAW_HOME: secondHome,
                HOME: ignoredHome,
                OPENCLAW_STATE_DIR: stateDir,
                OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins",
              },
            }),
        };
      },
    },
    {
      name: "does not reuse cached registries across different plugin SDK resolution preferences",
      setup: () => {
        useNoBundledPlugins();
        const plugin = writePlugin({
          id: "cache-sdk-resolution",
          filename: "cache-sdk-resolution.cjs",
          body: `module.exports = { id: "cache-sdk-resolution", register() {} };`,
        });

        const options = {
          workspaceDir: plugin.dir,
          config: {
            plugins: {
              allow: ["cache-sdk-resolution"],
              load: {
                paths: [plugin.file],
              },
            },
          },
        };

        return {
          loadFirst: () => loadOpenClawPlugins(options),
          loadVariant: () =>
            loadOpenClawPlugins({
              ...options,
              pluginSdkResolution: "workspace" as PluginSdkResolutionPreference,
            }),
        };
      },
    },
    {
      name: "does not reuse cached registries across gateway subagent binding modes",
      setup: () => {
        useNoBundledPlugins();
        const plugin = writePlugin({
          id: "cache-gateway-shared",
          filename: "cache-gateway-shared.cjs",
          body: `module.exports = { id: "cache-gateway-shared", register() {} };`,
        });

        const options = {
          workspaceDir: plugin.dir,
          config: {
            plugins: {
              allow: ["cache-gateway-shared"],
              load: {
                paths: [plugin.file],
              },
            },
          },
        };

        return {
          loadFirst: () => loadOpenClawPlugins(options),
          loadVariant: () =>
            loadOpenClawPlugins({
              ...options,
              runtimeOptions: {
                allowGatewaySubagentBinding: true,
              },
            }),
        };
      },
    },
  ])("$name", ({ setup }) => {
    expectCacheMissThenHit(setup());
  });

  it("evicts least recently used registries when the loader cache exceeds its cap", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "cache-eviction",
      filename: "cache-eviction.cjs",
      body: `module.exports = { id: "cache-eviction", register() {} };`,
    });
    const previousCacheCap = testing.maxPluginRegistryCacheEntries;
    testing.setMaxPluginRegistryCacheEntriesForTest(4);
    const stateDirs = Array.from({ length: testing.maxPluginRegistryCacheEntries + 1 }, () =>
      makeTempDir(),
    );

    const loadWithStateDir = (stateDir: string) =>
      loadOpenClawPlugins({
        env: {
          ...process.env,
          OPENCLAW_STATE_DIR: stateDir,
          OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins",
        },
        config: {
          plugins: {
            allow: ["cache-eviction"],
            load: {
              paths: [plugin.file],
            },
          },
        },
      });

    try {
      const first = loadWithStateDir(stateDirs[0] ?? makeTempDir());
      const second = loadWithStateDir(stateDirs[1] ?? makeTempDir());

      expect(loadWithStateDir(stateDirs[0] ?? makeTempDir())).toBe(first);

      for (const stateDir of stateDirs.slice(2)) {
        loadWithStateDir(stateDir);
      }

      expect(loadWithStateDir(stateDirs[0] ?? makeTempDir())).toBe(first);
      expect(loadWithStateDir(stateDirs[1] ?? makeTempDir())).not.toBe(second);
    } finally {
      testing.setMaxPluginRegistryCacheEntriesForTest(previousCacheCap);
    }
  });

  it("normalizes bundled plugin env overrides against the provided env", () => {
    const bundledDir = makeTempDir();
    const homeDir = path.dirname(bundledDir);
    const override = `~/${path.basename(bundledDir)}`;
    const plugin = writePlugin({
      id: "tilde-bundled",
      dir: path.join(bundledDir, "tilde-bundled"),
      filename: "index.cjs",
      body: `module.exports = { id: "tilde-bundled", register() {} };`,
    });

    const registry = loadOpenClawPlugins({
      env: {
        ...process.env,
        HOME: homeDir,
        OPENCLAW_HOME: undefined,
        OPENCLAW_BUNDLED_PLUGINS_DIR: override,
      },
      config: {
        plugins: {
          allow: ["tilde-bundled"],
          entries: {
            "tilde-bundled": { enabled: true },
          },
        },
      },
    });

    expect(
      fs.realpathSync(registry.plugins.find((entry) => entry.id === "tilde-bundled")?.source ?? ""),
    ).toBe(fs.realpathSync(plugin.file));
  });

  it("prefers OPENCLAW_HOME over HOME for env-expanded load paths", () => {
    const ignoredHome = makeTempDir();
    const openclawHome = makeTempDir();
    const stateDir = makeTempDir();
    const bundledDir = makeTempDir();
    const plugin = writePlugin({
      id: "openclaw-home-demo",
      dir: path.join(openclawHome, "plugins", "openclaw-home-demo"),
      filename: "index.cjs",
      body: `module.exports = { id: "openclaw-home-demo", register() {} };`,
    });

    const registry = loadOpenClawPlugins({
      env: {
        ...process.env,
        HOME: ignoredHome,
        OPENCLAW_HOME: openclawHome,
        OPENCLAW_STATE_DIR: stateDir,
        OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir,
      },
      config: {
        plugins: {
          allow: ["openclaw-home-demo"],
          entries: {
            "openclaw-home-demo": { enabled: true },
          },
          load: {
            paths: ["~/plugins/openclaw-home-demo"],
          },
        },
      },
    });

    expect(
      fs.realpathSync(
        registry.plugins.find((entry) => entry.id === "openclaw-home-demo")?.source ?? "",
      ),
    ).toBe(fs.realpathSync(plugin.file));
  });

  it("loads plugins when source and root differ only by realpath alias", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "alias-safe",
      filename: "alias-safe.cjs",
      body: `module.exports = { id: "alias-safe", register() {} };`,
    });
    const realRoot = fs.realpathSync(plugin.dir);
    if (realRoot === plugin.dir) {
      return;
    }

    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        allow: ["alias-safe"],
      },
    });

    const loaded = registry.plugins.find((entry) => entry.id === "alias-safe");
    expect(loaded?.status).toBe("loaded");
  });

  it("denylist disables plugins even if allowed", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "blocked",
      body: `module.exports = { id: "blocked", register() {} };`,
    });

    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        allow: ["blocked"],
        deny: ["blocked"],
      },
    });

    const blocked = registry.plugins.find((entry) => entry.id === "blocked");
    expect(blocked?.status).toBe("disabled");
  });

  it("fails fast on invalid plugin config", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "configurable",
      filename: "configurable.cjs",
      body: `module.exports = { id: "configurable", register() {} };`,
    });

    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        entries: {
          configurable: {
            config: "nope" as unknown as Record<string, unknown>,
          },
        },
      },
    });

    const configurable = registry.plugins.find((entry) => entry.id === "configurable");
    expect(configurable?.status).toBe("error");
    expectDiagnosticContaining({
      registry,
      level: "error",
      pluginId: "configurable",
      message: "invalid config",
    });
  });

  it("repairs incomplete registered channel metadata before storing registry entries", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "channel-meta-repair",
      filename: "channel-meta-repair.cjs",
      body: `module.exports = { id: "channel-meta-repair", register(api) {
  api.registerChannel({
    plugin: {
      id: "telegram",
      meta: {
        id: "telegram"
      },
      capabilities: { chatTypes: ["direct"] },
      config: {
        listAccountIds: () => [],
        resolveAccount: () => ({ accountId: "default" })
      },
      outbound: { deliveryMode: "direct" }
    }
  });
} };`,
    });

    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        allow: ["channel-meta-repair"],
      },
    });

    const telegram = registry.channels.find((entry) => entry.plugin.id === "telegram")?.plugin;
    expect(telegram?.meta.id).toBe("telegram");
    expect(telegram?.meta.label).toBe("Telegram");
    expect(telegram?.meta.docsPath).toBe("/channels/telegram");
    expectDiagnosticContaining({
      registry,
      level: "warn",
      message:
        'channel "telegram" registered incomplete metadata; filled missing label, selectionLabel, docsPath, blurb',
    });
  });

  it("throws when strict plugin loading sees plugin errors", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "configurable",
      filename: "configurable.cjs",
      body: `module.exports = { id: "configurable", register() {} };`,
    });

    expect(() =>
      loadOpenClawPlugins({
        cache: false,
        throwOnLoadError: true,
        config: {
          plugins: {
            enabled: true,
            load: { paths: [plugin.file] },
            allow: ["configurable"],
            entries: {
              configurable: {
                enabled: true,
                config: "nope" as unknown as Record<string, unknown>,
              },
            },
          },
        },
      }),
    ).toThrow("plugin load failed: configurable: invalid config: <root>: must be object");
  });

  it("fails when plugin export id mismatches manifest id", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "manifest-id",
      filename: "manifest-id.cjs",
      body: `module.exports = { id: "export-id", register() {} };`,
    });

    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        allow: ["manifest-id"],
      },
    });

    const loaded = registry.plugins.find((entry) => entry.id === "manifest-id");
    expect(loaded?.status).toBe("error");
    expect(loaded?.error).toBe(
      'plugin id mismatch (config uses "manifest-id", export uses "export-id")',
    );
    expectDiagnosticContaining({
      registry,
      level: "error",
      pluginId: "manifest-id",
      message: 'plugin id mismatch (config uses "manifest-id", export uses "export-id")',
    });
  });

  it("can include plugin export shape when register is missing", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "missing-register-shape",
      filename: "missing-register-shape.cjs",
      body: `module.exports = { default: { default: { id: "missing-register-shape" } } };`,
    });

    const registry = withEnv({ OPENCLAW_PLUGIN_LOAD_DEBUG: "1" }, () =>
      loadRegistryFromSinglePlugin({
        plugin,
        pluginConfig: {
          allow: ["missing-register-shape"],
        },
      }),
    );

    const loaded = registry.plugins.find((entry) => entry.id === "missing-register-shape");
    expect(loaded?.status).toBe("error");
    expect(loaded?.error).toContain("plugin export missing register/activate");
    expect(loaded?.error).toContain("module shape:");
    expect(loaded?.error).toContain("export:object keys=default");
    expect(loaded?.error).toContain("export.default:object keys=default");
  });

  it.each([
    {
      id: "wrong-channel-entry",
      kind: "bundled-channel-entry",
      error: "bundled channel entry requires setup-runtime loader",
    },
    {
      id: "wrong-channel-setup-entry",
      kind: "bundled-channel-setup-entry",
      error: "bundled channel setup entry requires setup-runtime loader",
    },
  ])("reports $kind loaded through the legacy plugin loader", ({ id, kind, error }) => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id,
      filename: `${id}.cjs`,
      body: `module.exports = { id: ${JSON.stringify(id)}, kind: ${JSON.stringify(kind)} };`,
    });
    const errors: string[] = [];

    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        allow: [id],
      },
      options: {
        logger: createErrorLogger(errors),
      },
    });

    const loaded = registry.plugins.find((entry) => entry.id === id);
    expect(loaded?.status).toBe("error");
    expect(loaded?.error).toBe(error);
    expectRegistryErrorDiagnostic({ registry, pluginId: id, message: error });
    expect(errors).toEqual([
      `[plugins] ${id} ${error}; ensure plugin is loaded via bundled channel discovery, not legacy plugin loader`,
    ]);
  });

  it("handles single-plugin channel, context engine, and cli validation", () => {
    useNoBundledPlugins();
    const scenarios = [
      {
        label: "registers channel plugins",
        pluginId: "channel-demo",
        body: `module.exports = { id: "channel-demo", register(api) {
  api.registerChannel({
    plugin: {
      id: "demo",
      meta: {
        id: "demo",
        label: "Demo",
        selectionLabel: "Demo",
        docsPath: "/channels/demo",
        blurb: "demo channel"
      },
      capabilities: { chatTypes: ["direct"] },
      config: {
        listAccountIds: () => [],
        resolveAccount: () => ({ accountId: "default" })
      },
      outbound: { deliveryMode: "direct" }
    }
  });
} };`,
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          const channel = registry.channels.find((entry) => entry.plugin.id === "demo");
          expect(channel?.plugin.id).toBe("demo");
        },
      },
      {
        label: "updates duplicate channel ids during same-plugin registration",
        pluginId: "channel-dup",
        body: `module.exports = { id: "channel-dup", register(api) {
  api.registerChannel({
    plugin: {
      id: "demo",
      meta: {
        id: "demo",
        label: "Demo Override",
        selectionLabel: "Demo Override",
        docsPath: "/channels/demo-override",
        blurb: "override"
      },
      capabilities: { chatTypes: ["direct"] },
      config: {
        listAccountIds: () => [],
        resolveAccount: () => ({ accountId: "default" })
      },
      outbound: { deliveryMode: "direct" }
    }
  });
  api.registerChannel({
    plugin: {
      id: "demo",
      meta: {
        id: "demo",
        label: "Demo Duplicate",
        selectionLabel: "Demo Duplicate",
        docsPath: "/channels/demo-duplicate",
        blurb: "duplicate"
      },
      capabilities: { chatTypes: ["direct"] },
      config: {
        listAccountIds: () => [],
        resolveAccount: () => ({ accountId: "default" })
      },
      outbound: { deliveryMode: "direct" }
    }
  });
} };`,
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          expect(countMatching(registry.channels, (entry) => entry.plugin.id === "demo")).toBe(1);
          expect(
            registry.channels.find((entry) => entry.plugin.id === "demo")?.plugin.meta?.label,
          ).toBe("Demo Duplicate");
        },
      },
      {
        label: "rejects malformed plugin context engine registration",
        pluginId: "context-engine-malformed",
        body: `module.exports = { id: "context-engine-malformed", register(api) {
  api.registerContextEngine({ id: "broken-context" });
} };`,
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          expectRegistryErrorDiagnostic({
            registry,
            pluginId: "context-engine-malformed",
            message: "context engine registration missing id",
          });
          expect(listContextEngineIds()).not.toContain("broken-context");
        },
      },
      {
        label: "rejects plugin context engine ids reserved by core",
        pluginId: "context-engine-core-collision",
        body: `module.exports = { id: "context-engine-core-collision", register(api) {
  api.registerContextEngine("legacy", () => ({}));
} };`,
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          expectRegistryErrorDiagnostic({
            registry,
            pluginId: "context-engine-core-collision",
            message: "context engine id reserved by core: legacy",
          });
        },
      },
      {
        label: "rejects malformed compaction provider registration",
        pluginId: "compaction-provider-malformed",
        body: `module.exports = { id: "compaction-provider-malformed", register(api) {
  api.registerCompactionProvider({ id: "broken-compaction", label: "Broken" });
} };`,
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          expectRegistryErrorDiagnostic({
            registry,
            pluginId: "compaction-provider-malformed",
            message: 'compaction provider "broken-compaction" registration missing summarize',
          });
          expect(listCompactionProviderIds()).not.toContain("broken-compaction");
        },
      },
      {
        label: "rejects malformed memory prompt supplement registration",
        pluginId: "memory-prompt-supplement-malformed",
        body: `module.exports = { id: "memory-prompt-supplement-malformed", register(api) {
  api.registerMemoryPromptSupplement({ id: "broken-memory-prompt" });
} };`,
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          expectRegistryErrorDiagnostic({
            registry,
            pluginId: "memory-prompt-supplement-malformed",
            message: "memory prompt supplement registration missing builder",
          });
          expect(listMemoryPromptSupplements()).toStrictEqual([]);
        },
      },
      {
        label: "requires plugin CLI registrars to declare explicit command roots",
        pluginId: "cli-missing-metadata",
        body: `module.exports = { id: "cli-missing-metadata", register(api) {
  api.registerCli(() => {});
} };`,
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          expect(registry.cliRegistrars).toHaveLength(0);
          expectRegistryErrorDiagnostic({
            registry,
            pluginId: "cli-missing-metadata",
            message: "cli registration missing explicit commands metadata",
          });
        },
      },
      {
        label: "registers node feature CLI commands under nodes",
        pluginId: "node-cli-feature",
        body: `module.exports = { id: "node-cli-feature", register(api) {
  api.registerNodeCliFeature(() => {}, {
    descriptors: [
      {
        name: "demo-node",
        description: "Demo node feature",
        hasSubcommands: true,
      },
    ],
  });
} };`,
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          expect(registry.cliRegistrars).toHaveLength(1);
          expect(registry.cliRegistrars[0]?.parentPath).toEqual(["nodes"]);
          expect(registry.cliRegistrars[0]?.commands).toEqual(["demo-node"]);
          expect(registry.cliRegistrars[0]?.descriptors).toEqual([
            {
              name: "demo-node",
              description: "Demo node feature",
              hasSubcommands: true,
            },
          ]);
        },
      },
    ] as const;

    runSinglePluginRegistryScenarios(scenarios);
  });

  it("registers plugin http routes", () => {
    useNoBundledPlugins();
    const scenarios = [
      {
        label: "defaults exact match",
        pluginId: "http-route-demo",
        routeOptions:
          '{ path: "/demo", auth: "gateway", handler: async (_req, res) => { res.statusCode = 200; res.end("ok"); } }',
        expectedPath: "/demo",
        expectedAuth: "gateway",
        expectedMatch: "exact",
        assert: expectRegisteredHttpRoute,
      },
      {
        label: "keeps explicit auth and match options",
        pluginId: "http-demo",
        routeOptions:
          '{ path: "/webhook", auth: "plugin", match: "prefix", handler: async () => false }',
        expectedPath: "/webhook",
        expectedAuth: "plugin",
        expectedMatch: "prefix",
        assert: expectRegisteredHttpRoute,
      },
    ] as const;

    runSinglePluginRegistryScenarios(
      scenarios.map((scenario) =>
        Object.assign({}, scenario, {
          body: `module.exports = { id: "${scenario.pluginId}", register(api) {
  api.registerHttpRoute(${scenario.routeOptions});
} };`,
        }),
      ),
    );
  });

  it("rejects duplicate plugin registrations", () => {
    useNoBundledPlugins();
    const scenarios = [
      {
        label: "plugin-visible hook names",
        ownerA: "hook-owner-a",
        ownerB: "hook-owner-b",
        buildBody: (ownerId: string) => `module.exports = { id: "${ownerId}", register(api) {
  api.registerHook("gateway:startup", () => {}, { name: "shared-hook" });
} };`,
        selectCount: (registry: ReturnType<typeof loadOpenClawPlugins>) =>
          countMatching(registry.hooks, (entry) => entry.entry.hook.name === "shared-hook"),
        duplicateMessage: "hook already registered: shared-hook (hook-owner-a)",
        assert: expectDuplicateRegistrationResult,
      },
      {
        label: "plugin service ids",
        ownerA: "service-owner-a",
        ownerB: "service-owner-b",
        buildBody: (ownerId: string) => `module.exports = { id: "${ownerId}", register(api) {
  api.registerService({ id: "shared-service", start() {} });
} };`,
        selectCount: (registry: ReturnType<typeof loadOpenClawPlugins>) =>
          countMatching(registry.services, (entry) => entry.service.id === "shared-service"),
        duplicateMessage: "service already registered: shared-service (service-owner-a)",
        assert: expectDuplicateRegistrationResult,
      },
      {
        label: "gateway discovery service ids",
        ownerA: "discovery-owner-a",
        ownerB: "discovery-owner-b",
        buildBody: (ownerId: string) => `module.exports = { id: "${ownerId}", register(api) {
  api.registerGatewayDiscoveryService({ id: "shared-discovery", advertise() {} });
} };`,
        selectCount: (registry: ReturnType<typeof loadOpenClawPlugins>) =>
          registry.gatewayDiscoveryServices.filter(
            (entry) => entry.service.id === "shared-discovery",
          ).length,
        duplicateMessage:
          "gateway discovery service already registered: shared-discovery (discovery-owner-a)",
        assertPrimaryOwner: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          expect(
            registry.plugins.find((entry) => entry.id === "discovery-owner-a")
              ?.gatewayDiscoveryServiceIds,
          ).toEqual(["shared-discovery"]);
        },
        assert: expectDuplicateRegistrationResult,
      },
      {
        label: "plugin context engine ids",
        ownerA: "context-engine-owner-a",
        ownerB: "context-engine-owner-b",
        buildBody: (ownerId: string) => `module.exports = { id: "${ownerId}", register(api) {
  api.registerContextEngine("shared-context-engine-loader-test", () => ({}));
} };`,
        selectCount: () => 1,
        duplicateMessage:
          "context engine already registered: shared-context-engine-loader-test (plugin:context-engine-owner-a)",
        assertPrimaryOwner: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          expect(
            registry.plugins.find((entry) => entry.id === "context-engine-owner-a")
              ?.contextEngineIds,
          ).toEqual(["shared-context-engine-loader-test"]);
        },
        assert: expectDuplicateRegistrationResult,
      },
      {
        label: "plugin CLI command roots",
        ownerA: "cli-owner-a",
        ownerB: "cli-owner-b",
        buildBody: (ownerId: string) => `module.exports = { id: "${ownerId}", register(api) {
  api.registerCli(() => {}, { commands: ["shared-cli"] });
} };`,
        selectCount: (registry: ReturnType<typeof loadOpenClawPlugins>) =>
          registry.cliRegistrars.length,
        duplicateMessage: "cli command already registered: shared-cli (cli-owner-a)",
        assertPrimaryOwner: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          expect(registry.cliRegistrars[0]?.pluginId).toBe("cli-owner-a");
        },
        assert: expectDuplicateRegistrationResult,
      },
    ] as const;

    runRegistryScenarios(scenarios, (scenario) => {
      const first = writePlugin({
        id: scenario.ownerA,
        filename: `${scenario.ownerA}.cjs`,
        body: scenario.buildBody(scenario.ownerA),
      });
      const second = writePlugin({
        id: scenario.ownerB,
        filename: `${scenario.ownerB}.cjs`,
        body: scenario.buildBody(scenario.ownerB),
      });
      return loadRegistryFromAllowedPlugins([first, second]);
    });
  });

  it("allows the same plugin to register the same service id twice", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "service-owner-self",
      filename: "service-owner-self.cjs",
      body: `module.exports = { id: "service-owner-self", register(api) {
  api.registerService({ id: "shared-service", start() {} });
  api.registerService({ id: "shared-service", start() {} });
} };`,
    });

    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        allow: ["service-owner-self"],
      },
    });

    expect(countMatching(registry.services, (entry) => entry.service.id === "shared-service")).toBe(
      1,
    );
    expectNoDiagnosticContaining({
      registry,
      message: "service already registered: shared-service",
    });
  });

  it("tracks regular services and gateway discovery services separately", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "split-service-owner",
      filename: "split-service-owner.cjs",
      body: `module.exports = { id: "split-service-owner", register(api) {
  api.registerService({ id: "shared-service", start() {} });
  api.registerGatewayDiscoveryService({ id: "shared-service", advertise() {} });
} };`,
    });

    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        allow: ["split-service-owner"],
      },
    });

    const record = registry.plugins.find((entry) => entry.id === "split-service-owner");
    expect(record?.services).toEqual(["shared-service"]);
    expect(record?.gatewayDiscoveryServiceIds).toEqual(["shared-service"]);
    expect(registry.services).toHaveLength(1);
    expect(registry.gatewayDiscoveryServices).toHaveLength(1);
    expect(registry.diagnostics).toStrictEqual([]);
  });

  it("rewrites removed registerHttpHandler failures into migration diagnostics", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "http-handler-legacy",
      filename: "http-handler-legacy.cjs",
      body: `module.exports = { id: "http-handler-legacy", register(api) {
  api.registerHttpHandler({ path: "/legacy", handler: async () => true });
} };`,
    });

    const errors: string[] = [];
    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        allow: ["http-handler-legacy"],
      },
      options: {
        logger: createErrorLogger(errors),
      },
    });

    const loaded = registry.plugins.find((entry) => entry.id === "http-handler-legacy");
    expect(loaded?.status).toBe("error");
    expect(loaded?.error).toContain("api.registerHttpHandler(...) was removed");
    expect(loaded?.error).toContain("api.registerHttpRoute(...)");
    expect(loaded?.error).toContain("registerPluginHttpRoute(...)");
    expectDiagnosticContaining({
      registry,
      message: "api.registerHttpHandler(...) was removed",
    });
    expect(
      errors.some((message) => message.includes("api.registerHttpHandler(...) was removed")),
    ).toBe(true);
  });

  it("does not rewrite unrelated registerHttpHandler helper failures", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "http-handler-local-helper",
      filename: "http-handler-local-helper.cjs",
      body: `module.exports = { id: "http-handler-local-helper", register() {
  const registerHttpHandler = undefined;
  registerHttpHandler();
} };`,
    });

    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        allow: ["http-handler-local-helper"],
      },
    });

    const loaded = registry.plugins.find((entry) => entry.id === "http-handler-local-helper");
    expect(loaded?.status).toBe("error");
    expect(loaded?.error).not.toContain("api.registerHttpHandler(...) was removed");
  });

  it("enforces plugin http route validation and conflict rules", () => {
    useNoBundledPlugins();
    const scenarios = [
      {
        label: "missing auth is rejected",
        buildPlugins: () => [
          writePlugin({
            id: "http-route-missing-auth",
            filename: "http-route-missing-auth.cjs",
            body: `module.exports = { id: "http-route-missing-auth", register(api) {
  api.registerHttpRoute({ path: "/demo", handler: async () => true });
} };`,
          }),
        ],
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          expect(
            registry.httpRoutes.find((entry) => entry.pluginId === "http-route-missing-auth"),
          ).toBeUndefined();
          expectDiagnosticContaining({
            registry,
            message: "http route registration missing or invalid auth",
          });
        },
      },
      {
        label: "same plugin can implicitly replace its own route",
        buildPlugins: () => [
          writePlugin({
            id: "http-route-replace-self",
            filename: "http-route-replace-self.cjs",
            body: `module.exports = { id: "http-route-replace-self", register(api) {
  api.registerHttpRoute({ path: "/demo", auth: "plugin", handler: async () => false });
  api.registerHttpRoute({ path: "/demo", auth: "plugin", handler: async () => true });
} };`,
          }),
        ],
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          const routes = registry.httpRoutes.filter(
            (entry) => entry.pluginId === "http-route-replace-self",
          );
          expect(routes).toHaveLength(1);
          expect(routes[0]?.path).toBe("/demo");
          expect(registry.diagnostics).toStrictEqual([]);
        },
      },
      {
        label: "cross-plugin replaceExisting is rejected",
        buildPlugins: () => [
          writePlugin({
            id: "http-route-owner-a",
            filename: "http-route-owner-a.cjs",
            body: `module.exports = { id: "http-route-owner-a", register(api) {
  api.registerHttpRoute({ path: "/demo", auth: "plugin", handler: async () => false });
} };`,
          }),
          writePlugin({
            id: "http-route-owner-b",
            filename: "http-route-owner-b.cjs",
            body: `module.exports = { id: "http-route-owner-b", register(api) {
  api.registerHttpRoute({ path: "/demo", auth: "plugin", replaceExisting: true, handler: async () => true });
} };`,
          }),
        ],
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          const route = registry.httpRoutes.find((entry) => entry.path === "/demo");
          expect(route?.pluginId).toBe("http-route-owner-a");
          expectDiagnosticContaining({
            registry,
            message: "http route replacement rejected",
          });
        },
      },
      {
        label: "mixed-auth overlaps are rejected",
        buildPlugins: () => [
          writePlugin({
            id: "http-route-overlap",
            filename: "http-route-overlap.cjs",
            body: `module.exports = { id: "http-route-overlap", register(api) {
  api.registerHttpRoute({ path: "/plugin/secure", auth: "gateway", match: "prefix", handler: async () => true });
  api.registerHttpRoute({ path: "/plugin/secure/report", auth: "plugin", match: "exact", handler: async () => true });
} };`,
          }),
        ],
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          const routes = registry.httpRoutes.filter(
            (entry) => entry.pluginId === "http-route-overlap",
          );
          expect(routes).toHaveLength(1);
          expect(routes[0]?.path).toBe("/plugin/secure");
          expectDiagnosticContaining({
            registry,
            message: "http route overlap rejected",
          });
        },
      },
      {
        label: "same-auth overlaps are allowed",
        buildPlugins: () => [
          writePlugin({
            id: "http-route-overlap-same-auth",
            filename: "http-route-overlap-same-auth.cjs",
            body: `module.exports = { id: "http-route-overlap-same-auth", register(api) {
  api.registerHttpRoute({ path: "/plugin/public", auth: "plugin", match: "prefix", handler: async () => true });
  api.registerHttpRoute({ path: "/plugin/public/report", auth: "plugin", match: "exact", handler: async () => true });
} };`,
          }),
        ],
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          const routes = registry.httpRoutes.filter(
            (entry) => entry.pluginId === "http-route-overlap-same-auth",
          );
          expect(routes).toHaveLength(2);
          expect(registry.diagnostics).toStrictEqual([]);
        },
      },
    ] as const;

    runRegistryScenarios(scenarios, (scenario) =>
      loadRegistryFromScenarioPlugins(scenario.buildPlugins()),
    );
  });

  it("respects explicit disable in config", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "config-disable",
      body: `module.exports = { id: "config-disable", register() {} };`,
    });

    const registry = loadOpenClawPlugins({
      cache: false,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          entries: {
            "config-disable": { enabled: false },
          },
        },
      },
    });

    const disabled = registry.plugins.find((entry) => entry.id === "config-disable");
    expect(disabled?.status).toBe("disabled");
  });

  it("loads bundled channel entries through nested default export wrappers", () => {
    useNoBundledPlugins();
    const pluginDir = makeTempDir();
    const fullMarker = path.join(pluginDir, "full-loaded.txt");

    fs.writeFileSync(
      path.join(pluginDir, "package.json"),
      JSON.stringify(
        {
          name: "@openclaw/nested-default-channel",
          openclaw: {
            extensions: ["./index.cjs"],
          },
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "nested-default-channel",
          configSchema: EMPTY_PLUGIN_SCHEMA,
          channels: ["nested-default-channel"],
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "index.cjs"),
      `module.exports = {
  default: {
    default: {
      id: "nested-default-channel",
      kind: "bundled-channel-entry",
      name: "Nested Default Channel",
      description: "interop-wrapped bundled channel entry",
      register(api) {
        require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
        api.registerChannel({
          plugin: {
            id: "nested-default-channel",
            meta: {
              id: "nested-default-channel",
              label: "Nested Default Channel",
              selectionLabel: "Nested Default Channel",
              docsPath: "/channels/nested-default-channel",
              blurb: "interop-wrapped bundled channel entry",
            },
            capabilities: { chatTypes: ["direct"] },
            config: {
              listAccountIds: () => ["default"],
              resolveAccount: () => ({ accountId: "default", token: "configured" }),
            },
            outbound: { deliveryMode: "direct" },
          },
        });
      },
    },
  },
};`,
      "utf-8",
    );

    const registry = loadOpenClawPlugins({
      cache: false,
      config: {
        channels: {
          "nested-default-channel": {
            enabled: true,
            token: "configured",
          },
        },
        plugins: {
          load: { paths: [pluginDir] },
          allow: ["nested-default-channel"],
        },
      },
    });

    expect(fs.existsSync(fullMarker)).toBe(true);
    expect(registry.plugins.find((entry) => entry.id === "nested-default-channel")?.status).toBe(
      "loaded",
    );
    expect(registry.channels.map((entry) => entry.plugin.id)).toContain("nested-default-channel");
  });

  it("does not treat manifest channel ids as scoped plugin id matches", () => {
    useNoBundledPlugins();
    const target = writePlugin({
      id: "target-plugin",
      filename: "target-plugin.cjs",
      body: `module.exports = { id: "target-plugin", register() {} };`,
    });
    const unrelated = writePlugin({
      id: "unrelated-plugin",
      filename: "unrelated-plugin.cjs",
      body: `module.exports = { id: "unrelated-plugin", register() { throw new Error("unrelated plugin should not load"); } };`,
    });
    fs.writeFileSync(
      path.join(unrelated.dir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "unrelated-plugin",
          configSchema: EMPTY_PLUGIN_SCHEMA,
          channels: ["target-plugin"],
        },
        null,
        2,
      ),
      "utf-8",
    );

    const registry = loadOpenClawPlugins({
      cache: false,
      config: {
        plugins: {
          load: { paths: [target.file, unrelated.file] },
          allow: ["target-plugin", "unrelated-plugin"],
          entries: {
            "target-plugin": { enabled: true },
            "unrelated-plugin": { enabled: true },
          },
        },
      },
      onlyPluginIds: ["target-plugin"],
    });

    expect(registry.plugins.map((entry) => entry.id)).toEqual(["target-plugin"]);
  });

  it("only setup-loads a disabled channel plugin when the caller scopes to the selected plugin", () => {
    useNoBundledPlugins();
    const marker = path.join(makeTempDir(), "lazy-channel-imported.txt");
    const plugin = writePlugin({
      id: "lazy-channel-plugin",
      filename: "lazy-channel.cjs",
      body: `require("node:fs").writeFileSync(${JSON.stringify(marker)}, "loaded", "utf-8");
module.exports = {
  id: "lazy-channel-plugin",
  register(api) {
    api.registerChannel({
      plugin: {
        id: "lazy-channel",
        meta: {
          id: "lazy-channel",
          label: "Lazy Channel",
          selectionLabel: "Lazy Channel",
          docsPath: "/channels/lazy-channel",
          blurb: "lazy test channel",
        },
        capabilities: { chatTypes: ["direct"] },
        config: {
          listAccountIds: () => [],
          resolveAccount: () => ({ accountId: "default" }),
        },
        outbound: { deliveryMode: "direct" },
      },
    });
  },
};`,
    });
    fs.writeFileSync(
      path.join(plugin.dir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "lazy-channel-plugin",
          configSchema: EMPTY_PLUGIN_SCHEMA,
          channels: ["lazy-channel"],
        },
        null,
        2,
      ),
      "utf-8",
    );
    const config = {
      plugins: {
        load: { paths: [plugin.file] },
        allow: ["lazy-channel-plugin"],
        entries: {
          "lazy-channel-plugin": { enabled: false },
        },
      },
    };

    const registry = loadOpenClawPlugins({
      cache: false,
      config,
    });

    expect(fs.existsSync(marker)).toBe(false);
    expect(registry.channelSetups).toHaveLength(0);
    expect(registry.plugins.find((entry) => entry.id === "lazy-channel-plugin")?.status).toBe(
      "disabled",
    );

    const broadSetupRegistry = loadOpenClawPlugins({
      cache: false,
      config,
      includeSetupOnlyChannelPlugins: true,
    });

    expect(fs.existsSync(marker)).toBe(false);
    expect(broadSetupRegistry.channelSetups).toHaveLength(0);
    expect(broadSetupRegistry.channels).toHaveLength(0);
    expect(
      broadSetupRegistry.plugins.find((entry) => entry.id === "lazy-channel-plugin")?.status,
    ).toBe("disabled");

    const scopedSetupRegistry = loadOpenClawPlugins({
      cache: false,
      config,
      includeSetupOnlyChannelPlugins: true,
      onlyPluginIds: ["lazy-channel-plugin"],
    });

    expect(fs.existsSync(marker)).toBe(true);
    expect(scopedSetupRegistry.channelSetups).toHaveLength(1);
    expect(scopedSetupRegistry.channels).toHaveLength(0);
    expect(
      scopedSetupRegistry.plugins.find((entry) => entry.id === "lazy-channel-plugin")?.status,
    ).toBe("disabled");
  });

  it.each([
    {
      name: "uses package setupEntry for selected setup-only channel loads",
      fixture: {
        id: "setup-entry-test",
        label: "Setup Entry Test",
        packageName: "@openclaw/setup-entry-test",
        fullBlurb: "full entry should not run in setup-only mode",
        setupBlurb: "setup entry",
        configured: false,
      },
      load: ({ pluginDir }: { pluginDir: string }) =>
        loadOpenClawPlugins({
          cache: false,
          config: {
            plugins: {
              load: { paths: [pluginDir] },
              allow: ["setup-entry-test"],
              entries: {
                "setup-entry-test": { enabled: false },
              },
            },
          },
          includeSetupOnlyChannelPlugins: true,
          onlyPluginIds: ["setup-entry-test"],
        }),
      expectFullLoaded: false,
      expectSetupLoaded: true,
      expectedChannels: 0,
    },
    {
      name: "keeps bundled setupEntry setup-only loads on the setup-safe path",
      fixture: {
        id: "setup-only-bundled-contract-test",
        label: "Setup Only Bundled Contract Test",
        packageName: "@openclaw/setup-only-bundled-contract-test",
        fullBlurb: "full entry should not run in setup-only mode",
        setupBlurb: "setup-only bundled contract",
        configured: false,
        useBundledSetupEntryContract: true,
      },
      load: ({ pluginDir }: { pluginDir: string }) =>
        loadOpenClawPlugins({
          cache: false,
          config: {
            plugins: {
              load: { paths: [pluginDir] },
              allow: ["setup-only-bundled-contract-test"],
              entries: {
                "setup-only-bundled-contract-test": { enabled: false },
              },
            },
          },
          includeSetupOnlyChannelPlugins: true,
          onlyPluginIds: ["setup-only-bundled-contract-test"],
        }),
      expectFullLoaded: false,
      expectSetupLoaded: true,
      expectedChannels: 0,
    },
    {
      name: "uses package setupEntry for enabled but unconfigured channel loads",
      fixture: {
        id: "setup-runtime-test",
        label: "Setup Runtime Test",
        packageName: "@openclaw/setup-runtime-test",
        fullBlurb: "full entry should not run while unconfigured",
        setupBlurb: "setup runtime",
        configured: false,
      },
      load: ({ pluginDir }: { pluginDir: string }) =>
        loadOpenClawPlugins({
          cache: false,
          config: {
            plugins: {
              load: { paths: [pluginDir] },
              allow: ["setup-runtime-test"],
            },
          },
        }),
      expectFullLoaded: false,
      expectSetupLoaded: true,
      expectedChannels: 1,
    },
    {
      name: "uses package setupEntry bundled contract for setup-runtime channel loads",
      fixture: {
        id: "setup-runtime-bundled-contract-test",
        label: "Setup Runtime Bundled Contract Test",
        packageName: "@openclaw/setup-runtime-bundled-contract-test",
        fullBlurb: "full entry should not run while unconfigured",
        setupBlurb: "setup runtime bundled contract",
        configured: false,
        useBundledSetupEntryContract: true,
      },
      load: ({ pluginDir }: { pluginDir: string }) =>
        loadOpenClawPlugins({
          cache: false,
          config: {
            plugins: {
              load: { paths: [pluginDir] },
              allow: ["setup-runtime-bundled-contract-test"],
            },
          },
        }),
      expectFullLoaded: true,
      expectSetupLoaded: true,
      expectedChannels: 1,
    },
    {
      name: "preserves bundled setupEntry split secrets for setup-runtime channel loads",
      fixture: {
        id: "setup-runtime-bundled-contract-secrets-test",
        label: "Setup Runtime Bundled Contract Secrets Test",
        packageName: "@openclaw/setup-runtime-bundled-contract-secrets-test",
        fullBlurb: "full entry should not run while unconfigured",
        setupBlurb: "setup runtime bundled contract secrets",
        configured: false,
        useBundledSetupEntryContract: true,
        splitBundledSetupSecrets: true,
      },
      load: ({ pluginDir }: { pluginDir: string }) =>
        loadOpenClawPlugins({
          cache: false,
          config: {
            plugins: {
              load: { paths: [pluginDir] },
              allow: ["setup-runtime-bundled-contract-secrets-test"],
            },
          },
        }),
      expectFullLoaded: true,
      expectSetupLoaded: true,
      expectedChannels: 1,
      expectedSetupSecretId: "channels.setup-runtime-bundled-contract-secrets-test.setup-token",
    },
    {
      name: "applies bundled setupEntry runtime setter for setup-runtime channel loads",
      fixture: {
        id: "setup-runtime-bundled-contract-runtime-test",
        label: "Setup Runtime Bundled Contract Runtime Test",
        packageName: "@openclaw/setup-runtime-bundled-contract-runtime-test",
        fullBlurb: "full entry should not run while unconfigured",
        setupBlurb: "setup runtime bundled contract runtime",
        configured: false,
        useBundledSetupEntryContract: true,
        bundledSetupRuntimeMarker: path.join(makeTempDir(), "setup-runtime-applied.txt"),
      },
      load: ({ pluginDir }: { pluginDir: string }) =>
        loadOpenClawPlugins({
          cache: false,
          config: {
            plugins: {
              load: { paths: [pluginDir] },
              allow: ["setup-runtime-bundled-contract-runtime-test"],
            },
          },
        }),
      expectFullLoaded: true,
      expectSetupLoaded: true,
      expectedChannels: 1,
      expectSetupRuntimeLoaded: true,
    },
    {
      name: "runs bundled setupEntry setup-runtime registrations before deferred full loads",
      fixture: {
        id: "setup-runtime-bundled-route-test",
        label: "Setup Runtime Bundled Route Test",
        packageName: "@openclaw/setup-runtime-bundled-route-test",
        fullBlurb: "full entry should defer while configured",
        setupBlurb: "setup runtime route",
        configured: true,
        startupDeferConfiguredChannelFullLoadUntilAfterListen: true,
        useBundledSetupEntryContract: true,
        bundledSetupRuntimeRoutePath: "/setup-runtime-route",
      },
      load: ({ pluginDir }: { pluginDir: string }) =>
        loadOpenClawPlugins({
          cache: false,
          preferSetupRuntimeForChannelPlugins: true,
          config: {
            channels: {
              "setup-runtime-bundled-route-test": {
                enabled: true,
                token: "configured",
              },
            },
            plugins: {
              load: { paths: [pluginDir] },
              allow: ["setup-runtime-bundled-route-test"],
            },
          },
        }),
      expectFullLoaded: false,
      expectSetupLoaded: true,
      expectedChannels: 1,
      expectedSetupRuntimeRoutePath: "/setup-runtime-route",
    },
    {
      name: "merges bundled runtime plugin into setup-runtime channel loads",
      fixture: {
        id: "setup-runtime-bundled-runtime-merge-test",
        label: "Setup Runtime Bundled Runtime Merge Test",
        packageName: "@openclaw/setup-runtime-bundled-runtime-merge-test",
        fullBlurb: "full runtime plugin",
        setupBlurb: "setup runtime override",
        configured: false,
        useBundledFullEntryContract: true,
        useBundledSetupEntryContract: true,
        bundledFullRuntimeMarker: path.join(makeTempDir(), "bundled-runtime-applied.txt"),
      },
      load: ({ pluginDir }: { pluginDir: string }) =>
        loadOpenClawPlugins({
          cache: false,
          config: {
            plugins: {
              load: { paths: [pluginDir] },
              allow: ["setup-runtime-bundled-runtime-merge-test"],
            },
          },
        }),
      expectFullLoaded: true,
      expectSetupLoaded: true,
      expectedChannels: 1,
      expectBundledFullRuntimeLoaded: true,
    },
    {
      name: "preserves external setupEntry runtime setter for deferred configured channel loads",
      fixture: {
        id: "setup-runtime-external-deferred-test",
        label: "Setup Runtime External Deferred Test",
        packageName: "@openclaw/setup-runtime-external-deferred-test",
        fullBlurb: "full entry should defer while configured",
        setupBlurb: "setup runtime external deferred",
        configured: true,
        startupDeferConfiguredChannelFullLoadUntilAfterListen: true,
        bundledSetupRuntimeMarker: path.join(makeTempDir(), "external-setup-runtime-applied.txt"),
      },
      load: ({ pluginDir }: { pluginDir: string }) =>
        loadOpenClawPlugins({
          cache: false,
          preferSetupRuntimeForChannelPlugins: true,
          config: {
            channels: {
              "setup-runtime-external-deferred-test": {
                enabled: true,
                token: "configured",
              },
            },
            plugins: {
              load: { paths: [pluginDir] },
              allow: ["setup-runtime-external-deferred-test"],
            },
          },
        }),
      expectFullLoaded: false,
      expectSetupLoaded: true,
      expectedChannels: 1,
      expectSetupRuntimeLoaded: true,
    },
    {
      name: "does not prefer setupEntry for configured channel loads without startup opt-in",
      fixture: {
        id: "setup-runtime-not-preferred-test",
        label: "Setup Runtime Not Preferred Test",
        packageName: "@openclaw/setup-runtime-not-preferred-test",
        fullBlurb: "full entry should still load without explicit startup opt-in",
        setupBlurb: "setup runtime not preferred",
        configured: true,
      },
      load: ({ pluginDir }: { pluginDir: string }) =>
        loadOpenClawPlugins({
          cache: false,
          preferSetupRuntimeForChannelPlugins: true,
          config: {
            channels: {
              "setup-runtime-not-preferred-test": {
                enabled: true,
                token: "configured",
              },
            },
            plugins: {
              load: { paths: [pluginDir] },
              allow: ["setup-runtime-not-preferred-test"],
            },
          },
        }),
      expectFullLoaded: true,
      expectSetupLoaded: false,
      expectedChannels: 1,
    },
  ])(
    "$name",
    ({
      fixture,
      load,
      expectFullLoaded,
      expectSetupLoaded,
      expectedChannels,
      expectedSetupSecretId,
      expectSetupRuntimeLoaded,
      expectBundledFullRuntimeLoaded,
      expectedSetupRuntimeRoutePath,
    }) => {
      const built = createSetupEntryChannelPluginFixture(fixture);
      const registry = load({ pluginDir: built.pluginDir });

      expect(fs.existsSync(built.fullMarker)).toBe(expectFullLoaded);
      expect(fs.existsSync(built.setupMarker)).toBe(expectSetupLoaded);
      expect(registry.channelSetups).toHaveLength(1);
      expect(registry.channels).toHaveLength(expectedChannels);
      if (fixture.bundledSetupRuntimeMarker) {
        expect(fs.existsSync(fixture.bundledSetupRuntimeMarker)).toBe(
          expectSetupRuntimeLoaded ?? false,
        );
      }
      if (fixture.bundledFullRuntimeMarker) {
        expect(fs.existsSync(fixture.bundledFullRuntimeMarker)).toBe(
          expectBundledFullRuntimeLoaded ?? false,
        );
      }
      if (expectedSetupSecretId) {
        expect(
          registry.channelSetups[0]?.plugin.secrets?.secretTargetRegistryEntries?.some(
            (entry) => entry.id === expectedSetupSecretId,
          ),
        ).toBe(true);
        expect(
          registry.channels[0]?.plugin.secrets?.secretTargetRegistryEntries?.some(
            (entry) => entry.id === expectedSetupSecretId,
          ),
        ).toBe(true);
      }
      if (expectedSetupRuntimeRoutePath) {
        expect(
          registry.httpRoutes.some(
            (route) =>
              route.pluginId === fixture.id && route.path === expectedSetupRuntimeRoutePath,
          ),
        ).toBe(true);
      }
    },
  );

  it("applies the bundled runtime setter before loading the merged setup-runtime plugin", () => {
    const runtimeMarker = path.join(makeTempDir(), "setup-runtime-before-load.txt");
    const built = createSetupEntryChannelPluginFixture({
      id: "setup-runtime-order-test",
      label: "Setup Runtime Order Test",
      packageName: "@openclaw/setup-runtime-order-test",
      fullBlurb: "full runtime plugin",
      setupBlurb: "setup runtime override",
      configured: false,
      useBundledFullEntryContract: true,
      useBundledSetupEntryContract: true,
      bundledFullRuntimeMarker: runtimeMarker,
      requireBundledFullRuntimeBeforeLoad: true,
    });

    const registry = loadOpenClawPlugins({
      cache: false,
      config: {
        plugins: {
          load: { paths: [built.pluginDir] },
          allow: ["setup-runtime-order-test"],
        },
      },
    });

    expect(registry.plugins.find((entry) => entry.id === "setup-runtime-order-test")?.status).toBe(
      "loaded",
    );
    expect(fs.existsSync(runtimeMarker)).toBe(true);
  });

  it("records setup runtime setter failures without aborting the full load pass", () => {
    const built = createSetupEntryChannelPluginFixture({
      id: "setup-runtime-error-test",
      label: "Setup Runtime Error Test",
      packageName: "@openclaw/setup-runtime-error-test",
      fullBlurb: "full runtime plugin",
      setupBlurb: "setup runtime override",
      configured: false,
      useBundledSetupEntryContract: true,
      bundledSetupRuntimeError: "broken setup runtime setter",
    });
    const helperPlugin = writePlugin({
      id: "setup-runtime-helper-test",
      filename: "setup-runtime-helper-test.cjs",
      body: `module.exports = { id: "setup-runtime-helper-test", register() {} };`,
    });

    const registry = loadOpenClawPlugins({
      cache: false,
      config: {
        plugins: {
          load: { paths: [built.pluginDir, helperPlugin.file] },
          allow: ["setup-runtime-error-test", "setup-runtime-helper-test"],
        },
      },
    });

    expect(registry.plugins.find((entry) => entry.id === "setup-runtime-error-test")?.status).toBe(
      "error",
    );
    expect(
      registry.plugins.find((entry) => entry.id === "setup-runtime-error-test")?.error,
    ).toContain("broken setup runtime setter");
    expect(registry.plugins.find((entry) => entry.id === "setup-runtime-helper-test")?.status).toBe(
      "loaded",
    );
  });

  it("rolls back setup-runtime registrations when setup side effects fail", () => {
    const built = createSetupEntryChannelPluginFixture({
      id: "setup-runtime-route-error-test",
      label: "Setup Runtime Route Error Test",
      packageName: "@openclaw/setup-runtime-route-error-test",
      fullBlurb: "full runtime plugin",
      setupBlurb: "setup runtime route",
      configured: true,
      startupDeferConfiguredChannelFullLoadUntilAfterListen: true,
      useBundledSetupEntryContract: true,
      bundledSetupRuntimeRoutePath: "/setup-runtime-route-error",
      bundledSetupRuntimeRegisterError: "broken setup-runtime registrar",
    });
    const helperPlugin = writePlugin({
      id: "setup-runtime-route-helper-test",
      filename: "setup-runtime-route-helper-test.cjs",
      body: `module.exports = { id: "setup-runtime-route-helper-test", register() {} };`,
    });

    const registry = loadOpenClawPlugins({
      cache: false,
      preferSetupRuntimeForChannelPlugins: true,
      config: {
        channels: {
          "setup-runtime-route-error-test": {
            enabled: true,
            token: "configured",
          },
        },
        plugins: {
          load: { paths: [built.pluginDir, helperPlugin.file] },
          allow: ["setup-runtime-route-error-test", "setup-runtime-route-helper-test"],
        },
      },
    });

    expect(
      registry.plugins.find((entry) => entry.id === "setup-runtime-route-error-test")?.status,
    ).toBe("error");
    expect(
      registry.plugins.find((entry) => entry.id === "setup-runtime-route-error-test")?.error,
    ).toContain("broken setup-runtime registrar");
    expect(registry.httpRoutes.some((route) => route.path === "/setup-runtime-route-error")).toBe(
      false,
    );
    expect(
      registry.plugins.find((entry) => entry.id === "setup-runtime-route-helper-test")?.status,
    ).toBe("loaded");
  });

  it("closes setup-runtime registration APIs after synchronous registration", async () => {
    const built = createSetupEntryChannelPluginFixture({
      id: "setup-runtime-late-route-test",
      label: "Setup Runtime Late Route Test",
      packageName: "@openclaw/setup-runtime-late-route-test",
      fullBlurb: "full runtime plugin",
      setupBlurb: "setup runtime route",
      configured: true,
      startupDeferConfiguredChannelFullLoadUntilAfterListen: true,
      useBundledSetupEntryContract: true,
      bundledSetupRuntimeRoutePath: "/setup-runtime-sync-route",
      bundledSetupRuntimeLateRoutePath: "/setup-runtime-late-route",
    });

    const registry = loadOpenClawPlugins({
      cache: false,
      preferSetupRuntimeForChannelPlugins: true,
      config: {
        channels: {
          "setup-runtime-late-route-test": {
            enabled: true,
            token: "configured",
          },
        },
        plugins: {
          load: { paths: [built.pluginDir] },
          allow: ["setup-runtime-late-route-test"],
        },
      },
    });

    await Promise.resolve();

    expect(registry.httpRoutes.some((route) => route.path === "/setup-runtime-sync-route")).toBe(
      true,
    );
    expect(registry.httpRoutes.some((route) => route.path === "/setup-runtime-late-route")).toBe(
      false,
    );
  });

  it("rejects mismatched bundled runtime entry ids before applying setup-runtime setters", () => {
    const runtimeMarker = path.join(makeTempDir(), "setup-runtime-mismatch.txt");
    const built = createSetupEntryChannelPluginFixture({
      id: "setup-runtime-mismatch-test",
      bundledFullEntryId: "wrong-runtime-id",
      label: "Setup Runtime Mismatch Test",
      packageName: "@openclaw/setup-runtime-mismatch-test",
      fullBlurb: "full runtime plugin",
      setupBlurb: "setup runtime override",
      configured: false,
      useBundledFullEntryContract: true,
      useBundledSetupEntryContract: true,
      bundledFullRuntimeMarker: runtimeMarker,
    });

    const registry = loadOpenClawPlugins({
      cache: false,
      config: {
        plugins: {
          load: { paths: [built.pluginDir] },
          allow: ["setup-runtime-mismatch-test"],
        },
      },
    });

    expect(
      registry.plugins.find((entry) => entry.id === "setup-runtime-mismatch-test")?.status,
    ).toBe("error");
    expect(
      registry.plugins.find((entry) => entry.id === "setup-runtime-mismatch-test")?.error,
    ).toContain('runtime entry uses "wrong-runtime-id"');
    expect(registry.channels).toHaveLength(0);
    expect(fs.existsSync(runtimeMarker)).toBe(false);
  });

  it("rejects mismatched bundled setup export ids before loading setup-runtime entry code", () => {
    const runtimeMarker = path.join(makeTempDir(), "setup-runtime-mismatch.txt");
    const built = createSetupEntryChannelPluginFixture({
      id: "setup-export-mismatch-test",
      bundledSetupEntryId: "wrong-setup-id",
      label: "Setup Export Mismatch Test",
      packageName: "@openclaw/setup-export-mismatch-test",
      fullBlurb: "full runtime plugin",
      setupBlurb: "setup runtime override",
      configured: false,
      useBundledFullEntryContract: true,
      useBundledSetupEntryContract: true,
      bundledFullRuntimeMarker: runtimeMarker,
    });

    const registry = loadOpenClawPlugins({
      cache: false,
      config: {
        plugins: {
          load: { paths: [built.pluginDir] },
          allow: ["setup-export-mismatch-test"],
        },
      },
    });

    expect(
      registry.plugins.find((entry) => entry.id === "setup-export-mismatch-test")?.status,
    ).toBe("error");
    expect(
      registry.plugins.find((entry) => entry.id === "setup-export-mismatch-test")?.error,
    ).toContain('setup export uses "wrong-setup-id"');
    expect(registry.channels).toHaveLength(0);
    expect(fs.existsSync(built.fullMarker)).toBe(false);
    expect(fs.existsSync(runtimeMarker)).toBe(false);
  });

  it("isolates loadSetupPlugin errors as per-plugin diagnostics instead of crashing registry load", () => {
    useNoBundledPlugins();
    const pluginDir = makeTempDir();

    // Plugin whose setup-entry uses the bundled contract but loadSetupPlugin() throws
    fs.writeFileSync(
      path.join(pluginDir, "package.json"),
      JSON.stringify(
        {
          name: "@openclaw/setup-entry-throws-test",
          openclaw: {
            extensions: ["./index.cjs"],
            setupEntry: "./setup-entry.cjs",
          },
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "setup-entry-throws-test",
          configSchema: EMPTY_PLUGIN_SCHEMA,
          channels: ["setup-entry-throws-test"],
        },
        null,
        2,
      ),
      "utf-8",
    );
    // index.cjs: full entry (should NOT be reached if setup-entry is used)
    fs.writeFileSync(
      path.join(pluginDir, "index.cjs"),
      `module.exports = { id: "setup-entry-throws-test", register() {} };`,
      "utf-8",
    );
    // setup-entry.cjs: bundled contract whose loadSetupPlugin throws
    fs.writeFileSync(
      path.join(pluginDir, "setup-entry.cjs"),
      `module.exports = {
  kind: "bundled-channel-setup-entry",
  loadSetupPlugin: () => { throw new Error("boom: setup plugin missing"); },
};`,
      "utf-8",
    );

    const registry = loadOpenClawPlugins({
      cache: false,
      config: {
        plugins: {
          load: { paths: [pluginDir] },
          allow: ["setup-entry-throws-test"],
        },
      },
    });

    // The registry load should NOT crash; the error should be recorded as a
    // per-plugin diagnostic rather than aborting the whole load.
    expect(registry.diagnostics.length).toBeGreaterThanOrEqual(1);
    const diagnostic = registry.diagnostics.find(
      (d) => d.pluginId === "setup-entry-throws-test" && d.level === "error",
    );
    expect(diagnostic?.message).toContain("failed to load setup entry");
  });

  it("keeps healthy sibling channel plugins loadable when a setup entry throws", () => {
    useNoBundledPlugins();
    const brokenDir = makeTempDir();

    fs.writeFileSync(
      path.join(brokenDir, "package.json"),
      JSON.stringify(
        {
          name: "@openclaw/setup-entry-throws-sibling-test",
          openclaw: {
            extensions: ["./index.cjs"],
            setupEntry: "./setup-entry.cjs",
          },
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(brokenDir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "setup-entry-throws-sibling-test",
          configSchema: EMPTY_PLUGIN_SCHEMA,
          channels: ["broken-chat"],
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(brokenDir, "index.cjs"),
      `module.exports = { id: "setup-entry-throws-sibling-test", register() {} };`,
      "utf-8",
    );
    fs.writeFileSync(
      path.join(brokenDir, "setup-entry.cjs"),
      `module.exports = {
  kind: "bundled-channel-setup-entry",
  loadSetupPlugin: () => { throw new Error("boom: setup plugin missing"); },
};`,
      "utf-8",
    );

    const healthy = writePlugin({
      id: "healthy-channel",
      filename: "healthy-channel.cjs",
      body: `module.exports = { id: "healthy-channel", register(api) {
  api.registerChannel({
    plugin: {
      id: "healthy-chat",
      meta: {
        id: "healthy-chat",
        label: "Healthy Chat",
        selectionLabel: "Healthy Chat",
        docsPath: "/channels/healthy-chat",
        blurb: "healthy sibling channel",
      },
      capabilities: { chatTypes: ["direct"] },
      config: {
        listAccountIds: () => [],
        resolveAccount: () => ({ accountId: "default" }),
      },
      outbound: { deliveryMode: "direct" },
    }
  });
} };`,
    });

    const registry = loadOpenClawPlugins({
      cache: false,
      config: {
        plugins: {
          enabled: true,
          load: { paths: [brokenDir, healthy.file] },
          allow: ["setup-entry-throws-sibling-test", "healthy-channel"],
        },
      },
    });

    const healthyMeta = registry.channels.find((entry) => entry.plugin.id === "healthy-chat")
      ?.plugin.meta;
    if (!healthyMeta) {
      throw new Error("expected healthy chat plugin metadata");
    }
    expect(healthyMeta?.label).toBe("Healthy Chat");
    expect(healthyMeta?.docsPath).toBe("/channels/healthy-chat");
    expect(registry.plugins.find((entry) => entry.id === "healthy-channel")?.status).toBe("loaded");
    expectDiagnosticContaining({
      registry,
      level: "error",
      pluginId: "setup-entry-throws-sibling-test",
      message: "failed to load setup entry",
    });
  });

  it("prefers setupEntry for configured channel loads during startup when opted in", () => {
    expect(
      testing.shouldLoadChannelPluginInSetupRuntime({
        manifestChannels: ["setup-runtime-preferred-test"],
        setupSource: "./setup-entry.cjs",
        startupDeferConfiguredChannelFullLoadUntilAfterListen: true,
        cfg: {
          channels: {
            "setup-runtime-preferred-test": {
              enabled: true,
              token: "configured",
            },
          },
        },
        env: {},
        preferSetupRuntimeForChannelPlugins: true,
      }),
    ).toBe(true);
  });

  it("prefers built bundled plugin artifacts over source TS when requested", () => {
    const repoRoot = makeTempDir();
    const sourceDir = path.join(repoRoot, "extensions", "startup-artifact-test");
    const runtimeDir = path.join(repoRoot, "dist-runtime", "extensions", "startup-artifact-test");
    mkdirSafe(sourceDir);
    mkdirSafe(runtimeDir);
    fs.writeFileSync(
      path.join(sourceDir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "startup-artifact-test",
          configSchema: EMPTY_PLUGIN_SCHEMA,
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(sourceDir, "index.ts"),
      'throw new Error("source TS should not load during gateway startup");\n',
      "utf-8",
    );
    fs.writeFileSync(
      path.join(runtimeDir, "index.js"),
      'module.exports = { id: "startup-artifact-test", register() {} };\n',
      "utf-8",
    );

    const registry = withEnv(
      {
        OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(repoRoot, "extensions"),
        OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR: "1",
        OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined,
      },
      () =>
        loadOpenClawPlugins({
          cache: false,
          preferBuiltPluginArtifacts: true,
          onlyPluginIds: ["startup-artifact-test"],
          config: {
            plugins: {
              allow: ["startup-artifact-test"],
              entries: {
                "startup-artifact-test": {
                  enabled: true,
                },
              },
            },
          },
        }),
    );

    expect(registry.plugins.find((entry) => entry.id === "startup-artifact-test")?.status).toBe(
      "loaded",
    );
  });

  it("prefers package-local dist artifacts for bundled source checkout plugins", () => {
    const repoRoot = makeTempDir();
    const sourceDir = path.join(repoRoot, "extensions", "startup-package-artifact-test");
    const runtimeDir = path.join(sourceDir, "dist");
    mkdirSafe(sourceDir);
    mkdirSafe(runtimeDir);
    fs.writeFileSync(
      path.join(sourceDir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "startup-package-artifact-test",
          configSchema: EMPTY_PLUGIN_SCHEMA,
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(sourceDir, "package.json"),
      JSON.stringify(
        {
          openclaw: {
            extensions: ["./index.ts"],
          },
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(sourceDir, "index.ts"),
      'throw new Error("source TS should not load during gateway startup");\n',
      "utf-8",
    );
    fs.writeFileSync(
      path.join(runtimeDir, "index.js"),
      'module.exports = { id: "startup-package-artifact-test", register() {} };\n',
      "utf-8",
    );

    const registry = withEnv(
      {
        OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(repoRoot, "extensions"),
        OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR: "1",
        OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined,
      },
      () =>
        loadOpenClawPlugins({
          cache: false,
          preferBuiltPluginArtifacts: true,
          onlyPluginIds: ["startup-package-artifact-test"],
          config: {
            plugins: {
              allow: ["startup-package-artifact-test"],
              entries: {
                "startup-package-artifact-test": {
                  enabled: true,
                },
              },
            },
          },
        }),
    );

    expect(
      registry.plugins.find((entry) => entry.id === "startup-package-artifact-test")?.status,
    ).toBe("loaded");
  });

  it("prefers package-local dist artifacts over workspace source TS when requested", () => {
    useNoBundledPlugins();
    const pluginDir = makeTempDir();
    const distDir = path.join(pluginDir, "dist");
    mkdirSafe(distDir);
    mkdirSafe(path.join(pluginDir, "src"));
    fs.writeFileSync(
      path.join(pluginDir, "package.json"),
      JSON.stringify(
        {
          openclaw: {
            extensions: ["./src/index.mts"],
          },
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "workspace-artifact-test",
          configSchema: EMPTY_PLUGIN_SCHEMA,
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "src", "index.mts"),
      'throw new Error("workspace source TS should not load during gateway startup");\n',
      "utf-8",
    );
    fs.writeFileSync(
      path.join(distDir, "index.mjs"),
      'export default { id: "workspace-artifact-test", register() {} };\n',
      "utf-8",
    );

    const registry = loadOpenClawPlugins({
      cache: false,
      preferBuiltPluginArtifacts: true,
      config: {
        plugins: {
          allow: ["workspace-artifact-test"],
          load: { paths: [pluginDir] },
          entries: {
            "workspace-artifact-test": {
              enabled: true,
            },
          },
        },
      },
    });

    expect(registry.plugins.find((entry) => entry.id === "workspace-artifact-test")?.status).toBe(
      "loaded",
    );
  });

  it("probes supported package-local dist artifact extensions before source TS", () => {
    useNoBundledPlugins();
    const pluginDir = makeTempDir();
    const distDir = path.join(pluginDir, "dist");
    mkdirSafe(distDir);
    mkdirSafe(path.join(pluginDir, "src"));
    fs.writeFileSync(
      path.join(pluginDir, "package.json"),
      JSON.stringify(
        {
          openclaw: {
            extensions: ["./src/index.ts"],
          },
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "workspace-artifact-extension-test",
          configSchema: EMPTY_PLUGIN_SCHEMA,
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "src", "index.ts"),
      'throw new Error("workspace source TS should not load during gateway startup");\n',
      "utf-8",
    );
    fs.writeFileSync(
      path.join(distDir, "index.mjs"),
      'export default { id: "workspace-artifact-extension-test", register() {} };\n',
      "utf-8",
    );

    const registry = loadOpenClawPlugins({
      cache: false,
      preferBuiltPluginArtifacts: true,
      config: {
        plugins: {
          allow: ["workspace-artifact-extension-test"],
          load: { paths: [pluginDir] },
          entries: {
            "workspace-artifact-extension-test": {
              enabled: true,
            },
          },
        },
      },
    });

    expect(
      registry.plugins.find((entry) => entry.id === "workspace-artifact-extension-test")?.status,
    ).toBe("loaded");
  });

  it("does not replace explicit JavaScript entries with package-local dist artifacts", () => {
    useNoBundledPlugins();
    const pluginDir = makeTempDir();
    const distDir = path.join(pluginDir, "dist");
    mkdirSafe(distDir);
    fs.writeFileSync(
      path.join(pluginDir, "package.json"),
      JSON.stringify(
        {
          openclaw: {
            extensions: ["./index.js"],
          },
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "workspace-explicit-js-test",
          configSchema: EMPTY_PLUGIN_SCHEMA,
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "index.js"),
      'export default { id: "workspace-explicit-js-test", register() {} };\n',
      "utf-8",
    );
    fs.writeFileSync(
      path.join(distDir, "index.js"),
      'throw new Error("explicit JS entry should not be replaced by dist");\n',
      "utf-8",
    );

    const registry = loadOpenClawPlugins({
      cache: false,
      preferBuiltPluginArtifacts: true,
      config: {
        plugins: {
          allow: ["workspace-explicit-js-test"],
          load: { paths: [pluginDir] },
          entries: {
            "workspace-explicit-js-test": {
              enabled: true,
            },
          },
        },
      },
    });

    expect(
      registry.plugins.find((entry) => entry.id === "workspace-explicit-js-test")?.status,
    ).toBe("loaded");
  });

  it("keeps package-local dist artifacts inside the plugin root boundary", () => {
    useNoBundledPlugins();
    const pluginDir = makeTempDir();
    const outsideDistDir = makeTempDir();
    mkdirSafe(path.join(pluginDir, "src"));
    fs.writeFileSync(
      path.join(pluginDir, "package.json"),
      JSON.stringify(
        {
          openclaw: {
            extensions: ["./src/index.mts"],
          },
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "workspace-artifact-symlink-test",
          configSchema: EMPTY_PLUGIN_SCHEMA,
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "src", "index.mts"),
      'throw new Error("workspace source TS should not load during gateway startup");\n',
      "utf-8",
    );
    fs.writeFileSync(
      path.join(outsideDistDir, "index.mjs"),
      'export default { id: "workspace-artifact-symlink-test", register() {} };\n',
      "utf-8",
    );
    try {
      fs.symlinkSync(outsideDistDir, path.join(pluginDir, "dist"), "dir");
    } catch {
      return;
    }

    const registry = loadOpenClawPlugins({
      cache: false,
      preferBuiltPluginArtifacts: true,
      config: {
        plugins: {
          allow: ["workspace-artifact-symlink-test"],
          load: { paths: [pluginDir] },
          entries: {
            "workspace-artifact-symlink-test": {
              enabled: true,
            },
          },
        },
      },
    });

    expect(
      registry.plugins.find((entry) => entry.id === "workspace-artifact-symlink-test")?.status,
    ).not.toBe("loaded");
    expectDiagnosticContaining({ registry, message: "escapes" });
  });

  it("blocks before_prompt_build but preserves legacy model overrides when prompt injection is disabled", async () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "hook-policy",
      filename: "hook-policy.cjs",
      body: `module.exports = { id: "hook-policy", register(api) {
  api.on("before_prompt_build", () => ({ prependContext: "prepend" }));
  api.on("before_agent_start", () => ({
    prependContext: "legacy",
    modelOverride: "demo-legacy-model",
    providerOverride: "demo-legacy-provider",
  }));
  api.on("before_model_resolve", () => ({ providerOverride: "demo-explicit-provider" }));
} };`,
    });

    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        allow: ["hook-policy"],
        entries: {
          "hook-policy": {
            hooks: {
              allowPromptInjection: false,
              allowConversationAccess: true,
            },
          },
        },
      },
    });

    expect(registry.plugins.find((entry) => entry.id === "hook-policy")?.status).toBe("loaded");
    expect(registry.typedHooks.map((entry) => entry.hookName)).toEqual([
      "before_agent_start",
      "before_model_resolve",
    ]);
    const runner = createHookRunner(registry);
    const legacyResult = await runner.runBeforeAgentStart({ prompt: "hello", messages: [] }, {});
    expect(legacyResult).toEqual({
      modelOverride: "demo-legacy-model",
      providerOverride: "demo-legacy-provider",
    });
    const blockedDiagnostics = registry.diagnostics.filter((diag) =>
      diag.message.includes(
        "blocked by plugins.entries.hook-policy.hooks.allowPromptInjection=false",
      ),
    );
    expect(blockedDiagnostics).toHaveLength(1);
    const constrainedDiagnostics = registry.diagnostics.filter((diag) =>
      diag.message.includes(
        "prompt fields constrained by plugins.entries.hook-policy.hooks.allowPromptInjection=false",
      ),
    );
    expect(constrainedDiagnostics).toHaveLength(1);
  });

  it("blocks next-turn injections when prompt injection is disabled", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "next-turn-policy",
      filename: "next-turn-policy.cjs",
      body: `module.exports = { id: "next-turn-policy", register(api) {
  void api.session.workflow.enqueueNextTurnInjection({
    sessionKey: "agent:main:main",
    text: "blocked context",
  });
} };`,
    });

    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        allow: ["next-turn-policy"],
        entries: {
          "next-turn-policy": {
            hooks: {
              allowPromptInjection: false,
            },
          },
        },
      },
    });

    expect(registry.plugins.find((entry) => entry.id === "next-turn-policy")?.status).toBe(
      "loaded",
    );
    expect(
      registry.diagnostics.some(
        (entry) =>
          entry.pluginId === "next-turn-policy" &&
          entry.message ===
            "next-turn injection blocked by plugins.entries.next-turn-policy.hooks.allowPromptInjection=false",
      ),
    ).toBe(true);
  });

  it("keeps prompt-injection typed hooks enabled by default", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "hook-policy-default",
      filename: "hook-policy-default.cjs",
      body: `module.exports = { id: "hook-policy-default", register(api) {
  api.on("before_prompt_build", () => ({ prependContext: "prepend" }));
  api.on("before_agent_start", () => ({ prependContext: "legacy" }));
} };`,
    });

    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        allow: ["hook-policy-default"],
      },
    });

    expect(registry.typedHooks.map((entry) => entry.hookName)).toEqual([
      "before_prompt_build",
      "before_agent_start",
    ]);
  });

  it("applies configured typed hook timeout overrides", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "hook-timeouts",
      filename: "hook-timeouts.cjs",
      body: `module.exports = { id: "hook-timeouts", register(api) {
  api.on("before_prompt_build", () => ({ prependContext: "prepend" }), { timeoutMs: 5000 });
  api.on("before_model_resolve", () => ({ providerOverride: "demo-provider" }));
  api.on("before_agent_start", () => ({ modelOverride: "demo-model" }));
} };`,
    });

    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        allow: ["hook-timeouts"],
        entries: {
          "hook-timeouts": {
            hooks: {
              allowConversationAccess: true,
              timeoutMs: 250,
              timeouts: {
                before_model_resolve: 750,
              },
            },
          },
        },
      },
    });

    expect(
      Object.fromEntries(registry.typedHooks.map((entry) => [entry.hookName, entry.timeoutMs])),
    ).toEqual({
      before_prompt_build: 250,
      before_model_resolve: 750,
      before_agent_start: 250,
    });
  });

  it("blocks conversation typed hooks for non-bundled plugins unless explicitly allowed", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "conversation-hooks",
      filename: "conversation-hooks.cjs",
      body: `module.exports = { id: "conversation-hooks", register(api) {
  api.on("before_model_resolve", () => undefined);
  api.on("before_agent_reply", () => undefined);
  api.on("llm_input", () => undefined);
  api.on("llm_output", () => undefined);
  api.on("before_agent_finalize", () => undefined);
  api.on("agent_end", () => undefined);
  api.on("before_agent_run", () => undefined);
} };`,
    });

    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        allow: ["conversation-hooks"],
      },
    });

    expect(registry.typedHooks).toStrictEqual([]);
    const blockedDiagnostics = registry.diagnostics.filter((diag) =>
      diag.message.includes(
        "non-bundled plugins must set plugins.entries.conversation-hooks.hooks.allowConversationAccess=true",
      ),
    );
    expect(blockedDiagnostics).toHaveLength(7);
  });

  it("allows conversation typed hooks for non-bundled plugins when explicitly enabled", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "conversation-hooks-allowed",
      filename: "conversation-hooks-allowed.cjs",
      body: `module.exports = { id: "conversation-hooks-allowed", register(api) {
  api.on("before_model_resolve", () => undefined);
  api.on("before_agent_reply", () => undefined);
  api.on("llm_input", () => undefined);
  api.on("llm_output", () => undefined);
  api.on("before_agent_finalize", () => undefined);
  api.on("agent_end", () => undefined);
  api.on("before_agent_run", () => undefined);
} };`,
    });

    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        allow: ["conversation-hooks-allowed"],
        entries: {
          "conversation-hooks-allowed": {
            hooks: {
              allowConversationAccess: true,
            },
          },
        },
      },
    });

    expect(registry.typedHooks.map((entry) => entry.hookName)).toEqual([
      "before_model_resolve",
      "before_agent_reply",
      "llm_input",
      "llm_output",
      "before_agent_finalize",
      "agent_end",
      "before_agent_run",
    ]);
  });

  it("normalizes legacy deactivate typed hooks onto gateway_stop", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "legacy-deactivate-hook",
      filename: "legacy-deactivate-hook.cjs",
      body: `module.exports = { id: "legacy-deactivate-hook", register(api) {
  api.on("deactivate", () => undefined);
} };`,
    });

    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        allow: ["legacy-deactivate-hook"],
        entries: {
          "legacy-deactivate-hook": {
            hooks: {
              timeoutMs: 250,
            },
          },
        },
      },
    });

    expect(registry.plugins.find((entry) => entry.id === "legacy-deactivate-hook")?.status).toBe(
      "loaded",
    );
    expect(registry.typedHooks.map((entry) => entry.hookName)).toEqual(["gateway_stop"]);
    expect(registry.typedHooks[0]?.timeoutMs).toBe(250);
    expect(
      registry.diagnostics.some(
        (diag) =>
          diag.pluginId === "legacy-deactivate-hook" &&
          diag.message ===
            'typed hook "deactivate" is deprecated (legacy-deactivate-hook-alias); use "gateway_stop". This compatibility alias will be removed after 2026-08-16.',
      ),
    ).toBe(true);
  });

  it("warns when plugins register deprecated subagent_spawning typed hooks", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "legacy-subagent-spawning-hook",
      filename: "legacy-subagent-spawning-hook.cjs",
      body: `module.exports = { id: "legacy-subagent-spawning-hook", register(api) {
  api.on("subagent_spawning", () => ({ status: "ok" }));
} };`,
    });

    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        allow: ["legacy-subagent-spawning-hook"],
      },
    });

    expect(
      registry.plugins.find((entry) => entry.id === "legacy-subagent-spawning-hook")?.status,
    ).toBe("loaded");
    expect(registry.typedHooks.map((entry) => entry.hookName)).toEqual(["subagent_spawning"]);
    expect(
      registry.diagnostics.some(
        (diag) =>
          diag.pluginId === "legacy-subagent-spawning-hook" &&
          diag.message ===
            'typed hook "subagent_spawning" is deprecated (legacy-subagent-spawning-hook); Core prepares thread-bound subagent bindings through channel session-binding adapters before `subagent_spawned` fires. Use `subagent_spawned` for observation; core session bindings for routing. This compatibility hook will be removed after 2026-08-30.',
      ),
    ).toBe(true);
  });

  it("ignores unknown typed hooks from plugins and keeps loading", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "hook-unknown",
      filename: "hook-unknown.cjs",
      body: `module.exports = { id: "hook-unknown", register(api) {
  api.on("totally_unknown_hook_name", () => ({ foo: "bar" }));
  api.on(123, () => ({ foo: "baz" }));
  api.on("before_model_resolve", () => ({ providerOverride: "demo-provider" }));
} };`,
    });

    const registry = loadRegistryFromSinglePlugin({
      plugin,
      pluginConfig: {
        allow: ["hook-unknown"],
        entries: {
          "hook-unknown": {
            hooks: {
              allowConversationAccess: true,
            },
          },
        },
      },
    });

    expect(registry.plugins.find((entry) => entry.id === "hook-unknown")?.status).toBe("loaded");
    expect(registry.typedHooks.map((entry) => entry.hookName)).toEqual(["before_model_resolve"]);
    const unknownHookDiagnostics = registry.diagnostics.filter((diag) =>
      diag.message.includes('unknown typed hook "'),
    );
    expect(unknownHookDiagnostics).toHaveLength(2);
    expect(
      unknownHookDiagnostics.some((diag) =>
        diag.message.includes('unknown typed hook "totally_unknown_hook_name" ignored'),
      ),
    ).toBe(true);
    expect(
      unknownHookDiagnostics.some((diag) =>
        diag.message.includes('unknown typed hook "123" ignored'),
      ),
    ).toBe(true);
  });

  it("enforces memory slot loading rules", () => {
    const scenarios = [
      {
        label: "enforces memory slot selection",
        loadRegistry: () => {
          const memoryA = writePlugin({
            id: "memory-a",
            body: memoryPluginBody("memory-a"),
          });
          const memoryB = writePlugin({
            id: "memory-b",
            body: memoryPluginBody("memory-b"),
          });

          return withEnv(
            {
              OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
              OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
            },
            () =>
              loadOpenClawPlugins({
                cache: false,
                config: {
                  plugins: {
                    load: { paths: [memoryA.file, memoryB.file] },
                    slots: { memory: "memory-b" },
                  },
                },
              }),
          );
        },
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          const a = registry.plugins.find((entry) => entry.id === "memory-a");
          const b = registry.plugins.find((entry) => entry.id === "memory-b");
          expect(b?.status).toBe("loaded");
          expect(a?.status).toBe("disabled");
        },
      },
      {
        label: "skips importing bundled memory plugins that are disabled by memory slot",
        loadRegistry: () => {
          const bundledDir = makeTempDir();
          const memoryADir = path.join(bundledDir, "memory-a");
          const memoryBDir = path.join(bundledDir, "memory-b");
          mkdirSafe(memoryADir);
          mkdirSafe(memoryBDir);
          writePlugin({
            id: "memory-a",
            dir: memoryADir,
            filename: "index.cjs",
            body: `throw new Error("memory-a should not be imported when slot selects memory-b");`,
          });
          writePlugin({
            id: "memory-b",
            dir: memoryBDir,
            filename: "index.cjs",
            body: memoryPluginBody("memory-b"),
          });
          fs.writeFileSync(
            path.join(memoryADir, "openclaw.plugin.json"),
            JSON.stringify(
              {
                id: "memory-a",
                kind: "memory",
                configSchema: EMPTY_PLUGIN_SCHEMA,
              },
              null,
              2,
            ),
            "utf-8",
          );
          fs.writeFileSync(
            path.join(memoryBDir, "openclaw.plugin.json"),
            JSON.stringify(
              {
                id: "memory-b",
                kind: "memory",
                configSchema: EMPTY_PLUGIN_SCHEMA,
              },
              null,
              2,
            ),
            "utf-8",
          );
          process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir;

          return loadOpenClawPlugins({
            cache: false,
            config: {
              plugins: {
                allow: ["memory-a", "memory-b"],
                slots: { memory: "memory-b" },
                entries: {
                  "memory-a": { enabled: true },
                  "memory-b": { enabled: true },
                },
              },
            },
          });
        },
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          const a = registry.plugins.find((entry) => entry.id === "memory-a");
          const b = registry.plugins.find((entry) => entry.id === "memory-b");
          expect(a?.status).toBe("disabled");
          expect(a?.error ?? "").toContain('memory slot set to "memory-b"');
          expect(b?.status).toBe("loaded");
        },
      },
      {
        label:
          "loads dreaming engine alongside a different memory slot plugin when dreaming is enabled",
        loadRegistry: () => {
          const bundledDir = makeTempDir();
          const memoryCoreDir = path.join(bundledDir, "memory-core");
          const memoryLanceDir = path.join(bundledDir, "memory-lancedb");
          mkdirSafe(memoryCoreDir);
          mkdirSafe(memoryLanceDir);
          writePlugin({
            id: "memory-core",
            dir: memoryCoreDir,
            filename: "index.cjs",
            body: memoryPluginBody("memory-core"),
          });
          writePlugin({
            id: "memory-lancedb",
            dir: memoryLanceDir,
            filename: "index.cjs",
            body: memoryPluginBody("memory-lancedb"),
          });
          const openSchema = { type: "object", additionalProperties: true };
          fs.writeFileSync(
            path.join(memoryCoreDir, "openclaw.plugin.json"),
            JSON.stringify(
              { id: "memory-core", kind: "memory", configSchema: EMPTY_PLUGIN_SCHEMA },
              null,
              2,
            ),
            "utf-8",
          );
          fs.writeFileSync(
            path.join(memoryLanceDir, "openclaw.plugin.json"),
            JSON.stringify(
              { id: "memory-lancedb", kind: "memory", configSchema: openSchema },
              null,
              2,
            ),
            "utf-8",
          );
          process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir;

          return loadOpenClawPlugins({
            cache: false,
            config: {
              plugins: {
                allow: ["memory-core", "memory-lancedb"],
                slots: { memory: "memory-lancedb" },
                entries: {
                  "memory-core": { enabled: true },
                  "memory-lancedb": { enabled: true, config: { dreaming: { enabled: true } } },
                },
              },
            },
          });
        },
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          const core = registry.plugins.find((entry) => entry.id === "memory-core");
          const lance = registry.plugins.find((entry) => entry.id === "memory-lancedb");
          expect(core?.status).toBe("loaded");
          expect(lance?.status).toBe("loaded");
          expect(lance?.memorySlotSelected).toBe(true);
          expect(core?.memorySlotSelected).not.toBe(true);
        },
      },
      {
        label: "excludes dreaming engine when dreaming is disabled and it is not the slot",
        loadRegistry: () => {
          const bundledDir = makeTempDir();
          const memoryCoreDir = path.join(bundledDir, "memory-core");
          const memoryLanceDir = path.join(bundledDir, "memory-lancedb");
          mkdirSafe(memoryCoreDir);
          mkdirSafe(memoryLanceDir);
          writePlugin({
            id: "memory-core",
            dir: memoryCoreDir,
            filename: "index.cjs",
            body: `throw new Error("memory-core should not load when dreaming is disabled");`,
          });
          writePlugin({
            id: "memory-lancedb",
            dir: memoryLanceDir,
            filename: "index.cjs",
            body: memoryPluginBody("memory-lancedb"),
          });
          fs.writeFileSync(
            path.join(memoryCoreDir, "openclaw.plugin.json"),
            JSON.stringify(
              { id: "memory-core", kind: "memory", configSchema: EMPTY_PLUGIN_SCHEMA },
              null,
              2,
            ),
            "utf-8",
          );
          fs.writeFileSync(
            path.join(memoryLanceDir, "openclaw.plugin.json"),
            JSON.stringify(
              { id: "memory-lancedb", kind: "memory", configSchema: EMPTY_PLUGIN_SCHEMA },
              null,
              2,
            ),
            "utf-8",
          );
          process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir;

          return loadOpenClawPlugins({
            cache: false,
            config: {
              plugins: {
                allow: ["memory-core", "memory-lancedb"],
                slots: { memory: "memory-lancedb" },
                entries: {
                  "memory-core": { enabled: true },
                  "memory-lancedb": { enabled: true },
                },
              },
            },
          });
        },
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          const core = registry.plugins.find((entry) => entry.id === "memory-core");
          const lance = registry.plugins.find((entry) => entry.id === "memory-lancedb");
          expect(core?.status).toBe("disabled");
          expect(lance?.status).toBe("loaded");
        },
      },
      {
        label: 'keeps memory slot "none" disabled even with stale memory-core dreaming config',
        loadRegistry: () => {
          const bundledDir = makeTempDir();
          const memoryCoreDir = path.join(bundledDir, "memory-core");
          mkdirSafe(memoryCoreDir);
          writePlugin({
            id: "memory-core",
            dir: memoryCoreDir,
            filename: "index.cjs",
            body: `throw new Error("memory-core should not load when memory slot is none");`,
          });
          fs.writeFileSync(
            path.join(memoryCoreDir, "openclaw.plugin.json"),
            JSON.stringify(
              { id: "memory-core", kind: "memory", configSchema: EMPTY_PLUGIN_SCHEMA },
              null,
              2,
            ),
            "utf-8",
          );
          process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir;

          return loadOpenClawPlugins({
            cache: false,
            config: {
              plugins: {
                allow: ["memory-core"],
                slots: { memory: "none" },
                entries: {
                  "memory-core": { enabled: true, config: { dreaming: { enabled: true } } },
                },
              },
            },
          });
        },
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          const core = registry.plugins.find((entry) => entry.id === "memory-core");
          expect(core?.status).toBe("disabled");
        },
      },
      {
        label: "disables memory plugins when slot is none",
        loadRegistry: () => {
          const memory = writePlugin({
            id: "memory-off",
            body: memoryPluginBody("memory-off"),
          });

          return withEnv(
            {
              OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
              OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
            },
            () =>
              loadOpenClawPlugins({
                cache: false,
                config: {
                  plugins: {
                    load: { paths: [memory.file] },
                    slots: { memory: "none" },
                  },
                },
              }),
          );
        },
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          const entry = registry.plugins.find((item) => item.id === "memory-off");
          expect(entry?.status).toBe("disabled");
        },
      },
    ] as const;

    runRegistryScenarios(scenarios, ({ loadRegistry }) => loadRegistry());
  });

  it("resolves duplicate plugin ids by source precedence", () => {
    const scenarios = [
      {
        label: "config load overrides bundled",
        pluginId: "shadow",
        bundledFilename: "shadow.cjs",
        loadRegistry: () => {
          writeBundledPlugin({
            id: "shadow",
            body: simplePluginBody("shadow"),
            filename: "shadow.cjs",
          });

          const override = writePlugin({
            id: "shadow",
            body: simplePluginBody("shadow"),
          });

          return loadOpenClawPlugins({
            cache: false,
            config: {
              plugins: {
                load: { paths: [override.file] },
                entries: {
                  shadow: { enabled: true },
                },
              },
            },
          });
        },
        expectedLoadedOrigin: "config",
        expectedDisabledOrigin: "bundled",
        assert: expectPluginSourcePrecedence,
      },
      {
        label: "bundled beats auto-discovered global duplicate",
        pluginId: "demo-bundled-duplicate",
        bundledFilename: "index.cjs",
        loadRegistry: () => {
          writeBundledPlugin({
            id: "demo-bundled-duplicate",
            body: simplePluginBody("demo-bundled-duplicate"),
          });
          return withStateDir((stateDir) => {
            const globalDir = path.join(stateDir, "extensions", "demo-bundled-duplicate");
            mkdirSafe(globalDir);
            writePlugin({
              id: "demo-bundled-duplicate",
              body: simplePluginBody("demo-bundled-duplicate"),
              dir: globalDir,
              filename: "index.cjs",
            });

            return loadOpenClawPlugins({
              cache: false,
              config: {
                plugins: {
                  allow: ["demo-bundled-duplicate"],
                  entries: {
                    "demo-bundled-duplicate": { enabled: true },
                  },
                },
              },
            });
          });
        },
        expectedLoadedOrigin: "bundled",
        expectedDisabledOrigin: "global",
        expectedDisabledError: "overridden by bundled plugin",
        assert: expectPluginSourcePrecedence,
      },
      {
        label: "installed global beats bundled duplicate",
        pluginId: "demo-installed-duplicate",
        bundledFilename: "index.cjs",
        loadRegistry: () => {
          writeBundledPlugin({
            id: "demo-installed-duplicate",
            body: simplePluginBody("demo-installed-duplicate"),
          });
          return withStateDir((stateDir) => {
            const globalDir = path.join(stateDir, "extensions", "demo-installed-duplicate");
            mkdirSafe(globalDir);
            writePlugin({
              id: "demo-installed-duplicate",
              body: simplePluginBody("demo-installed-duplicate"),
              dir: globalDir,
              filename: "index.cjs",
            });
            writePersistedInstalledPluginIndexInstallRecordsSync(
              {
                "demo-installed-duplicate": {
                  source: "npm",
                  installPath: globalDir,
                },
              },
              { stateDir },
            );

            return loadOpenClawPlugins({
              cache: false,
              config: {
                plugins: {
                  allow: ["demo-installed-duplicate"],
                  entries: {
                    "demo-installed-duplicate": { enabled: true },
                  },
                },
              },
            });
          });
        },
        expectedLoadedOrigin: "global",
        expectedDisabledOrigin: "bundled",
        expectedDisabledError: "overridden by global plugin",
        expectDuplicateWarning: false,
        assert: expectPluginSourcePrecedence,
      },
      {
        label: "transient installed memory plugin beats bundled duplicate",
        pluginId: "memory-lancedb",
        bundledFilename: "index.cjs",
        loadRegistry: () => {
          writeBundledPlugin({
            id: "memory-lancedb",
            body: memoryPluginBody("memory-lancedb"),
          });
          return withStateDir((stateDir) => {
            const globalDir = path.join(stateDir, "node_modules", "@openclaw", "memory-lancedb");
            mkdirSafe(globalDir);
            const globalPlugin = writePlugin({
              id: "memory-lancedb",
              body: `module.exports = {
                id: "memory-lancedb",
                kind: "memory",
                register(api) {
                  api.registerTool({
                    name: "memory_recall",
                    description: "Recall memories",
                    parameters: {},
                    execute: async () => ({ content: [{ type: "text", text: "ok" }] }),
                  });
                },
              };`,
              dir: globalDir,
              filename: "index.cjs",
            });
            updatePluginManifest(globalPlugin, {
              kind: "memory",
              contracts: { tools: ["memory_recall"] },
            });
            fs.writeFileSync(
              path.join(globalDir, "package.json"),
              JSON.stringify(
                {
                  name: "@openclaw/memory-lancedb",
                  version: "2026.5.12-beta.1",
                  openclaw: { extensions: ["./index.cjs"] },
                },
                null,
                2,
              ),
              "utf-8",
            );

            return loadOpenClawPlugins({
              cache: false,
              config: {
                plugins: {
                  allow: ["memory-lancedb"],
                  slots: { memory: "memory-lancedb" },
                  entries: {
                    "memory-lancedb": { enabled: true },
                  },
                  installs: {
                    "memory-lancedb": {
                      source: "npm",
                      spec: "@openclaw/memory-lancedb",
                      resolvedName: "@openclaw/memory-lancedb",
                      resolvedVersion: "2026.5.12-beta.1",
                      installPath: globalDir,
                    },
                  },
                },
              },
            });
          });
        },
        expectedLoadedOrigin: "global",
        expectedDisabledOrigin: "bundled",
        expectedDisabledError: "overridden by global plugin",
        expectDuplicateWarning: false,
        assert: (
          registry: PluginRegistry,
          scenario: Parameters<typeof expectPluginSourcePrecedence>[1],
        ) => {
          expectPluginSourcePrecedence(registry, scenario);
          expect(
            registry.tools.flatMap((entry) => entry.names),
            scenario.label,
          ).toContain("memory_recall");
        },
      },
    ] as const;

    runRegistryScenarios(scenarios, (scenario) => scenario.loadRegistry());
  });

  it("warns about open allowlists only for auto-discovered plugins", () => {
    useNoBundledPlugins();
    clearPluginLoaderCache();
    const scenarios = [
      {
        label: "explicit config path stays quiet",
        pluginId: "warn-open-allow-config",
        loads: 1,
        expectedWarnings: 0,
        loadRegistry: (warnings: string[]) => {
          const plugin = writePlugin({
            id: "warn-open-allow-config",
            body: simplePluginBody("warn-open-allow-config"),
          });
          return loadOpenClawPlugins({
            cache: false,
            logger: createWarningLogger(warnings),
            config: {
              plugins: {
                load: { paths: [plugin.file] },
              },
            },
          });
        },
      },
      {
        label: "workspace discovery warns once",
        pluginId: "warn-open-allow-workspace",
        loads: 2,
        expectedWarnings: 1,
        loadRegistry: (() => {
          const { workspaceDir } = writeWorkspacePlugin({
            id: "warn-open-allow-workspace",
          });
          return (warnings: string[]) =>
            loadOpenClawPlugins({
              cache: false,
              workspaceDir,
              logger: createWarningLogger(warnings),
              config: {
                plugins: {
                  enabled: true,
                },
              },
            });
        })(),
      },
    ] as const;

    runScenarioCases(scenarios, (scenario) => {
      const warnings: string[] = [];

      for (let index = 0; index < scenario.loads; index += 1) {
        scenario.loadRegistry(warnings);
      }

      expectOpenAllowWarnings({
        warnings,
        pluginId: scenario.pluginId,
        expectedWarnings: scenario.expectedWarnings,
        label: scenario.label,
      });
    });
  });

  it("handles workspace-discovered plugins according to trust and precedence", () => {
    useNoBundledPlugins();
    const scenarios = [
      {
        label: "untrusted workspace plugins stay disabled",
        pluginId: "workspace-helper",
        loadRegistry: () => {
          const { workspaceDir } = writeWorkspacePlugin({
            id: "workspace-helper",
          });

          return loadOpenClawPlugins({
            cache: false,
            workspaceDir,
            config: {
              plugins: {
                enabled: true,
              },
            },
          });
        },
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          expectPluginOriginAndStatus({
            registry,
            pluginId: "workspace-helper",
            origin: "workspace",
            status: "disabled",
            label: "untrusted workspace plugins stay disabled",
            errorIncludes: "workspace plugin (disabled by default)",
          });
        },
      },
      {
        label: "trusted workspace plugins load",
        pluginId: "workspace-helper",
        loadRegistry: () => {
          const { workspaceDir } = writeWorkspacePlugin({
            id: "workspace-helper",
          });

          return loadOpenClawPlugins({
            cache: false,
            workspaceDir,
            config: {
              plugins: {
                enabled: true,
                allow: ["workspace-helper"],
              },
            },
          });
        },
        assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
          expectPluginOriginAndStatus({
            registry,
            pluginId: "workspace-helper",
            origin: "workspace",
            status: "loaded",
            label: "trusted workspace plugins load",
          });
        },
      },
      {
        label: "bundled plugins stay ahead of trusted workspace duplicates",
        pluginId: "shadowed",
        expectedLoadedOrigin: "bundled",
        expectedDisabledOrigin: "workspace",
        expectedDisabledError: "overridden by bundled plugin",
        loadRegistry: () => {
          writeBundledPlugin({
            id: "shadowed",
          });
          const { workspaceDir } = writeWorkspacePlugin({
            id: "shadowed",
          });

          return loadOpenClawPlugins({
            cache: false,
            workspaceDir,
            config: {
              plugins: {
                enabled: true,
                allow: ["shadowed"],
                entries: {
                  shadowed: { enabled: true },
                },
              },
            },
          });
        },
        assert: (registry: PluginRegistry) => {
          expectPluginSourcePrecedence(registry, {
            pluginId: "shadowed",
            expectedLoadedOrigin: "bundled",
            expectedDisabledOrigin: "workspace",
            expectedDisabledError: "overridden by bundled plugin",
            label: "bundled plugins stay ahead of trusted workspace duplicates",
          });
        },
      },
    ] as const;

    runRegistryScenarios(scenarios, (scenario) => scenario.loadRegistry());
  });

  it("loads bundled plugins when manifest metadata opts into default enablement", () => {
    const { bundledDir, plugin } = writeBundledPlugin({
      id: "profile-aware",
      body: simplePluginBody("profile-aware"),
    });
    fs.writeFileSync(
      path.join(plugin.dir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "profile-aware",
          enabledByDefault: true,
          configSchema: EMPTY_PLUGIN_SCHEMA,
        },
        null,
        2,
      ),
      "utf-8",
    );

    const registry = loadOpenClawPlugins({
      cache: false,
      workspaceDir: bundledDir,
      config: {
        plugins: {
          enabled: true,
        },
      },
    });

    const bundledPlugin = registry.plugins.find((entry) => entry.id === "profile-aware");
    expect(bundledPlugin?.origin).toBe("bundled");
    expect(bundledPlugin?.status).toBe("loaded");
  });

  it("keeps scoped and unscoped plugin ids distinct", () => {
    useNoBundledPlugins();
    const scoped = writePlugin({
      id: "@team/shadowed",
      body: simplePluginBody("@team/shadowed"),
      filename: "scoped.cjs",
    });
    const unscoped = writePlugin({
      id: "shadowed",
      body: simplePluginBody("shadowed"),
      filename: "unscoped.cjs",
    });

    const registry = loadOpenClawPlugins({
      cache: false,
      config: {
        plugins: {
          load: { paths: [scoped.file, unscoped.file] },
          allow: ["@team/shadowed", "shadowed"],
        },
      },
    });

    expect(registry.plugins.find((entry) => entry.id === "@team/shadowed")?.status).toBe("loaded");
    expect(registry.plugins.find((entry) => entry.id === "shadowed")?.status).toBe("loaded");
    expectNoDiagnosticContaining({ registry, message: "duplicate plugin id" });
  });

  it("evaluates load-path provenance warnings", () => {
    useNoBundledPlugins();
    const scenarios = [
      {
        label: "does not warn when loaded non-bundled plugin is in plugins.allow",
        loadRegistry: () => {
          return withStateDir((stateDir) => {
            const globalDir = path.join(stateDir, "extensions", "rogue");
            mkdirSafe(globalDir);
            writePlugin({
              id: "rogue",
              body: simplePluginBody("rogue"),
              dir: globalDir,
              filename: "index.cjs",
            });

            const warnings: string[] = [];
            const registry = loadOpenClawPlugins({
              cache: false,
              logger: createWarningLogger(warnings),
              config: {
                plugins: {
                  allow: ["rogue"],
                },
              },
            });

            return { registry, warnings, pluginId: "rogue", expectWarning: false };
          });
        },
      },
      {
        label: "warns when loaded non-bundled plugin has no provenance and no allowlist is set",
        loadRegistry: () => {
          const stateDir = makeTempDir();
          return withEnv({ OPENCLAW_STATE_DIR: stateDir }, () => {
            const globalDir = path.join(stateDir, "extensions", "rogue");
            mkdirSafe(globalDir);
            writePlugin({
              id: "rogue",
              body: `module.exports = { id: "rogue", register() {} };`,
              dir: globalDir,
              filename: "index.cjs",
            });

            const warnings: string[] = [];
            const registry = loadOpenClawPlugins({
              cache: false,
              logger: createWarningLogger(warnings),
              config: {
                plugins: {
                  enabled: true,
                },
              },
            });

            return { registry, warnings, pluginId: "rogue", expectWarning: true };
          });
        },
      },
      {
        label: "does not warn about missing provenance for env-resolved load paths",
        loadRegistry: () => {
          const { plugin, env } = createEnvResolvedPluginFixture("tracked-load-path");
          const warnings: string[] = [];
          const registry = loadOpenClawPlugins({
            cache: false,
            logger: createWarningLogger(warnings),
            env,
            config: {
              plugins: {
                load: { paths: ["~/plugins/tracked-load-path"] },
                allow: [plugin.id],
              },
            },
          });

          return {
            registry,
            warnings,
            pluginId: plugin.id,
            expectWarning: false,
            expectedSource: plugin.file,
          };
        },
      },
      {
        label: "does not warn about missing provenance for env-resolved install paths",
        loadRegistry: () => {
          const { plugin, env } = createEnvResolvedPluginFixture("tracked-install-path");
          const warnings: string[] = [];
          const registry = loadOpenClawPlugins({
            cache: false,
            logger: createWarningLogger(warnings),
            env,
            config: {
              plugins: {
                load: { paths: [plugin.file] },
                allow: [plugin.id],
                installs: {
                  [plugin.id]: {
                    source: "path",
                    installPath: `~/plugins/${plugin.id}`,
                    sourcePath: `~/plugins/${plugin.id}`,
                  },
                },
              },
            },
          });

          return {
            registry,
            warnings,
            pluginId: plugin.id,
            expectWarning: false,
            expectedSource: plugin.file,
          };
        },
      },
      {
        label: "does not warn when install paths resolve through a symlinked state root",
        loadRegistry: () => {
          useNoBundledPlugins();
          const stateDir = makeTempDir();
          const realHome = path.join(stateDir, "real-home");
          const linkedHome = path.join(stateDir, "linked-home");
          mkdirSafe(realHome);
          fs.symlinkSync(realHome, linkedHome, process.platform === "win32" ? "junction" : "dir");

          const pluginDir = path.join(
            realHome,
            ".openclaw",
            "npm",
            "node_modules",
            "@example",
            "tracked-symlink-install",
          );
          mkdirSafe(pluginDir);
          const plugin = writePlugin({
            id: "tracked-symlink-install",
            body: simplePluginBody("tracked-symlink-install"),
            dir: pluginDir,
            filename: "index.cjs",
          });
          writePersistedInstalledPluginIndexInstallRecordsSync(
            {
              [plugin.id]: {
                source: "npm",
                spec: "@example/tracked-symlink-install@1.0.0",
                installPath: path.join(
                  linkedHome,
                  ".openclaw",
                  "npm",
                  "node_modules",
                  "@example",
                  "tracked-symlink-install",
                ),
                version: "1.0.0",
              },
            },
            { stateDir },
          );

          const warnings: string[] = [];
          const registry = loadOpenClawPlugins({
            cache: false,
            logger: createWarningLogger(warnings),
            env: {
              ...process.env,
              OPENCLAW_STATE_DIR: stateDir,
              OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins",
            },
            config: {
              plugins: {
                enabled: true,
              },
            },
          });

          return {
            registry,
            warnings,
            pluginId: plugin.id,
            expectWarning: false,
          };
        },
      },
    ] as const;

    runScenarioCases(scenarios, (scenario) => {
      const loadedScenario = scenario.loadRegistry();
      const expectedSource =
        "expectedSource" in loadedScenario && typeof loadedScenario.expectedSource === "string"
          ? loadedScenario.expectedSource
          : undefined;
      expectLoadedPluginProvenance({
        scenario,
        ...loadedScenario,
        expectedSource,
      });
    });
  });

  it("uses the source runtime snapshot allowlist for plugin trust checks", () => {
    useNoBundledPlugins();
    const stateDir = makeTempDir();
    withEnv({ OPENCLAW_STATE_DIR: stateDir }, () => {
      const globalDir = path.join(stateDir, "extensions", "trusted-plugin");
      mkdirSafe(globalDir);
      writePlugin({
        id: "trusted-plugin",
        body: simplePluginBody("trusted-plugin"),
        dir: globalDir,
        filename: "index.cjs",
      });
      const untrustedDir = path.join(stateDir, "extensions", "untrusted-plugin");
      mkdirSafe(untrustedDir);
      writePlugin({
        id: "untrusted-plugin",
        body: simplePluginBody("untrusted-plugin"),
        dir: untrustedDir,
        filename: "index.cjs",
      });
      const runtimeConfig = {
        plugins: {
          enabled: true,
          allow: ["runtime-added-plugin"],
        },
      } satisfies PluginLoadConfig;
      const sourceConfig = {
        plugins: {
          enabled: true,
          allow: ["trusted-plugin"],
        },
      } satisfies PluginLoadConfig;
      setRuntimeConfigSnapshot(runtimeConfig, sourceConfig);

      const warnings: string[] = [];
      const registry = loadOpenClawPlugins({
        cache: false,
        logger: createWarningLogger(warnings),
        config: runtimeConfig,
      });

      expect(registry.plugins.find((entry) => entry.id === "trusted-plugin")?.status).toBe(
        "loaded",
      );
      const untrustedPlugin = registry.plugins.find((entry) => entry.id === "untrusted-plugin");
      expect(untrustedPlugin?.status).toBe("disabled");
      expect(untrustedPlugin?.error).toBe("not in allowlist");
      expect(warnings.join("\n")).not.toContain("plugins.allow is empty");
      expect(
        warnings.filter(
          (message) =>
            message.includes("trusted-plugin") &&
            message.includes("loaded without install/load-path provenance"),
        ),
      ).toEqual([]);
    });
  });

  it.each([
    {
      name: "rejects plugin entry files that escape plugin root via symlink",
      id: "symlinked",
      linkKind: "symlink" as const,
    },
    {
      name: "rejects plugin entry files that escape plugin root via hardlink",
      id: "hardlinked",
      linkKind: "hardlink" as const,
      skip: process.platform === "win32",
    },
  ])("$name", ({ id, linkKind, skip }) => {
    if (skip) {
      return;
    }
    expectEscapingEntryRejected({
      id,
      linkKind,
      sourceBody: `module.exports = { id: "${id}", register() { throw new Error("should not run"); } };`,
    });
  });

  it("allows bundled plugin entry files that are hardlinked aliases", () => {
    if (process.platform === "win32") {
      return;
    }
    const bundledDir = makeTempDir();
    const pluginDir = path.join(bundledDir, "hardlinked-bundled");
    mkdirSafe(pluginDir);

    const outsideDir = makeTempDir();
    const outsideEntry = path.join(outsideDir, "outside.cjs");
    fs.writeFileSync(
      outsideEntry,
      'module.exports = { id: "hardlinked-bundled", register() {} };',
      "utf-8",
    );
    const plugin = writePlugin({
      id: "hardlinked-bundled",
      body: 'module.exports = { id: "hardlinked-bundled", register() {} };',
      dir: pluginDir,
      filename: "index.cjs",
    });
    fs.rmSync(plugin.file);
    try {
      fs.linkSync(outsideEntry, plugin.file);
    } catch (err) {
      if ((err as NodeJS.ErrnoException).code === "EXDEV") {
        return;
      }
      throw err;
    }

    process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir;
    const registry = loadOpenClawPlugins({
      cache: false,
      workspaceDir: bundledDir,
      config: {
        plugins: {
          entries: {
            "hardlinked-bundled": { enabled: true },
          },
          allow: ["hardlinked-bundled"],
        },
      },
    });

    const record = registry.plugins.find((entry) => entry.id === "hardlinked-bundled");
    expect(record?.status).toBe("loaded");
    expectNoDiagnosticContaining({ registry, message: "unsafe plugin path" });
  });

  it("preserves runtime reflection semantics when runtime is lazily initialized", () => {
    useNoBundledPlugins();
    const stateDir = makeTempDir();
    const plugin = writePlugin({
      id: "runtime-introspection",
      filename: "runtime-introspection.cjs",
      body: `module.exports = { id: "runtime-introspection", register(api) {
  const runtime = api.runtime ?? {};
  const keys = Object.keys(runtime);
  for (const key of ["channel", "mediaUnderstanding", "llm"]) {
    if (!keys.includes(key)) {
      throw new Error("runtime " + key + " key missing");
    }
    if (!(key in runtime)) {
      throw new Error("runtime " + key + " missing from has check");
    }
    if (!Object.getOwnPropertyDescriptor(runtime, key)) {
      throw new Error("runtime " + key + " descriptor missing");
    }
  }
} };`,
    });

    const registry = withEnv({ OPENCLAW_STATE_DIR: stateDir }, () =>
      loadRegistryFromSinglePlugin({
        plugin,
        pluginConfig: {
          allow: ["runtime-introspection"],
        },
        options: {
          onlyPluginIds: ["runtime-introspection"],
        },
      }),
    );

    const record = registry.plugins.find((entry) => entry.id === "runtime-introspection");
    expect(record?.status).toBe("loaded");
  });

  it("supports legacy plugins importing monolithic plugin-sdk root", () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "legacy-root-import",
      filename: "legacy-root-import.cjs",
      body: `module.exports = {
  id: "legacy-root-import",
  configSchema: (require("openclaw/plugin-sdk").emptyPluginConfigSchema)(),
        register() {},
      };`,
    });

    const registry = withEnv({ OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins" }, () =>
      loadOpenClawPlugins({
        cache: false,
        workspaceDir: plugin.dir,
        config: {
          plugins: {
            load: { paths: [plugin.file] },
            allow: ["legacy-root-import"],
          },
        },
      }),
    );
    const record = registry.plugins.find((entry) => entry.id === "legacy-root-import");
    expect(
      record?.status,
      JSON.stringify({ error: record?.error, diagnostics: registry.diagnostics }, null, 2),
    ).toBe("loaded");
  });

  it("supports legacy plugins subscribing to diagnostic events from the root sdk", async () => {
    useNoBundledPlugins();
    const seenKey = "__openclawLegacyRootDiagnosticSeen";
    delete (globalThis as Record<string, unknown>)[seenKey];

    const plugin = writePlugin({
      id: "legacy-root-diagnostic-listener",
      filename: "legacy-root-diagnostic-listener.cjs",
      body: `module.exports = {
  id: "legacy-root-diagnostic-listener",
  configSchema: (require("openclaw/plugin-sdk").emptyPluginConfigSchema)(),
  register() {
    const { onDiagnosticEvent } = require("openclaw/plugin-sdk");
    if (typeof onDiagnosticEvent !== "function") {
      throw new Error("missing onDiagnosticEvent root export");
    }
    globalThis.${seenKey} = [];
    onDiagnosticEvent((event) => {
      globalThis.${seenKey}.push({
        type: event.type,
        sessionKey: event.sessionKey,
      });
    });
  },
};`,
    });

    try {
      const registry = withEnv(
        { OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins" },
        () =>
          loadOpenClawPlugins({
            cache: false,
            workspaceDir: plugin.dir,
            config: {
              plugins: {
                load: { paths: [plugin.file] },
                allow: ["legacy-root-diagnostic-listener"],
              },
            },
          }),
      );
      const record = registry.plugins.find(
        (entry) => entry.id === "legacy-root-diagnostic-listener",
      );
      expect(
        record?.status,
        JSON.stringify({ error: record?.error, diagnostics: registry.diagnostics }, null, 2),
      ).toBe("loaded");

      emitDiagnosticEvent({
        type: "model.usage",
        sessionKey: "agent:main:test:dm:peer",
        usage: { total: 1 },
      });
      await waitForDiagnosticEventsDrained();

      expect((globalThis as Record<string, unknown>)[seenKey]).toEqual([
        {
          type: "model.usage",
          sessionKey: "agent:main:test:dm:peer",
        },
      ]);
    } finally {
      delete (globalThis as Record<string, unknown>)[seenKey];
    }
  });

  it("suppresses trust warning logs for non-activating snapshot loads", () => {
    useNoBundledPlugins();
    const stateDir = makeTempDir();
    withEnv({ OPENCLAW_STATE_DIR: stateDir }, () => {
      const globalDir = path.join(stateDir, "extensions", "rogue");
      mkdirSafe(globalDir);
      writePlugin({
        id: "rogue",
        body: simplePluginBody("rogue"),
        dir: globalDir,
        filename: "index.cjs",
      });

      const warnings: string[] = [];
      const registry = loadOpenClawPlugins({
        activate: false,
        cache: false,
        logger: createWarningLogger(warnings),
        config: {
          plugins: {
            enabled: true,
          },
        },
      });

      expect(warnings).toStrictEqual([]);
      expectDiagnosticContaining({
        registry,
        level: "warn",
        pluginId: "rogue",
        message: "loaded without install/load-path provenance",
      });
    });
  });

  it("loads source TypeScript plugins that route through local runtime shims", () => {
    const plugin = writePlugin({
      id: "source-runtime-shim",
      filename: "source-runtime-shim.ts",
      body: `import "./runtime-shim.ts";

export default {
  id: "source-runtime-shim",
  register() {},
};`,
    });
    fs.writeFileSync(
      path.join(plugin.dir, "runtime-shim.ts"),
      `import { helperValue } from "./helper.js";

export const runtimeValue = helperValue;`,
      "utf-8",
    );
    fs.writeFileSync(
      path.join(plugin.dir, "helper.ts"),
      `export const helperValue = "ok";`,
      "utf-8",
    );

    const registry = loadOpenClawPlugins({
      cache: false,
      workspaceDir: plugin.dir,
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["source-runtime-shim"],
        },
      },
    });

    const record = registry.plugins.find((entry) => entry.id === "source-runtime-shim");
    expect(record?.status).toBe("loaded");
  });

  it("converts Windows absolute import specifiers to file URLs only for module loading", () => {
    const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
    try {
      expect(testing.toSafeImportPath("C:\\Users\\alice\\plugin\\index.mjs")).toBe(
        "file:///C:/Users/alice/plugin/index.mjs",
      );
      expect(testing.toSafeImportPath("C:\\Users\\alice\\plugin folder\\x#y.mjs")).toBe(
        "file:///C:/Users/alice/plugin%20folder/x%23y.mjs",
      );
      expect(testing.toSafeImportPath("\\\\server\\share\\plugin\\index.mjs")).toBe(
        "file://server/share/plugin/index.mjs",
      );
      expect(testing.toSafeImportPath("file:///C:/Users/alice/plugin/index.mjs")).toBe(
        "file:///C:/Users/alice/plugin/index.mjs",
      );
      expect(testing.toSafeImportPath("./relative/index.mjs")).toBe("./relative/index.mjs");
    } finally {
      platformSpy.mockRestore();
    }
  });
});
