// Covers plugin loader CLI metadata without activating plugin runtimes.
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { afterAll, afterEach, describe, expect, it } from "vitest";
import {
  defineBundledChannelEntry,
  type OpenClawPluginApi,
} from "../plugin-sdk/channel-entry-contract.js";
import { loadOpenClawPluginCliRegistry, loadOpenClawPlugins } from "./loader.js";
import {
  cleanupPluginLoaderFixturesForTest,
  EMPTY_PLUGIN_SCHEMA,
  inlineChannelPluginEntryFactorySource,
  makeTempDir,
  resetPluginLoaderTestStateForTest,
  useNoBundledPlugins,
  writePlugin,
} from "./loader.test-fixtures.js";

afterEach(() => {
  resetPluginLoaderTestStateForTest();
});

afterAll(() => {
  cleanupPluginLoaderFixturesForTest();
});

describe("plugin loader CLI metadata", () => {
  it.each([
    {
      id: "wrong-cli-channel-entry",
      kind: "bundled-channel-entry",
      error: "bundled channel entry requires setup-runtime loader",
    },
    {
      id: "wrong-cli-channel-setup-entry",
      kind: "bundled-channel-setup-entry",
      error: "bundled channel setup entry requires setup-runtime loader",
    },
  ])(
    "reports $kind loaded through CLI metadata legacy plugin path",
    async ({ 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 = await loadOpenClawPluginCliRegistry({
        cache: false,
        logger: {
          info: () => {},
          warn: () => {},
          error: (msg: string) => errors.push(msg),
          debug: () => {},
        },
        config: {
          plugins: {
            load: { paths: [plugin.file] },
            allow: [id],
          },
        },
      });

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

  it("suppresses trust warning logs during CLI metadata loads", async () => {
    useNoBundledPlugins();
    const stateDir = makeTempDir();
    const globalDir = path.join(stateDir, "extensions", "rogue");
    fs.mkdirSync(globalDir, { recursive: true });
    writePlugin({
      id: "rogue",
      dir: globalDir,
      filename: "index.cjs",
      body: `module.exports = {
  id: "rogue",
  register(api) {
    api.registerCli(() => {}, {
      descriptors: [
        {
          name: "rogue",
          description: "Rogue CLI metadata",
          hasSubcommands: true,
        },
      ],
    });
  },
};`,
    });

    const warnings: string[] = [];
    const registry = await loadOpenClawPluginCliRegistry({
      env: { ...process.env, OPENCLAW_STATE_DIR: stateDir },
      logger: {
        info: () => {},
        warn: (msg: string) => warnings.push(msg),
        error: () => {},
        debug: () => {},
      },
      config: {
        plugins: {
          enabled: true,
        },
      },
    });

    expect(warnings).toStrictEqual([]);
    expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain("rogue");
  });

  it("passes validated plugin config into non-activating CLI metadata loads", async () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "config-cli",
      filename: "config-cli.cjs",
      body: `module.exports = {
  id: "config-cli",
  register(api) {
    if (!api.pluginConfig || api.pluginConfig.token !== "ok") {
      throw new Error("missing plugin config");
    }
    api.registerCli(() => {}, {
      descriptors: [
        {
          name: "cfg",
          description: "Config-backed CLI command",
          hasSubcommands: true,
        },
      ],
    });
  },
};`,
    });
    fs.writeFileSync(
      path.join(plugin.dir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "config-cli",
          configSchema: {
            type: "object",
            additionalProperties: false,
            properties: {
              token: { type: "string" },
            },
            required: ["token"],
          },
        },
        null,
        2,
      ),
      "utf-8",
    );

    const registry = await loadOpenClawPluginCliRegistry({
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["config-cli"],
          entries: {
            "config-cli": {
              config: {
                token: "ok",
              },
            },
          },
        },
      },
    });

    expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain("cfg");
    expect(registry.plugins.find((entry) => entry.id === "config-cli")?.status).toBe("loaded");
  });

  it("uses the real channel entry in cli-metadata mode for CLI metadata capture", async () => {
    useNoBundledPlugins();
    const pluginDir = makeTempDir();
    const fullMarker = path.join(pluginDir, "full-loaded.txt");
    const modeMarker = path.join(pluginDir, "registration-mode.txt");
    const runtimeMarker = path.join(pluginDir, "runtime-set.txt");

    fs.writeFileSync(
      path.join(pluginDir, "package.json"),
      JSON.stringify(
        {
          name: "@openclaw/cli-metadata-channel",
          openclaw: { extensions: ["./index.cjs"], setupEntry: "./setup-entry.cjs" },
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "cli-metadata-channel",
          configSchema: EMPTY_PLUGIN_SCHEMA,
          channels: ["cli-metadata-channel"],
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "index.cjs"),
      `${inlineChannelPluginEntryFactorySource()}
require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
module.exports = {
  ...defineChannelPluginEntry({
    id: "cli-metadata-channel",
    name: "CLI Metadata Channel",
    description: "cli metadata channel",
    setRuntime() {
      require("node:fs").writeFileSync(${JSON.stringify(runtimeMarker)}, "loaded", "utf-8");
    },
    plugin: {
      id: "cli-metadata-channel",
      meta: {
        id: "cli-metadata-channel",
        label: "CLI Metadata Channel",
        selectionLabel: "CLI Metadata Channel",
        docsPath: "/channels/cli-metadata-channel",
        blurb: "cli metadata channel",
      },
      capabilities: { chatTypes: ["direct"] },
      config: {
        listAccountIds: () => [],
        resolveAccount: () => ({ accountId: "default" }),
      },
      outbound: { deliveryMode: "direct" },
    },
    registerCliMetadata(api) {
      require("node:fs").writeFileSync(
        ${JSON.stringify(modeMarker)},
        String(api.registrationMode),
        "utf-8",
      );
      api.registerCli(() => {}, {
        descriptors: [
          {
            name: "cli-metadata-channel",
            description: "Channel CLI metadata",
            hasSubcommands: true,
          },
        ],
      });
    },
    registerFull() {
      throw new Error("full channel entry should not run during CLI metadata capture");
    },
  }),
};`,
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "setup-entry.cjs"),
      `throw new Error("setup entry should not load during CLI metadata capture");`,
      "utf-8",
    );

    const registry = await loadOpenClawPluginCliRegistry({
      config: {
        plugins: {
          load: { paths: [pluginDir] },
          allow: ["cli-metadata-channel"],
        },
      },
    });

    expect(fs.existsSync(fullMarker)).toBe(true);
    expect(fs.existsSync(runtimeMarker)).toBe(false);
    expect(fs.readFileSync(modeMarker, "utf-8")).toBe("cli-metadata");
    expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain(
      "cli-metadata-channel",
    );
  });

  it("skips bundled channel full entries that do not provide a dedicated cli-metadata entry", async () => {
    const bundledRoot = makeTempDir();
    const pluginDir = path.join(bundledRoot, "bundled-skip-channel");
    const fullMarker = path.join(pluginDir, "full-loaded.txt");

    fs.mkdirSync(pluginDir, { recursive: true });
    process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledRoot;

    fs.writeFileSync(
      path.join(pluginDir, "package.json"),
      JSON.stringify(
        {
          name: "@openclaw/bundled-skip-channel",
          openclaw: { extensions: ["./index.cjs"] },
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "bundled-skip-channel",
          configSchema: EMPTY_PLUGIN_SCHEMA,
          channels: ["bundled-skip-channel"],
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "index.cjs"),
      `require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
module.exports = {
  id: "bundled-skip-channel",
  register() {
    throw new Error("bundled channel full entry should not load during CLI metadata capture");
  },
};`,
      "utf-8",
    );

    const registry = await loadOpenClawPluginCliRegistry({
      config: {
        plugins: {
          allow: ["bundled-skip-channel"],
          entries: {
            "bundled-skip-channel": {
              enabled: true,
            },
          },
        },
      },
    });

    expect(fs.existsSync(fullMarker)).toBe(false);
    expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).not.toContain(
      "bundled-skip-channel",
    );
    expect(registry.plugins.find((entry) => entry.id === "bundled-skip-channel")?.status).toBe(
      "loaded",
    );
  });

  it("prefers bundled channel cli-metadata entries over full channel entries", async () => {
    const bundledRoot = makeTempDir();
    const pluginDir = path.join(bundledRoot, "bundled-cli-channel");
    const fullMarker = path.join(pluginDir, "full-loaded.txt");
    const cliMarker = path.join(pluginDir, "cli-loaded.txt");

    fs.mkdirSync(pluginDir, { recursive: true });
    process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledRoot;

    fs.writeFileSync(
      path.join(pluginDir, "package.json"),
      JSON.stringify(
        {
          name: "@openclaw/bundled-cli-channel",
          openclaw: { extensions: ["./index.cjs"] },
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "bundled-cli-channel",
          configSchema: EMPTY_PLUGIN_SCHEMA,
          channels: ["bundled-cli-channel"],
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "index.cjs"),
      `require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
module.exports = {
  id: "bundled-cli-channel",
  register() {
    throw new Error("bundled channel full entry should not load during CLI metadata capture");
  },
};`,
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "cli-metadata.cjs"),
      `module.exports = {
  id: "bundled-cli-channel",
  register(api) {
    require("node:fs").writeFileSync(${JSON.stringify(cliMarker)}, "loaded", "utf-8");
    api.registerCli(() => {}, {
      descriptors: [
        {
          name: "bundled-cli-channel",
          description: "Bundled channel CLI metadata",
          hasSubcommands: true,
        },
      ],
    });
  },
};`,
      "utf-8",
    );

    const registry = await loadOpenClawPluginCliRegistry({
      config: {
        plugins: {
          allow: ["bundled-cli-channel"],
          entries: {
            "bundled-cli-channel": {
              enabled: true,
            },
          },
        },
      },
    });

    expect(fs.existsSync(fullMarker)).toBe(false);
    expect(fs.existsSync(cliMarker)).toBe(true);
    expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain(
      "bundled-cli-channel",
    );
  });

  it("skips bundled non-channel full entries that do not provide a dedicated cli-metadata entry", async () => {
    const bundledRoot = makeTempDir();
    const pluginDir = path.join(bundledRoot, "bundled-skip-provider");
    const fullMarker = path.join(pluginDir, "full-loaded.txt");

    fs.mkdirSync(pluginDir, { recursive: true });
    process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledRoot;

    fs.writeFileSync(
      path.join(pluginDir, "package.json"),
      JSON.stringify(
        {
          name: "@openclaw/bundled-skip-provider",
          openclaw: { extensions: ["./index.cjs"] },
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "bundled-skip-provider",
          configSchema: EMPTY_PLUGIN_SCHEMA,
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "index.cjs"),
      `require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
module.exports = {
  id: "bundled-skip-provider",
  register() {
    throw new Error("bundled provider full entry should not load during CLI metadata capture");
  },
};`,
      "utf-8",
    );

    const registry = await loadOpenClawPluginCliRegistry({
      config: {
        plugins: {
          allow: ["bundled-skip-provider"],
          entries: {
            "bundled-skip-provider": {
              enabled: true,
            },
          },
        },
      },
    });

    expect(fs.existsSync(fullMarker)).toBe(false);
    expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).not.toContain(
      "bundled-skip-provider",
    );
    expect(registry.plugins.find((entry) => entry.id === "bundled-skip-provider")?.status).toBe(
      "loaded",
    );
  });

  it("collects channel CLI metadata during full plugin loads", () => {
    useNoBundledPlugins();
    const pluginDir = makeTempDir();
    const modeMarker = path.join(pluginDir, "registration-mode.txt");
    const fullMarker = path.join(pluginDir, "full-loaded.txt");

    fs.writeFileSync(
      path.join(pluginDir, "package.json"),
      JSON.stringify(
        {
          name: "@openclaw/full-cli-metadata-channel",
          openclaw: { extensions: ["./index.cjs"] },
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "full-cli-metadata-channel",
          configSchema: EMPTY_PLUGIN_SCHEMA,
          channels: ["full-cli-metadata-channel"],
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "index.cjs"),
      `${inlineChannelPluginEntryFactorySource()}
module.exports = {
  ...defineChannelPluginEntry({
    id: "full-cli-metadata-channel",
    name: "Full CLI Metadata Channel",
    description: "full cli metadata channel",
    plugin: {
      id: "full-cli-metadata-channel",
      meta: {
        id: "full-cli-metadata-channel",
        label: "Full CLI Metadata Channel",
        selectionLabel: "Full CLI Metadata Channel",
        docsPath: "/channels/full-cli-metadata-channel",
        blurb: "full cli metadata channel",
      },
      capabilities: { chatTypes: ["direct"] },
      config: {
        listAccountIds: () => [],
        resolveAccount: () => ({ accountId: "default" }),
      },
      outbound: { deliveryMode: "direct" },
    },
    registerCliMetadata(api) {
      require("node:fs").writeFileSync(
        ${JSON.stringify(modeMarker)},
        String(api.registrationMode),
        "utf-8",
      );
      api.registerCli(() => {}, {
        descriptors: [
          {
            name: "full-cli-metadata-channel",
            description: "Full-load channel CLI metadata",
            hasSubcommands: true,
          },
        ],
      });
    },
    registerFull() {
      require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
    },
  }),
};`,
      "utf-8",
    );

    const registry = loadOpenClawPlugins({
      cache: false,
      config: {
        plugins: {
          load: { paths: [pluginDir] },
          allow: ["full-cli-metadata-channel"],
        },
      },
    });

    expect(fs.readFileSync(modeMarker, "utf-8")).toBe("full");
    expect(fs.existsSync(fullMarker)).toBe(true);
    expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain(
      "full-cli-metadata-channel",
    );
  });

  it("collects channel CLI metadata during discovery plugin loads", () => {
    useNoBundledPlugins();
    const pluginDir = makeTempDir();
    const modeMarker = path.join(pluginDir, "registration-mode.txt");
    const fullMarker = path.join(pluginDir, "full-loaded.txt");
    const runtimeMarker = path.join(pluginDir, "runtime-set.txt");

    fs.writeFileSync(
      path.join(pluginDir, "package.json"),
      JSON.stringify(
        {
          name: "@openclaw/discovery-cli-metadata-channel",
          openclaw: { extensions: ["./index.cjs"] },
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "discovery-cli-metadata-channel",
          configSchema: EMPTY_PLUGIN_SCHEMA,
          channels: ["discovery-cli-metadata-channel"],
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "index.cjs"),
      `${inlineChannelPluginEntryFactorySource()}
module.exports = {
  ...defineChannelPluginEntry({
    id: "discovery-cli-metadata-channel",
    name: "Discovery CLI Metadata Channel",
    description: "discovery cli metadata channel",
    setRuntime() {
      require("node:fs").writeFileSync(${JSON.stringify(runtimeMarker)}, "loaded", "utf-8");
    },
    plugin: {
      id: "discovery-cli-metadata-channel",
      meta: {
        id: "discovery-cli-metadata-channel",
        label: "Discovery CLI Metadata Channel",
        selectionLabel: "Discovery CLI Metadata Channel",
        docsPath: "/channels/discovery-cli-metadata-channel",
        blurb: "discovery cli metadata channel",
      },
      capabilities: { chatTypes: ["direct"] },
      config: {
        listAccountIds: () => [],
        resolveAccount: () => ({ accountId: "default" }),
      },
      outbound: { deliveryMode: "direct" },
    },
    registerCliMetadata(api) {
      require("node:fs").writeFileSync(
        ${JSON.stringify(modeMarker)},
        String(api.registrationMode),
        "utf-8",
      );
      api.registerCli(() => {}, {
        descriptors: [
          {
            name: "discovery-cli-metadata-channel",
            description: "Discovery-load channel CLI metadata",
            hasSubcommands: true,
          },
        ],
      });
    },
    registerFull() {
      require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
    },
  }),
};`,
      "utf-8",
    );

    const registry = loadOpenClawPlugins({
      activate: false,
      cache: false,
      config: {
        plugins: {
          load: { paths: [pluginDir] },
          allow: ["discovery-cli-metadata-channel"],
          entries: {
            "discovery-cli-metadata-channel": {
              enabled: true,
            },
          },
        },
      },
    });

    expect(fs.readFileSync(modeMarker, "utf-8")).toBe("discovery");
    expect(fs.existsSync(fullMarker)).toBe(false);
    expect(fs.existsSync(runtimeMarker)).toBe(true);
    expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain(
      "discovery-cli-metadata-channel",
    );
  });

  it("can force channel runtime entries for CLI registration when setup entries exist", () => {
    useNoBundledPlugins();
    const pluginDir = makeTempDir();
    const modeMarker = path.join(pluginDir, "registration-mode.txt");
    const setupMarker = path.join(pluginDir, "setup-loaded.txt");

    fs.writeFileSync(
      path.join(pluginDir, "package.json"),
      JSON.stringify(
        {
          name: "@openclaw/force-runtime-cli-channel",
          openclaw: { extensions: ["./index.cjs"], setupEntry: "./setup-entry.cjs" },
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "force-runtime-cli-channel",
          configSchema: EMPTY_PLUGIN_SCHEMA,
          channels: ["force-runtime-cli-channel"],
        },
        null,
        2,
      ),
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "index.cjs"),
      `${inlineChannelPluginEntryFactorySource()}
module.exports = {
  ...defineChannelPluginEntry({
    id: "force-runtime-cli-channel",
    name: "Force Runtime CLI Channel",
    description: "force runtime cli channel",
    plugin: {
      id: "force-runtime-cli-channel",
      meta: {
        id: "force-runtime-cli-channel",
        label: "Force Runtime CLI Channel",
        selectionLabel: "Force Runtime CLI Channel",
        docsPath: "/channels/force-runtime-cli-channel",
        blurb: "force runtime cli channel",
      },
      capabilities: { chatTypes: ["direct"] },
      config: {
        listAccountIds: () => [],
        resolveAccount: () => ({ accountId: "default" }),
      },
      outbound: { deliveryMode: "direct" },
    },
    registerCliMetadata(api) {
      require("node:fs").writeFileSync(
        ${JSON.stringify(modeMarker)},
        String(api.registrationMode),
        "utf-8",
      );
      api.registerCli(() => {}, {
        descriptors: [
          {
            name: "force-runtime-cli-channel",
            description: "Forced runtime channel CLI metadata",
            hasSubcommands: true,
          },
        ],
      });
    },
  }),
};`,
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "setup-entry.cjs"),
      `require("node:fs").writeFileSync(${JSON.stringify(setupMarker)}, "loaded", "utf-8");`,
      "utf-8",
    );

    const registry = loadOpenClawPlugins({
      activate: false,
      cache: false,
      forceFullRuntimeForChannelPlugins: true,
      config: {
        plugins: {
          load: { paths: [pluginDir] },
          allow: ["force-runtime-cli-channel"],
          entries: {
            "force-runtime-cli-channel": {
              enabled: true,
            },
          },
        },
      },
    });

    expect(fs.existsSync(setupMarker)).toBe(false);
    expect(fs.readFileSync(modeMarker, "utf-8")).toBe("discovery");
    expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain(
      "force-runtime-cli-channel",
    );
  });

  it("sets bundled channel runtime before discovery CLI metadata registration", () => {
    const pluginDir = makeTempDir();
    const runtimeMarker = path.join(pluginDir, "runtime-set.txt");
    const channelPluginPath = path.join(pluginDir, "channel.cjs");
    const runtimePath = path.join(pluginDir, "runtime.cjs");
    fs.writeFileSync(
      channelPluginPath,
      `exports.plugin = {
  id: "bundled-discovery-cli",
  meta: {
    id: "bundled-discovery-cli",
    label: "Bundled Discovery CLI",
    selectionLabel: "Bundled Discovery CLI",
    docsPath: "/channels/bundled-discovery-cli",
    blurb: "bundled discovery cli",
  },
  capabilities: { chatTypes: ["direct"] },
  config: {
    listAccountIds: () => [],
    resolveAccount: () => ({ accountId: "default" }),
  },
  outbound: { deliveryMode: "direct" },
};`,
      "utf-8",
    );
    fs.writeFileSync(
      runtimePath,
      `exports.setRuntime = () => {
  require("node:fs").writeFileSync(${JSON.stringify(runtimeMarker)}, "loaded", "utf-8");
};`,
      "utf-8",
    );

    const commands: string[] = [];
    const channels: string[] = [];
    const entry = defineBundledChannelEntry({
      id: "bundled-discovery-cli",
      name: "Bundled Discovery CLI",
      description: "bundled discovery cli",
      importMetaUrl: pathToFileURL(path.join(pluginDir, "index.cjs")).href,
      plugin: {
        specifier: "./channel.cjs",
        exportName: "plugin",
      },
      runtime: {
        specifier: "./runtime.cjs",
        exportName: "setRuntime",
      },
      registerCliMetadata(api) {
        api.registerCli(() => {}, {
          descriptors: [
            {
              name: "bundled-discovery-cli",
              description: "Bundled discovery CLI metadata",
              hasSubcommands: true,
            },
          ],
        });
      },
      registerFull() {
        throw new Error("full registration should not run during discovery");
      },
    });

    entry.register({
      registrationMode: "discovery",
      runtime: {} as OpenClawPluginApi["runtime"],
      registerChannel: (registration) => {
        const plugin = "plugin" in registration ? registration.plugin : registration;
        channels.push(plugin.id);
      },
      registerCli: (_register, options) => {
        commands.push(...(options?.descriptors ?? []).map((descriptor) => descriptor.name));
      },
    } as OpenClawPluginApi);

    expect(channels).toEqual(["bundled-discovery-cli"]);
    expect(fs.existsSync(runtimeMarker)).toBe(true);
    expect(commands).toEqual(["bundled-discovery-cli"]);
  });

  it("sanitizes plugin CLI descriptor descriptions and rejects unsafe command names", async () => {
    useNoBundledPlugins();
    const unsafeDescription =
      "Open \u001B]8;;https://example.test\u0007link\u001B]8;;\u0007 now\u001B[2J";
    const plugin = writePlugin({
      id: "unsafe-cli-descriptors",
      filename: "unsafe-cli-descriptors.cjs",
      body: `module.exports = {
  id: "unsafe-cli-descriptors",
  register(api) {
    api.registerCli(() => {}, {
      commands: ["bad\\ncommand"],
      descriptors: [
        {
          name: "safe-command",
          description: ${JSON.stringify(unsafeDescription)},
          hasSubcommands: false,
        },
        {
          name: "bad\\nname",
          description: "Bad descriptor",
          hasSubcommands: false,
        },
      ],
    });
  },
};`,
    });

    const registry = await loadOpenClawPluginCliRegistry({
      cache: false,
      config: {
        plugins: {
          load: { paths: [plugin.dir] },
          allow: ["unsafe-cli-descriptors"],
        },
      },
    });

    expect(registry.cliRegistrars).toHaveLength(1);
    expect(registry.cliRegistrars[0]?.commands).toEqual(["safe-command"]);
    expect(registry.cliRegistrars[0]?.descriptors).toEqual([
      {
        name: "safe-command",
        description: "Open link now",
        hasSubcommands: false,
      },
    ]);
    expect(registry.diagnostics.map((diag) => diag.message)).toEqual([
      'invalid cli descriptor name: "bad\\nname"',
      'invalid cli command name: "bad\\ncommand"',
    ]);
  });

  it("rejects async plugin registration when collecting CLI metadata", async () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "async-cli",
      filename: "async-cli.cjs",
      body: `module.exports = {
  id: "async-cli",
  async register(api) {
    await Promise.resolve();
    api.registerCli(() => {}, {
      descriptors: [
        {
          name: "async-cli",
          description: "Async CLI metadata",
          hasSubcommands: true,
        },
      ],
    });
  },
};`,
    });

    const registry = await loadOpenClawPluginCliRegistry({
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["async-cli"],
        },
      },
    });

    expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).not.toContain("async-cli");
    const loaded = registry.plugins.find((entry) => entry.id === "async-cli");
    expect(loaded?.status).toBe("error");
    expect(loaded?.failurePhase).toBe("register");
    expect(loaded?.error).toContain("plugin register must be synchronous");
  });

  it("applies memory slot gating to non-bundled CLI metadata loads", async () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "memory-external",
      filename: "memory-external.cjs",
      body: `module.exports = {
  id: "memory-external",
  kind: "memory",
  register(api) {
    api.registerCli(() => {}, {
      descriptors: [
        {
          name: "memory-external",
          description: "External memory CLI metadata",
          hasSubcommands: true,
        },
      ],
    });
  },
};`,
    });
    fs.writeFileSync(
      path.join(plugin.dir, "openclaw.plugin.json"),
      JSON.stringify(
        {
          id: "memory-external",
          kind: "memory",
          configSchema: EMPTY_PLUGIN_SCHEMA,
        },
        null,
        2,
      ),
      "utf-8",
    );

    const registry = await loadOpenClawPluginCliRegistry({
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["memory-external"],
          slots: { memory: "memory-other" },
        },
      },
    });

    expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).not.toContain(
      "memory-external",
    );
    const memory = registry.plugins.find((entry) => entry.id === "memory-external");
    expect(memory?.status).toBe("disabled");
    expect(memory?.error ?? "").toContain('memory slot set to "memory-other"');
  });

  it("re-evaluates memory slot gating after resolving exported plugin kind", async () => {
    useNoBundledPlugins();
    const plugin = writePlugin({
      id: "memory-export-only",
      filename: "memory-export-only.cjs",
      body: `module.exports = {
  id: "memory-export-only",
  kind: "memory",
  register(api) {
    api.registerCli(() => {}, {
      descriptors: [
        {
          name: "memory-export-only",
          description: "Export-only memory CLI metadata",
          hasSubcommands: true,
        },
      ],
    });
  },
};`,
    });

    const registry = await loadOpenClawPluginCliRegistry({
      config: {
        plugins: {
          load: { paths: [plugin.file] },
          allow: ["memory-export-only"],
          slots: { memory: "memory-other" },
        },
      },
    });

    expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).not.toContain(
      "memory-export-only",
    );
    const memory = registry.plugins.find((entry) => entry.id === "memory-export-only");
    expect(memory?.status).toBe("disabled");
    expect(memory?.error ?? "").toContain('memory slot set to "memory-other"');
  });
});
