const { describe, it, afterEach } = require("node:test");
const assert = require("node:assert");
const fs = require("fs");
const path = require("path");
const os = require("os");

const {
  checkAgentIntegrations,
  findOpenClawPluginEntry,
  findOpencodePluginEntry,
} = require("../src/doctor-detectors/agent-integrations");
const { GEMINI_HOOK_EVENTS } = require("../hooks/gemini-install");
const { ANTIGRAVITY_HOOK_EVENTS, __test: antigravityInstallTest } = require("../hooks/antigravity-install");
const { QWEN_CODE_HOOK_EVENTS, buildQwenCodeHookCommand } = require("../hooks/qwen-code-install");
const { HOOK_ENTRIES: CODEWHALE_HOOK_ENTRIES } = require("../hooks/codewhale-install");
const { QODER_HOOK_EVENTS, buildQoderHookCommand } = require("../hooks/qoder-install");

const tempDirs = [];

function makeTempDir() {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "clawd-doctor-agent-"));
  tempDirs.push(dir);
  return dir;
}

function writeJson(filePath, value) {
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
  fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf8");
}

function writeText(filePath, value) {
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
  fs.writeFileSync(filePath, value, "utf8");
}

function baseDescriptor(overrides = {}) {
  const root = makeTempDir();
  const parentDir = path.join(root, ".agent");
  return {
    agentId: "test-agent",
    agentName: "Test Agent",
    eventSource: "hook",
    parentDir,
    configPath: path.join(parentDir, "settings.json"),
    configMode: "file",
    autoInstall: true,
    marker: "test-hook.js",
    nested: false,
    ...overrides,
  };
}

function runOne(descriptor, options = {}) {
  return checkAgentIntegrations({
    fs,
    prefs: options.prefs || {},
    descriptors: [descriptor],
    server: options.server || null,
    validateCommand: options.validateCommand || (() => ({
      ok: true,
      nodeBin: "/node",
      scriptPath: "/app/hooks/test-hook.js",
    })),
  }).details[0];
}

function suspiciousShrinkGuardServer() {
  return {
    getClaudeHookGuardStatus: () => ({
      type: "suspicious-shrink",
      at: 1234,
      before: { keyCount: 5, hookCount: 12, thirdPartyHookCount: 2 },
      after: { keyCount: 1, hookCount: 0, thirdPartyHookCount: 0 },
    }),
  };
}

function geminiHooksConfig(commandForEvent = (event) => `"/node" "/app/hooks/gemini-hook.js" ${event}`) {
  const hooks = {};
  for (const event of GEMINI_HOOK_EVENTS) {
    hooks[event] = [{
      matcher: "*",
      hooks: [{ name: "clawd", type: "command", command: commandForEvent(event) }],
    }];
  }
  return hooks;
}

function antigravityDescriptor() {
  const root = makeTempDir();
  const parentDir = path.join(root, ".gemini", "config");
  return baseDescriptor({
    agentId: "antigravity-cli",
    agentName: "Antigravity CLI",
    marker: "antigravity-hook.js",
    parentDir,
    configPath: path.join(parentDir, "hooks.json"),
    configMode: "antigravity-hooks",
    hookEvents: ANTIGRAVITY_HOOK_EVENTS,
  });
}

function antigravityHooksConfig(commandForEvent = (event) => `"/node" "/app/hooks/antigravity-hook.js" ${event}`) {
  // D2: state-only — no PreToolUse.
  return {
    clawd: {
      PreInvocation: [{ type: "command", command: commandForEvent("PreInvocation") }],
      PostToolUse: [{
        matcher: "*",
        hooks: [{ type: "command", command: commandForEvent("PostToolUse") }],
      }],
      PostInvocation: [{ type: "command", command: commandForEvent("PostInvocation") }],
      Stop: [{ type: "command", command: commandForEvent("Stop") }],
    },
  };
}

function writeAntigravityHooks(descriptor, hooks = antigravityHooksConfig()) {
  writeJson(descriptor.configPath, hooks);
}

function qwenDescriptor() {
  const root = makeTempDir();
  const parentDir = path.join(root, ".qwen");
  return baseDescriptor({
    agentId: "qwen-code",
    agentName: "Qwen Code",
    marker: "qwen-code-hook.js",
    parentDir,
    configPath: path.join(parentDir, "settings.json"),
    configMode: "file",
    nested: true,
    hookEvents: QWEN_CODE_HOOK_EVENTS,
  });
}

function qoderDescriptor() {
  const root = makeTempDir();
  const parentDir = path.join(root, ".qoder");
  return baseDescriptor({
    agentId: "qoder",
    agentName: "Qoder",
    marker: "qoder-hook.js",
    parentDir,
    configPath: path.join(parentDir, "settings.json"),
    configMode: "file",
    nested: true,
    hookEvents: QODER_HOOK_EVENTS,
  });
}

function qoderHooksConfig(commandForEvent = (event) => `"/node" "/app/hooks/qoder-hook.js" ${event}`) {
  const hooks = {};
  for (const event of QODER_HOOK_EVENTS) {
    hooks[event] = [{
      matcher: "*",
      hooks: [{ name: "clawd", type: "command", command: commandForEvent(event) }],
    }];
  }
  return hooks;
}

function codewhaleDescriptor() {
  const root = makeTempDir();
  const parentDir = path.join(root, ".codewhale");
  return baseDescriptor({
    agentId: "codewhale",
    agentName: "CodeWhale",
    parentDir,
    configPath: path.join(parentDir, "config.toml"),
    commandMarker: "codewhale-hook.js",
    marker: "managed by clawd-on-desk",
    configMode: "codewhale-hooks-toml",
    hookEvents: CODEWHALE_HOOK_ENTRIES.map((entry) => entry[0]),
  });
}

function codewhaleToml(events = CODEWHALE_HOOK_ENTRIES.map((entry) => entry[0]), commandForEvent = (event) => `"/node" "/app/hooks/codewhale-hook.js" "${event}"`) {
  return [
    "[hooks]",
    "enabled = true",
    "",
    ...events.flatMap((event) => [
      "[[hooks.hooks]]",
      "# managed by clawd-on-desk",
      `event = "${event}"`,
      `command = '''${commandForEvent(event)}'''`,
      "background = true",
      "",
    ]),
  ].join("\n");
}

function qwenHooksConfig(commandForEvent = (event) => `"/node" "/app/hooks/qwen-code-hook.js" ${event}`) {
  const hooks = {};
  for (const event of QWEN_CODE_HOOK_EVENTS) {
    hooks[event] = [{
      matcher: "*",
      hooks: [{ name: "clawd", type: "command", command: commandForEvent(event) }],
    }];
  }
  return { hooks };
}

function codexDescriptor() {
  const root = makeTempDir();
  const parentDir = path.join(root, ".codex");
  return baseDescriptor({
    agentId: "codex",
    marker: "codex-hook.js",
    parentDir,
    configPath: path.join(parentDir, "hooks.json"),
    nested: true,
    supplementary: {
      key: "hooks",
      configPath: path.join(parentDir, "config.toml"),
    },
  });
}

function codexHooksConfig(events) {
  const hooks = {};
  for (const event of events) {
    hooks[event] = [{ hooks: [{ command: `"/node" "/app/hooks/codex-hook.js" ${event}` }] }];
  }
  return { hooks };
}

function codexTrustState(descriptor, events) {
  return [
    "[features]",
    "hooks = true",
    "",
    ...events.flatMap((event) => [
      `[hooks.state.'${descriptor.configPath}:${event.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase()}:0:0']`,
      `trusted_hash = "sha256:${"a".repeat(64)}"`,
      "",
    ]),
  ].join("\n");
}

afterEach(() => {
  while (tempDirs.length) {
    fs.rmSync(tempDirs.pop(), { recursive: true, force: true });
  }
});

describe("checkAgentIntegrations", () => {
  it("returns not-installed when parent dir is missing", () => {
    const detail = runOne(baseDescriptor());
    assert.strictEqual(detail.status, "not-installed");
    assert.strictEqual(detail.level, "info");
    assert.strictEqual(detail.parentDirExists, false);
  });

  it("reports not-managed before disabled for uninstalled agents", () => {
    const descriptor = baseDescriptor({ agentId: "gemini-cli" });
    const detail = runOne(descriptor, {
      prefs: {
        agents: {
          "gemini-cli": {
            integrationInstalled: false,
            enabled: false,
          },
        },
      },
    });

    assert.strictEqual(detail.status, "not-managed");
    assert.strictEqual(detail.level, "info");
  });

  it("keeps enabled Hermes missing install info-only when another integration is ok", () => {
    const okDescriptor = baseDescriptor({
      agentId: "ok-agent",
      marker: "ok-hook.js",
    });
    writeJson(okDescriptor.configPath, {
      hooks: {
        Stop: [{ command: '"/node" "/app/hooks/ok-hook.js" Stop' }],
      },
    });
    const hermesDescriptor = baseDescriptor({
      agentId: "hermes",
      marker: "clawd-on-desk",
      configMode: "plugin-dir",
    });

    const result = checkAgentIntegrations({
      fs,
      prefs: { agents: { hermes: { enabled: true } } },
      descriptors: [okDescriptor, hermesDescriptor],
      validateCommand: () => ({
        ok: true,
        nodeBin: "/node",
        scriptPath: "/app/hooks/ok-hook.js",
      }),
    });

    const hermes = result.details.find((detail) => detail.agentId === "hermes");
    assert.strictEqual(result.status, "pass");
    assert.strictEqual(hermes.status, "not-installed");
    assert.strictEqual(hermes.level, "info");
  });

  it("returns not-connected when config is missing for an auto-installed agent", () => {
    const descriptor = baseDescriptor();
    fs.mkdirSync(descriptor.parentDir, { recursive: true });

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "not-connected");
    assert.strictEqual(detail.level, "warning");
    assert.strictEqual(detail.configFileExists, false);
    assert.deepStrictEqual(detail.fixAction, { type: "agent-integration", agentId: "test-agent" });
  });

  it("explains Claude hook loss when the suspicious-shrink guard recently fired", () => {
    const descriptor = baseDescriptor({
      agentId: "claude-code",
      agentName: "Claude Code",
      marker: "clawd-hook.js",
      nested: true,
    });
    writeJson(descriptor.configPath, { hooks: {} });

    const detail = runOne(descriptor, {
      server: suspiciousShrinkGuardServer(),
    });

    assert.strictEqual(detail.status, "not-connected");
    assert.match(detail.detail, /paused automatic Claude hook repair/);
    assert.strictEqual(detail.claudeHookGuard.type, "suspicious-shrink");
    assert.deepStrictEqual(detail.fixAction, { type: "agent-integration", agentId: "claude-code" });
  });

  it("does not show the Claude guard notice for other agents", () => {
    const descriptor = baseDescriptor();
    writeJson(descriptor.configPath, { hooks: {} });

    const detail = runOne(descriptor, {
      server: suspiciousShrinkGuardServer(),
    });

    assert.strictEqual(detail.status, "not-connected");
    assert.match(detail.detail, /has no test-hook\.js command/);
    assert.doesNotMatch(detail.detail, /paused automatic Claude hook repair/);
    assert.strictEqual(detail.claudeHookGuard, undefined);
  });

  it("does not show the Claude guard notice when Claude hooks are connected", () => {
    const descriptor = baseDescriptor({
      agentId: "claude-code",
      agentName: "Claude Code",
      marker: "clawd-hook.js",
      nested: true,
    });
    writeJson(descriptor.configPath, {
      hooks: {
        Stop: [{
          matcher: "",
          hooks: [{ command: '"/node" "/app/hooks/clawd-hook.js" Stop' }],
        }],
      },
    });

    const detail = runOne(descriptor, {
      server: suspiciousShrinkGuardServer(),
    });

    assert.strictEqual(detail.status, "ok");
    assert.doesNotMatch(detail.detail, /paused automatic Claude hook repair/);
    assert.strictEqual(detail.claudeHookGuard, undefined);
  });

  it("keeps the original Claude hook loss detail when no guard status is available", () => {
    const descriptor = baseDescriptor({
      agentId: "claude-code",
      agentName: "Claude Code",
      marker: "clawd-hook.js",
      nested: true,
    });
    writeJson(descriptor.configPath, { hooks: {} });

    const detail = runOne(descriptor, {
      server: {},
    });

    assert.strictEqual(detail.status, "not-connected");
    assert.match(detail.detail, /has no clawd-hook\.js command/);
    assert.doesNotMatch(detail.detail, /paused automatic Claude hook repair/);
    assert.strictEqual(detail.claudeHookGuard, undefined);
  });

  it("returns config-corrupt when JSON parsing fails", () => {
    const descriptor = baseDescriptor();
    fs.mkdirSync(descriptor.parentDir, { recursive: true });
    fs.writeFileSync(descriptor.configPath, "{ nope", "utf8");

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "config-corrupt");
    assert.strictEqual(detail.level, "warning");
    assert.strictEqual(detail.fixAction, undefined);
  });

  it("validates flat hook commands and marks ok", () => {
    const descriptor = baseDescriptor();
    writeJson(descriptor.configPath, {
      hooks: {
        Stop: [{ command: '"/node" "/app/hooks/test-hook.js"' }],
      },
    });

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "ok");
    assert.strictEqual(detail.commandCount, 1);
  });

  it("validates nested hook commands when descriptor requests nested mode", () => {
    const descriptor = baseDescriptor({ nested: true });
    writeJson(descriptor.configPath, {
      hooks: {
        Stop: [{
          hooks: [{ command: '"/node" "/app/hooks/test-hook.js"' }],
        }],
      },
    });

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "ok");
  });

  it("validates Gemini nested hook commands for every required event", () => {
    const descriptor = baseDescriptor({
      agentId: "gemini-cli",
      marker: "gemini-hook.js",
      nested: true,
    });
    writeJson(descriptor.configPath, {
      hooks: geminiHooksConfig(),
    });

    const seen = [];
    const detail = runOne(descriptor, {
      validateCommand: (command) => {
        seen.push(command);
        return {
          ok: true,
          nodeBin: "/node",
          scriptPath: "/app/hooks/gemini-hook.js",
        };
      },
    });

    assert.strictEqual(seen.length, GEMINI_HOOK_EVENTS.length);
    assert.strictEqual(detail.status, "ok");
    assert.strictEqual(detail.commandCount, GEMINI_HOOK_EVENTS.length);
    assert.deepStrictEqual(detail.supplementary, {
      key: "gemini_hooks",
      value: "enabled",
      detail: "hooksConfig allows Clawd Gemini hooks",
    });
  });

  it("warns when Gemini is missing any required hook event", () => {
    const descriptor = baseDescriptor({
      agentId: "gemini-cli",
      marker: "gemini-hook.js",
      nested: true,
    });
    writeJson(descriptor.configPath, {
      hooks: {
        BeforeTool: [{
          matcher: "*",
          hooks: [{ name: "clawd", type: "command", command: '"/node" "/app/hooks/gemini-hook.js" BeforeTool' }],
        }],
      },
    });

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "not-connected");
    assert.strictEqual(detail.level, "warning");
    assert.ok(detail.missingGeminiHookEvents.includes("SessionStart"));
    assert.ok(detail.missingGeminiHookEvents.includes("AfterTool"));
    assert.deepStrictEqual(detail.fixAction, { type: "agent-integration", agentId: "gemini-cli" });
  });

  it("turns Gemini ok into warning when hooksConfig.enabled=false", () => {
    const descriptor = baseDescriptor({
      agentId: "gemini-cli",
      marker: "gemini-hook.js",
      nested: true,
    });
    writeJson(descriptor.configPath, {
      hooks: geminiHooksConfig(),
      hooksConfig: {
        enabled: false,
      },
    });

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "not-connected");
    assert.strictEqual(detail.level, "warning");
    assert.strictEqual(detail.detail, "Gemini hooks are disabled in settings.json; Clawd preserves this user setting and will not receive hook events");
    assert.deepStrictEqual(detail.supplementary, {
      key: "gemini_hooks",
      value: "disabled-global",
      detail: "hooksConfig.enabled is false",
    });
    assert.strictEqual(detail.fixAction, undefined);
  });

  it("reports disabled Gemini hooks even when command coverage is incomplete", () => {
    const descriptor = baseDescriptor({
      agentId: "gemini-cli",
      marker: "gemini-hook.js",
      nested: true,
    });
    writeJson(descriptor.configPath, {
      hooks: {
        BeforeTool: [{
          matcher: "*",
          hooks: [{ name: "clawd", type: "command", command: '"/node" "/app/hooks/gemini-hook.js" BeforeTool' }],
        }],
      },
      hooksConfig: {
        enabled: false,
      },
    });

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "not-connected");
    assert.strictEqual(detail.detail, "Gemini hooks are disabled in settings.json; Clawd preserves this user setting and will not receive hook events");
    assert.deepStrictEqual(detail.supplementary, {
      key: "gemini_hooks",
      value: "disabled-global",
      detail: "hooksConfig.enabled is false",
    });
    assert.strictEqual(detail.fixAction, undefined);
  });

  it("turns Gemini ok into warning when hooksConfig.disabled includes clawd", () => {
    const descriptor = baseDescriptor({
      agentId: "gemini-cli",
      marker: "gemini-hook.js",
      nested: true,
    });
    writeJson(descriptor.configPath, {
      hooks: geminiHooksConfig(),
      hooksConfig: {
        disabled: ["clawd"],
      },
    });

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "not-connected");
    assert.strictEqual(detail.level, "warning");
    assert.deepStrictEqual(detail.supplementary, {
      key: "gemini_hooks",
      value: "disabled-clawd",
      detail: 'hooksConfig.disabled includes "clawd"',
    });
    assert.strictEqual(detail.fixAction, undefined);
  });

  it("does not treat legacy disabled Gemini hook command strings as a stable disabled signal", () => {
    const descriptor = baseDescriptor({
      agentId: "gemini-cli",
      marker: "gemini-hook.js",
      nested: true,
    });
    writeJson(descriptor.configPath, {
      hooks: geminiHooksConfig(),
      hooksConfig: {
        disabled: ['"/node" "/app/hooks/gemini-hook.js" BeforeTool'],
      },
    });

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "ok");
    assert.strictEqual(detail.level, null);
    assert.deepStrictEqual(detail.supplementary, {
      key: "gemini_hooks",
      value: "enabled",
      detail: "hooksConfig allows Clawd Gemini hooks",
    });
    assert.strictEqual(detail.fixAction, undefined);
  });

  it("reports missing Antigravity hooks as repairable not-connected", () => {
    const descriptor = antigravityDescriptor();
    fs.mkdirSync(descriptor.parentDir, { recursive: true });

    const detail = runOne(descriptor);

    assert.strictEqual(detail.status, "not-connected");
    assert.strictEqual(detail.eventSource, "hook");
    assert.deepStrictEqual(detail.fixAction, { type: "agent-integration", agentId: "antigravity-cli" });
  });

  it("validates Antigravity hooks for every required event", () => {
    const descriptor = antigravityDescriptor();
    writeAntigravityHooks(descriptor);

    const seen = [];
    const detail = runOne(descriptor, {
      validateCommand: (command) => {
        seen.push(command);
        return {
          ok: true,
          nodeBin: "/node",
          scriptPath: "/app/hooks/antigravity-hook.js",
        };
      },
    });

    assert.strictEqual(seen.length, ANTIGRAVITY_HOOK_EVENTS.length);
    assert.strictEqual(detail.status, "ok");
    assert.strictEqual(detail.commandCount, ANTIGRAVITY_HOOK_EVENTS.length);
    assert.strictEqual(detail.scriptPath, "/app/hooks/antigravity-hook.js");
  });

  it("validates Windows Antigravity EncodedCommand hooks for every required event", () => {
    const descriptor = antigravityDescriptor();
    const nodeBin = "C:\\Program Files\\nodejs\\node.exe";
    const scriptPath = "D:/app/hooks/antigravity-hook.js";
    writeAntigravityHooks(descriptor, antigravityHooksConfig((event) =>
      antigravityInstallTest.buildWindowsAntigravityHookCommand(
        nodeBin,
        scriptPath,
        event,
        {
          platform: "win32",
          powerShellBin: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
        }
      )
    ));

    const seen = [];
    const detail = runOne(descriptor, {
      validateCommand: (command) => {
        seen.push(command);
        assert.strictEqual(command.includes("antigravity-hook.js"), false);
        return {
          ok: true,
          nodeBin,
          scriptPath,
        };
      },
    });

    assert.strictEqual(seen.length, ANTIGRAVITY_HOOK_EVENTS.length);
    assert.strictEqual(detail.status, "ok");
    assert.strictEqual(detail.commandCount, ANTIGRAVITY_HOOK_EVENTS.length);
    assert.strictEqual(detail.scriptPath, scriptPath);
  });

  it("warns when Antigravity hooks are missing any required event", () => {
    const descriptor = antigravityDescriptor();
    writeAntigravityHooks(descriptor, {
      clawd: {
        PreToolUse: [{
          matcher: "*",
          hooks: [{ type: "command", command: '"/node" "/app/hooks/antigravity-hook.js" PreToolUse' }],
        }],
      },
    });

    const detail = runOne(descriptor);

    assert.strictEqual(detail.status, "not-connected");
    assert.strictEqual(detail.level, "warning");
    assert.ok(detail.missingAntigravityHookEvents.includes("PreInvocation"));
    assert.ok(detail.missingAntigravityHookEvents.includes("Stop"));
    assert.deepStrictEqual(detail.fixAction, { type: "agent-integration", agentId: "antigravity-cli" });
  });

  it("returns broken-path when Antigravity hook commands fail validation", () => {
    const descriptor = antigravityDescriptor();
    writeAntigravityHooks(descriptor);

    const detail = runOne(descriptor, {
      validateCommand: () => ({
        ok: false,
        issue: "scriptPath-missing",
        nodeBin: "/node",
        scriptPath: "/missing/antigravity-hook.js",
      }),
    });

    assert.strictEqual(detail.status, "broken-path");
    assert.strictEqual(detail.hookCommandIssue, "scriptPath-missing");
    assert.strictEqual(detail.brokenAntigravityHookEvent, "PreInvocation");
    assert.deepStrictEqual(detail.fixAction, { type: "agent-integration", agentId: "antigravity-cli" });
  });

  it("validates Qwen Code hooks for every required event", () => {
    const descriptor = qwenDescriptor();
    writeJson(descriptor.configPath, qwenHooksConfig());

    const seen = [];
    const detail = runOne(descriptor, {
      validateCommand: (command) => {
        seen.push(command);
        return {
          ok: true,
          nodeBin: "/node",
          scriptPath: "/app/hooks/qwen-code-hook.js",
        };
      },
    });

    assert.strictEqual(seen.length, QWEN_CODE_HOOK_EVENTS.length);
    assert.strictEqual(detail.status, "ok");
    assert.strictEqual(detail.commandCount, QWEN_CODE_HOOK_EVENTS.length);
    assert.strictEqual(detail.scriptPath, "/app/hooks/qwen-code-hook.js");
    assert.deepStrictEqual(detail.supplementary, {
      key: "qwen_hooks",
      value: "enabled",
      detail: "settings.json allows Clawd Qwen hooks",
    });
  });

  it("validates Windows Qwen Code EncodedCommand hooks for every required event", () => {
    const descriptor = qwenDescriptor();
    const nodeBin = "C:\\Program Files\\nodejs\\node.exe";
    const scriptPath = "D:/app/hooks/qwen-code-hook.js";
    writeJson(descriptor.configPath, qwenHooksConfig((event) =>
      buildQwenCodeHookCommand(
        nodeBin,
        scriptPath,
        event,
        {
          platform: "win32",
          powerShellBin: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
        }
      )
    ));

    const seen = [];
    const detail = runOne(descriptor, {
      validateCommand: (command) => {
        seen.push(command);
        assert.strictEqual(command.includes("qwen-code-hook.js"), false);
        return {
          ok: true,
          nodeBin,
          scriptPath,
        };
      },
    });

    assert.strictEqual(seen.length, QWEN_CODE_HOOK_EVENTS.length);
    assert.strictEqual(detail.status, "ok");
    assert.strictEqual(detail.commandCount, QWEN_CODE_HOOK_EVENTS.length);
    assert.strictEqual(detail.scriptPath, scriptPath);
  });

  it("warns when Qwen Code is missing any required hook event", () => {
    const descriptor = qwenDescriptor();
    writeJson(descriptor.configPath, {
      hooks: {
        PreToolUse: [{
          matcher: "*",
          hooks: [{ name: "clawd", type: "command", command: '"/node" "/app/hooks/qwen-code-hook.js" PreToolUse' }],
        }],
      },
    });

    const detail = runOne(descriptor);

    assert.strictEqual(detail.status, "not-connected");
    assert.strictEqual(detail.level, "warning");
    assert.ok(detail.missingQwenHookEvents.includes("SessionStart"));
    assert.ok(detail.missingQwenHookEvents.includes("PermissionRequest"));
    assert.deepStrictEqual(detail.fixAction, { type: "agent-integration", agentId: "qwen-code" });
  });

  it("does not offer automatic repair when Qwen hooks are disabled globally", () => {
    const descriptor = qwenDescriptor();
    writeJson(descriptor.configPath, {
      ...qwenHooksConfig(),
      disableAllHooks: true,
    });

    const detail = runOne(descriptor);

    assert.strictEqual(detail.status, "not-connected");
    assert.strictEqual(detail.level, "warning");
    assert.strictEqual(detail.detail, "Qwen Code hooks are disabled in settings.json; Clawd preserves this user setting and will not receive hook events");
    assert.deepStrictEqual(detail.supplementary, {
      key: "qwen_hooks",
      value: "disabled-global",
      detail: "disableAllHooks is true",
    });
    assert.strictEqual(detail.fixAction, undefined);
  });

  it("validates Qoder state-only hooks through the generic file-mode path", () => {
    const descriptor = qoderDescriptor();
    writeJson(descriptor.configPath, { hooks: qoderHooksConfig() });

    const seen = [];
    const detail = runOne(descriptor, {
      validateCommand: (command) => {
        seen.push(command);
        return { ok: true, nodeBin: "/node", scriptPath: "/app/hooks/qoder-hook.js" };
      },
    });

    assert.strictEqual(detail.status, "ok");
    assert.strictEqual(detail.commandCount, QODER_HOOK_EVENTS.length);
    assert.ok(seen.every((command) => command.includes("qoder-hook.js")));
  });

  it("detects portable Windows Qoder hooks (bash-safe form, marker in plain text)", () => {
    const descriptor = qoderDescriptor();
    const nodeBin = "C:\\Program Files\\nodejs\\node.exe";
    const scriptPath = "D:/app/hooks/qoder-hook.js";
    writeJson(descriptor.configPath, { hooks: qoderHooksConfig((event) =>
      buildQoderHookCommand(nodeBin, scriptPath, event, { platform: "win32" })
    ) });

    const seen = [];
    const detail = runOne(descriptor, {
      validateCommand: (command) => {
        seen.push(command);
        // Portable form keeps the marker in plain text and no backslashes.
        assert.strictEqual(command.includes("qoder-hook.js"), true);
        assert.strictEqual(command.includes("\\"), false);
        return { ok: true, nodeBin, scriptPath };
      },
    });

    assert.strictEqual(detail.status, "ok");
    assert.strictEqual(detail.commandCount, QODER_HOOK_EVENTS.length);
  });

  it("detects legacy Windows EncodedCommand Qoder hooks even though the marker is base64-wrapped", () => {
    const { buildWindowsEncodedNodeHookCommand } = require("../hooks/json-utils");
    const descriptor = qoderDescriptor();
    const nodeBin = "C:\\Program Files\\nodejs\\node.exe";
    const scriptPath = "D:/app/hooks/qoder-hook.js";
    writeJson(descriptor.configPath, { hooks: qoderHooksConfig((event) =>
      buildWindowsEncodedNodeHookCommand(nodeBin, scriptPath, [event], {
        powerShellBin: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
      })
    ) });

    const seen = [];
    const detail = runOne(descriptor, {
      validateCommand: (command) => {
        seen.push(command);
        // Plain command text must not leak the marker — it lives in the base64 blob.
        assert.strictEqual(command.includes("qoder-hook.js"), false);
        return { ok: true, nodeBin, scriptPath };
      },
    });

    assert.strictEqual(detail.status, "ok");
    assert.strictEqual(detail.commandCount, QODER_HOOK_EVENTS.length);
  });

  it("warns and offers repair when Qoder has no Clawd hook", () => {
    const descriptor = qoderDescriptor();
    writeJson(descriptor.configPath, { hooks: {} });

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "not-connected");
    assert.match(detail.detail, /has no qoder-hook\.js command/);
    assert.deepStrictEqual(detail.fixAction, { type: "agent-integration", agentId: "qoder" });
  });

  it("validates CodeWhale TOML hooks", () => {
    const descriptor = codewhaleDescriptor();
    writeText(descriptor.configPath, codewhaleToml());

    const seen = [];
    const detail = runOne(descriptor, {
      validateCommand: (command) => {
        seen.push(command);
        return { ok: true, nodeBin: "/node", scriptPath: "/app/hooks/codewhale-hook.js" };
      },
    });

    assert.strictEqual(detail.status, "ok");
    assert.strictEqual(detail.commandCount, CODEWHALE_HOOK_ENTRIES.length);
    assert.ok(seen.every((command) => command.includes("codewhale-hook.js")));
    assert.strictEqual(detail.scriptPath, "/app/hooks/codewhale-hook.js");
  });

  it("warns and offers repair when CodeWhale is missing managed hook events", () => {
    const descriptor = codewhaleDescriptor();
    writeText(descriptor.configPath, codewhaleToml(["session_start"]));

    const detail = runOne(descriptor);

    assert.strictEqual(detail.status, "not-connected");
    assert.ok(detail.missingCodewhaleHookEvents.includes("session_end"));
    assert.deepStrictEqual(detail.fixAction, { type: "agent-integration", agentId: "codewhale" });
  });

  it("warns and offers repair when CodeWhale hooks are disabled", () => {
    const descriptor = codewhaleDescriptor();
    writeText(descriptor.configPath, codewhaleToml().replace("enabled = true", "enabled = false"));

    const detail = runOne(descriptor);

    assert.strictEqual(detail.status, "not-connected");
    assert.deepStrictEqual(detail.supplementary, {
      key: "codewhale_hooks",
      value: "disabled",
      detail: "[hooks].enabled is false",
    });
    assert.deepStrictEqual(detail.fixAction, { type: "agent-integration", agentId: "codewhale" });
  });

  it("warns and offers repair when CodeWhale hook commands fail validation", () => {
    const descriptor = codewhaleDescriptor();
    writeText(descriptor.configPath, codewhaleToml());

    const detail = runOne(descriptor, {
      validateCommand: () => ({
        ok: false,
        reason: "missing-script",
        scriptPath: "/missing/codewhale-hook.js",
      }),
    });

    assert.strictEqual(detail.status, "broken-path");
    assert.match(detail.detail, /parse-failed/);
    assert.deepStrictEqual(detail.fixAction, { type: "agent-integration", agentId: "codewhale" });
  });

  it("does not offer automatic repair when Antigravity Clawd hooks are disabled", () => {
    const descriptor = antigravityDescriptor();
    writeAntigravityHooks(descriptor, {
      clawd: {
        enabled: false,
        ...antigravityHooksConfig().clawd,
      },
    });

    const detail = runOne(descriptor);

    assert.strictEqual(detail.status, "not-connected");
    assert.strictEqual(detail.detail, "Antigravity Clawd hooks are disabled in hooks.json; Clawd preserves this user setting and will not receive hook events");
    assert.deepStrictEqual(detail.supplementary, {
      key: "antigravity_hooks",
      value: "disabled-clawd",
      detail: "clawd.enabled is false",
    });
    assert.strictEqual(detail.fixAction, undefined);
  });

  it("returns broken-path when all matching commands fail validation", () => {
    const descriptor = baseDescriptor();
    writeJson(descriptor.configPath, {
      hooks: {
        Stop: [{ command: '"/node" "/missing/test-hook.js"' }],
      },
    });

    const detail = runOne(descriptor, {
      validateCommand: () => ({
        ok: false,
        issue: "scriptPath-missing",
        nodeBin: "/node",
        scriptPath: "/missing/test-hook.js",
      }),
    });
    assert.strictEqual(detail.status, "broken-path");
    assert.strictEqual(detail.hookCommandIssue, "scriptPath-missing");
    assert.deepStrictEqual(detail.fixAction, { type: "agent-integration", agentId: "test-agent" });
  });

  it("extracts Kimi TOML commands and validates scriptPath", () => {
    const root = makeTempDir();
    const parentDir = path.join(root, ".kimi");
    const descriptor = baseDescriptor({
      agentId: "kimi-cli",
      marker: "kimi-hook.js",
      configMode: "toml-text",
      parentDir,
      configPath: path.join(parentDir, "config.toml"),
    });
    fs.mkdirSync(descriptor.parentDir, { recursive: true });
    fs.writeFileSync(
      descriptor.configPath,
      '[[hooks]]\nevent = "Stop"\ncommand = \'"node" "/missing/kimi-hook.js"\'\n',
      "utf8"
    );

    const detail = runOne(descriptor, {
      validateCommand: (command) => {
        assert.strictEqual(command, '"node" "/missing/kimi-hook.js"');
        return {
          ok: false,
          issue: "scriptPath-missing",
          nodeBin: "node",
          scriptPath: "/missing/kimi-hook.js",
        };
      },
    });
    assert.strictEqual(detail.status, "broken-path");
    assert.strictEqual(detail.hookCommandIssue, "scriptPath-missing");
  });

  it("judges the kimi-code target when both generations exist (#563)", () => {
    const root = makeTempDir();
    const legacyDir = path.join(root, ".kimi");
    const kimiCodeDir = path.join(root, ".kimi-code");
    const descriptor = baseDescriptor({
      agentId: "kimi-cli",
      marker: "kimi-hook.js",
      configMode: "toml-text",
      parentDir: legacyDir,
      configPath: path.join(legacyDir, "config.toml"),
      configTargets: [
        { label: "kimi-code", parentDir: kimiCodeDir, configPath: path.join(kimiCodeDir, "config.toml") },
        { label: "legacy", parentDir: legacyDir, configPath: path.join(legacyDir, "config.toml") },
      ],
    });
    fs.mkdirSync(legacyDir, { recursive: true });
    fs.mkdirSync(kimiCodeDir, { recursive: true });
    // Legacy config carries a healthy hook; the kimi-code config is missing —
    // doctor must judge the kimi-code (priority) target and say not-connected.
    fs.writeFileSync(
      path.join(legacyDir, "config.toml"),
      '[[hooks]]\nevent = "Stop"\ncommand = \'"node" "/app/hooks/kimi-hook.js"\'\n',
      "utf8"
    );

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "not-connected");
    assert.ok(String(detail.configPath).includes(".kimi-code"));
  });

  it("falls back to the legacy target when kimi-code is absent (#563)", () => {
    const root = makeTempDir();
    const legacyDir = path.join(root, ".kimi");
    const kimiCodeDir = path.join(root, ".kimi-code");
    const descriptor = baseDescriptor({
      agentId: "kimi-cli",
      marker: "kimi-hook.js",
      configMode: "toml-text",
      parentDir: legacyDir,
      configPath: path.join(legacyDir, "config.toml"),
      configTargets: [
        { label: "kimi-code", parentDir: kimiCodeDir, configPath: path.join(kimiCodeDir, "config.toml") },
        { label: "legacy", parentDir: legacyDir, configPath: path.join(legacyDir, "config.toml") },
      ],
    });
    fs.mkdirSync(legacyDir, { recursive: true });
    fs.writeFileSync(
      path.join(legacyDir, "config.toml"),
      '[[hooks]]\nevent = "Stop"\ncommand = \'"node" "/app/hooks/kimi-hook.js"\'\n',
      "utf8"
    );

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "ok");
    const judged = String(detail.configPath);
    assert.ok(judged.includes(".kimi") && !judged.includes(".kimi-code"));
  });

  it("lists both generation dirs when neither exists (#563)", () => {
    const root = makeTempDir();
    const legacyDir = path.join(root, ".kimi");
    const kimiCodeDir = path.join(root, ".kimi-code");
    const descriptor = baseDescriptor({
      agentId: "kimi-cli",
      marker: "kimi-hook.js",
      configMode: "toml-text",
      parentDir: legacyDir,
      configPath: path.join(legacyDir, "config.toml"),
      configTargets: [
        { label: "kimi-code", parentDir: kimiCodeDir, configPath: path.join(kimiCodeDir, "config.toml") },
        { label: "legacy", parentDir: legacyDir, configPath: path.join(legacyDir, "config.toml") },
      ],
    });

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "not-installed");
    assert.ok(detail.detail.includes(".kimi-code") && detail.detail.includes(".kimi"));
  });

  it("turns Codex ok into warning when hooks=false", () => {
    const descriptor = codexDescriptor();
    writeJson(descriptor.configPath, codexHooksConfig(["Stop"]));
    fs.writeFileSync(descriptor.supplementary.configPath, "[features]\nhooks = false\n", "utf8");

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "not-connected");
    assert.strictEqual(detail.supplementary.value, "disabled");
    assert.deepStrictEqual(detail.fixAction, {
      type: "agent-integration",
      agentId: "codex",
      forceCodexHooksFeature: true,
    });
  });


  // #544: Windows Clawd writes dual-field entries — commandWindows carries
  // the PowerShell form codex actually runs on Windows, command carries a
  // WSL-interop form only executable inside WSL. The doctor must validate
  // the field THIS platform's codex resolves; blanket-validating `command`
  // flagged every dual-field Windows install as broken-path, and Repair
  // regenerated the same fields forever.
  it("validates commandWindows on win32 for dual-field Codex entries (#544)", () => {
    const descriptor = codexDescriptor();
    const psForm = '& "C:\\Program Files\\nodejs\\node.exe" "D:/app/hooks/codex-hook.js"';
    const interopForm = '"/mnt/c/Program Files/nodejs/node.exe" "D:/app/hooks/codex-hook.js"';
    writeJson(descriptor.configPath, {
      hooks: {
        Stop: [{ hooks: [{ type: "command", command: interopForm, commandWindows: psForm, timeout: 30 }] }],
      },
    });
    fs.writeFileSync(descriptor.supplementary.configPath, codexTrustState(descriptor, ["Stop"]), "utf8");

    const seen = [];
    const result = checkAgentIntegrations({
      fs,
      prefs: {},
      descriptors: [descriptor],
      server: null,
      platform: "win32",
      validateCommand: (command) => {
        seen.push(command);
        return {
          ok: true,
          nodeBin: "C:\\Program Files\\nodejs\\node.exe",
          scriptPath: "D:/app/hooks/codex-hook.js",
        };
      },
    });

    assert.deepStrictEqual(seen, [psForm]);
    assert.strictEqual(result.details[0].status, "ok");
  });

  it("validates the POSIX command field for dual-field Codex entries off win32", () => {
    const descriptor = codexDescriptor();
    const psForm = '& "C:\\Program Files\\nodejs\\node.exe" "D:/app/hooks/codex-hook.js"';
    const interopForm = '"/mnt/c/Program Files/nodejs/node.exe" "D:/app/hooks/codex-hook.js"';
    writeJson(descriptor.configPath, {
      hooks: {
        Stop: [{ hooks: [{ type: "command", command: interopForm, commandWindows: psForm, timeout: 30 }] }],
      },
    });
    fs.writeFileSync(descriptor.supplementary.configPath, codexTrustState(descriptor, ["Stop"]), "utf8");

    const seen = [];
    const result = checkAgentIntegrations({
      fs,
      prefs: {},
      descriptors: [descriptor],
      server: null,
      platform: "linux",
      validateCommand: (command) => {
        seen.push(command);
        return { ok: true, nodeBin: "/mnt/c/Program Files/nodejs/node.exe", scriptPath: "D:/app/hooks/codex-hook.js" };
      },
    });

    assert.deepStrictEqual(seen, [interopForm]);
    assert.strictEqual(result.details[0].status, "ok");
  });

  it("reports Codex hooks=false even when hook registration is missing", () => {
    const descriptor = codexDescriptor();
    writeJson(descriptor.configPath, { hooks: {} });
    fs.writeFileSync(descriptor.supplementary.configPath, "[features]\nhooks = false\n", "utf8");

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "not-connected");
    assert.strictEqual(detail.level, "warning");
    assert.deepStrictEqual(detail.supplementary, {
      key: "hooks",
      value: "disabled",
      detail: "hooks=false",
    });
    assert.deepStrictEqual(detail.fixAction, {
      type: "agent-integration",
      agentId: "codex",
      forceCodexHooksFeature: true,
    });
  });
  it("turns Codex ok into warning when hooks need Codex review", () => {
    const descriptor = codexDescriptor();
    writeJson(descriptor.configPath, codexHooksConfig(["PermissionRequest", "Stop"]));
    fs.writeFileSync(descriptor.supplementary.configPath, "[features]\nhooks = true\n", "utf8");

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "needs-review");
    assert.strictEqual(detail.level, "warning");
    assert.strictEqual(detail.fixAction, undefined);
    assert.deepStrictEqual(detail.supplementary, {
      key: "hooks",
      value: "enabled",
      detail: "hooks=true",
    });
    assert.strictEqual(detail.codexHookTrust.value, "needs-review");
    assert.strictEqual(detail.codexHookTrust.totalCount, 2);
    assert.deepStrictEqual(detail.codexHookTrust.missingEvents, ["PermissionRequest", "Stop"]);
    assert.match(detail.codexHookTrust.detail, /Codex \/hooks review/);
  });

  it("keeps Codex ok when Codex hook trust state exists", () => {
    const descriptor = codexDescriptor();
    const events = ["PermissionRequest", "Stop"];
    writeJson(descriptor.configPath, codexHooksConfig(events));
    fs.writeFileSync(descriptor.supplementary.configPath, codexTrustState(descriptor, events), "utf8");

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "ok");
    assert.strictEqual(detail.level, null);
    assert.strictEqual(detail.fixAction, undefined);
    assert.strictEqual(detail.codexHookTrust.value, "trusted");
    assert.strictEqual(detail.codexHookTrust.trustedCount, 2);
  });

  it("scans Kiro agent configs and reports fully-valid files", () => {
    const root = makeTempDir();
    const agentsDir = path.join(root, ".kiro", "agents");
    const descriptor = baseDescriptor({
      agentId: "kiro-cli",
      marker: "kiro-hook.js",
      parentDir: path.join(root, ".kiro"),
      configPath: agentsDir,
      configMode: "dir",
      nested: true,
    });
    writeJson(path.join(agentsDir, "clawd.json"), {
      hooks: {
        stop: [{ hooks: [{ command: '"/node" "/app/hooks/kiro-hook.js"' }] }],
      },
    });

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "ok");
    assert.deepStrictEqual(detail.kiroScan.fullyValidFiles, ["clawd.json"]);
  });

  it("does not offer automatic repair when Kiro agent configs are corrupt", () => {
    const root = makeTempDir();
    const agentsDir = path.join(root, ".kiro", "agents");
    const descriptor = baseDescriptor({
      agentId: "kiro-cli",
      marker: "kiro-hook.js",
      parentDir: path.join(root, ".kiro"),
      configPath: agentsDir,
      configMode: "dir",
      nested: true,
    });
    fs.mkdirSync(agentsDir, { recursive: true });
    fs.writeFileSync(path.join(agentsDir, "broken.json"), "{ nope", "utf8");

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "config-corrupt");
    assert.strictEqual(detail.fixAction, undefined);
  });

  function piDescriptor() {
    const root = makeTempDir();
    const parentDir = path.join(root, ".pi", "agent");
    return baseDescriptor({
      agentId: "pi",
      agentName: "Pi",
      eventSource: "extension",
      parentDir,
      configPath: path.join(parentDir, "extensions", "clawd-on-desk"),
      configMode: "pi-extension",
      marker: "index.ts",
      coreFile: "pi-extension-core.js",
      markerFile: ".clawd-managed.json",
    });
  }

  it("reports missing Pi extension as repairable not-connected", () => {
    const descriptor = piDescriptor();
    fs.mkdirSync(descriptor.parentDir, { recursive: true });

    const detail = runOne(descriptor);

    assert.strictEqual(detail.status, "not-connected");
    assert.strictEqual(detail.eventSource, "extension");
    assert.deepStrictEqual(detail.fixAction, { type: "agent-integration", agentId: "pi" });
  });

  it("reports unmanaged Pi extension directory as needs-review without repair action", () => {
    const descriptor = piDescriptor();
    fs.mkdirSync(descriptor.configPath, { recursive: true });
    fs.writeFileSync(path.join(descriptor.configPath, "index.ts"), "export default function() {}\n", "utf8");

    const detail = runOne(descriptor);

    assert.strictEqual(detail.status, "needs-review");
    assert.strictEqual(detail.level, "warning");
    assert.strictEqual(detail.fixAction, undefined);
  });

  it("reports managed Pi extension as ok", () => {
    const descriptor = piDescriptor();
    writeJson(path.join(descriptor.configPath, ".clawd-managed.json"), {
      app: "clawd-on-desk",
      integration: "pi",
      managed: true,
    });
    fs.writeFileSync(path.join(descriptor.configPath, "index.ts"), "export default function() {}\n", "utf8");
    fs.writeFileSync(path.join(descriptor.configPath, "pi-extension-core.js"), "module.exports = {}\n", "utf8");

    const detail = runOne(descriptor);

    assert.strictEqual(detail.status, "ok");
    assert.strictEqual(detail.extensionFileExists, true);
    assert.strictEqual(detail.coreFileExists, true);
  });

  it("reports managed Pi extension with missing copied files as repairable broken-path", () => {
    const descriptor = piDescriptor();
    writeJson(path.join(descriptor.configPath, ".clawd-managed.json"), {
      app: "clawd-on-desk",
      integration: "pi",
      managed: true,
    });
    fs.writeFileSync(path.join(descriptor.configPath, "index.ts"), "export default function() {}\n", "utf8");

    const detail = runOne(descriptor);

    assert.strictEqual(detail.status, "broken-path");
    assert.strictEqual(detail.coreFileExists, false);
    assert.deepStrictEqual(detail.fixAction, { type: "agent-integration", agentId: "pi" });
  });

  it("reports opencode stale absolute plugin paths", () => {
    const root = makeTempDir();
    const parentDir = path.join(root, ".config", "opencode");
    const pluginPath = path.join(root, "missing", "opencode-plugin");
    const descriptor = baseDescriptor({
      agentId: "opencode",
      marker: "opencode-plugin",
      parentDir,
      configPath: path.join(parentDir, "opencode.json"),
      detection: "opencode-plugin",
    });
    writeJson(descriptor.configPath, { plugin: [pluginPath] });

    const detail = runOne(descriptor);
    assert.strictEqual(detail.status, "broken-path");
    assert.strictEqual(detail.opencodeEntryIssue, "directory-missing");
    assert.deepStrictEqual(detail.fixAction, { type: "agent-integration", agentId: "opencode" });
  });

  function openClawDescriptor() {
    const root = makeTempDir();
    const parentDir = path.join(root, ".openclaw");
    return baseDescriptor({
      agentId: "openclaw",
      agentName: "OpenClaw",
      eventSource: "plugin-event",
      parentDir,
      configPath: path.join(parentDir, "openclaw.json"),
      configMode: "openclaw-plugin",
      marker: "openclaw-plugin",
      pluginId: "clawd-on-desk",
    });
  }

  function makeOpenClawPluginDir(root) {
    const pluginDir = path.join(root, "hooks", "openclaw-plugin");
    fs.mkdirSync(pluginDir, { recursive: true });
    fs.writeFileSync(path.join(pluginDir, "index.js"), "export default { id: 'clawd-on-desk', register() {} };\n", "utf8");
    writeJson(path.join(pluginDir, "openclaw.plugin.json"), {
      id: "clawd-on-desk",
      name: "Clawd on Desk",
      description: "test",
      activation: { onStartup: true },
      configSchema: { type: "object", additionalProperties: false, properties: {} },
    });
    return pluginDir;
  }

  it("reports missing OpenClaw plugin config as repairable not-connected", () => {
    const descriptor = openClawDescriptor();
    fs.mkdirSync(descriptor.parentDir, { recursive: true });

    const detail = runOne(descriptor);

    assert.strictEqual(detail.status, "not-connected");
    assert.strictEqual(detail.eventSource, "plugin-event");
    assert.deepStrictEqual(detail.fixAction, { type: "agent-integration", agentId: "openclaw" });
  });

  it("reports OpenClaw JSON5 configs as needs-review instead of corrupting them", () => {
    const descriptor = openClawDescriptor();
    fs.mkdirSync(descriptor.parentDir, { recursive: true });
    fs.writeFileSync(descriptor.configPath, "{ // json5\n plugins: {} }\n", "utf8");

    const detail = runOne(descriptor);

    assert.strictEqual(detail.status, "needs-review");
    assert.match(detail.detail, /not strict JSON/);
    assert.strictEqual(detail.fixAction, undefined);
  });

  it("reports valid OpenClaw plugin paths as ok", () => {
    const descriptor = openClawDescriptor();
    const pluginDir = makeOpenClawPluginDir(path.dirname(descriptor.parentDir));
    writeJson(descriptor.configPath, {
      plugins: {
        load: { paths: [pluginDir] },
        entries: {
          "clawd-on-desk": {
            enabled: true,
            hooks: { allowConversationAccess: false },
          },
        },
      },
    });

    const detail = runOne(descriptor);

    assert.strictEqual(detail.status, "ok");
    assert.strictEqual(detail.openclawEntry, pluginDir);
  });

  it("reports OpenClaw stale plugin paths as repairable broken-path", () => {
    const descriptor = openClawDescriptor();
    const pluginDir = path.join(path.dirname(descriptor.parentDir), "missing", "openclaw-plugin");
    writeJson(descriptor.configPath, {
      plugins: {
        load: { paths: [pluginDir] },
        entries: { "clawd-on-desk": { enabled: true } },
      },
    });

    const detail = runOne(descriptor);

    assert.strictEqual(detail.status, "broken-path");
    assert.strictEqual(detail.openclawEntryIssue, "directory-missing");
    assert.deepStrictEqual(detail.fixAction, { type: "agent-integration", agentId: "openclaw" });
  });

  it("checks Hermes plugin directory files and enabled marker", () => {
    const root = makeTempDir();
    const parentDir = path.join(root, ".hermes");
    const pluginDir = path.join(parentDir, "plugins", "clawd-on-desk");
    const descriptor = baseDescriptor({
      agentId: "hermes",
      marker: "clawd-on-desk",
      parentDir,
      configPath: pluginDir,
      configMode: "plugin-dir",
      managedFiles: ["plugin.yaml", "__init__.py"],
      configFilePath: path.join(parentDir, "config.yaml"),
    });
    fs.mkdirSync(pluginDir, { recursive: true });
    fs.writeFileSync(path.join(pluginDir, "plugin.yaml"), "name: clawd-on-desk\n", "utf8");
    fs.writeFileSync(path.join(pluginDir, "__init__.py"), "# plugin\n", "utf8");
    fs.writeFileSync(descriptor.configFilePath, "plugins:\n  enabled:\n    - clawd-on-desk\n", "utf8");

    const detail = runOne(descriptor, {
      prefs: { agents: { hermes: { enabled: true } } },
    });

    assert.strictEqual(detail.status, "ok");
    assert.strictEqual(detail.pluginEnabled, true);
  });

  it("reports Hermes plugin directory missing managed files as repairable", () => {
    const root = makeTempDir();
    const parentDir = path.join(root, ".hermes");
    const pluginDir = path.join(parentDir, "plugins", "clawd-on-desk");
    const descriptor = baseDescriptor({
      agentId: "hermes",
      marker: "clawd-on-desk",
      parentDir,
      configPath: pluginDir,
      configMode: "plugin-dir",
      managedFiles: ["plugin.yaml", "__init__.py"],
      configFilePath: path.join(parentDir, "config.yaml"),
    });
    fs.mkdirSync(pluginDir, { recursive: true });
    fs.writeFileSync(path.join(pluginDir, "plugin.yaml"), "name: clawd-on-desk\n", "utf8");

    const detail = runOne(descriptor, {
      prefs: { agents: { hermes: { enabled: true } } },
    });

    assert.strictEqual(detail.status, "not-connected");
    assert.deepStrictEqual(detail.missingPluginFiles, ["__init__.py"]);
    assert.deepStrictEqual(detail.fixAction, { type: "agent-integration", agentId: "hermes" });
  });

  it("does not report Hermes ok when clawd-on-desk appears only in disabled plugins", () => {
    const root = makeTempDir();
    const parentDir = path.join(root, ".hermes");
    const pluginDir = path.join(parentDir, "plugins", "clawd-on-desk");
    const descriptor = baseDescriptor({
      agentId: "hermes",
      marker: "clawd-on-desk",
      parentDir,
      configPath: pluginDir,
      configMode: "plugin-dir",
      managedFiles: ["plugin.yaml", "__init__.py"],
      configFilePath: path.join(parentDir, "config.yaml"),
    });
    fs.mkdirSync(pluginDir, { recursive: true });
    fs.writeFileSync(path.join(pluginDir, "plugin.yaml"), "name: clawd-on-desk\n", "utf8");
    fs.writeFileSync(path.join(pluginDir, "__init__.py"), "# plugin\n", "utf8");
    fs.writeFileSync(
      descriptor.configFilePath,
      "plugins:\n  enabled: []\n  disabled:\n    - clawd-on-desk\n",
      "utf8"
    );

    const detail = runOne(descriptor, {
      prefs: { agents: { hermes: { enabled: true } } },
    });

    assert.strictEqual(detail.status, "not-connected");
    assert.strictEqual(detail.pluginEnabled, false);
    assert.deepStrictEqual(detail.fixAction, { type: "agent-integration", agentId: "hermes" });
  });

  it("accepts Hermes inline enabled plugin lists", () => {
    const root = makeTempDir();
    const parentDir = path.join(root, ".hermes");
    const pluginDir = path.join(parentDir, "plugins", "clawd-on-desk");
    const descriptor = baseDescriptor({
      agentId: "hermes",
      marker: "clawd-on-desk",
      parentDir,
      configPath: pluginDir,
      configMode: "plugin-dir",
      managedFiles: ["plugin.yaml", "__init__.py"],
      configFilePath: path.join(parentDir, "config.yaml"),
    });
    fs.mkdirSync(pluginDir, { recursive: true });
    fs.writeFileSync(path.join(pluginDir, "plugin.yaml"), "name: clawd-on-desk\n", "utf8");
    fs.writeFileSync(path.join(pluginDir, "__init__.py"), "# plugin\n", "utf8");
    fs.writeFileSync(
      descriptor.configFilePath,
      "plugins:\n  enabled: [\"clawd-on-desk\"]\n  disabled: []\n",
      "utf8"
    );

    const detail = runOne(descriptor, {
      prefs: { agents: { hermes: { enabled: true } } },
    });

    assert.strictEqual(detail.status, "ok");
    assert.strictEqual(detail.pluginEnabled, true);
  });

  it("keeps Hermes disabled as info-only by default", () => {
    const descriptor = baseDescriptor({
      agentId: "hermes",
      marker: "clawd-on-desk",
      configMode: "plugin-dir",
    });

    const detail = runOne(descriptor, {
      prefs: { agents: { hermes: { enabled: false } } },
    });

    assert.strictEqual(detail.status, "disabled");
    assert.strictEqual(detail.level, "info");
  });

  it("adds a non-failing note when per-agent permission bubbles are disabled", () => {
    const descriptor = baseDescriptor({ agentId: "codex", marker: "codex-hook.js" });
    writeJson(descriptor.configPath, {
      hooks: {
        Stop: [{ command: '"/node" "/app/hooks/codex-hook.js"' }],
      },
    });

    const detail = runOne(descriptor, {
      prefs: { agents: { codex: { enabled: true, permissionsEnabled: false } } },
    });
    assert.strictEqual(detail.status, "ok");
    assert.strictEqual(detail.permissionsEnabled, false);
    assert.strictEqual(detail.permissionBubbleDetail, "permission bubbles disabled for this agent");
  });

  it("aggregates all-info states as pass (no false critical) when nothing is active", () => {
    // none-global agents (info status `manual-only`) + missing agents only.
    // Every integration being disabled / manual / not installed is a user or
    // environment choice, not a fault, so the summary must stay green (#490).
    const result = checkAgentIntegrations({
      fs,
      descriptors: [
        baseDescriptor({ agentId: "hypothetical-none-global", configMode: "none-global" }),
        baseDescriptor({ agentId: "missing-agent" }),
      ],
    });
    assert.strictEqual(result.status, "pass");
    assert.strictEqual(result.level, null);
  });

  it("stays warning when a real problem coexists with info-only integrations", () => {
    // One auto-install agent with a missing config (not-connected → warning)
    // plus a disabled agent (info). The warning must still drive the summary;
    // the #490 de-escalation only applies when nothing is actually wrong.
    const brokenDescriptor = baseDescriptor({ agentId: "broken-agent", marker: "broken-hook.js" });
    fs.mkdirSync(brokenDescriptor.parentDir, { recursive: true });
    const disabledDescriptor = baseDescriptor({ agentId: "off-agent", marker: "off-hook.js" });

    const result = checkAgentIntegrations({
      fs,
      prefs: { agents: { "off-agent": { enabled: false } } },
      descriptors: [brokenDescriptor, disabledDescriptor],
    });

    assert.strictEqual(result.status, "warning");
    assert.strictEqual(result.level, "warning");
    assert.strictEqual(result.warningCount, 1);
    assert.strictEqual(result.okCount, 0);
  });

  it("keeps the integration summary in warning when Gemini hooks are disabled", () => {
    const descriptor = baseDescriptor({
      agentId: "gemini-cli",
      marker: "gemini-hook.js",
      nested: true,
    });
    writeJson(descriptor.configPath, {
      hooks: geminiHooksConfig(),
      hooksConfig: {
        enabled: false,
      },
    });

    const result = checkAgentIntegrations({
      fs,
      descriptors: [descriptor],
      validateCommand: () => ({
        ok: true,
        nodeBin: "/node",
        scriptPath: "/app/hooks/gemini-hook.js",
      }),
    });
    assert.strictEqual(result.status, "warning");
    assert.strictEqual(result.level, "warning");
    assert.strictEqual(result.warningCount, 1);
    assert.strictEqual(result.okCount, 0);
  });
});

describe("findOpencodePluginEntry", () => {
  it("matches only absolute plugin entries by basename", () => {
    const absEntry = "C:\\clawd\\hooks\\opencode-plugin";
    assert.strictEqual(
      findOpencodePluginEntry(["vendor/opencode-plugin", absEntry], "opencode-plugin"),
      absEntry
    );
  });
});

describe("findOpenClawPluginEntry", () => {
  it("matches only absolute plugin entries by basename", () => {
    const absEntry = "C:\\clawd\\hooks\\openclaw-plugin";
    assert.strictEqual(
      findOpenClawPluginEntry(["vendor/openclaw-plugin", absEntry], "openclaw-plugin"),
      absEntry
    );
  });
});
