import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { bundledDistPluginFile } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { PluginInstallRecord } from "../config/types.plugins.js";
import { discoverOpenClawPlugins } from "./discovery.js";
import { listBuiltRuntimeEntryCandidates } from "./package-entrypoints.js";
import {
  cleanupTrackedTempDirs,
  makeTrackedTempDir,
  mkdirSafeDir,
} from "./test-helpers/fs-fixtures.js";

vi.mock("./bundled-dir.js", async (importOriginal) => {
  const actual = await importOriginal<typeof import("./bundled-dir.js")>();
  return {
    ...actual,
    resolveBundledPluginsDir: (env: NodeJS.ProcessEnv = process.env) =>
      env.OPENCLAW_BUNDLED_PLUGINS_DIR ?? actual.resolveBundledPluginsDir(env),
  };
});

const tempDirs: string[] = [];

function makeTempDir() {
  return makeTrackedTempDir("openclaw-plugins", tempDirs);
}

const mkdirSafe = mkdirSafeDir;

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 withOpenClawPackageArgv<T>(packageRoot: string, fn: () => T): T {
  mkdirSafe(path.join(packageRoot, "bin"));
  fs.writeFileSync(path.join(packageRoot, "package.json"), '{"name":"openclaw"}\n', "utf-8");
  const originalArgv = process.argv;
  process.argv = [originalArgv[0] ?? "node", path.join(packageRoot, "bin", "openclaw")];
  try {
    return fn();
  } finally {
    process.argv = originalArgv;
  }
}

function symlinkDirectory(target: string, linkPath: string): void {
  fs.symlinkSync(target, linkPath, process.platform === "win32" ? "junction" : "dir");
}

const canCreateDirectorySymlinks = (() => {
  const probeDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-symlink-probe-"));
  const targetDir = path.join(probeDir, "target");
  const linkDir = path.join(probeDir, "link");
  try {
    fs.mkdirSync(targetDir);
    symlinkDirectory(targetDir, linkDir);
    return true;
  } catch {
    return false;
  } finally {
    fs.rmSync(probeDir, { recursive: true, force: true });
  }
})();

function normalizePathForAssertion(value: string | undefined): string | undefined {
  if (!value) {
    return value;
  }
  return value.replace(/\\/g, "/");
}

function hasDiagnosticSourceSuffix(
  diagnostics: Array<{ source?: string }>,
  suffix: string,
): boolean {
  const normalizedSuffix = normalizePathForAssertion(suffix);
  return diagnostics.some((entry) =>
    normalizePathForAssertion(entry.source)?.endsWith(normalizedSuffix ?? suffix),
  );
}

function buildDiscoveryEnv(stateDir: string): NodeJS.ProcessEnv {
  const bundledPluginsDir = path.join(stateDir, "empty-bundled-plugins");
  mkdirSafe(bundledPluginsDir);
  return {
    OPENCLAW_STATE_DIR: stateDir,
    OPENCLAW_HOME: undefined,
    OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
    OPENCLAW_BUNDLED_PLUGINS_DIR: bundledPluginsDir,
  };
}

function buildDiscoveryEnvWithOverrides(
  stateDir: string,
  overrides: Partial<NodeJS.ProcessEnv> = {},
): NodeJS.ProcessEnv {
  const enablesBundledOverride =
    Object.prototype.hasOwnProperty.call(overrides, "OPENCLAW_BUNDLED_PLUGINS_DIR") &&
    overrides.OPENCLAW_BUNDLED_PLUGINS_DIR !== undefined;
  return {
    ...buildDiscoveryEnv(stateDir),
    ...(enablesBundledOverride ? { OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined } : {}),
    ...overrides,
  };
}

function buildBundledDiscoveryEnv(stateDir: string): NodeJS.ProcessEnv {
  return {
    ...buildDiscoveryEnv(stateDir),
    OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined,
    OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
  };
}

async function discoverWithStateDir(
  stateDir: string,
  params: Parameters<typeof discoverOpenClawPlugins>[0],
) {
  return discoverOpenClawPlugins({ ...params, env: buildDiscoveryEnv(stateDir) });
}

function discoverWithEnv(params: Parameters<typeof discoverOpenClawPlugins>[0]) {
  return discoverOpenClawPlugins(params);
}

function writePluginPackageManifest(params: {
  packageDir: string;
  packageName: string;
  extensions: string[];
  runtimeExtensions?: string[];
  setupEntry?: string;
  runtimeSetupEntry?: string;
  compatPluginApi?: string;
}) {
  fs.writeFileSync(
    path.join(params.packageDir, "package.json"),
    JSON.stringify({
      name: params.packageName,
      openclaw: {
        extensions: params.extensions,
        ...(params.runtimeExtensions ? { runtimeExtensions: params.runtimeExtensions } : {}),
        ...(params.setupEntry ? { setupEntry: params.setupEntry } : {}),
        ...(params.runtimeSetupEntry ? { runtimeSetupEntry: params.runtimeSetupEntry } : {}),
        ...(params.compatPluginApi ? { compat: { pluginApi: params.compatPluginApi } } : {}),
      },
    }),
    "utf-8",
  );
}

function writePluginManifest(params: {
  pluginDir: string;
  id: string;
  requiresPlugins?: string[];
}) {
  fs.writeFileSync(
    path.join(params.pluginDir, "openclaw.plugin.json"),
    JSON.stringify({
      id: params.id,
      ...(params.requiresPlugins ? { requiresPlugins: params.requiresPlugins } : {}),
      configSchema: { type: "object" },
    }),
    "utf-8",
  );
}

function writePluginEntry(filePath: string) {
  fs.writeFileSync(filePath, "export default function () {}", "utf-8");
}

function writeStandalonePlugin(filePath: string, source = "export default function () {}") {
  mkdirSafe(path.dirname(filePath));
  fs.writeFileSync(filePath, source, "utf-8");
}

function mockLinuxMountInfo(mountPoints: readonly string[]) {
  const originalReadFileSync = fs.readFileSync;
  return vi.spyOn(fs, "readFileSync").mockImplementation((filePath, options) => {
    if (filePath === "/proc/self/mountinfo") {
      return mountPoints
        .map(
          (mountPoint, index) => `${100 + index} 99 0:${index} / ${mountPoint} rw - tmpfs tmpfs rw`,
        )
        .join("\n");
    }
    return originalReadFileSync(filePath, options as never) as never;
  });
}

function createPackagePlugin(params: {
  packageDir: string;
  packageName: string;
  extensions: string[];
  pluginId?: string;
  compatPluginApi?: string;
}) {
  mkdirSafe(params.packageDir);
  writePluginPackageManifest({
    packageDir: params.packageDir,
    packageName: params.packageName,
    extensions: params.extensions,
    ...(params.compatPluginApi ? { compatPluginApi: params.compatPluginApi } : {}),
  });
  if (params.pluginId) {
    writePluginManifest({ pluginDir: params.packageDir, id: params.pluginId });
  }
}

function createPackagePluginWithEntry(params: {
  packageDir: string;
  packageName: string;
  pluginId?: string;
  entryPath?: string;
  writeBuiltRuntime?: boolean;
  compatPluginApi?: string;
}) {
  const entryPath = params.entryPath ?? "src/index.ts";
  mkdirSafe(path.dirname(path.join(params.packageDir, entryPath)));
  createPackagePlugin({
    packageDir: params.packageDir,
    packageName: params.packageName,
    extensions: [`./${entryPath}`],
    ...(params.pluginId ? { pluginId: params.pluginId } : {}),
    ...(params.compatPluginApi ? { compatPluginApi: params.compatPluginApi } : {}),
  });
  writePluginEntry(path.join(params.packageDir, entryPath));
  if (params.writeBuiltRuntime ?? listBuiltRuntimeEntryCandidates(entryPath).length > 0) {
    const runtimeEntry = listBuiltRuntimeEntryCandidates(entryPath)[0];
    if (runtimeEntry) {
      const runtimePath = path.join(params.packageDir, runtimeEntry.replace(/^\.\//u, ""));
      mkdirSafe(path.dirname(runtimePath));
      writePluginEntry(runtimePath);
    }
  }
}

function createBundleRoot(bundleDir: string, markerPath: string, manifest?: unknown) {
  mkdirSafe(path.dirname(path.join(bundleDir, markerPath)));
  if (manifest) {
    fs.writeFileSync(path.join(bundleDir, markerPath), JSON.stringify(manifest), "utf-8");
    return;
  }
  mkdirSafe(path.join(bundleDir, markerPath));
}

function expectCandidateIds(
  candidates: Array<{ idHint: string }>,
  params: { includes?: readonly string[]; excludes?: readonly string[] },
) {
  const ids = candidates.map((candidate) => candidate.idHint);
  params.includes?.forEach((includedId) => {
    expect(ids).toContain(includedId);
  });
  params.excludes?.forEach((excludedId) => {
    expect(ids).not.toContain(excludedId);
  });
}

function findCandidateById<T extends { idHint?: string }>(candidates: T[], idHint: string) {
  return candidates.find((candidate) => candidate.idHint === idHint);
}

function requireCandidateById<T extends { idHint?: string }>(candidates: T[], idHint: string): T {
  const candidate = findCandidateById(candidates, idHint);
  if (!candidate) {
    throw new Error(`expected plugin candidate ${idHint}`);
  }
  return candidate;
}

function expectCandidateSource(
  candidates: Array<{ idHint?: string; source?: string }>,
  idHint: string,
  source: string,
) {
  const actualSource = findCandidateById(candidates, idHint)?.source;
  const normalizeSource = (value: string | undefined) =>
    value && fs.existsSync(value) ? fs.realpathSync(value) : value;
  expect(normalizeSource(actualSource)).toBe(normalizeSource(source));
}

function expectEscapesPackageDiagnostic(diagnostics: Array<{ message: string }>) {
  expect(diagnostics.some((entry) => entry.message.includes("escapes package directory"))).toBe(
    true,
  );
}

function expectEscapesPackageDiagnosticForPlugin(
  diagnostics: Array<{ message: string; pluginId?: string }>,
  pluginId: string,
) {
  expect(
    diagnostics.some(
      (entry) => entry.pluginId === pluginId && entry.message.includes("escapes package directory"),
    ),
  ).toBe(true);
}

function expectDiagnostic(params: {
  diagnostics: Array<{
    level?: string;
    message: string;
    pluginId?: string;
    source?: string;
  }>;
  messageIncludes: string;
  level?: string;
  pluginId?: string;
  source?: string;
}) {
  const matched = params.diagnostics.some(
    (diagnostic) =>
      diagnostic.message.includes(params.messageIncludes) &&
      (params.level === undefined || diagnostic.level === params.level) &&
      (params.pluginId === undefined || diagnostic.pluginId === params.pluginId) &&
      (params.source === undefined || diagnostic.source === params.source),
  );
  expect(matched).toBe(true);
}

function expectNoDiagnostic(params: {
  diagnostics: Array<{
    message: string;
    pluginId?: string;
    source?: string;
  }>;
  messageIncludes: string;
  pluginId?: string;
  source?: string;
}) {
  const matched = params.diagnostics.some(
    (diagnostic) =>
      diagnostic.message.includes(params.messageIncludes) &&
      (params.pluginId === undefined || diagnostic.pluginId === params.pluginId) &&
      (params.source === undefined || diagnostic.source === params.source),
  );
  expect(matched).toBe(false);
}

function expectCandidateFields(
  candidate:
    | {
        idHint?: string;
        format?: string;
        bundleFormat?: string;
        source?: string;
        rootDir?: string;
        origin?: string;
      }
    | undefined,
  expected: Record<string, unknown>,
) {
  if (!candidate) {
    throw new Error("Expected plugin candidate");
  }
  for (const [key, value] of Object.entries(expected)) {
    expect(candidate[key as keyof typeof candidate], key).toBe(value);
  }
}

function expectCandidatePresence(
  result: Awaited<ReturnType<typeof discoverOpenClawPlugins>>,
  params: { present?: readonly string[]; absent?: readonly string[] },
) {
  const ids = result.candidates.map((candidate) => candidate.idHint);
  params.present?.forEach((pluginId) => {
    expect(ids).toContain(pluginId);
  });
  params.absent?.forEach((pluginId) => {
    expect(ids).not.toContain(pluginId);
  });
}

function expectCandidateOrder(
  candidates: Array<{ idHint: string }>,
  expectedIds: readonly string[],
) {
  expect(candidates.map((candidate) => candidate.idHint)).toEqual(expectedIds);
}

function expectBundleCandidateMatch(params: {
  candidates: Array<{
    idHint?: string;
    format?: string;
    bundleFormat?: string;
    source?: string;
    rootDir?: string;
  }>;
  idHint: string;
  bundleFormat: string;
  source: string;
  expectRootDir?: boolean;
}) {
  const bundle = requireCandidateById(params.candidates, params.idHint);
  expectCandidateFields(bundle, {
    idHint: params.idHint,
    format: "bundle",
    bundleFormat: params.bundleFormat,
    source: params.source,
  });
  if (params.expectRootDir) {
    expect(normalizePathForAssertion(bundle?.rootDir)).toBe(
      normalizePathForAssertion(fs.realpathSync(params.source)),
    );
  }
}

async function expectRejectedPackageExtensionEntry(params: {
  stateDir: string;
  setup: (stateDir: string) => boolean | void;
  expectedDiagnostic?: "escapes" | "none" | "not_found" | "runtime";
  expectedId?: string;
  expectedDiagnosticPluginId?: string;
}) {
  if (params.setup(params.stateDir) === false) {
    return;
  }
  const result = await discoverWithStateDir(params.stateDir, {});

  if (params.expectedId) {
    expectCandidatePresence(result, { absent: [params.expectedId] });
  } else {
    expect(result.candidates).toHaveLength(0);
  }
  if (params.expectedDiagnostic === "escapes") {
    if (params.expectedDiagnosticPluginId) {
      expectEscapesPackageDiagnosticForPlugin(
        result.diagnostics,
        params.expectedDiagnosticPluginId,
      );
    } else {
      expectEscapesPackageDiagnostic(result.diagnostics);
    }
    return;
  }
  if (params.expectedDiagnostic === "runtime") {
    expect(
      result.diagnostics.some(
        (entry) => entry.level === "warn" && entry.message.includes("compiled runtime output"),
      ),
    ).toBe(true);
    return;
  }
  if (params.expectedDiagnostic === "not_found") {
    expect(
      result.diagnostics.some(
        (entry) => entry.level === "error" && entry.message.includes("extension entry not found"),
      ),
    ).toBe(true);
    return;
  }
  expect(result.diagnostics).toStrictEqual([]);
}

afterEach(() => {
  vi.restoreAllMocks();
  cleanupTrackedTempDirs(tempDirs);
});

describe("discoverOpenClawPlugins", () => {
  it("discovers global and workspace extensions", async () => {
    const stateDir = makeTempDir();
    const workspaceDir = path.join(stateDir, "workspace");

    createPackagePluginWithEntry({
      packageDir: path.join(stateDir, "extensions", "alpha"),
      packageName: "@openclaw/alpha",
      pluginId: "alpha",
    });
    createPackagePluginWithEntry({
      packageDir: path.join(workspaceDir, ".openclaw", "extensions", "beta"),
      packageName: "@openclaw/beta",
      pluginId: "beta",
    });

    const { candidates } = await discoverWithStateDir(stateDir, { workspaceDir });
    expectCandidateIds(candidates, { includes: ["alpha", "beta"] });
  });

  it("ignores standalone helper scripts in auto-discovered extension roots", async () => {
    const stateDir = makeTempDir();
    const workspaceDir = path.join(stateDir, "workspace");
    const globalExt = path.join(stateDir, "extensions");
    const workspaceExt = path.join(workspaceDir, ".openclaw", "extensions");
    mkdirSafe(globalExt);
    mkdirSafe(workspaceExt);
    fs.writeFileSync(path.join(globalExt, "my-helper.mjs"), "export default {}", "utf-8");
    fs.writeFileSync(path.join(workspaceExt, "workspace-helper.js"), "export default {}", "utf-8");

    const { candidates, diagnostics } = await discoverWithStateDir(stateDir, { workspaceDir });

    expectCandidateIds(candidates, { excludes: ["my-helper", "workspace-helper"] });
    expect(diagnostics).toStrictEqual([]);
  });

  it("warns without blocking when a plugin requires a missing plugin", async () => {
    const stateDir = makeTempDir();
    const pluginDir = path.join(stateDir, "extensions", "diffs-language-pack");
    createPackagePluginWithEntry({
      packageDir: pluginDir,
      packageName: "@openclaw/diffs-language-pack",
      pluginId: "diffs-language-pack",
    });
    writePluginManifest({
      pluginDir,
      id: "diffs-language-pack",
      requiresPlugins: ["diffs"],
    });

    const result = await discoverWithStateDir(stateDir, {});

    expectCandidatePresence(result, { present: ["diffs-language-pack"] });
    expectDiagnostic({
      diagnostics: result.diagnostics,
      level: "warn",
      pluginId: "diffs-language-pack",
      messageIncludes: 'requires plugin "diffs"',
    });
  });

  it("does not warn when a required plugin is discoverable", async () => {
    const stateDir = makeTempDir();
    const extensionsDir = path.join(stateDir, "extensions");
    const languagePackDir = path.join(extensionsDir, "diffs-language-pack");
    createPackagePluginWithEntry({
      packageDir: languagePackDir,
      packageName: "@openclaw/diffs-language-pack",
      pluginId: "diffs-language-pack",
    });
    writePluginManifest({
      pluginDir: languagePackDir,
      id: "diffs-language-pack",
      requiresPlugins: ["diffs"],
    });
    createPackagePluginWithEntry({
      packageDir: path.join(extensionsDir, "diffs"),
      packageName: "@openclaw/diffs",
      pluginId: "diffs",
    });

    const result = await discoverWithStateDir(stateDir, {});

    expectCandidatePresence(result, { present: ["diffs-language-pack", "diffs"] });
    expectNoDiagnostic({
      diagnostics: result.diagnostics,
      pluginId: "diffs-language-pack",
      messageIncludes: 'requires plugin "diffs"',
    });
  });

  it.skipIf(!canCreateDirectorySymlinks)(
    "discovers symlinked plugin directories in global roots",
    async () => {
      const stateDir = makeTempDir();
      const globalExt = path.join(stateDir, "extensions");
      mkdirSafe(globalExt);

      const linkedPluginDir = path.join(stateDir, "linked-plugin-src");
      createPackagePluginWithEntry({
        packageDir: linkedPluginDir,
        packageName: "@openclaw/linked-plugin",
        pluginId: "linked-plugin",
      });

      symlinkDirectory(linkedPluginDir, path.join(globalExt, "linked-plugin"));

      const { candidates, diagnostics } = await discoverWithStateDir(stateDir, {});
      expectCandidateIds(candidates, { includes: ["linked-plugin"] });
      expect(findCandidateById(candidates, "linked-plugin")?.rootDir).toBe(
        fs.realpathSync(linkedPluginDir),
      );
      expect(diagnostics).toStrictEqual([]);
    },
  );

  it.skipIf(!canCreateDirectorySymlinks)(
    "discovers symlinked plugin directories in workspace roots",
    async () => {
      const stateDir = makeTempDir();
      const workspaceDir = path.join(stateDir, "workspace");
      const workspaceExt = path.join(workspaceDir, ".openclaw", "extensions");
      mkdirSafe(workspaceExt);

      const linkedPluginDir = path.join(stateDir, "workspace-linked-plugin-src");
      createPackagePluginWithEntry({
        packageDir: linkedPluginDir,
        packageName: "@openclaw/workspace-linked-plugin",
        pluginId: "workspace-linked-plugin",
      });

      symlinkDirectory(linkedPluginDir, path.join(workspaceExt, "workspace-linked-plugin"));

      const { candidates, diagnostics } = await discoverWithStateDir(stateDir, { workspaceDir });
      expectCandidateIds(candidates, { includes: ["workspace-linked-plugin"] });
      expect(findCandidateById(candidates, "workspace-linked-plugin")?.rootDir).toBe(
        fs.realpathSync(linkedPluginDir),
      );
      expect(diagnostics).toStrictEqual([]);
    },
  );

  it.skipIf(process.platform === "win32" || !canCreateDirectorySymlinks)(
    "ignores broken symlinked plugin directories in scanned roots",
    async () => {
      const stateDir = makeTempDir();
      const globalExt = path.join(stateDir, "extensions");
      mkdirSafe(globalExt);

      symlinkDirectory(path.join(stateDir, "missing-plugin-src"), path.join(globalExt, "missing"));

      const { candidates, diagnostics } = await discoverWithStateDir(stateDir, {});
      expectCandidateIds(candidates, { excludes: ["missing"] });
      expect(diagnostics).toStrictEqual([]);
    },
  );

  it("does not recurse arbitrary workspace directories for plugin auto-discovery", () => {
    const stateDir = makeTempDir();
    const workspaceDir = path.join(stateDir, "workspace");
    const workspaceExt = path.join(workspaceDir, ".openclaw", "extensions");

    const expectedWorkspacePluginDir = path.join(workspaceExt, "workspace-plugin");
    createPackagePluginWithEntry({
      packageDir: expectedWorkspacePluginDir,
      packageName: "@openclaw/workspace-plugin",
      pluginId: "workspace-plugin",
    });

    const unrelatedWorkspaceDir = path.join(workspaceDir, "lobster-integrations", "bin");
    createPackagePluginWithEntry({
      packageDir: unrelatedWorkspaceDir,
      packageName: "@openclaw/stray-workspace-plugin",
    });

    const result = discoverOpenClawPlugins({
      workspaceDir,
      env: buildDiscoveryEnv(stateDir),
    });

    expectCandidatePresence(result, {
      present: ["workspace-plugin"],
      absent: ["stray-workspace-plugin"],
    });
    expect(result.diagnostics).toStrictEqual([]);
  });

  it("resolves tilde workspace dirs against the provided env", () => {
    const stateDir = makeTempDir();
    const homeDir = makeTempDir();
    const workspaceRoot = path.join(homeDir, "workspace");
    createPackagePluginWithEntry({
      packageDir: path.join(workspaceRoot, ".openclaw", "extensions", "tilde-workspace"),
      packageName: "@openclaw/tilde-workspace",
      pluginId: "tilde-workspace",
    });

    const result = discoverOpenClawPlugins({
      workspaceDir: "~/workspace",
      env: {
        ...buildDiscoveryEnv(stateDir),
        HOME: homeDir,
      },
    });

    expectCandidatePresence(result, { present: ["tilde-workspace"] });
  });

  it("ignores backup and disabled plugin directories in scanned roots", async () => {
    const stateDir = makeTempDir();
    const globalExt = path.join(stateDir, "extensions");
    mkdirSafe(globalExt);

    const backupDir = path.join(globalExt, "feishu.backup-20260222");
    mkdirSafe(backupDir);
    fs.writeFileSync(path.join(backupDir, "index.ts"), "export default function () {}", "utf-8");

    const disabledDir = path.join(globalExt, "telegram.disabled.20260222");
    mkdirSafe(disabledDir);
    fs.writeFileSync(path.join(disabledDir, "index.ts"), "export default function () {}", "utf-8");

    const bakDir = path.join(globalExt, "discord.bak");
    mkdirSafe(bakDir);
    fs.writeFileSync(path.join(bakDir, "index.ts"), "export default function () {}", "utf-8");

    const liveDir = path.join(globalExt, "live");
    mkdirSafe(liveDir);
    fs.writeFileSync(path.join(liveDir, "index.ts"), "export default function () {}", "utf-8");

    const { candidates } = await discoverWithStateDir(stateDir, {});
    expectCandidateIds(candidates, {
      includes: ["live"],
      excludes: ["feishu.backup-20260222", "telegram.disabled.20260222", "discord.bak"],
    });
  });

  it("does not warn about source checkout deps when bundled plugins are disabled", () => {
    const stateDir = makeTempDir();
    const packageRoot = makeTempDir();
    mkdirSafe(path.join(packageRoot, "src"));
    const extensionDir = path.join(packageRoot, "extensions", "twitch");
    mkdirSafe(extensionDir);
    fs.writeFileSync(path.join(packageRoot, ".git"), "gitdir: /tmp/fake.git\n", "utf-8");
    fs.writeFileSync(
      path.join(packageRoot, "pnpm-workspace.yaml"),
      "packages:\n  - .\n  - extensions/*\n",
      "utf-8",
    );
    fs.writeFileSync(
      path.join(extensionDir, "package.json"),
      '{"name":"@openclaw/twitch"}\n',
      "utf-8",
    );
    fs.writeFileSync(path.join(extensionDir, "openclaw.plugin.json"), '{"id":"twitch"}\n', "utf-8");

    const result = withOpenClawPackageArgv(packageRoot, () =>
      discoverOpenClawPlugins({ env: buildDiscoveryEnv(stateDir) }),
    );

    expect(result.diagnostics.map((entry) => entry.message).join("\n")).not.toContain(
      "pnpm install",
    );
  });

  it("does not treat repo-level live or test files as plugin entrypoints", () => {
    const stateDir = makeTempDir();
    const packageRoot = path.join(stateDir, "node_modules", "openclaw");
    const bundledDir = path.join(packageRoot, "dist", "extensions");
    mkdirSafe(bundledDir);

    writeStandalonePlugin(
      path.join(bundledDir, "video-generation-providers.live.test.ts"),
      "export default {}",
    );
    writeStandalonePlugin(
      path.join(bundledDir, "music-generation-providers.live.test.ts"),
      "export default {}",
    );
    createPackagePluginWithEntry({
      packageDir: path.join(bundledDir, "real-plugin"),
      packageName: "@openclaw/real-plugin",
      pluginId: "real-plugin",
    });

    const { candidates, diagnostics } = withOpenClawPackageArgv(packageRoot, () =>
      discoverOpenClawPlugins({
        env: {
          ...buildDiscoveryEnv(stateDir),
          OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined,
          OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir,
        },
      }),
    );

    expectCandidateOrder(candidates, ["real-plugin"]);
    expect(diagnostics).toStrictEqual([]);
  });

  it("ignores packaged bundled plugin paths in configured load paths", () => {
    const stateDir = makeTempDir();
    const packageRoot = path.join(stateDir, "node_modules", "openclaw");
    const bundledRoot = path.join(packageRoot, "dist", "extensions");
    const bundledPluginDir = path.join(bundledRoot, "feishu");
    mkdirSafe(bundledPluginDir);
    writePluginManifest({ pluginDir: bundledPluginDir, id: "feishu" });
    writePluginEntry(path.join(bundledPluginDir, "index.js"));

    const { candidates, diagnostics } = withOpenClawPackageArgv(packageRoot, () =>
      discoverOpenClawPlugins({
        extraPaths: [bundledPluginDir],
        env: {
          ...buildDiscoveryEnv(stateDir),
          OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined,
          OPENCLAW_BUNDLED_PLUGINS_DIR: bundledRoot,
        },
      }),
    );

    expectCandidateFields(findCandidateById(candidates, "feishu"), { origin: "bundled" });
    expect(countMatching(candidates, (candidate) => candidate.idHint === "feishu")).toBe(1);
    expect(diagnostics).toHaveLength(1);
    expectDiagnostic({
      diagnostics,
      level: "warn",
      source: bundledPluginDir,
      messageIncludes: "ignored plugins.load.paths entry",
    });
  });

  it("ignores legacy bundled plugin load paths that would shadow packaged bundled plugins", () => {
    const stateDir = makeTempDir();
    const packageRoot = path.join(stateDir, "node_modules", "openclaw");
    const bundledRoot = path.join(packageRoot, "dist-runtime", "extensions");
    const bundledPluginDir = path.join(bundledRoot, "telegram");
    const legacyPluginDir = path.join(packageRoot, "extensions", "telegram");
    mkdirSafe(bundledPluginDir);
    mkdirSafe(legacyPluginDir);
    mkdirSafe(path.join(packageRoot, "dist", "extensions"));
    writePluginManifest({ pluginDir: bundledPluginDir, id: "telegram" });
    writePluginManifest({ pluginDir: legacyPluginDir, id: "telegram" });
    writePluginEntry(path.join(bundledPluginDir, "index.js"));
    writePluginEntry(path.join(legacyPluginDir, "index.js"));

    const { candidates, diagnostics } = withOpenClawPackageArgv(packageRoot, () =>
      discoverOpenClawPlugins({
        extraPaths: [legacyPluginDir],
        env: {
          ...buildDiscoveryEnv(stateDir),
          OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined,
          OPENCLAW_BUNDLED_PLUGINS_DIR: bundledRoot,
        },
      }),
    );

    expectCandidateFields(findCandidateById(candidates, "telegram"), { origin: "bundled" });
    expect(countMatching(candidates, (candidate) => candidate.idHint === "telegram")).toBe(1);
    expect(diagnostics).toHaveLength(1);
    expectDiagnostic({
      diagnostics,
      level: "warn",
      source: legacyPluginDir,
      messageIncludes: "legacy bundled plugin directory",
    });
  });

  it("discovers bind-mounted bundled source overlays before packaged dist bundles", () => {
    const stateDir = makeTempDir();
    const packageRoot = path.join(stateDir, "node_modules", "openclaw");
    const bundledRoot = path.join(packageRoot, "dist", "extensions");
    const bundledPluginDir = path.join(bundledRoot, "synology-chat");
    const sourcePluginDir = path.join(packageRoot, "extensions", "synology-chat");
    createPackagePluginWithEntry({
      packageDir: bundledPluginDir,
      packageName: "@openclaw/synology-chat",
      pluginId: "synology-chat",
      entryPath: "index.js",
    });
    createPackagePluginWithEntry({
      packageDir: sourcePluginDir,
      packageName: "@openclaw/synology-chat",
      pluginId: "synology-chat",
    });
    mockLinuxMountInfo([sourcePluginDir]);
    const sourceEntryPath = path.join(sourcePluginDir, "src", "index.ts");
    const bundledEntryPath = path.join(bundledPluginDir, "index.js");

    const { candidates, diagnostics } = withOpenClawPackageArgv(packageRoot, () =>
      discoverOpenClawPlugins({
        env: {
          ...buildDiscoveryEnv(stateDir),
          OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined,
          OPENCLAW_BUNDLED_PLUGINS_DIR: bundledRoot,
        },
      }),
    );

    const synologyCandidates = candidates.filter(
      (candidate) => candidate.idHint === "synology-chat",
    );
    expect(synologyCandidates).toHaveLength(2);
    expectCandidateFields(synologyCandidates[0], {
      origin: "bundled",
      rootDir: fs.realpathSync(sourcePluginDir),
      source: fs.realpathSync(sourceEntryPath),
    });
    expectCandidateFields(synologyCandidates[1], {
      origin: "bundled",
      rootDir: fs.realpathSync(bundledPluginDir),
      source: fs.realpathSync(bundledEntryPath),
    });
    expect(diagnostics).toHaveLength(1);
    expectDiagnostic({
      diagnostics,
      level: "warn",
      source: sourcePluginDir,
      messageIncludes: "bind-mounted bundled plugin source overlay",
    });
  });

  it("keeps copied source plugin dirs inert when they are not mounted overlays", () => {
    const stateDir = makeTempDir();
    const packageRoot = path.join(stateDir, "node_modules", "openclaw");
    const bundledRoot = path.join(packageRoot, "dist", "extensions");
    const bundledPluginDir = path.join(bundledRoot, "synology-chat");
    const sourcePluginDir = path.join(packageRoot, "extensions", "synology-chat");
    createPackagePluginWithEntry({
      packageDir: bundledPluginDir,
      packageName: "@openclaw/synology-chat",
      pluginId: "synology-chat",
      entryPath: "index.js",
    });
    createPackagePluginWithEntry({
      packageDir: sourcePluginDir,
      packageName: "@openclaw/synology-chat",
      pluginId: "synology-chat",
    });
    mockLinuxMountInfo([]);
    const bundledEntryPath = path.join(bundledPluginDir, "index.js");

    const { candidates, diagnostics } = withOpenClawPackageArgv(packageRoot, () =>
      discoverOpenClawPlugins({
        env: {
          ...buildDiscoveryEnv(stateDir),
          OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined,
          OPENCLAW_BUNDLED_PLUGINS_DIR: bundledRoot,
        },
      }),
    );

    expectCandidateFields(findCandidateById(candidates, "synology-chat"), {
      origin: "bundled",
      rootDir: fs.realpathSync(bundledPluginDir),
      source: fs.realpathSync(bundledEntryPath),
    });
    expect(countMatching(candidates, (candidate) => candidate.idHint === "synology-chat")).toBe(1);
    expect(diagnostics).toStrictEqual([]);
  });

  it("loads package extension packs", async () => {
    const stateDir = makeTempDir();
    const globalExt = path.join(stateDir, "extensions", "pack");
    mkdirSafe(path.join(globalExt, "src"));
    mkdirSafe(path.join(globalExt, "dist"));

    writePluginPackageManifest({
      packageDir: globalExt,
      packageName: "pack",
      extensions: ["./src/one.ts", "./src/two.ts"],
    });
    writePluginEntry(path.join(globalExt, "src", "one.ts"));
    writePluginEntry(path.join(globalExt, "src", "two.ts"));
    writePluginEntry(path.join(globalExt, "dist", "one.js"));
    writePluginEntry(path.join(globalExt, "dist", "two.js"));

    const { candidates } = await discoverWithStateDir(stateDir, {});
    expectCandidateIds(candidates, { includes: ["pack/one", "pack/two"] });
  });

  it("discovers untracked global package plugins that point at TypeScript source", async () => {
    const stateDir = makeTempDir();
    const pluginDir = path.join(stateDir, "extensions", "local-source-pack");
    mkdirSafe(pluginDir);

    writePluginPackageManifest({
      packageDir: pluginDir,
      packageName: "@openclaw/local-source-pack",
      extensions: ["./index.ts"],
    });
    writePluginManifest({ pluginDir, id: "local-source-pack" });
    writePluginEntry(path.join(pluginDir, "index.ts"));

    const result = await discoverWithStateDir(stateDir, {});

    expectCandidateSource(
      result.candidates,
      "local-source-pack",
      fs.realpathSync(path.join(pluginDir, "index.ts")),
    );
    expectNoDiagnostic({
      diagnostics: result.diagnostics,
      pluginId: "local-source-pack",
      messageIncludes: "requires compiled runtime output",
    });
  });

  it("allows linked local install records to point at TypeScript source entries", async () => {
    const stateDir = makeTempDir();
    const pluginDir = path.join(stateDir, "extensions", "linked-source-pack");
    mkdirSafe(path.join(pluginDir, "src"));

    writePluginPackageManifest({
      packageDir: pluginDir,
      packageName: "@openclaw/linked-source-pack",
      extensions: ["./src/index.ts"],
      setupEntry: "./src/setup-entry.ts",
    });
    writePluginManifest({ pluginDir, id: "linked-source-pack" });
    writePluginEntry(path.join(pluginDir, "src", "index.ts"));
    writePluginEntry(path.join(pluginDir, "src", "setup-entry.ts"));

    const installRecords = {
      "linked-source-pack": {
        source: "path",
        installPath: pluginDir,
        sourcePath: pluginDir,
      },
    } satisfies Record<string, PluginInstallRecord>;
    const result = await discoverWithStateDir(stateDir, { installRecords });

    expectCandidateSource(
      result.candidates,
      "linked-source-pack",
      fs.realpathSync(path.join(pluginDir, "src", "index.ts")),
    );
    expectCandidateFields(requireCandidateById(result.candidates, "linked-source-pack"), {
      setupSource: fs.realpathSync(path.join(pluginDir, "src", "setup-entry.ts")),
    });
    expectNoDiagnostic({
      diagnostics: result.diagnostics,
      pluginId: "linked-source-pack",
      messageIncludes: "requires compiled runtime output",
    });
  });

  it("still requires compiled runtime output for tracked installed package plugins", async () => {
    const stateDir = makeTempDir();
    const pluginDir = path.join(stateDir, "extensions", "source-only-pack");
    mkdirSafe(path.join(pluginDir, "src"));

    writePluginPackageManifest({
      packageDir: pluginDir,
      packageName: "@openclaw/source-only-pack",
      extensions: ["./src/index.ts"],
    });
    writePluginEntry(path.join(pluginDir, "src", "index.ts"));

    const installRecords = {
      "source-only-pack": {
        source: "path",
        installPath: pluginDir,
      },
    } satisfies Record<string, PluginInstallRecord>;
    const result = await discoverWithStateDir(stateDir, { installRecords });

    expectCandidateIds(result.candidates, { excludes: ["source-only-pack"] });
    expect(
      result.diagnostics.some(
        (entry) =>
          entry.level === "warn" &&
          entry.pluginId === "source-only-pack" &&
          entry.message.includes("requires compiled runtime output") &&
          entry.message.includes("./dist/index.js") &&
          entry.message.includes("plugin packaging issue") &&
          entry.message.includes("disable/uninstall the plugin"),
      ),
    ).toBe(true);
    expect(
      result.diagnostics.some(
        (entry) =>
          entry.pluginId === "source-only-pack" && entry.message.includes("openclaw doctor --fix"),
      ),
    ).toBe(false);
    expect(result.diagnostics).toHaveLength(1);
  });

  it("treats install record sourcePath dirs as managed during global scans", async () => {
    const stateDir = makeTempDir();
    const sourceDir = path.join(stateDir, "extensions", "source-path-pack");
    const installDir = path.join(stateDir, "installed", "source-path-pack");
    mkdirSafe(path.join(sourceDir, "src"));
    mkdirSafe(installDir);

    writePluginPackageManifest({
      packageDir: sourceDir,
      packageName: "@openclaw/source-path-pack",
      extensions: ["./src/index.ts"],
    });
    writePluginEntry(path.join(sourceDir, "src", "index.ts"));

    const installRecords = {
      "source-path-pack": {
        source: "path",
        installPath: installDir,
        sourcePath: sourceDir,
      },
    } satisfies Record<string, PluginInstallRecord>;
    const result = await discoverWithStateDir(stateDir, { installRecords });

    expectCandidateIds(result.candidates, { excludes: ["source-path-pack"] });
    expectDiagnostic({
      diagnostics: result.diagnostics,
      level: "warn",
      pluginId: "source-path-pack",
      messageIncludes: "requires compiled runtime output",
      source: sourceDir,
    });
    expect(result.diagnostics).toHaveLength(1);
  });

  it.skipIf(!canCreateDirectorySymlinks)(
    "treats symlinked install record sourcePath dirs as managed during global scans",
    async () => {
      const stateDir = makeTempDir();
      const globalExt = path.join(stateDir, "extensions");
      const actualSourceDir = path.join(stateDir, "source-checkouts", "source-path-symlink-pack");
      const linkedSourceDir = path.join(globalExt, "source-path-symlink-pack");
      const installDir = path.join(stateDir, "installed", "source-path-symlink-pack");
      mkdirSafe(globalExt);
      mkdirSafe(path.join(actualSourceDir, "src"));
      mkdirSafe(installDir);

      writePluginPackageManifest({
        packageDir: actualSourceDir,
        packageName: "@openclaw/source-path-symlink-pack",
        extensions: ["./src/index.ts"],
      });
      writePluginEntry(path.join(actualSourceDir, "src", "index.ts"));
      symlinkDirectory(actualSourceDir, linkedSourceDir);

      const installRecords = {
        "source-path-symlink-pack": {
          source: "path",
          installPath: installDir,
          sourcePath: linkedSourceDir,
        },
      } satisfies Record<string, PluginInstallRecord>;
      const result = await discoverWithStateDir(stateDir, { installRecords });

      expectCandidateIds(result.candidates, { excludes: ["source-path-symlink-pack"] });
      expectDiagnostic({
        diagnostics: result.diagnostics,
        level: "warn",
        pluginId: "source-path-symlink-pack",
        messageIncludes: "requires compiled runtime output",
        source: linkedSourceDir,
      });
      expect(result.diagnostics).toHaveLength(1);
    },
  );

  it("discovers global plugin directories with package metadata but no package entries", async () => {
    const stateDir = makeTempDir();
    const pluginDir = path.join(stateDir, "extensions", "metadata-only-pack");
    mkdirSafe(pluginDir);
    fs.writeFileSync(
      path.join(pluginDir, "package.json"),
      JSON.stringify({ name: "@openclaw/metadata-only-pack", version: "0.0.1" }),
      "utf-8",
    );
    writePluginManifest({ pluginDir, id: "metadata-only-pack" });
    writePluginEntry(path.join(pluginDir, "index.ts"));

    const result = await discoverWithStateDir(stateDir, {});

    expectCandidateSource(
      result.candidates,
      "metadata-only-pack",
      fs.realpathSync(path.join(pluginDir, "index.ts")),
    );
  });

  it("warns on legacy npm declaration stubs without loading workspace node_modules", async () => {
    const stateDir = makeTempDir();
    const pluginDir = path.join(stateDir, "extensions", "guardrail-bridge");
    mkdirSafe(pluginDir);
    fs.writeFileSync(
      path.join(pluginDir, "openclaw.extension.json"),
      JSON.stringify({
        name: "guardrail-bridge",
        type: "npm",
        npmSpec: "@guardrail-bridge/guardrail-bridge@1.0.0",
      }),
      "utf-8",
    );

    const result = await discoverWithStateDir(stateDir, {});

    expectCandidateIds(result.candidates, { excludes: ["guardrail-bridge"] });
    expectDiagnostic({
      diagnostics: result.diagnostics,
      level: "warn",
      pluginId: "guardrail-bridge",
      source: path.join(pluginDir, "openclaw.extension.json"),
      messageIncludes: 'run "openclaw doctor --fix"',
    });
  });

  it("keeps explicit runtime extension entries strict for untracked global packages", async () => {
    const stateDir = makeTempDir();
    const pluginDir = path.join(stateDir, "extensions", "missing-runtime-pack");
    mkdirSafe(pluginDir);

    writePluginPackageManifest({
      packageDir: pluginDir,
      packageName: "@openclaw/missing-runtime-pack",
      extensions: ["./index.ts"],
      runtimeExtensions: ["./dist/index.js"],
    });
    writePluginManifest({ pluginDir, id: "missing-runtime-pack" });
    writePluginEntry(path.join(pluginDir, "index.ts"));

    const result = await discoverWithStateDir(stateDir, {});

    expectCandidateIds(result.candidates, { excludes: ["missing-runtime-pack"] });
    expectDiagnostic({
      diagnostics: result.diagnostics,
      level: "error",
      source: pluginDir,
      messageIncludes: "runtime extension entry not found: ./dist/index.js",
    });
  });

  it("lets a valid bundled plugin win when a managed package is source-only TypeScript", () => {
    const stateDir = makeTempDir();
    const bundledDir = path.join(stateDir, "bundled");
    const bundledPluginDir = path.join(bundledDir, "discord");
    const installedPluginDir = path.join(stateDir, "extensions", "discord");
    mkdirSafe(bundledPluginDir);
    mkdirSafe(path.join(installedPluginDir, "src"));

    writePluginPackageManifest({
      packageDir: bundledPluginDir,
      packageName: "@openclaw/discord",
      extensions: ["./index.js"],
    });
    writePluginManifest({ pluginDir: bundledPluginDir, id: "discord" });
    writePluginEntry(path.join(bundledPluginDir, "index.js"));

    writePluginPackageManifest({
      packageDir: installedPluginDir,
      packageName: "@openclaw/discord",
      extensions: ["./src/index.ts"],
    });
    writePluginManifest({ pluginDir: installedPluginDir, id: "discord" });
    writePluginEntry(path.join(installedPluginDir, "src", "index.ts"));

    const result = discoverOpenClawPlugins({
      env: buildDiscoveryEnvWithOverrides(stateDir, {
        OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir,
      }),
      installRecords: {
        discord: {
          source: "path",
          installPath: installedPluginDir,
        },
      },
    });

    const discordCandidates = result.candidates.filter(
      (candidate) => candidate.idHint === "discord",
    );
    expect(discordCandidates).toHaveLength(1);
    expectCandidateFields(discordCandidates[0], {
      origin: "bundled",
      source: fs.realpathSync(path.join(bundledPluginDir, "index.js")),
    });
    expect(
      result.diagnostics.some(
        (entry) =>
          entry.pluginId === "discord" &&
          entry.message.includes("requires compiled runtime output"),
      ),
    ).toBe(true);
  });

  it("reuses one filesystem realpath lookup per package root within a discovery run", () => {
    const stateDir = makeTempDir();
    const packageDir = path.join(stateDir, "extensions", "pack");
    mkdirSafe(path.join(packageDir, "src"));
    mkdirSafe(path.join(packageDir, "dist"));

    writePluginPackageManifest({
      packageDir,
      packageName: "pack",
      extensions: ["./src/one.ts", "./src/two.ts"],
    });
    writePluginEntry(path.join(packageDir, "src", "one.ts"));
    writePluginEntry(path.join(packageDir, "src", "two.ts"));
    writePluginEntry(path.join(packageDir, "dist", "one.js"));
    writePluginEntry(path.join(packageDir, "dist", "two.js"));

    const realpathSync = vi.spyOn(fs, "realpathSync");
    const { candidates } = discoverOpenClawPlugins({
      env: buildDiscoveryEnv(stateDir),
    });

    expectCandidateIds(candidates, { includes: ["pack/one", "pack/two"] });
    expect(
      realpathSync.mock.calls.filter(
        ([targetPath]) => path.resolve(String(targetPath)) === path.resolve(packageDir),
      ),
    ).toHaveLength(1);
  });

  it.skipIf(!canCreateDirectorySymlinks)(
    "reuses the canonical realpath cache entry for symlinked package roots",
    () => {
      const stateDir = makeTempDir();
      const realPackageDir = path.join(stateDir, "real-pack");
      mkdirSafe(path.join(realPackageDir, "src"));

      writePluginPackageManifest({
        packageDir: realPackageDir,
        packageName: "pack",
        extensions: ["./src/index.ts"],
      });
      writePluginEntry(path.join(realPackageDir, "src", "index.ts"));

      const linkedPackageDir = path.join(stateDir, "linked-pack");
      symlinkDirectory(realPackageDir, linkedPackageDir);
      const canonicalPackageDir = fs.realpathSync(realPackageDir);

      const realpathSync = vi.spyOn(fs, "realpathSync");
      const { candidates } = discoverOpenClawPlugins({
        extraPaths: [linkedPackageDir, canonicalPackageDir],
        env: buildDiscoveryEnv(stateDir),
      });

      expectCandidateIds(candidates, { includes: ["pack"] });
      expect(
        realpathSync.mock.calls.filter(([targetPath]) => {
          const resolved = path.resolve(String(targetPath));
          return (
            resolved === path.resolve(linkedPackageDir) ||
            resolved === path.resolve(canonicalPackageDir)
          );
        }),
      ).toHaveLength(1);
    },
  );

  it("uses explicit runtime extension entries for installed package plugins", async () => {
    const stateDir = makeTempDir();
    const pluginDir = path.join(stateDir, "extensions", "runtime-pack");
    mkdirSafe(path.join(pluginDir, "src"));
    mkdirSafe(path.join(pluginDir, "dist"));

    writePluginPackageManifest({
      packageDir: pluginDir,
      packageName: "@openclaw/runtime-pack",
      extensions: ["./src/index.ts"],
      runtimeExtensions: ["./dist/index.js"],
      setupEntry: "./src/setup-entry.ts",
      runtimeSetupEntry: "./dist/setup-entry.js",
    });
    writePluginEntry(path.join(pluginDir, "src", "index.ts"));
    writePluginEntry(path.join(pluginDir, "src", "setup-entry.ts"));
    writePluginEntry(path.join(pluginDir, "dist", "index.js"));
    writePluginEntry(path.join(pluginDir, "dist", "setup-entry.js"));

    const { candidates } = await discoverWithStateDir(stateDir, {});
    const candidate = findCandidateById(candidates, "runtime-pack");
    expect(fs.realpathSync(candidate?.source ?? "")).toBe(
      fs.realpathSync(path.join(pluginDir, "dist", "index.js")),
    );
    expect(fs.realpathSync(candidate?.setupSource ?? "")).toBe(
      fs.realpathSync(path.join(pluginDir, "dist", "setup-entry.js")),
    );
  });

  it("rejects missing explicit runtime setup entries for installed package plugins", async () => {
    const stateDir = makeTempDir();
    const pluginDir = path.join(stateDir, "extensions", "missing-runtime-setup-pack");
    mkdirSafe(path.join(pluginDir, "src"));
    mkdirSafe(path.join(pluginDir, "dist"));

    writePluginPackageManifest({
      packageDir: pluginDir,
      packageName: "@openclaw/missing-runtime-setup-pack",
      extensions: ["./dist/index.js"],
      setupEntry: "./src/setup-entry.ts",
      runtimeSetupEntry: "./dist/setup-entry.js",
    });
    writePluginEntry(path.join(pluginDir, "dist", "index.js"));
    writePluginEntry(path.join(pluginDir, "src", "setup-entry.ts"));

    const result = await discoverWithStateDir(stateDir, {});
    const candidate = requireCandidateById(result.candidates, "missing-runtime-setup-pack");

    expect(candidate.setupSource).toBeUndefined();
    expect(
      result.diagnostics.some(
        (entry) =>
          entry.level === "error" &&
          entry.message.includes("runtime setup entry not found") &&
          entry.message.includes("./dist/setup-entry.js"),
      ),
    ).toBe(true);
  });

  it("reports missing declared setup entries for package plugins", async () => {
    const stateDir = makeTempDir();
    const pluginDir = path.join(stateDir, "extensions", "missing-setup-pack");
    mkdirSafe(path.join(pluginDir, "dist"));

    writePluginPackageManifest({
      packageDir: pluginDir,
      packageName: "@openclaw/missing-setup-pack",
      extensions: ["./dist/index.js"],
      setupEntry: "./src/setup-entry.ts",
    });
    writePluginEntry(path.join(pluginDir, "dist", "index.js"));

    const result = await discoverWithStateDir(stateDir, {});
    const candidate = requireCandidateById(result.candidates, "missing-setup-pack");

    expect(candidate.setupSource).toBeUndefined();
    expectDiagnostic({
      diagnostics: result.diagnostics,
      level: "error",
      source: pluginDir,
      messageIncludes: "setup entry not found: src/setup-entry.ts",
    });
  });

  it("rejects package runtimeExtensions that do not match extension entries", async () => {
    const stateDir = makeTempDir();
    const pluginDir = path.join(stateDir, "extensions", "runtime-mismatch-pack");
    mkdirSafe(path.join(pluginDir, "src"));
    mkdirSafe(path.join(pluginDir, "dist"));

    writePluginPackageManifest({
      packageDir: pluginDir,
      packageName: "@openclaw/runtime-mismatch-pack",
      extensions: ["./src/one.ts", "./src/two.ts"],
      runtimeExtensions: ["./dist/one.js"],
    });
    writePluginEntry(path.join(pluginDir, "src", "one.ts"));
    writePluginEntry(path.join(pluginDir, "src", "two.ts"));
    writePluginEntry(path.join(pluginDir, "dist", "one.js"));

    const result = await discoverWithStateDir(stateDir, {});

    expectCandidatePresence(result, { absent: ["runtime-mismatch-pack"] });
    expect(
      result.diagnostics.some(
        (entry) =>
          entry.level === "error" &&
          entry.message.includes("runtimeExtensions length (1)") &&
          entry.message.includes("extensions length (2)"),
      ),
    ).toBe(true);
  });

  it("rejects blank package runtimeExtensions before falling back to inferred entries", async () => {
    const stateDir = makeTempDir();
    const pluginDir = path.join(stateDir, "extensions", "runtime-blank-pack");
    mkdirSafe(path.join(pluginDir, "src"));
    mkdirSafe(path.join(pluginDir, "dist"));

    writePluginPackageManifest({
      packageDir: pluginDir,
      packageName: "@openclaw/runtime-blank-pack",
      extensions: ["./src/index.ts"],
      runtimeExtensions: [" "],
    });
    writePluginEntry(path.join(pluginDir, "src", "index.ts"));
    writePluginEntry(path.join(pluginDir, "dist", "index.js"));

    const result = await discoverWithStateDir(stateDir, {});

    expectCandidatePresence(result, { absent: ["runtime-blank-pack"] });
    expect(
      result.diagnostics.some(
        (entry) =>
          entry.level === "error" &&
          entry.message.includes("openclaw.runtimeExtensions[0]") &&
          entry.message.includes("non-empty string"),
      ),
    ).toBe(true);
  });

  it("rejects blank package extensions instead of falling back to inferred entries", async () => {
    const stateDir = makeTempDir();
    const pluginDir = path.join(stateDir, "extensions", "extension-blank-pack");
    mkdirSafe(path.join(pluginDir, "dist"));

    writePluginPackageManifest({
      packageDir: pluginDir,
      packageName: "@openclaw/extension-blank-pack",
      extensions: ["./dist/index.js", " "],
    });
    writePluginEntry(path.join(pluginDir, "dist", "index.js"));

    const result = await discoverWithStateDir(stateDir, {});

    expectCandidatePresence(result, { absent: ["extension-blank-pack"] });
    expect(
      result.diagnostics.some(
        (entry) =>
          entry.level === "error" &&
          entry.message.includes("openclaw.extensions[1]") &&
          entry.message.includes("non-empty string"),
      ),
    ).toBe(true);
  });

  it("infers built dist entries for installed TypeScript package plugins", async () => {
    const stateDir = makeTempDir();
    const pluginDir = path.join(stateDir, "extensions", "built-peer-pack");
    mkdirSafe(path.join(pluginDir, "src"));
    mkdirSafe(path.join(pluginDir, "dist"));

    writePluginPackageManifest({
      packageDir: pluginDir,
      packageName: "@openclaw/built-peer-pack",
      extensions: ["src/index.ts"],
      setupEntry: "src/setup-entry.ts",
    });
    writePluginEntry(path.join(pluginDir, "src", "index.ts"));
    writePluginEntry(path.join(pluginDir, "src", "setup-entry.ts"));
    writePluginEntry(path.join(pluginDir, "src", "index.js"));
    writePluginEntry(path.join(pluginDir, "src", "setup-entry.js"));
    writePluginEntry(path.join(pluginDir, "dist", "index.js"));
    writePluginEntry(path.join(pluginDir, "dist", "setup-entry.js"));

    const { candidates } = await discoverWithStateDir(stateDir, {});
    const candidate = findCandidateById(candidates, "built-peer-pack");
    expect(fs.realpathSync(candidate?.source ?? "")).toBe(
      fs.realpathSync(path.join(pluginDir, "dist", "index.js")),
    );
    expect(fs.realpathSync(candidate?.setupSource ?? "")).toBe(
      fs.realpathSync(path.join(pluginDir, "dist", "setup-entry.js")),
    );
  });

  it("preserves nested entry paths when inferring installed dist entries", async () => {
    const stateDir = makeTempDir();
    const pluginDir = path.join(stateDir, "extensions", "nested-pack");
    mkdirSafe(path.join(pluginDir, "plugin"));
    mkdirSafe(path.join(pluginDir, "dist", "plugin"));

    writePluginPackageManifest({
      packageDir: pluginDir,
      packageName: "@openclaw/nested-pack",
      extensions: ["./plugin/index.ts"],
    });
    writePluginEntry(path.join(pluginDir, "plugin", "index.ts"));
    writePluginEntry(path.join(pluginDir, "dist", "plugin", "index.js"));

    const { candidates } = await discoverWithStateDir(stateDir, {});
    const candidate = findCandidateById(candidates, "nested-pack");
    expect(fs.realpathSync(candidate?.source ?? "")).toBe(
      fs.realpathSync(path.join(pluginDir, "dist", "plugin", "index.js")),
    );
  });

  it("keeps workspace package TypeScript entries unless runtime entries are explicit", () => {
    const stateDir = makeTempDir();
    const workspaceDir = path.join(stateDir, "workspace");
    const pluginDir = path.join(workspaceDir, ".openclaw", "extensions", "workspace-pack");
    mkdirSafe(path.join(pluginDir, "src"));
    mkdirSafe(path.join(pluginDir, "dist"));

    writePluginPackageManifest({
      packageDir: pluginDir,
      packageName: "@openclaw/workspace-pack",
      extensions: ["./src/index.ts"],
    });
    writePluginEntry(path.join(pluginDir, "src", "index.ts"));
    writePluginEntry(path.join(pluginDir, "dist", "index.js"));

    const { candidates } = discoverOpenClawPlugins({
      workspaceDir,
      env: buildDiscoveryEnv(stateDir),
    });
    expect(fs.realpathSync(findCandidateById(candidates, "workspace-pack")?.source ?? "")).toBe(
      fs.realpathSync(path.join(pluginDir, "src", "index.ts")),
    );
  });

  it("skips incompatible non-bundled package plugin API candidates during discovery", () => {
    const stateDir = makeTempDir();
    const globalExt = path.join(stateDir, "extensions");
    const pluginDir = path.join(globalExt, "future-channel");
    createPackagePluginWithEntry({
      packageDir: pluginDir,
      packageName: "@openclaw/future-channel",
      pluginId: "future-channel",
      compatPluginApi: ">=2026.5.27-beta.2",
    });

    const { candidates, diagnostics } = discoverOpenClawPlugins({
      env: buildDiscoveryEnvWithOverrides(stateDir, {
        OPENCLAW_COMPATIBILITY_HOST_VERSION: "2026.5.27-beta.1",
      }),
    });

    expectCandidateIds(candidates, { excludes: ["future-channel"] });
    expectDiagnostic({
      diagnostics,
      level: "warn",
      pluginId: "future-channel",
      source: path.join(pluginDir, "package.json"),
      messageIncludes:
        "plugin requires plugin API >=2026.5.27-beta.2, but this host is 2026.5.27-beta.1; skipping discovery",
    });
  });

  it("skips malformed non-bundled package plugin API candidates during discovery", () => {
    const stateDir = makeTempDir();
    const globalExt = path.join(stateDir, "extensions");
    const pluginDir = path.join(globalExt, "malformed-channel");
    mkdirSafe(pluginDir);
    fs.writeFileSync(
      path.join(pluginDir, "package.json"),
      JSON.stringify({
        name: "@openclaw/malformed-channel",
        openclaw: {
          extensions: ["./index.js"],
          plugin: { id: "malformed-channel" },
          compat: { pluginApi: 20260527 },
        },
      }),
      "utf-8",
    );
    writePluginEntry(path.join(pluginDir, "index.js"));

    const { candidates, diagnostics } = discoverOpenClawPlugins({
      env: buildDiscoveryEnvWithOverrides(stateDir, {
        OPENCLAW_COMPATIBILITY_HOST_VERSION: "2026.5.27",
      }),
    });

    expectCandidateIds(candidates, { excludes: ["malformed-channel"] });
    expectDiagnostic({
      diagnostics,
      level: "warn",
      pluginId: "malformed-channel",
      source: path.join(pluginDir, "package.json"),
      messageIncludes:
        "invalid package plugin API metadata: package.json openclaw.compat.pluginApi must be a string; skipping discovery",
    });
  });

  it("checks non-bundled package plugin API before package entry validation", () => {
    const stateDir = makeTempDir();
    const globalExt = path.join(stateDir, "extensions");
    const pluginDir = path.join(globalExt, "future-shape");
    mkdirSafe(pluginDir);
    fs.writeFileSync(
      path.join(pluginDir, "package.json"),
      JSON.stringify({
        name: "@openclaw/future-shape",
        openclaw: {
          extensions: { runtime: "./src/index.ts" },
          compat: { pluginApi: ">=2026.5.27-beta.2" },
        },
      }),
      "utf-8",
    );

    const { candidates, diagnostics } = discoverOpenClawPlugins({
      env: buildDiscoveryEnvWithOverrides(stateDir, {
        OPENCLAW_COMPATIBILITY_HOST_VERSION: "2026.5.27-beta.1",
      }),
    });

    expectCandidateIds(candidates, { excludes: ["future-shape"] });
    expectDiagnostic({
      diagnostics,
      level: "warn",
      pluginId: "future-shape",
      source: path.join(pluginDir, "package.json"),
      messageIncludes:
        "plugin requires plugin API >=2026.5.27-beta.2, but this host is 2026.5.27-beta.1; skipping discovery",
    });
    expectNoDiagnostic({ diagnostics, messageIncludes: "openclaw.extensions" });
  });

  it("discovers same-floor beta non-bundled package plugin API candidates", () => {
    const stateDir = makeTempDir();
    const globalExt = path.join(stateDir, "extensions");
    createPackagePluginWithEntry({
      packageDir: path.join(globalExt, "current-channel"),
      packageName: "@openclaw/current-channel",
      pluginId: "current-channel",
      compatPluginApi: ">=2026.5.27-beta.1",
    });

    const { candidates, diagnostics } = discoverOpenClawPlugins({
      env: buildDiscoveryEnvWithOverrides(stateDir, {
        OPENCLAW_COMPATIBILITY_HOST_VERSION: "2026.5.27-beta.1",
      }),
    });

    expectCandidateIds(candidates, { includes: ["current-channel"] });
    expect(diagnostics).toStrictEqual([]);
  });

  it("discovers present bundled package plugins without package metadata gates", () => {
    const stateDir = makeTempDir();
    const bundledDir = path.join(stateDir, "bundled");
    const pluginDir = path.join(bundledDir, "downloadable");
    mkdirSafe(pluginDir);
    fs.writeFileSync(
      path.join(pluginDir, "package.json"),
      JSON.stringify({
        name: "@openclaw/downloadable",
        openclaw: {
          extensions: ["./index.ts"],
          compat: { pluginApi: ">=2099.1.1" },
        },
      }),
      "utf-8",
    );
    writePluginManifest({ pluginDir, id: "downloadable" });
    writePluginEntry(path.join(pluginDir, "index.ts"));

    const { candidates } = discoverOpenClawPlugins({
      env: buildDiscoveryEnvWithOverrides(stateDir, {
        OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir,
      }),
    });

    expectCandidateIds(candidates, { includes: ["downloadable"] });
  });

  it("keeps trusted bundled package fallback from missing TypeScript metadata to JavaScript", () => {
    const stateDir = makeTempDir();
    const bundledDir = path.join(stateDir, "bundled");
    const pluginDir = path.join(bundledDir, "downloadable");
    mkdirSafe(pluginDir);
    fs.writeFileSync(
      path.join(pluginDir, "package.json"),
      JSON.stringify({
        name: "@openclaw/downloadable",
        openclaw: {
          extensions: ["./index.ts"],
        },
      }),
      "utf-8",
    );
    writePluginManifest({ pluginDir, id: "downloadable" });
    writePluginEntry(path.join(pluginDir, "index.js"));

    const { candidates, diagnostics } = discoverOpenClawPlugins({
      env: buildDiscoveryEnvWithOverrides(stateDir, {
        OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir,
      }),
    });

    expectCandidateSource(
      candidates,
      "downloadable",
      fs.realpathSync(path.join(pluginDir, "index.js")),
    );
    expect(diagnostics).toStrictEqual([]);
  });

  it("discovers source-checkout-only bundled plugins alongside built bundled plugins", () => {
    const stateDir = makeTempDir();
    const packageRoot = path.join(stateDir, "openclaw");
    const bundledDir = path.join(packageRoot, "dist", "extensions");
    const sourceDir = path.join(packageRoot, "extensions");
    const builtPluginDir = path.join(bundledDir, "shipped");
    const sourceBuiltPluginDir = path.join(sourceDir, "shipped");
    const sourceOnlyPluginDir = path.join(sourceDir, "downloadable");
    mkdirSafe(path.join(packageRoot, "src"));
    mkdirSafe(builtPluginDir);
    mkdirSafe(sourceBuiltPluginDir);
    mkdirSafe(sourceOnlyPluginDir);
    fs.writeFileSync(path.join(packageRoot, ".git"), "gitdir: /tmp/fake.git\n", "utf-8");
    fs.writeFileSync(path.join(packageRoot, "pnpm-workspace.yaml"), "packages: []\n", "utf-8");

    writePluginPackageManifest({
      packageDir: builtPluginDir,
      packageName: "@openclaw/shipped",
      extensions: ["./index.js"],
    });
    writePluginManifest({ pluginDir: builtPluginDir, id: "shipped" });
    writePluginEntry(path.join(builtPluginDir, "index.js"));
    writePluginPackageManifest({
      packageDir: sourceBuiltPluginDir,
      packageName: "@openclaw/shipped",
      extensions: ["./index.ts"],
    });
    writePluginManifest({ pluginDir: sourceBuiltPluginDir, id: "shipped" });
    writePluginEntry(path.join(sourceBuiltPluginDir, "index.ts"));
    fs.writeFileSync(
      path.join(sourceOnlyPluginDir, "package.json"),
      JSON.stringify({
        name: "@openclaw/downloadable",
        openclaw: {
          extensions: ["./index.ts"],
        },
      }),
      "utf-8",
    );
    writePluginManifest({ pluginDir: sourceOnlyPluginDir, id: "downloadable" });
    writePluginEntry(path.join(sourceOnlyPluginDir, "index.ts"));

    const { candidates } = discoverOpenClawPlugins({
      env: buildDiscoveryEnvWithOverrides(stateDir, {
        OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir,
      }),
    });

    expectCandidateIds(candidates, { includes: ["shipped", "downloadable"] });
    expect(fs.realpathSync(findCandidateById(candidates, "shipped")?.source ?? "")).toBe(
      fs.realpathSync(path.join(builtPluginDir, "index.js")),
    );
    expect(fs.realpathSync(findCandidateById(candidates, "downloadable")?.source ?? "")).toBe(
      fs.realpathSync(path.join(sourceOnlyPluginDir, "index.ts")),
    );
  });

  it("does not discover nested node_modules copies under installed plugins", async () => {
    const stateDir = makeTempDir();
    const pluginDir = path.join(stateDir, "extensions", "opik-openclaw");
    const nestedDiffsDir = path.join(
      pluginDir,
      "node_modules",
      "openclaw",
      "dist",
      "extensions",
      "diffs",
    );
    mkdirSafe(path.join(pluginDir, "src"));
    mkdirSafe(path.join(pluginDir, "dist"));
    mkdirSafe(nestedDiffsDir);

    writePluginPackageManifest({
      packageDir: pluginDir,
      packageName: "@opik/opik-openclaw",
      extensions: ["./src/index.ts"],
    });
    writePluginManifest({ pluginDir, id: "opik-openclaw" });
    fs.writeFileSync(
      path.join(pluginDir, "src", "index.ts"),
      "export default function () {}",
      "utf-8",
    );
    fs.writeFileSync(
      path.join(pluginDir, "dist", "index.js"),
      "export default function () {}",
      "utf-8",
    );

    writePluginPackageManifest({
      packageDir: path.join(pluginDir, "node_modules", "openclaw"),
      packageName: "openclaw",
      extensions: [`./${bundledDistPluginFile("diffs", "index.js")}`],
    });
    writePluginManifest({ pluginDir: nestedDiffsDir, id: "diffs" });
    fs.writeFileSync(
      path.join(nestedDiffsDir, "index.js"),
      "module.exports = { id: 'diffs', register() {} };",
      "utf-8",
    );

    const { candidates } = await discoverWithStateDir(stateDir, {});
    expectCandidateOrder(candidates, ["opik-openclaw"]);
  });

  it("skips dependency and build directories while scanning workspace roots", () => {
    const stateDir = makeTempDir();
    const workspaceDir = path.join(stateDir, "workspace");
    const workspaceRoot = path.join(workspaceDir, ".openclaw", "extensions");
    const workspacePluginDir = path.join(workspaceRoot, "workspace-plugin");
    const nestedNodeModulesDir = path.join(workspaceRoot, "node_modules", "openclaw");
    const nestedDistDir = path.join(workspaceRoot, "dist", "extensions", "diffs");
    mkdirSafe(path.join(workspacePluginDir, "src"));
    mkdirSafe(path.join(nestedNodeModulesDir, "src"));
    mkdirSafe(nestedDistDir);

    createPackagePluginWithEntry({
      packageDir: workspacePluginDir,
      packageName: "@openclaw/workspace-plugin",
      pluginId: "workspace-plugin",
    });

    createPackagePluginWithEntry({
      packageDir: nestedNodeModulesDir,
      packageName: "openclaw",
      pluginId: "node-modules-copy",
    });

    writePluginManifest({ pluginDir: nestedDistDir, id: "dist-copy" });
    fs.writeFileSync(
      path.join(nestedDistDir, "index.js"),
      "module.exports = { id: 'dist-copy', register() {} };",
      "utf-8",
    );

    const { candidates } = discoverOpenClawPlugins({
      workspaceDir,
      env: buildDiscoveryEnv(stateDir),
    });

    expectCandidateOrder(candidates, ["workspace-plugin"]);
  });

  it.each([
    {
      name: "derives unscoped ids for scoped packages",
      setup: (stateDir: string) => {
        const packageDir = path.join(stateDir, "extensions", "voice-call-pack");
        createPackagePluginWithEntry({
          packageDir,
          packageName: "@openclaw/voice-call",
          entryPath: "src/index.ts",
        });
        return {};
      },
      includes: ["voice-call"],
    },
    {
      name: "strips provider suffixes from package-derived ids",
      setup: (stateDir: string) => {
        const packageDir = path.join(stateDir, "extensions", "local-provider-pack");
        createPackagePluginWithEntry({
          packageDir,
          packageName: "@example/local-provider",
          pluginId: "local",
          entryPath: "src/index.ts",
        });
        return {};
      },
      includes: ["local"],
      excludes: ["local-provider"],
    },
    {
      name: "normalizes bundled speech package ids to canonical plugin ids",
      setup: (stateDir: string) => {
        for (const [dirName, packageName, pluginId] of [
          ["elevenlabs-speech-pack", "@openclaw/elevenlabs-speech", "elevenlabs"],
          ["microsoft-speech-pack", "@openclaw/microsoft-speech", "microsoft"],
        ] as const) {
          const packageDir = path.join(stateDir, "extensions", dirName);
          createPackagePluginWithEntry({
            packageDir,
            packageName,
            pluginId,
            entryPath: "src/index.ts",
          });
        }
        return {};
      },
      includes: ["elevenlabs", "microsoft"],
      excludes: ["elevenlabs-speech", "microsoft-speech"],
    },
    {
      name: "treats configured directory paths as plugin packages",
      setup: (stateDir: string) => {
        const packageDir = path.join(stateDir, "packs", "demo-plugin-dir");
        createPackagePluginWithEntry({
          packageDir,
          packageName: "@openclaw/demo-plugin-dir",
          entryPath: "index.js",
        });
        return { extraPaths: [packageDir] };
      },
      includes: ["demo-plugin-dir"],
    },
  ] as const)("$name", async ({ setup, includes, excludes }) => {
    const stateDir = makeTempDir();
    const discoverParams = setup(stateDir);
    const { candidates } = await discoverWithStateDir(stateDir, discoverParams);
    expectCandidateIds(candidates, { includes, excludes });
  });

  it.each([
    {
      name: "auto-detects Codex bundles as bundle candidates",
      idHint: "sample-bundle",
      bundleFormat: "codex",
      setup: (stateDir: string) => {
        const bundleDir = path.join(stateDir, "extensions", "sample-bundle");
        createBundleRoot(bundleDir, ".codex-plugin/plugin.json", {
          name: "Sample Bundle",
          skills: "skills",
        });
        mkdirSafe(path.join(bundleDir, "skills"));
        return bundleDir;
      },
      expectRootDir: true,
    },
    {
      name: "auto-detects manifestless Claude bundles from the default layout",
      idHint: "claude-bundle",
      bundleFormat: "claude",
      setup: (stateDir: string) => {
        const bundleDir = path.join(stateDir, "extensions", "claude-bundle");
        mkdirSafe(path.join(bundleDir, "commands"));
        fs.writeFileSync(
          path.join(bundleDir, "settings.json"),
          '{"hideThinkingBlock":true}',
          "utf-8",
        );
        return bundleDir;
      },
    },
    {
      name: "auto-detects Cursor bundles as bundle candidates",
      idHint: "cursor-bundle",
      bundleFormat: "cursor",
      setup: (stateDir: string) => {
        const bundleDir = path.join(stateDir, "extensions", "cursor-bundle");
        createBundleRoot(bundleDir, ".cursor-plugin/plugin.json", {
          name: "Cursor Bundle",
        });
        mkdirSafe(path.join(bundleDir, ".cursor", "commands"));
        return bundleDir;
      },
    },
  ] as const)("$name", async ({ idHint, bundleFormat, setup, expectRootDir }) => {
    const stateDir = makeTempDir();
    const bundleDir = setup(stateDir);
    const { candidates } = await discoverWithStateDir(stateDir, {});

    expectBundleCandidateMatch({
      candidates,
      idHint,
      bundleFormat,
      source: bundleDir,
      expectRootDir,
    });
  });

  it.each([
    {
      name: "falls back to legacy index discovery when a scanned bundle sidecar is malformed",
      bundleMarker: ".claude-plugin/plugin.json",
      setup: (stateDir: string) => {
        const pluginDir = path.join(stateDir, "extensions", "legacy-with-bad-bundle");
        mkdirSafe(path.dirname(path.join(pluginDir, ".claude-plugin", "plugin.json")));
        fs.writeFileSync(path.join(pluginDir, "index.ts"), "export default {}", "utf-8");
        fs.writeFileSync(path.join(pluginDir, ".claude-plugin", "plugin.json"), "{", "utf-8");
        return {};
      },
    },
    {
      name: "falls back to legacy index discovery for configured paths with malformed bundle sidecars",
      bundleMarker: ".codex-plugin/plugin.json",
      setup: (stateDir: string) => {
        const pluginDir = path.join(stateDir, "plugins", "legacy-with-bad-bundle");
        mkdirSafe(path.dirname(path.join(pluginDir, ".codex-plugin", "plugin.json")));
        fs.writeFileSync(path.join(pluginDir, "index.ts"), "export default {}", "utf-8");
        fs.writeFileSync(path.join(pluginDir, ".codex-plugin", "plugin.json"), "{", "utf-8");
        return { extraPaths: [pluginDir] };
      },
    },
  ] as const)("$name", async ({ setup, bundleMarker }) => {
    const stateDir = makeTempDir();
    const result = await discoverWithStateDir(stateDir, setup(stateDir));
    const legacy = findCandidateById(result.candidates, "legacy-with-bad-bundle");

    expect(legacy?.format).toBe("openclaw");
    expect(hasDiagnosticSourceSuffix(result.diagnostics, bundleMarker)).toBe(true);
  });

  it.each([
    {
      name: "blocks extension entries that escape package directory",
      expectedDiagnostic: "escapes" as const,
      setup: (stateDir: string) => {
        const globalExt = path.join(stateDir, "extensions", "escape-pack");
        const outside = path.join(stateDir, "outside.js");
        mkdirSafe(globalExt);
        writePluginPackageManifest({
          packageDir: globalExt,
          packageName: "@openclaw/escape-pack",
          extensions: ["../../outside.js"],
        });
        fs.writeFileSync(outside, "export default function () {}", "utf-8");
      },
    },
    {
      name: "blocks parent-segment TypeScript entries before built runtime inference",
      expectedDiagnostic: "escapes" as const,
      setup: (stateDir: string) => {
        const globalExt = path.join(stateDir, "extensions", "escape-pack");
        mkdirSafe(path.join(globalExt, "src"));
        writePluginPackageManifest({
          packageDir: globalExt,
          packageName: "@openclaw/escape-pack",
          extensions: ["../src/index.ts"],
        });
        fs.writeFileSync(path.join(globalExt, "src", "index.js"), "export default {}", "utf-8");
      },
    },
    {
      name: "blocks escaping source entries before explicit runtime entries",
      expectedDiagnostic: "escapes" as const,
      setup: (stateDir: string) => {
        const globalExt = path.join(stateDir, "extensions", "escape-pack");
        mkdirSafe(path.join(globalExt, "dist"));
        writePluginPackageManifest({
          packageDir: globalExt,
          packageName: "@openclaw/escape-pack",
          extensions: ["../src/index.ts"],
          runtimeExtensions: ["./dist/index.js"],
        });
        fs.writeFileSync(path.join(globalExt, "dist", "index.js"), "export default {}", "utf-8");
      },
    },
    {
      name: "rejects missing TypeScript package runtime entries without escape diagnostics",
      expectedDiagnostic: "not_found" as const,
      setup: (stateDir: string) => {
        const globalExt = path.join(stateDir, "extensions", "missing-entry-pack");
        mkdirSafe(globalExt);
        writePluginPackageManifest({
          packageDir: globalExt,
          packageName: "@openclaw/missing-entry-pack",
          extensions: ["./missing.ts"],
        });
        return true;
      },
    },
    {
      name: "rejects package extension entries that escape via symlink",
      expectedDiagnostic: "escapes" as const,
      expectedId: "pack",
      setup: (stateDir: string) => {
        const globalExt = path.join(stateDir, "extensions", "pack");
        const outsideDir = path.join(stateDir, "outside");
        const linkedDir = path.join(globalExt, "linked");
        mkdirSafe(globalExt);
        mkdirSafe(outsideDir);
        fs.writeFileSync(path.join(outsideDir, "escape.ts"), "export default {}", "utf-8");
        try {
          symlinkDirectory(outsideDir, linkedDir);
        } catch {
          return false;
        }
        writePluginPackageManifest({
          packageDir: globalExt,
          packageName: "@openclaw/pack",
          extensions: ["./linked/escape.ts"],
        });
        return true;
      },
    },
    {
      name: "rejects package extension entries that are hardlinked aliases",
      expectedDiagnostic: "escapes" as const,
      expectedId: "pack",
      expectedDiagnosticPluginId: "pack",
      setup: (stateDir: string) => {
        if (process.platform === "win32") {
          return false;
        }
        const globalExt = path.join(stateDir, "extensions", "pack");
        const outsideDir = path.join(stateDir, "outside");
        const outsideFile = path.join(outsideDir, "escape.ts");
        const linkedFile = path.join(globalExt, "escape.ts");
        mkdirSafe(globalExt);
        mkdirSafe(outsideDir);
        fs.writeFileSync(outsideFile, "export default {}", "utf-8");
        try {
          fs.linkSync(outsideFile, linkedFile);
        } catch (err) {
          if ((err as NodeJS.ErrnoException).code === "EXDEV") {
            return false;
          }
          throw err;
        }
        writePluginPackageManifest({
          packageDir: globalExt,
          packageName: "@openclaw/pack",
          extensions: ["./escape.ts"],
        });
        return true;
      },
    },
    {
      name: "rejects hardlinked TypeScript entries before built runtime inference",
      expectedDiagnostic: "escapes" as const,
      expectedId: "pack",
      setup: (stateDir: string) => {
        if (process.platform === "win32") {
          return false;
        }
        const globalExt = path.join(stateDir, "extensions", "pack");
        const outsideDir = path.join(stateDir, "outside");
        const outsideFile = path.join(outsideDir, "escape.ts");
        const linkedFile = path.join(globalExt, "escape.ts");
        mkdirSafe(path.join(globalExt, "dist"));
        mkdirSafe(outsideDir);
        fs.writeFileSync(outsideFile, "export default {}", "utf-8");
        fs.writeFileSync(path.join(globalExt, "dist", "escape.js"), "export default {}", "utf-8");
        try {
          fs.linkSync(outsideFile, linkedFile);
        } catch (err) {
          if ((err as NodeJS.ErrnoException).code === "EXDEV") {
            return false;
          }
          throw err;
        }
        writePluginPackageManifest({
          packageDir: globalExt,
          packageName: "@openclaw/pack",
          extensions: ["./escape.ts"],
        });
        return true;
      },
    },
    {
      name: "rejects hardlinked inferred built runtime entries instead of falling back to source",
      expectedDiagnostic: "escapes" as const,
      expectedId: "pack",
      setup: (stateDir: string) => {
        if (process.platform === "win32") {
          return false;
        }
        const globalExt = path.join(stateDir, "extensions", "pack");
        const outsideDir = path.join(stateDir, "outside");
        const outsideFile = path.join(outsideDir, "index.js");
        const linkedFile = path.join(globalExt, "dist", "index.js");
        mkdirSafe(path.join(globalExt, "src"));
        mkdirSafe(path.dirname(linkedFile));
        mkdirSafe(outsideDir);
        fs.writeFileSync(path.join(globalExt, "src", "index.ts"), "export default {}", "utf-8");
        fs.writeFileSync(outsideFile, "export default {}", "utf-8");
        try {
          fs.linkSync(outsideFile, linkedFile);
        } catch (err) {
          if ((err as NodeJS.ErrnoException).code === "EXDEV") {
            return false;
          }
          throw err;
        }
        writePluginPackageManifest({
          packageDir: globalExt,
          packageName: "@openclaw/pack",
          extensions: ["./src/index.ts"],
        });
        return true;
      },
    },
  ] as const)("$name", async ({ setup, expectedDiagnostic, expectedId }) => {
    const stateDir = makeTempDir();
    await expectRejectedPackageExtensionEntry({
      stateDir,
      setup,
      expectedDiagnostic,
      ...(expectedId ? { expectedId } : {}),
    });
  });

  it("blocks escaping setup entries before explicit runtime setup entries", async () => {
    const stateDir = makeTempDir();
    const globalExt = path.join(stateDir, "extensions", "escape-pack");
    mkdirSafe(path.join(globalExt, "dist"));
    writePluginPackageManifest({
      packageDir: globalExt,
      packageName: "@openclaw/escape-pack",
      extensions: ["./dist/index.js"],
      setupEntry: "../src/setup-entry.ts",
      runtimeSetupEntry: "./dist/setup-entry.js",
    });
    fs.writeFileSync(path.join(globalExt, "dist", "index.js"), "export default {}", "utf-8");
    fs.writeFileSync(path.join(globalExt, "dist", "setup-entry.js"), "export default {}", "utf-8");

    const result = await discoverWithStateDir(stateDir, {});
    const candidate = requireCandidateById(result.candidates, "escape-pack");

    expect(candidate.setupSource).toBeUndefined();
    expectEscapesPackageDiagnostic(result.diagnostics);
  });

  it("ignores package manifests that are hardlinked aliases", async () => {
    if (process.platform === "win32") {
      return;
    }
    const stateDir = makeTempDir();
    const globalExt = path.join(stateDir, "extensions", "pack");
    const outsideDir = path.join(stateDir, "outside");
    const outsideManifest = path.join(outsideDir, "package.json");
    const linkedManifest = path.join(globalExt, "package.json");
    mkdirSafe(globalExt);
    mkdirSafe(outsideDir);
    fs.writeFileSync(path.join(globalExt, "entry.ts"), "export default {}", "utf-8");
    fs.writeFileSync(
      outsideManifest,
      JSON.stringify({
        name: "@openclaw/pack",
        openclaw: { extensions: ["./entry.ts"] },
      }),
      "utf-8",
    );
    try {
      fs.linkSync(outsideManifest, linkedManifest);
    } catch (err) {
      if ((err as NodeJS.ErrnoException).code === "EXDEV") {
        return;
      }
      throw err;
    }

    const { candidates } = await discoverWithStateDir(stateDir, {});

    expect(candidates.map((candidate) => candidate.idHint)).not.toContain("pack");
  });

  it.runIf(process.platform !== "win32")("blocks world-writable plugin paths", async () => {
    const stateDir = makeTempDir();
    const pluginDir = path.join(stateDir, "extensions", "world-open");
    createPackagePluginWithEntry({
      packageDir: pluginDir,
      packageName: "@openclaw/world-open",
      pluginId: "world-open",
    });
    fs.chmodSync(pluginDir, 0o777);

    const result = await discoverWithStateDir(stateDir, {});

    expect(result.candidates).toHaveLength(0);
    expectDiagnostic({
      diagnostics: result.diagnostics,
      messageIncludes: "world-writable path",
    });
  });

  it.runIf(process.platform !== "win32")(
    "repairs world-writable bundled plugin dirs before loading them",
    async () => {
      const stateDir = makeTempDir();
      const packageRoot = path.join(stateDir, "node_modules", "openclaw");
      const bundledDir = path.join(packageRoot, "dist", "extensions");
      const packDir = path.join(bundledDir, "demo-pack");
      mkdirSafe(packDir);
      fs.writeFileSync(path.join(packDir, "index.ts"), "export default function () {}", "utf-8");
      fs.chmodSync(packDir, 0o777);

      const result = withOpenClawPackageArgv(packageRoot, () =>
        discoverOpenClawPlugins({
          env: { ...process.env, ...buildBundledDiscoveryEnv(stateDir) },
        }),
      );

      expect(result.candidates.map((candidate) => candidate.idHint)).toContain("demo-pack");
      expectNoDiagnostic({
        diagnostics: result.diagnostics,
        source: packDir,
        messageIncludes: "world-writable path",
      });
      expect(fs.statSync(packDir).mode & 0o777).toBe(0o755);
    },
  );

  it.runIf(process.platform !== "win32" && typeof process.getuid === "function")(
    "blocks suspicious ownership when uid mismatch is detected",
    async () => {
      const stateDir = makeTempDir();
      createPackagePluginWithEntry({
        packageDir: path.join(stateDir, "extensions", "owner-mismatch"),
        packageName: "@openclaw/owner-mismatch",
        pluginId: "owner-mismatch",
      });

      const actualUid = (process as NodeJS.Process & { getuid: () => number }).getuid();
      const result = await discoverWithStateDir(stateDir, { ownershipUid: actualUid + 1 });
      const shouldBlockForMismatch = actualUid !== 0;
      expect(result.candidates).toHaveLength(shouldBlockForMismatch ? 0 : 1);
      const hasSuspiciousOwnershipDiagnostic = result.diagnostics.some((diagnostic) =>
        diagnostic.message.includes("suspicious ownership"),
      );
      expect(hasSuspiciousOwnershipDiagnostic).toBe(shouldBlockForMismatch);
      if (shouldBlockForMismatch) {
        expectDiagnostic({
          diagnostics: result.diagnostics,
          pluginId: "owner-mismatch",
          messageIncludes: "suspicious ownership",
        });
      }
    },
  );

  it.runIf(process.platform !== "win32")("deduplicates blocked candidate diagnostics", () => {
    const stateDir = makeTempDir();
    const globalExt = path.join(stateDir, "extensions");
    mkdirSafe(globalExt);
    const blockedDir = path.join(globalExt, "blocked-plugin");
    mkdirSafe(blockedDir);
    fs.writeFileSync(path.join(blockedDir, "index.ts"), "export default function () {}", "utf-8");
    fs.chmodSync(blockedDir, 0o777);

    try {
      const result = discoverOpenClawPlugins({
        env: {
          ...buildDiscoveryEnv(stateDir),
          OPENCLAW_PLUGINS_PATHS: blockedDir,
        },
      });
      const blockedDiagnostics = result.diagnostics.filter(
        (diag) =>
          diag.pluginId === "blocked-plugin" &&
          diag.message.includes("blocked plugin candidate: world-writable path"),
      );
      expect(blockedDiagnostics).toHaveLength(1);
    } finally {
      fs.chmodSync(blockedDir, 0o755);
    }
  });

  it.runIf(process.platform !== "win32")(
    "uses native manifest ids for blocked index-file directory diagnostics",
    () => {
      const stateDir = makeTempDir();
      const pluginDir = path.join(stateDir, "alias-dir");
      mkdirSafe(pluginDir);
      writePluginManifest({ pluginDir, id: "actual-id" });
      writePluginEntry(path.join(pluginDir, "index.ts"));
      fs.chmodSync(pluginDir, 0o777);

      try {
        const result = discoverOpenClawPlugins({
          extraPaths: [pluginDir],
          env: {
            ...buildDiscoveryEnv(stateDir),
          },
        });
        expect(result.candidates).toHaveLength(0);
        const diagnostic = result.diagnostics.find(
          (entry) =>
            entry.pluginId === "actual-id" &&
            (entry.source ?? "").endsWith("alias-dir") &&
            entry.message.includes("blocked plugin candidate: world-writable path"),
        );
        if (!diagnostic) {
          throw new Error("Expected world-writable plugin candidate diagnostic");
        }
      } finally {
        fs.chmodSync(pluginDir, 0o755);
      }
    },
  );

  it("reflects plugin root changes on the next discovery call", () => {
    const stateDir = makeTempDir();
    const pluginDir = path.join(stateDir, "extensions", "fresh");
    createPackagePluginWithEntry({
      packageDir: pluginDir,
      packageName: "@openclaw/fresh",
      pluginId: "fresh",
    });

    const env = buildDiscoveryEnvWithOverrides(stateDir);
    const first = discoverWithEnv({ env });
    expect(first.candidates.map((candidate) => candidate.idHint)).toContain("fresh");

    fs.rmSync(pluginDir, { recursive: true, force: true });

    const second = discoverWithEnv({ env });
    expect(second.candidates.map((candidate) => candidate.idHint)).not.toContain("fresh");
  });

  it("discovers bundled and global plugins for each workspace-specific scan", () => {
    const stateDir = makeTempDir();
    const packageRoot = path.join(stateDir, "node_modules", "openclaw");
    const bundledDir = path.join(packageRoot, "dist", "extensions");
    const globalExt = path.join(stateDir, "extensions");
    const workspaceA = path.join(stateDir, "workspace-a");
    const workspaceB = path.join(stateDir, "workspace-b");

    createPackagePluginWithEntry({
      packageDir: path.join(bundledDir, "bundled-plugin"),
      packageName: "@openclaw/bundled-plugin",
      pluginId: "bundled-plugin",
    });
    createPackagePluginWithEntry({
      packageDir: path.join(globalExt, "global-plugin"),
      packageName: "@openclaw/global-plugin",
      pluginId: "global-plugin",
    });
    createPackagePluginWithEntry({
      packageDir: path.join(workspaceA, ".openclaw", "extensions", "workspace-a-plugin"),
      packageName: "@openclaw/workspace-a-plugin",
      pluginId: "workspace-a-plugin",
    });
    createPackagePluginWithEntry({
      packageDir: path.join(workspaceB, ".openclaw", "extensions", "workspace-b-plugin"),
      packageName: "@openclaw/workspace-b-plugin",
      pluginId: "workspace-b-plugin",
    });

    const env = {
      ...buildDiscoveryEnv(stateDir),
      OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined,
      OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir,
    };
    const first = withOpenClawPackageArgv(packageRoot, () =>
      discoverWithEnv({ workspaceDir: workspaceA, env }),
    );
    expectCandidatePresence(first, {
      present: ["bundled-plugin", "global-plugin", "workspace-a-plugin"],
      absent: ["workspace-b-plugin"],
    });

    const second = withOpenClawPackageArgv(packageRoot, () =>
      discoverWithEnv({ workspaceDir: workspaceB, env }),
    );
    expectCandidatePresence(second, {
      present: ["bundled-plugin", "global-plugin", "workspace-b-plugin"],
      absent: ["workspace-a-plugin"],
    });
  });

  it.each([
    {
      name: "does not reuse discovery results across env root changes",
      setup: () => {
        const stateDirA = makeTempDir();
        const stateDirB = makeTempDir();
        createPackagePluginWithEntry({
          packageDir: path.join(stateDirA, "extensions", "alpha"),
          packageName: "@openclaw/alpha",
          pluginId: "alpha",
        });
        createPackagePluginWithEntry({
          packageDir: path.join(stateDirB, "extensions", "beta"),
          packageName: "@openclaw/beta",
          pluginId: "beta",
        });
        return {
          first: discoverWithEnv({ env: buildDiscoveryEnvWithOverrides(stateDirA) }),
          second: discoverWithEnv({ env: buildDiscoveryEnvWithOverrides(stateDirB) }),
          assert: (
            first: ReturnType<typeof discoverWithEnv>,
            second: ReturnType<typeof discoverWithEnv>,
          ) => {
            expectCandidatePresence(first, { present: ["alpha"], absent: ["beta"] });
            expectCandidatePresence(second, { present: ["beta"], absent: ["alpha"] });
          },
        };
      },
    },
    {
      name: "does not reuse extra-path discovery across env home changes",
      setup: () => {
        const stateDir = makeTempDir();
        const homeA = makeTempDir();
        const homeB = makeTempDir();
        const pluginA = path.join(homeA, "plugins", "demo.ts");
        const pluginB = path.join(homeB, "plugins", "demo.ts");
        writeStandalonePlugin(pluginA, "export default {}");
        writeStandalonePlugin(pluginB, "export default {}");
        return {
          first: discoverWithEnv({
            extraPaths: ["~/plugins/demo.ts"],
            env: buildDiscoveryEnvWithOverrides(stateDir, { HOME: homeA }),
          }),
          second: discoverWithEnv({
            extraPaths: ["~/plugins/demo.ts"],
            env: buildDiscoveryEnvWithOverrides(stateDir, { HOME: homeB }),
          }),
          assert: (
            first: ReturnType<typeof discoverWithEnv>,
            second: ReturnType<typeof discoverWithEnv>,
          ) => {
            expectCandidateSource(first.candidates, "demo", pluginA);
            expectCandidateSource(second.candidates, "demo", pluginB);
          },
        };
      },
    },
  ] as const)("$name", ({ setup }) => {
    const { first, second, assert } = setup();
    assert(first, second);
  });

  it("discovers standalone files from configured load-path directories", () => {
    const stateDir = makeTempDir();
    const pluginDir = path.join(stateDir, "plugins");
    const pluginPath = path.join(pluginDir, "alpha.ts");
    writeStandalonePlugin(pluginPath, "export default {}");

    const result = discoverWithEnv({
      extraPaths: [pluginDir],
      env: buildDiscoveryEnvWithOverrides(stateDir),
    });

    expectCandidateSource(result.candidates, "alpha", pluginPath);
  });

  it("preserves configured load-path order", () => {
    const stateDir = makeTempDir();
    const pluginA = path.join(stateDir, "plugins", "alpha.ts");
    const pluginB = path.join(stateDir, "plugins", "beta.ts");
    writeStandalonePlugin(pluginA, "export default {}");
    writeStandalonePlugin(pluginB, "export default {}");

    const env = buildDiscoveryEnvWithOverrides(stateDir);

    const first = discoverWithEnv({
      extraPaths: [pluginA, pluginB],
      env,
    });
    const second = discoverWithEnv({
      extraPaths: [pluginB, pluginA],
      env,
    });

    expectCandidateOrder(first.candidates, ["alpha", "beta"]);
    expectCandidateOrder(second.candidates, ["beta", "alpha"]);
  });
});
