import { describe, it, expect, vi } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { Command } from 'commander';
import { SkillInstaller } from '../skills/index.js';
import {
  toSkillListRows,
  renderSkillsTable,
  renderSkillInfo,
  summarizeSkillsRefresh,
  renderSkillsRefreshSummary,
  installSkillFromDirectory,
  uninstallSkillByName,
  toSkillInstallerPlanView,
  renderSkillInstallerPlan,
  toSkillInstallPreflightView,
  renderSkillInstallPreflight,
  toSkillInstallerExecutionStubView,
  renderSkillInstallerExecutionStub,
  toSkillInstallerExecutionStubFromPreflight,
  evaluateInstallerExecutionPolicy,
  toInstallerExecutionStepEnvelopes,
  mergeInstallerExecutionResults,
  runInstallerCommandsWithPolicy,
  noOpSkillInstallerCommandRunner,
  createShellSkillInstallerCommandRunner,
  checkCommandAgainstAllowlist,
  emitShellRunnerAuditEvents,
  calculateShellRunnerHashCoveragePercent,
  computeShellRunnerAuditTrendSnapshot,
  evaluateShellRunnerPromotionPolicy,
  evaluateShellRunnerRolloutGuardrails,
  hashSkillInstallerAuditCommand,
  recommendShellRunnerRolloutPhase,
  sanitizeSkillInstallerAuditReason,
  summarizeShellRunnerAuditWindow,
  toShellRunnerPromotionContract,
  resolveSkillInstallerCommandRunner,
  runSkillExecuteAction,
  runSkillInstallAction,
  registerSkillsCommand,
} from './skills.js';
import type { Skill } from '../skills/index.js';
import type { AuditEvent } from '../audit/types.js';

function buildSkill(overrides: Partial<Skill>): Skill {
  return {
    manifest: {
      name: 'sample-skill',
      description: 'Sample skill',
      version: '1.0.0',
      tier: 'workspace',
      ...overrides.manifest,
    },
    instructions: '# Sample skill',
    directory: '/tmp/sample-skill',
    available: true,
    ...overrides,
  };
}

function writeSkillsCliConfig(
  configPath: string,
  opts: {
    managedDir: string;
    bundledDir: string;
    workspaceDir: string;
    installationExecution?: 'disabled' | 'enabled';
    allowShellRunner?: boolean;
    shellRunnerAllowlist?: string[];
    shellRunnerGovernanceOwner?: string;
    auditEnabled?: boolean;
    auditPath?: string;
  },
): void {
  const allowlist = opts.shellRunnerAllowlist ?? [];
  const auditLines = opts.auditPath
    ? ['audit:', `  enabled: ${opts.auditEnabled ?? true}`, `  path: ${opts.auditPath}`]
    : [];
  const governanceOwnerLines = opts.shellRunnerGovernanceOwner
    ? ['  shell_runner_governance:', `    owner: '${opts.shellRunnerGovernanceOwner}'`]
    : [];
  writeFileSync(
    configPath,
    [
      'models:',
      '  default:',
      '    provider: ollama',
      '    model: test-model',
      'skills:',
      `  managed_dir: ${opts.managedDir}`,
      `  bundled_dir: ${opts.bundledDir}`,
      `  workspace_dir: ${opts.workspaceDir}`,
      `  installation_execution: ${opts.installationExecution ?? 'disabled'}`,
      `  allow_shell_runner: ${opts.allowShellRunner ?? false}`,
      `  shell_runner_allowlist: [${allowlist.map((item) => `'${item}'`).join(', ')}]`,
      ...governanceOwnerLines,
      ...auditLines,
    ].join('\n'),
    'utf-8',
  );
}

describe('skills CLI helpers', () => {
  it('maps and sorts skill rows', () => {
    const rows = toSkillListRows([
      buildSkill({
        manifest: {
          name: 'zeta',
          description: 'zeta',
          version: '1.0.0',
          tier: 'managed',
        },
      }),
      buildSkill({
        manifest: {
          name: 'alpha',
          description: 'alpha',
          version: '1.0.0',
          tier: 'bundled',
        },
      }),
    ]);

    expect(rows.map((row) => row.name)).toEqual(['alpha', 'zeta']);
    expect(rows[0]?.status).toBe('available');
  });

  it('includes unavailable reason text', () => {
    const rows = toSkillListRows([
      buildSkill({
        available: false,
        unavailableReasons: ['Required binary not found', 'Missing API key'],
      }),
    ]);

    expect(rows[0]?.status).toBe('unavailable');
    expect(rows[0]?.reason).toBe('Required binary not found; Missing API key');
  });

  it('renders a no-skills message when empty', () => {
    expect(renderSkillsTable([])).toBe('No skills found.');
  });

  it('renders detailed skill info for available skill', () => {
    const output = renderSkillInfo(
      buildSkill({
        manifest: {
          name: 'deploy',
          description: 'Deployment helper',
          version: '2.0.0',
          tier: 'bundled',
          author: 'Flynn',
          tools: ['shell.exec', 'git.status'],
        },
        directory: '/opt/flynn/skills/deploy',
      }),
    );

    expect(output).toContain('Name: deploy');
    expect(output).toContain('Status: available');
    expect(output).toContain('Tools: shell.exec, git.status');
    expect(output).toContain('Directory: /opt/flynn/skills/deploy');
  });

  it('renders unavailable reasons when skill is unavailable', () => {
    const output = renderSkillInfo(
      buildSkill({
        available: false,
        unavailableReasons: ['Required binary not found'],
      }),
    );

    expect(output).toContain('Status: unavailable');
    expect(output).toContain('Unavailable reasons: Required binary not found');
  });

  it('renders dry-run installer plan when manifest installers are present', () => {
    const output = renderSkillInfo(
      buildSkill({
        manifest: {
          name: 'installer-aware',
          description: 'Installer-aware skill',
          version: '1.0.0',
          tier: 'bundled',
          installers: [
            { type: 'download', url: 'https://example.com/tool.tgz', destination: '/tmp/tool.tgz' },
          ],
        },
      }),
    );

    expect(output).toContain('Installer plan mode: dry-run');
    expect(output).toContain('Installer planned steps:');
    expect(output).toContain('[download] download https://example.com/tool.tgz -> /tmp/tool.tgz');
  });

  it('builds installer plan view for automation output', () => {
    const view = toSkillInstallerPlanView(
      buildSkill({
        manifest: {
          name: 'plan-target',
          description: 'Plan me',
          version: '3.2.1',
          tier: 'managed',
          installers: [{ type: 'download', url: 'https://example.com/bin.tar.gz' }],
        },
      }),
    );

    expect(view.skill.name).toBe('plan-target');
    expect(view.mode).toBe('dry-run');
    expect(view.steps.length).toBe(1);
    expect(view.steps[0]?.installerType).toBe('download');
  });

  it('renders installer plan summary text', () => {
    const output = renderSkillInstallerPlan({
      skill: { name: 'plan-target', tier: 'bundled', version: '1.0.0' },
      mode: 'dry-run',
      steps: [{ installerType: 'download', command: 'download https://example.com/tool -> /tmp/tool' }],
      skipped: [{ installerType: 'brew', reason: 'brew not available in PATH' }],
    });

    expect(output).toContain("Installer plan for 'plan-target'");
    expect(output).toContain('Planned steps:');
    expect(output).toContain('[download] download https://example.com/tool -> /tmp/tool');
    expect(output).toContain('Skipped steps:');
    expect(output).toContain('[brew] brew not available in PATH');
  });

  it('builds install preflight view from a source skill directory', () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const sourceDir = join(root, 'source-skill');
    mkdirSync(sourceDir, { recursive: true });
    writeFileSync(join(sourceDir, 'SKILL.md'), '# Preflight Skill\nInstructions');
    writeFileSync(
      join(sourceDir, 'manifest.json'),
      JSON.stringify({
        name: 'preflight-skill',
        description: 'Preflight test',
        version: '1.0.0',
        installers: [{ type: 'download', url: 'https://example.com/tool.tgz' }],
      }),
      'utf-8',
    );

    const view = toSkillInstallPreflightView(sourceDir);

    expect(view).not.toBeNull();
    expect(view?.skill.name).toBe('preflight-skill');
    expect(view?.steps[0]?.installerType).toBe('download');

    rmSync(root, { recursive: true, force: true });
  });

  it('returns null install preflight view when source is invalid', () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const sourceDir = join(root, 'invalid-source-skill');
    mkdirSync(sourceDir, { recursive: true });

    const view = toSkillInstallPreflightView(sourceDir);

    expect(view).toBeNull();
    rmSync(root, { recursive: true, force: true });
  });

  it('renders install preflight output text', () => {
    const output = renderSkillInstallPreflight({
      sourcePath: '/tmp/source-skill',
      skill: { name: 'preflight-skill', tier: 'managed', version: '1.0.0' },
      mode: 'dry-run',
      steps: [{ installerType: 'download', command: 'download https://example.com/tool.tgz -> <default destination>' }],
      skipped: [],
    });

    expect(output).toContain("Install preflight for 'preflight-skill' from /tmp/source-skill");
    expect(output).toContain('Planned installer steps:');
    expect(output).toContain('[download] download https://example.com/tool.tgz -> <default destination>');
  });

  it('builds installer execution stub view from skill plan', () => {
    const view = toSkillInstallerExecutionStubView(
      buildSkill({
        manifest: {
          name: 'exec-stub',
          description: 'Execution stub test',
          version: '1.1.0',
          tier: 'managed',
          installers: [{ type: 'download', url: 'https://example.com/tool.tgz' }],
        },
      }),
    );

    expect(view.execution).toBe('stub');
    expect(view.mode).toBe('stub');
    expect(view.confirmed).toBe(false);
    expect(view.execution_enabled).toBe(false);
    expect(view.executed).toEqual([]);
    expect(view.reason).toBe('execution_disabled');
    expect(view.attempted.length).toBe(1);
    expect(view.attempted[0]).toEqual({
      installer_type: 'download',
      command: expect.stringContaining('download https://example.com/tool.tgz'),
    });
    expect(view.results[0]).toEqual({
      installer_type: 'download',
      command: expect.stringContaining('download https://example.com/tool.tgz'),
      status: 'skipped',
      reason: 'execution_disabled',
    });
    expect(view.wouldRun.length).toBe(1);
    expect(view.wouldRun[0]).toContain('download https://example.com/tool.tgz');
  });

  it('renders installer execution stub output text', () => {
    const output = renderSkillInstallerExecutionStub({
      skill: { name: 'exec-stub', tier: 'bundled', version: '1.0.0' },
      execution: 'stub',
      mode: 'stub',
      confirmed: false,
      execution_enabled: false,
      executed: [],
      reason: 'execution_disabled',
      attempted: [{ installer_type: 'brew', command: 'brew install jq' }],
      results: [{ installer_type: 'brew', command: 'brew install jq', status: 'skipped', reason: 'execution_disabled' }],
      wouldRun: ['brew install jq'],
      skipped: [{ installerType: 'node', reason: 'neither pnpm nor npm available in PATH' }],
    });

    expect(output).toContain("Installer execution stub for 'exec-stub'");
    expect(output).toContain('No installer commands were executed.');
    expect(output).toContain('Would run:');
    expect(output).toContain('- brew install jq');
    expect(output).toContain('Skipped:');
    expect(output).toContain('Results:');
  });

  it('renders execution report text when commands are executed', () => {
    const output = renderSkillInstallerExecutionStub({
      skill: { name: 'exec-stub', tier: 'bundled', version: '1.0.0' },
      execution: 'stub',
      mode: 'stub',
      confirmed: true,
      execution_enabled: true,
      executed: ['brew install jq'],
      reason: 'execution_enabled',
      attempted: [{ installer_type: 'brew', command: 'brew install jq' }],
      results: [{ installer_type: 'brew', command: 'brew install jq', status: 'succeeded', reason: 'runner_reported_success' }],
      wouldRun: ['brew install jq'],
      skipped: [],
    });

    expect(output).toContain('Installer commands were executed.');
    expect(output).toContain('[brew] succeeded brew install jq (runner_reported_success)');
  });

  it('renders policy guidance when execution is blocked by config policy', () => {
    const output = renderSkillInstallerExecutionStub({
      skill: { name: 'exec-stub', tier: 'bundled', version: '1.0.0' },
      execution: 'stub',
      mode: 'install',
      confirmed: true,
      execution_enabled: false,
      executed: [],
      reason: 'execution_policy_disabled',
      attempted: [{ installer_type: 'brew', command: 'brew install jq' }],
      results: [{ installer_type: 'brew', command: 'brew install jq', status: 'skipped', reason: 'execution_policy_disabled' }],
      wouldRun: ['brew install jq'],
      skipped: [],
    });

    expect(output).toContain('Execution policy blocked installer commands.');
    expect(output).toContain('skills.installation_execution: enabled');
  });

  it('derives execution stub view from preflight data', () => {
    const preflight = {
      sourcePath: '/tmp/source-skill',
      skill: { name: 'exec-stub', tier: 'managed' as const, version: '1.0.0' },
      mode: 'dry-run' as const,
      steps: [{ installerType: 'download', command: 'download https://example.com/a.tgz -> /tmp/a.tgz' }],
      skipped: [],
    };

    const view = toSkillInstallerExecutionStubFromPreflight(preflight, { mode: 'install', confirmed: true });

    expect(view.execution).toBe('stub');
    expect(view.mode).toBe('install');
    expect(view.confirmed).toBe(true);
    expect(view.execution_enabled).toBe(false);
    expect(view.executed).toEqual([]);
    expect(view.reason).toBe('execution_disabled');
    expect(view.attempted).toEqual([
      {
        installer_type: 'download',
        command: 'download https://example.com/a.tgz -> /tmp/a.tgz',
      },
    ]);
    expect(view.results).toEqual([
      {
        installer_type: 'download',
        command: 'download https://example.com/a.tgz -> /tmp/a.tgz',
        status: 'skipped',
        reason: 'execution_disabled',
      },
    ]);
    expect(view.wouldRun).toEqual(['download https://example.com/a.tgz -> /tmp/a.tgz']);
  });

  it('builds blocked step envelopes when confirmation is required', () => {
    const policy = evaluateInstallerExecutionPolicy({ mode: 'install', confirmed: false, executionRequested: true });

    const envelopes = toInstallerExecutionStepEnvelopes(
      [{ installerType: 'brew', command: 'brew install jq' }],
      policy,
    );

    expect(envelopes.attempted).toEqual([{ installer_type: 'brew', command: 'brew install jq' }]);
    expect(envelopes.results).toEqual([
      {
        installer_type: 'brew',
        command: 'brew install jq',
        status: 'blocked',
        reason: 'confirmation_required',
      },
    ]);
  });

  it('marks install execution policy as confirmation_required when not confirmed', () => {
    const policy = evaluateInstallerExecutionPolicy({ mode: 'install', confirmed: false, executionRequested: true });

    expect(policy.confirmed).toBe(false);
    expect(policy.execution_enabled).toBe(false);
    expect(policy.reason).toBe('confirmation_required');
  });

  it('keeps execution policy disabled after confirmation', () => {
    const policy = evaluateInstallerExecutionPolicy({ mode: 'install', confirmed: true });

    expect(policy.confirmed).toBe(true);
    expect(policy.execution_enabled).toBe(false);
    expect(policy.reason).toBe('execution_disabled');
  });

  it('enables install execution only when requested and confirmed', () => {
    const policy = evaluateInstallerExecutionPolicy({ mode: 'install', confirmed: true, executionRequested: true });

    expect(policy.confirmed).toBe(true);
    expect(policy.execution_enabled).toBe(true);
    expect(policy.reason).toBe('execution_enabled');
  });

  it('keeps execution disabled when config policy is disabled', () => {
    const policy = evaluateInstallerExecutionPolicy({
      mode: 'install',
      confirmed: true,
      executionRequested: true,
      configPolicyEnabled: false,
    });

    expect(policy.confirmed).toBe(true);
    expect(policy.execution_enabled).toBe(false);
    expect(policy.reason).toBe('execution_policy_disabled');
  });

  it('matches shell command allowlist patterns with wildcard support', () => {
    expect(checkCommandAgainstAllowlist('npm install -g zx', ['npm install*'])).toBe(true);
    expect(checkCommandAgainstAllowlist('node -e "process.exit(0)"', ['node -e*'])).toBe(true);
    expect(checkCommandAgainstAllowlist('rm -rf /tmp/demo', ['npm install*'])).toBe(false);
    expect(checkCommandAgainstAllowlist('echo hi', [])).toBe(false);
  });

  it('emits audit denied events for allowlist-blocked shell commands', () => {
    const logger = {
      skillsInstallerExecutionBlocked: vi.fn(),
      skillsInstallerCommandResult: vi.fn(),
    };

    emitShellRunnerAuditEvents({
      skillName: 'audit-skill',
      phase: 'install',
      executionRequested: true,
      executionEnabled: true,
      reason: 'execution_enabled',
      results: [
        {
          installer_type: 'download',
          command: 'download https://example.com/pkg.tgz -> <default destination>',
          status: 'failed',
          reason: 'allowlist_blocked',
        },
      ],
      logger,
    });

    expect(logger.skillsInstallerCommandResult).toHaveBeenCalledWith(
      expect.objectContaining({
        skill_name: 'audit-skill',
        phase: 'install',
        status: 'failed',
        reason: 'allowlist_blocked',
      }),
    );
    expect(logger.skillsInstallerCommandResult).toHaveBeenCalledWith(
      expect.objectContaining({
        command: hashSkillInstallerAuditCommand('download https://example.com/pkg.tgz -> <default destination>'),
      }),
    );
    expect(logger.skillsInstallerExecutionBlocked).not.toHaveBeenCalled();
  });

  it('hashes audit command values deterministically for non-sensitive commands', () => {
    const command = 'download https://example.com/tool.tgz -> <default destination>';
    expect(hashSkillInstallerAuditCommand(command)).toBe(hashSkillInstallerAuditCommand(command));
    expect(hashSkillInstallerAuditCommand(command)).toMatch(/^sha256:[a-f0-9]{64}$/);
  });

  it('sanitizes spawn_error reason values for audit events', () => {
    expect(sanitizeSkillInstallerAuditReason('spawn_error:ENOENT some sensitive detail')).toBe('spawn_error');
    expect(sanitizeSkillInstallerAuditReason('allowlist_blocked')).toBe('allowlist_blocked');
  });

  it('reports shell runner rollout guardrail blockers', () => {
    const guardrails = evaluateShellRunnerRolloutGuardrails(
      {
        installation_execution: 'disabled',
        allow_shell_runner: false,
        shell_runner_allowlist: ['*'],
        shell_runner_governance: {
          review_cadence_days: 7,
          promotion_min_success_rate: 0.9,
        },
        load: { watch: false, watch_debounce_ms: 250 },
      },
      false,
    );

    expect(guardrails.blockers).toEqual([
      'skills.installation_execution must be enabled',
      'skills.allow_shell_runner must be true',
      "skills.shell_runner_allowlist cannot include wildcard-only '*' patterns",
      'audit.enabled must be true for shell runner rollout review',
    ]);
  });

  it('requires governance owner when shell runner is enabled', () => {
    const guardrails = evaluateShellRunnerRolloutGuardrails(
      {
        installation_execution: 'enabled',
        allow_shell_runner: true,
        shell_runner_allowlist: ['npm install*'],
        shell_runner_governance: {
          review_cadence_days: 7,
          promotion_min_success_rate: 0.9,
        },
        load: { watch: false, watch_debounce_ms: 250 },
      },
      true,
    );

    expect(guardrails.blockers).toContain('skills.shell_runner_governance.owner must be set when shell runner is enabled');
  });

  it('summarizes shell runner audit windows with hash coverage and failures', () => {
    const events: AuditEvent[] = [
      {
        timestamp: 1,
        level: 'debug',
        event_type: 'skills.installer.command_result',
        event: {
          skill_name: 'audit-skill',
          phase: 'install',
          installer_type: 'node',
          command: 'sha256:abc',
          status: 'succeeded',
          reason: 'runner_reported_success',
        },
      },
      {
        timestamp: 2,
        level: 'warn',
        event_type: 'skills.installer.command_result',
        event: {
          skill_name: 'audit-skill',
          phase: 'install',
          installer_type: 'download',
          command: 'download https://example.com/pkg.tgz',
          status: 'failed',
          reason: 'allowlist_blocked',
        },
      },
      {
        timestamp: 3,
        level: 'warn',
        event_type: 'skills.installer.execution_blocked',
        event: {
          skill_name: 'audit-skill',
          phase: 'execute',
          execution_requested: true,
          execution_enabled: false,
          reason: 'execution_policy_disabled',
          attempted_command_count: 1,
        },
      },
    ];

    expect(summarizeShellRunnerAuditWindow(events)).toEqual({
      command_result_total: 2,
      command_result_failed: 1,
      allowlist_blocked: 1,
      execution_blocked: 1,
      hashed_command_count: 1,
      unhashed_command_count: 1,
    });
  });

  it('calculates hash coverage percentage for shell runner summaries', () => {
    expect(
      calculateShellRunnerHashCoveragePercent({
        command_result_total: 0,
        command_result_failed: 0,
        allowlist_blocked: 0,
        execution_blocked: 0,
        hashed_command_count: 0,
        unhashed_command_count: 0,
      }),
    ).toBe(0);

    expect(
      calculateShellRunnerHashCoveragePercent({
        command_result_total: 4,
        command_result_failed: 0,
        allowlist_blocked: 0,
        execution_blocked: 0,
        hashed_command_count: 3,
        unhashed_command_count: 1,
      }),
    ).toBe(75);
  });

  it('computes shell runner trend snapshot across current and previous windows', () => {
    const now = 1_000_000;
    const oneDay = 24 * 60 * 60 * 1000;
    const window = 7 * oneDay;
    const currentWindowStart = now - window;

    const events: AuditEvent[] = [
      {
        timestamp: now - oneDay,
        level: 'warn',
        event_type: 'skills.installer.command_result',
        event: {
          skill_name: 'demo',
          phase: 'install',
          installer_type: 'node',
          command: 'sha256:new-a',
          status: 'failed',
          reason: 'exit_code_1',
        },
      },
      {
        timestamp: now - oneDay * 2,
        level: 'warn',
        event_type: 'skills.installer.command_result',
        event: {
          skill_name: 'demo',
          phase: 'install',
          installer_type: 'node',
          command: 'sha256:new-b',
          status: 'failed',
          reason: 'allowlist_blocked',
        },
      },
      {
        timestamp: now - window - oneDay,
        level: 'warn',
        event_type: 'skills.installer.command_result',
        event: {
          skill_name: 'demo',
          phase: 'install',
          installer_type: 'node',
          command: 'sha256:old-a',
          status: 'failed',
          reason: 'allowlist_blocked',
        },
      },
      {
        timestamp: now - window - oneDay * 2,
        level: 'info',
        event_type: 'skills.installer.command_result',
        event: {
          skill_name: 'demo',
          phase: 'install',
          installer_type: 'node',
          command: 'legacy-old-command',
          status: 'succeeded',
          reason: 'runner_reported_success',
        },
      },
    ];

    const trend = computeShellRunnerAuditTrendSnapshot({
      events,
      currentWindowStartMs: currentWindowStart,
      currentWindowEndMs: now,
    });

    expect(trend.current.command_result_failed).toBe(2);
    expect(trend.previous.command_result_failed).toBe(1);
    expect(trend.deltas.failures).toBe(1);
    expect(trend.current.allowlist_blocked).toBe(1);
    expect(trend.previous.allowlist_blocked).toBe(1);
    expect(trend.deltas.allowlist_blocks).toBe(0);
    expect(trend.deltas.hash_coverage_pct).toBe(50);
  });

  it('evaluates promotion policy with governance cadence and success thresholds', () => {
    const policy = evaluateShellRunnerPromotionPolicy({
      trend: {
        current: {
          command_result_total: 4,
          command_result_failed: 1,
          allowlist_blocked: 0,
          execution_blocked: 0,
          hashed_command_count: 4,
          unhashed_command_count: 0,
        },
        previous: {
          command_result_total: 4,
          command_result_failed: 0,
          allowlist_blocked: 0,
          execution_blocked: 0,
          hashed_command_count: 4,
          unhashed_command_count: 0,
        },
        deltas: {
          failures: 1,
          allowlist_blocks: 0,
          hash_coverage_pct: 0,
        },
      },
      reviewedWindowDays: 7,
      governance: {
        review_cadence_days: 7,
        promotion_min_success_rate: 0.9,
      },
    });

    expect(policy.eligible).toBe(false);
    expect(policy.recommendation).toBe('not_eligible');
    expect(policy.blockers).toContain('success rate 75.00% below minimum 90.00%');
    expect(policy.blockers).toContain('failures increased by 1 vs previous window');
  });

  it('builds machine-readable promotion contract with gate status and blockers', () => {
    const contract = toShellRunnerPromotionContract({
      generatedAt: '2026-02-13T00:00:00.000Z',
      days: 7,
      recommendation: 'guarded_review',
      guardrails: { blockers: ['skills.installation_execution must be enabled'] },
      summary: {
        command_result_total: 4,
        command_result_failed: 1,
        allowlist_blocked: 0,
        execution_blocked: 0,
        hashed_command_count: 3,
        unhashed_command_count: 1,
      },
      trend: {
        current: {
          command_result_total: 4,
          command_result_failed: 1,
          allowlist_blocked: 0,
          execution_blocked: 0,
          hashed_command_count: 3,
          unhashed_command_count: 1,
        },
        previous: {
          command_result_total: 4,
          command_result_failed: 0,
          allowlist_blocked: 0,
          execution_blocked: 0,
          hashed_command_count: 4,
          unhashed_command_count: 0,
        },
        deltas: {
          failures: 1,
          allowlist_blocks: 0,
          hash_coverage_pct: -25,
        },
      },
      promotionPolicy: {
        eligible: false,
        recommendation: 'not_eligible',
        cadence_days: 7,
        reviewed_window_days: 7,
        success_rate: 0.75,
        minimum_success_rate: 0.9,
        failures_delta: 1,
        allowlist_blocks_delta: 0,
        hash_coverage_delta_pct: -25,
        blockers: ['success rate 75.00% below minimum 90.00%'],
      },
      governance: {
        owner: 'skills-team',
        review_cadence_days: 7,
        promotion_min_success_rate: 0.9,
      },
    });

    expect(contract.schema).toBe('skills.rollout.promotion_contract.v1');
    expect(contract.gate.status).toBe('fail');
    expect(contract.gate.exit_code).toBe(1);
    expect(contract.gate.blockers).toContain('skills.installation_execution must be enabled');
    expect(contract.gate.blockers).toContain('success rate 75.00% below minimum 90.00%');
    expect(contract.summary.hash_coverage_pct).toBe(75);
  });

  it('marks promotion policy eligible when thresholds and trends are healthy', () => {
    const policy = evaluateShellRunnerPromotionPolicy({
      trend: {
        current: {
          command_result_total: 5,
          command_result_failed: 0,
          allowlist_blocked: 0,
          execution_blocked: 0,
          hashed_command_count: 5,
          unhashed_command_count: 0,
        },
        previous: {
          command_result_total: 5,
          command_result_failed: 1,
          allowlist_blocked: 1,
          execution_blocked: 0,
          hashed_command_count: 4,
          unhashed_command_count: 1,
        },
        deltas: {
          failures: -1,
          allowlist_blocks: -1,
          hash_coverage_pct: 20,
        },
      },
      reviewedWindowDays: 7,
      governance: {
        review_cadence_days: 7,
        promotion_min_success_rate: 0.9,
      },
    });

    expect(policy.eligible).toBe(true);
    expect(policy.recommendation).toBe('eligible');
    expect(policy.blockers).toEqual([]);
  });

  it('recommends rollout phase from guardrails and audit summary', () => {
    expect(
      recommendShellRunnerRolloutPhase(
        { blockers: ['skills.installation_execution must be enabled'] },
        {
          command_result_total: 1,
          command_result_failed: 0,
          allowlist_blocked: 0,
          execution_blocked: 0,
          hashed_command_count: 1,
          unhashed_command_count: 0,
        },
      ),
    ).toBe('locked');

    expect(
      recommendShellRunnerRolloutPhase(
        { blockers: [] },
        {
          command_result_total: 0,
          command_result_failed: 0,
          allowlist_blocked: 0,
          execution_blocked: 0,
          hashed_command_count: 0,
          unhashed_command_count: 0,
        },
      ),
    ).toBe('guarded_observe');

    expect(
      recommendShellRunnerRolloutPhase(
        { blockers: [] },
        {
          command_result_total: 4,
          command_result_failed: 1,
          allowlist_blocked: 0,
          execution_blocked: 0,
          hashed_command_count: 4,
          unhashed_command_count: 0,
        },
      ),
    ).toBe('guarded_review');

    expect(
      recommendShellRunnerRolloutPhase(
        { blockers: [] },
        {
          command_result_total: 3,
          command_result_failed: 0,
          allowlist_blocked: 0,
          execution_blocked: 0,
          hashed_command_count: 3,
          unhashed_command_count: 0,
        },
      ),
    ).toBe('expand_candidate');
  });

  it('emits hashed command values for both successful and failed audit command results', () => {
    const logger = {
      skillsInstallerExecutionBlocked: vi.fn(),
      skillsInstallerCommandResult: vi.fn(),
    };

    emitShellRunnerAuditEvents({
      skillName: 'audit-skill',
      phase: 'execute',
      executionRequested: true,
      executionEnabled: true,
      reason: 'execution_enabled',
      results: [
        {
          installer_type: 'node',
          command: 'node -e "process.exit(0)"',
          status: 'succeeded',
          reason: 'runner_reported_success',
        },
        {
          installer_type: 'node',
          command: 'node -e "process.exit(7)"',
          status: 'failed',
          reason: 'exit_code_7',
        },
      ],
      logger,
    });

    expect(logger.skillsInstallerCommandResult).toHaveBeenNthCalledWith(
      1,
      expect.objectContaining({
        command: hashSkillInstallerAuditCommand('node -e "process.exit(0)"'),
        status: 'succeeded',
      }),
    );
    expect(logger.skillsInstallerCommandResult).toHaveBeenNthCalledWith(
      2,
      expect.objectContaining({
        command: hashSkillInstallerAuditCommand('node -e "process.exit(7)"'),
        status: 'failed',
      }),
    );
  });

  it('emits audit denied event when shell execution is policy-blocked', () => {
    const logger = {
      skillsInstallerExecutionBlocked: vi.fn(),
      skillsInstallerCommandResult: vi.fn(),
    };

    emitShellRunnerAuditEvents({
      skillName: 'audit-skill',
      phase: 'execute',
      executionRequested: true,
      executionEnabled: false,
      reason: 'execution_policy_disabled',
      results: [],
      logger,
    });

    expect(logger.skillsInstallerExecutionBlocked).toHaveBeenCalledWith(
      expect.objectContaining({
        skill_name: 'audit-skill',
        phase: 'execute',
        reason: 'execution_policy_disabled',
      }),
    );
    expect(logger.skillsInstallerCommandResult).not.toHaveBeenCalled();
  });

  it('resolves installer runner mode and validates invalid values', () => {
    const noop = resolveSkillInstallerCommandRunner();
    expect('error' in noop).toBe(false);
    if (!('error' in noop)) {
      expect(noop.mode).toBe('noop');
      expect(noop.runner).toBe(noOpSkillInstallerCommandRunner);
    }

    const shell = resolveSkillInstallerCommandRunner('shell');
    expect('error' in shell).toBe(false);
    if (!('error' in shell)) {
      expect(shell.mode).toBe('shell');
      expect(typeof shell.runner.run).toBe('function');
    }

    const invalid = resolveSkillInstallerCommandRunner('invalid');
    expect(invalid).toEqual({ error: "Invalid runner 'invalid'. Allowed values: noop, shell." });
  });

  it('does not invoke command runner when policy disables execution', () => {
    const runner = {
      run: vi.fn((_commands: string[]) => [{ command: 'should-not-run', status: 'succeeded' as const }]),
    };

    const executed = runInstallerCommandsWithPolicy(
      ['brew install jq'],
      { confirmed: false, execution_enabled: false, reason: 'confirmation_required' },
      runner,
    );

    expect(executed).toEqual([]);
    expect(runner.run).not.toHaveBeenCalled();
  });

  it('supports pluggable command runner when policy enables execution', () => {
    const runner = {
      run: vi.fn((commands: string[]) => commands.map((command) => ({ command, status: 'succeeded' as const }))),
    };

    const executed = runInstallerCommandsWithPolicy(
      ['brew install jq'],
      { confirmed: true, execution_enabled: true, reason: 'execution_disabled' },
      runner,
    );

    expect(executed).toEqual([{ command: 'brew install jq', status: 'succeeded' }]);
    expect(runner.run).toHaveBeenCalledWith(['brew install jq']);
  });

  it('shell command runner reports succeeded command status', async () => {
    // This test must not rely on actually spawning a shell (some sandboxed
    // environments disallow spawnSync(/bin/sh) with EPERM).
    vi.resetModules();
    const mockSpawnSync = vi.fn(() => ({ status: 0, signal: null, error: undefined }));
    vi.doMock('child_process', async () => {
      const actual = await vi.importActual<typeof import('child_process')>('child_process');
      return { ...actual, spawnSync: mockSpawnSync };
    });

    const mod = await import('./skills.js');
    const runner = mod.createShellSkillInstallerCommandRunner();

    const results = runner.run(['node -e "process.exit(0)"']);

    expect(results).toEqual([
      {
        command: 'node -e "process.exit(0)"',
        status: 'succeeded',
      },
    ]);
  });

  it('shell command runner reports failed command with exit code reason', async () => {
    vi.resetModules();
    const mockSpawnSync = vi.fn(() => ({ status: 7, signal: null, error: undefined }));
    vi.doMock('child_process', async () => {
      const actual = await vi.importActual<typeof import('child_process')>('child_process');
      return { ...actual, spawnSync: mockSpawnSync };
    });

    const mod = await import('./skills.js');
    const runner = mod.createShellSkillInstallerCommandRunner();

    const results = runner.run(['node -e "process.exit(7)"']);

    expect(results).toEqual([
      {
        command: 'node -e "process.exit(7)"',
        status: 'failed',
        reason: 'exit_code_7',
      },
    ]);
  });

  it('shell command runner blocks commands outside allowlist', () => {
    const runner = createShellSkillInstallerCommandRunner(['npm install*']);

    const results = runner.run(['node -e "process.exit(0)"']);

    expect(results).toEqual([
      {
        command: 'node -e "process.exit(0)"',
        status: 'failed',
        reason: 'allowlist_blocked',
      },
    ]);
  });

  it('maps runner command results into structured per-step statuses', () => {
    const attempted = [
      { installer_type: 'brew', command: 'brew install jq' },
      { installer_type: 'node', command: 'pnpm add -g zx' },
    ];

    const results = mergeInstallerExecutionResults(
      attempted,
      { confirmed: true, execution_enabled: true, reason: 'execution_disabled' },
      [
        { command: 'brew install jq', status: 'succeeded', reason: 'ok' },
        { command: 'pnpm add -g zx', status: 'failed', reason: 'exit_code_1' },
      ],
    );

    expect(results).toEqual([
      {
        installer_type: 'brew',
        command: 'brew install jq',
        status: 'succeeded',
        reason: 'ok',
      },
      {
        installer_type: 'node',
        command: 'pnpm add -g zx',
        status: 'failed',
        reason: 'exit_code_1',
      },
    ]);
  });

  it('marks attempted steps failed when runner does not report a result', () => {
    const results = mergeInstallerExecutionResults(
      [{ installer_type: 'brew', command: 'brew install jq' }],
      { confirmed: true, execution_enabled: true, reason: 'execution_disabled' },
      [],
    );

    expect(results).toEqual([
      {
        installer_type: 'brew',
        command: 'brew install jq',
        status: 'failed',
        reason: 'runner_no_result',
      },
    ]);
  });

  it('fills missing commands with runner_no_result when runner only returns partial results', () => {
    const attempted = [
      { installer_type: 'brew', command: 'brew install jq' },
      { installer_type: 'node', command: 'pnpm add -g zx' },
    ];

    const results = mergeInstallerExecutionResults(
      attempted,
      { confirmed: true, execution_enabled: true, reason: 'execution_enabled' },
      [{ command: 'brew install jq', status: 'succeeded', reason: 'ok' }],
    );

    expect(results).toEqual([
      {
        installer_type: 'brew',
        command: 'brew install jq',
        status: 'succeeded',
        reason: 'ok',
      },
      {
        installer_type: 'node',
        command: 'pnpm add -g zx',
        status: 'failed',
        reason: 'runner_no_result',
      },
    ]);
  });

  it('applies deterministic fallback reasons when runner omits reason fields', () => {
    const attempted = [
      { installer_type: 'brew', command: 'brew install jq' },
      { installer_type: 'node', command: 'pnpm add -g zx' },
    ];

    const results = mergeInstallerExecutionResults(
      attempted,
      { confirmed: true, execution_enabled: true, reason: 'execution_enabled' },
      [
        { command: 'brew install jq', status: 'succeeded' },
        { command: 'pnpm add -g zx', status: 'failed' },
      ],
    );

    expect(results).toEqual([
      {
        installer_type: 'brew',
        command: 'brew install jq',
        status: 'succeeded',
        reason: 'runner_reported_success',
      },
      {
        installer_type: 'node',
        command: 'pnpm add -g zx',
        status: 'failed',
        reason: 'runner_reported_failure',
      },
    ]);
  });

  it('summarizes refresh counts across status and tiers', () => {
    const summary = summarizeSkillsRefresh([
      buildSkill({ manifest: { name: 'a', description: 'a', version: '1.0.0', tier: 'bundled' } }),
      buildSkill({ manifest: { name: 'b', description: 'b', version: '1.0.0', tier: 'managed' } }),
      buildSkill({ available: false, manifest: { name: 'c', description: 'c', version: '1.0.0', tier: 'workspace' } }),
    ]);

    expect(summary.total).toBe(3);
    expect(summary.available).toBe(2);
    expect(summary.unavailable).toBe(1);
    expect(summary.tiers).toEqual({ bundled: 1, managed: 1, workspace: 1 });
  });

  it('renders refresh summary text', () => {
    const output = renderSkillsRefreshSummary({
      total: 4,
      available: 3,
      unavailable: 1,
      tiers: { bundled: 2, managed: 1, workspace: 1 },
    });

    expect(output).toContain('Refreshed 4 skills');
    expect(output).toContain('By tier: bundled=2, managed=1, workspace=1');
  });

  it('installs a local skill directory', () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const sourceDir = join(root, 'source-skill');
    const managedDir = join(root, 'managed');
    mkdirSync(sourceDir, { recursive: true });
    writeFileSync(join(sourceDir, 'SKILL.md'), '# My Skill\nInstructions');
    writeFileSync(
      join(sourceDir, 'manifest.json'),
      JSON.stringify({ name: 'my-skill', description: 'My skill', version: '1.0.0' }),
      'utf-8',
    );

    const installer = new SkillInstaller(managedDir);
    const result = installSkillFromDirectory(installer, sourceDir);

    expect(result.error).toBeUndefined();
    expect(result.skill?.manifest.name).toBe('my-skill');
    expect(existsSync(join(managedDir, 'my-skill', 'SKILL.md'))).toBe(true);

    rmSync(root, { recursive: true, force: true });
  });

  it('returns an error when source directory is missing', () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const installer = new SkillInstaller(join(root, 'managed'));

    const result = installSkillFromDirectory(installer, join(root, 'does-not-exist'));

    expect(result.skill).toBeUndefined();
    expect(result.error).toContain('does not exist');

    rmSync(root, { recursive: true, force: true });
  });

  it('supports plan-only install action mode without installing', () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const sourceDir = join(root, 'source-skill');
    const managedDir = join(root, 'managed');
    mkdirSync(sourceDir, { recursive: true });
    writeFileSync(join(sourceDir, 'SKILL.md'), '# Plan Skill\nInstructions');
    writeFileSync(
      join(sourceDir, 'manifest.json'),
      JSON.stringify({
        name: 'plan-skill',
        description: 'Plan only',
        version: '1.0.0',
        installers: [{ type: 'download', url: 'https://example.com/plan.tgz' }],
      }),
      'utf-8',
    );

    const installer = new SkillInstaller(managedDir);
    const result = runSkillInstallAction(installer, sourceDir, { mode: 'plan-only', asJson: false, confirmed: false });

    expect(result.ok).toBe(true);
    expect(existsSync(join(managedDir, 'plan-skill', 'SKILL.md'))).toBe(false);

    rmSync(root, { recursive: true, force: true });
  });

  it('emits plan-only JSON with no-op execution receipt fields', () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const sourceDir = join(root, 'source-skill');
    const managedDir = join(root, 'managed');
    mkdirSync(sourceDir, { recursive: true });
    writeFileSync(join(sourceDir, 'SKILL.md'), '# Plan Skill\nInstructions');
    writeFileSync(
      join(sourceDir, 'manifest.json'),
      JSON.stringify({
        name: 'plan-skill',
        description: 'Plan only',
        version: '1.0.0',
        installers: [{ type: 'download', url: 'https://example.com/plan.tgz' }],
      }),
      'utf-8',
    );

    const installer = new SkillInstaller(managedDir);
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);

    const result = runSkillInstallAction(installer, sourceDir, { mode: 'plan-only', asJson: true, confirmed: true });

    expect(result.ok).toBe(true);
    expect(logSpy).toHaveBeenCalledTimes(1);
    const payload = JSON.parse(String(logSpy.mock.calls[0]?.[0]));
    expect(payload.execution.confirmed).toBe(true);
    expect(payload.execution.mode).toBe('plan-only');
    expect(payload.execution.execution_enabled).toBe(false);
    expect(payload.execution.executed).toEqual([]);
    expect(payload.execution.reason).toBe('execution_disabled');
    expect(payload.execution.attempted).toEqual([
      {
        installer_type: 'download',
        command: 'download https://example.com/plan.tgz -> <default destination>',
      },
    ]);
    expect(payload.execution.results).toEqual([
      {
        installer_type: 'download',
        command: 'download https://example.com/plan.tgz -> <default destination>',
        status: 'skipped',
        reason: 'execution_disabled',
      },
    ]);

    logSpy.mockRestore();
    rmSync(root, { recursive: true, force: true });
  });

  it('emits install JSON with no-op execution receipt fields', () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const sourceDir = join(root, 'source-skill');
    const managedDir = join(root, 'managed');
    mkdirSync(sourceDir, { recursive: true });
    writeFileSync(join(sourceDir, 'SKILL.md'), '# Install Skill\nInstructions');
    writeFileSync(
      join(sourceDir, 'manifest.json'),
      JSON.stringify({
        name: 'install-skill',
        description: 'Install',
        version: '1.0.0',
        installers: [{ type: 'download', url: 'https://example.com/install.tgz' }],
      }),
      'utf-8',
    );

    const installer = new SkillInstaller(managedDir);
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);

    const result = runSkillInstallAction(installer, sourceDir, { mode: 'install', asJson: true, confirmed: false });

    expect(result.ok).toBe(true);
    expect(logSpy.mock.calls.length).toBeGreaterThan(0);
    const payload = JSON.parse(String(logSpy.mock.calls[logSpy.mock.calls.length - 1]?.[0]));
    expect(payload.execution.confirmed).toBe(false);
    expect(payload.execution.mode).toBe('install');
    expect(payload.execution.execution_enabled).toBe(false);
    expect(payload.execution.executed).toEqual([]);
    expect(payload.execution.reason).toBe('confirmation_required');
    expect(payload.execution.attempted).toEqual([
      {
        installer_type: 'download',
        command: 'download https://example.com/install.tgz -> <default destination>',
      },
    ]);
    expect(payload.execution.results).toEqual([
      {
        installer_type: 'download',
        command: 'download https://example.com/install.tgz -> <default destination>',
        status: 'blocked',
        reason: 'confirmation_required',
      },
    ]);

    logSpy.mockRestore();
    rmSync(root, { recursive: true, force: true });
  });

  it('uses no-op command runner by default in install flow', () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const sourceDir = join(root, 'source-skill');
    const managedDir = join(root, 'managed');
    mkdirSync(sourceDir, { recursive: true });
    writeFileSync(join(sourceDir, 'SKILL.md'), '# Install Skill\nInstructions');
    writeFileSync(
      join(sourceDir, 'manifest.json'),
      JSON.stringify({
        name: 'install-skill',
        description: 'Install',
        version: '1.0.0',
        installers: [{ type: 'download', url: 'https://example.com/install-confirmed.tgz' }],
      }),
      'utf-8',
    );

    const installer = new SkillInstaller(managedDir);
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    const runnerSpy = vi.spyOn(noOpSkillInstallerCommandRunner, 'run');

    const result = runSkillInstallAction(installer, sourceDir, { mode: 'install', asJson: true, confirmed: true });

    expect(result.ok).toBe(true);
    expect(runnerSpy).not.toHaveBeenCalled();
    const payload = JSON.parse(String(logSpy.mock.calls[logSpy.mock.calls.length - 1]?.[0]));
    expect(payload.execution.executed).toEqual([]);
    expect(payload.execution.results).toEqual([
      {
        installer_type: 'download',
        command: 'download https://example.com/install-confirmed.tgz -> <default destination>',
        status: 'skipped',
        reason: 'execution_disabled',
      },
    ]);

    runnerSpy.mockRestore();
    logSpy.mockRestore();
    rmSync(root, { recursive: true, force: true });
  });

  it('runs installer commands when execution is requested and confirmed', () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const sourceDir = join(root, 'source-skill');
    const managedDir = join(root, 'managed');
    mkdirSync(sourceDir, { recursive: true });
    writeFileSync(join(sourceDir, 'SKILL.md'), '# Install Skill\nInstructions');
    writeFileSync(
      join(sourceDir, 'manifest.json'),
      JSON.stringify({
        name: 'install-skill',
        description: 'Install',
        version: '1.0.0',
        installers: [{ type: 'download', url: 'https://example.com/install-exec.tgz' }],
      }),
      'utf-8',
    );

    const installer = new SkillInstaller(managedDir);
    const runner = {
      run: vi.fn((commands: string[]) =>
        commands.map((command) => ({ command, status: 'succeeded' as const, reason: 'runner_reported_success' })),
      ),
    };
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);

    const result = runSkillInstallAction(installer, sourceDir, {
      mode: 'install',
      asJson: true,
      confirmed: true,
      executionRequested: true,
      commandRunner: runner,
    });

    expect(result.ok).toBe(true);
    expect(runner.run).toHaveBeenCalledTimes(1);
    const payload = JSON.parse(String(logSpy.mock.calls[logSpy.mock.calls.length - 1]?.[0]));
    expect(payload.execution.execution_enabled).toBe(true);
    expect(payload.execution.reason).toBe('execution_enabled');
    expect(payload.execution.executed).toEqual(['download https://example.com/install-exec.tgz -> <default destination>']);
    expect(payload.execution.results).toEqual([
      {
        installer_type: 'download',
        command: 'download https://example.com/install-exec.tgz -> <default destination>',
        status: 'succeeded',
        reason: 'runner_reported_success',
      },
    ]);

    logSpy.mockRestore();
    rmSync(root, { recursive: true, force: true });
  });

  it('execute action honors opt-in execution and runner selection', () => {
    const skill = buildSkill({
      manifest: {
        name: 'execute-skill',
        description: 'Execute me',
        version: '1.0.0',
        tier: 'managed',
        installers: [{ type: 'download', url: 'https://example.com/execute.tgz' }],
      },
    });

    const runner = {
      run: vi.fn((commands: string[]) =>
        commands.map((command) => ({ command, status: 'succeeded' as const, reason: 'runner_reported_success' })),
      ),
    };
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);

    const result = runSkillExecuteAction(skill, {
      asJson: true,
      confirmed: true,
      executionRequested: true,
      commandRunner: runner,
    });

    expect(result.ok).toBe(true);
    expect(result.execution.execution_enabled).toBe(true);
    expect(result.execution.reason).toBe('execution_enabled');
    expect(runner.run).toHaveBeenCalledTimes(1);
    const payload = JSON.parse(String(logSpy.mock.calls[0]?.[0]));
    expect(payload.execution_enabled).toBe(true);
    expect(payload.executed).toEqual(['download https://example.com/execute.tgz -> <default destination>']);

    logSpy.mockRestore();
  });

  it('requires --yes confirmation for uninstall helper', () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const installer = new SkillInstaller(join(root, 'managed'));

    const result = uninstallSkillByName(installer, 'any-skill', { confirm: false });

    expect(result.removed).toBeUndefined();
    expect(result.error).toContain('--yes');

    rmSync(root, { recursive: true, force: true });
  });

  it('uninstalls managed skill when confirmed', () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const sourceDir = join(root, 'source-skill');
    const managedDir = join(root, 'managed');
    mkdirSync(sourceDir, { recursive: true });
    writeFileSync(join(sourceDir, 'SKILL.md'), '# Managed\nInstructions');
    writeFileSync(
      join(sourceDir, 'manifest.json'),
      JSON.stringify({ name: 'managed-skill', description: 'Managed', version: '1.0.0' }),
      'utf-8',
    );

    const installer = new SkillInstaller(managedDir);
    installSkillFromDirectory(installer, sourceDir);

    const result = uninstallSkillByName(installer, 'managed-skill', { confirm: true });

    expect(result.error).toBeUndefined();
    expect(result.removed).toBe(true);
    expect(existsSync(join(managedDir, 'managed-skill', 'SKILL.md'))).toBe(false);

    rmSync(root, { recursive: true, force: true });
  });

  it('blocks uninstall of bundled/workspace-only skill', () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const installer = new SkillInstaller(join(root, 'managed'));

    const result = uninstallSkillByName(installer, 'bundled-only', {
      confirm: true,
      discoveredSkill: buildSkill({
        manifest: {
          name: 'bundled-only',
          description: 'Bundled',
          version: '1.0.0',
          tier: 'bundled',
        },
      }),
    });

    expect(result.removed).toBeUndefined();
    expect(result.error).toContain('cannot be uninstalled from managed skills');

    rmSync(root, { recursive: true, force: true });
  });

  it('skills install reports invalid runner via CLI option parsing path', async () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const configPath = join(root, 'config.yaml');
    const managedDir = join(root, 'managed');
    const bundledDir = join(root, 'bundled');
    const workspaceDir = join(root, 'workspace');
    mkdirSync(managedDir, { recursive: true });
    mkdirSync(bundledDir, { recursive: true });
    mkdirSync(workspaceDir, { recursive: true });
    writeSkillsCliConfig(configPath, { managedDir, bundledDir, workspaceDir, installationExecution: 'enabled' });

    const program = new Command();
    registerSkillsCommand(program);

    const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
    process.exitCode = undefined;

    await program.parseAsync(['skills', 'install', '/tmp/any-skill', '--runner', 'invalid', '-c', configPath], { from: 'user' });

    expect(errorSpy).toHaveBeenCalledWith("Invalid runner 'invalid'. Allowed values: noop, shell.");
    expect(process.exitCode).toBe(1);

    errorSpy.mockRestore();
    process.exitCode = undefined;
    rmSync(root, { recursive: true, force: true });
  });

  it('skills install rejects shell runner when config disallows it', async () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const configPath = join(root, 'config.yaml');
    const managedDir = join(root, 'managed');
    const bundledDir = join(root, 'bundled');
    const workspaceDir = join(root, 'workspace');
    mkdirSync(managedDir, { recursive: true });
    mkdirSync(bundledDir, { recursive: true });
    mkdirSync(workspaceDir, { recursive: true });
    writeSkillsCliConfig(configPath, { managedDir, bundledDir, workspaceDir, allowShellRunner: false });

    const program = new Command();
    registerSkillsCommand(program);

    const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    process.exitCode = undefined;

    await program.parseAsync(['skills', 'install', '/tmp/any-skill', '--runner', 'shell', '-c', configPath], { from: 'user' });

    expect(errorSpy).toHaveBeenCalledWith("Runner 'shell' is disabled by config. Set skills.allow_shell_runner: true to enable it.");
    expect(logSpy).not.toHaveBeenCalled();
    expect(process.exitCode).toBe(1);

    errorSpy.mockRestore();
    logSpy.mockRestore();
    process.exitCode = undefined;
    rmSync(root, { recursive: true, force: true });
  });

  it('skills install marks shell execution as allowlist_blocked when command is not allowed', async () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const configPath = join(root, 'config.yaml');
    const sourceDir = join(root, 'source-skill');
    const managedDir = join(root, 'managed');
    const bundledDir = join(root, 'bundled');
    const workspaceDir = join(root, 'workspace');
    mkdirSync(sourceDir, { recursive: true });
    mkdirSync(managedDir, { recursive: true });
    mkdirSync(bundledDir, { recursive: true });
    mkdirSync(workspaceDir, { recursive: true });
    writeSkillsCliConfig(configPath, {
      managedDir,
      bundledDir,
      workspaceDir,
      installationExecution: 'enabled',
      allowShellRunner: true,
      shellRunnerAllowlist: ['echo*'],
    });
    writeFileSync(join(sourceDir, 'SKILL.md'), '# Install Skill\nInstructions');
    writeFileSync(
      join(sourceDir, 'manifest.json'),
      JSON.stringify({
        name: 'cli-install-allowlist-blocked',
        description: 'CLI install allowlist blocked',
        version: '1.0.0',
        installers: [{ type: 'download', url: 'https://example.com/cli-install-allowlist-blocked.tgz' }],
      }),
      'utf-8',
    );

    const program = new Command();
    registerSkillsCommand(program);

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    process.exitCode = undefined;

    await program.parseAsync(
      ['skills', 'install', sourceDir, '--json', '--execute', '--confirm', '--runner', 'shell', '-c', configPath],
      { from: 'user' },
    );

    const payload = JSON.parse(String(logSpy.mock.calls[logSpy.mock.calls.length - 1]?.[0]));
    expect(payload.execution.execution_enabled).toBe(true);
    expect(payload.execution.results).toEqual([
      {
        installer_type: 'download',
        command: 'download https://example.com/cli-install-allowlist-blocked.tgz -> <default destination>',
        status: 'failed',
        reason: 'allowlist_blocked',
      },
    ]);

    logSpy.mockRestore();
    process.exitCode = undefined;
    rmSync(root, { recursive: true, force: true });
  });

  it('skills install parses execute flags and emits execution-enabled JSON receipt', async () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const configPath = join(root, 'config.yaml');
    const sourceDir = join(root, 'source-skill');
    const managedDir = join(root, 'managed');
    const bundledDir = join(root, 'bundled');
    const workspaceDir = join(root, 'workspace');
    mkdirSync(sourceDir, { recursive: true });
    mkdirSync(managedDir, { recursive: true });
    mkdirSync(bundledDir, { recursive: true });
    mkdirSync(workspaceDir, { recursive: true });
    writeSkillsCliConfig(configPath, { managedDir, bundledDir, workspaceDir, installationExecution: 'enabled' });
    writeFileSync(join(sourceDir, 'SKILL.md'), '# Install Skill\nInstructions');
    writeFileSync(
      join(sourceDir, 'manifest.json'),
      JSON.stringify({
        name: 'cli-install-skill',
        description: 'CLI install parse',
        version: '1.0.0',
        installers: [{ type: 'download', url: 'https://example.com/cli-install.tgz' }],
      }),
      'utf-8',
    );

    const program = new Command();
    registerSkillsCommand(program);

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    process.exitCode = undefined;

    await program.parseAsync(
      ['skills', 'install', sourceDir, '--json', '--execute', '--confirm', '--runner', 'noop', '-c', configPath],
      { from: 'user' },
    );

    const payload = JSON.parse(String(logSpy.mock.calls[logSpy.mock.calls.length - 1]?.[0]));
    expect(payload.execution.execution_enabled).toBe(true);
    expect(payload.execution.reason).toBe('execution_enabled');

    logSpy.mockRestore();
    process.exitCode = undefined;
    rmSync(root, { recursive: true, force: true });
  });

  it('skills install JSON uses execution_disabled fallback when --execute is omitted', async () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const configPath = join(root, 'config.yaml');
    const sourceDir = join(root, 'source-skill');
    const managedDir = join(root, 'managed');
    const bundledDir = join(root, 'bundled');
    const workspaceDir = join(root, 'workspace');
    mkdirSync(sourceDir, { recursive: true });
    mkdirSync(managedDir, { recursive: true });
    mkdirSync(bundledDir, { recursive: true });
    mkdirSync(workspaceDir, { recursive: true });
    writeSkillsCliConfig(configPath, { managedDir, bundledDir, workspaceDir });
    writeFileSync(join(sourceDir, 'SKILL.md'), '# Install Skill\nInstructions');
    writeFileSync(
      join(sourceDir, 'manifest.json'),
      JSON.stringify({
        name: 'cli-install-no-exec',
        description: 'CLI install no execute',
        version: '1.0.0',
        installers: [{ type: 'download', url: 'https://example.com/cli-install-no-exec.tgz' }],
      }),
      'utf-8',
    );

    const program = new Command();
    registerSkillsCommand(program);

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    process.exitCode = undefined;

    await program.parseAsync(['skills', 'install', sourceDir, '--json', '--confirm', '-c', configPath], { from: 'user' });

    const payload = JSON.parse(String(logSpy.mock.calls[logSpy.mock.calls.length - 1]?.[0]));
    expect(payload.execution.execution_enabled).toBe(false);
    expect(payload.execution.reason).toBe('execution_disabled');
    expect(payload.execution.results).toEqual([
      {
        installer_type: 'download',
        command: 'download https://example.com/cli-install-no-exec.tgz -> <default destination>',
        status: 'skipped',
        reason: 'execution_disabled',
      },
    ]);

    logSpy.mockRestore();
    process.exitCode = undefined;
    rmSync(root, { recursive: true, force: true });
  });

  it('skills install JSON uses confirmation_required fallback when --confirm is omitted', async () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const configPath = join(root, 'config.yaml');
    const sourceDir = join(root, 'source-skill');
    const managedDir = join(root, 'managed');
    const bundledDir = join(root, 'bundled');
    const workspaceDir = join(root, 'workspace');
    mkdirSync(sourceDir, { recursive: true });
    mkdirSync(managedDir, { recursive: true });
    mkdirSync(bundledDir, { recursive: true });
    mkdirSync(workspaceDir, { recursive: true });
    writeSkillsCliConfig(configPath, { managedDir, bundledDir, workspaceDir });
    writeFileSync(join(sourceDir, 'SKILL.md'), '# Install Skill\nInstructions');
    writeFileSync(
      join(sourceDir, 'manifest.json'),
      JSON.stringify({
        name: 'cli-install-no-confirm',
        description: 'CLI install no confirm',
        version: '1.0.0',
        installers: [{ type: 'download', url: 'https://example.com/cli-install-no-confirm.tgz' }],
      }),
      'utf-8',
    );

    const program = new Command();
    registerSkillsCommand(program);

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    process.exitCode = undefined;

    await program.parseAsync(['skills', 'install', sourceDir, '--json', '-c', configPath], { from: 'user' });

    const payload = JSON.parse(String(logSpy.mock.calls[logSpy.mock.calls.length - 1]?.[0]));
    expect(payload.execution.execution_enabled).toBe(false);
    expect(payload.execution.reason).toBe('confirmation_required');
    expect(payload.execution.results).toEqual([
      {
        installer_type: 'download',
        command: 'download https://example.com/cli-install-no-confirm.tgz -> <default destination>',
        status: 'blocked',
        reason: 'confirmation_required',
      },
    ]);

    logSpy.mockRestore();
    process.exitCode = undefined;
    rmSync(root, { recursive: true, force: true });
  });

  it('skills install rejects --execute without --confirm', async () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const configPath = join(root, 'config.yaml');
    const managedDir = join(root, 'managed');
    const bundledDir = join(root, 'bundled');
    const workspaceDir = join(root, 'workspace');
    mkdirSync(managedDir, { recursive: true });
    mkdirSync(bundledDir, { recursive: true });
    mkdirSync(workspaceDir, { recursive: true });
    writeSkillsCliConfig(configPath, { managedDir, bundledDir, workspaceDir });

    const program = new Command();
    registerSkillsCommand(program);

    const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    process.exitCode = undefined;

    await program.parseAsync(['skills', 'install', '/tmp/any-skill', '--execute', '-c', configPath], { from: 'user' });

    expect(errorSpy).toHaveBeenCalledWith('`--execute` requires `--confirm`. No installer commands were run.');
    expect(logSpy).not.toHaveBeenCalled();
    expect(process.exitCode).toBe(1);

    errorSpy.mockRestore();
    logSpy.mockRestore();
    process.exitCode = undefined;
    rmSync(root, { recursive: true, force: true });
  });

  it('skills execute parses execute flags and emits execution-enabled JSON receipt', async () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const configPath = join(root, 'config.yaml');
    const managedDir = join(root, 'managed');
    const bundledDir = join(root, 'bundled');
    const workspaceDir = join(root, 'workspace');
    const skillDir = join(managedDir, 'cli-exec-skill');
    mkdirSync(skillDir, { recursive: true });
    mkdirSync(bundledDir, { recursive: true });
    mkdirSync(workspaceDir, { recursive: true });
    writeSkillsCliConfig(configPath, { managedDir, bundledDir, workspaceDir, installationExecution: 'enabled' });
    writeFileSync(join(skillDir, 'SKILL.md'), '# Execute Skill\nInstructions');
    writeFileSync(
      join(skillDir, 'manifest.json'),
      JSON.stringify({
        name: 'cli-exec-skill',
        description: 'CLI execute parse',
        version: '1.0.0',
        installers: [{ type: 'download', url: 'https://example.com/cli-exec.tgz' }],
      }),
      'utf-8',
    );

    const program = new Command();
    registerSkillsCommand(program);

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    process.exitCode = undefined;

    await program.parseAsync(
      ['skills', 'execute', 'cli-exec-skill', '--json', '--execute', '--confirm', '--runner', 'noop', '-c', configPath],
      { from: 'user' },
    );

    const payload = JSON.parse(String(logSpy.mock.calls[0]?.[0]));
    expect(payload.execution_enabled).toBe(true);
    expect(payload.reason).toBe('execution_enabled');

    logSpy.mockRestore();
    process.exitCode = undefined;
    rmSync(root, { recursive: true, force: true });
  });

  it('skills execute keeps execution policy-disabled even with --execute --confirm', async () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const configPath = join(root, 'config.yaml');
    const managedDir = join(root, 'managed');
    const bundledDir = join(root, 'bundled');
    const workspaceDir = join(root, 'workspace');
    const skillDir = join(managedDir, 'cli-exec-policy-disabled');
    mkdirSync(skillDir, { recursive: true });
    mkdirSync(bundledDir, { recursive: true });
    mkdirSync(workspaceDir, { recursive: true });
    writeSkillsCliConfig(configPath, { managedDir, bundledDir, workspaceDir, installationExecution: 'disabled' });
    writeFileSync(join(skillDir, 'SKILL.md'), '# Execute Skill\nInstructions');
    writeFileSync(
      join(skillDir, 'manifest.json'),
      JSON.stringify({
        name: 'cli-exec-policy-disabled',
        description: 'CLI execute policy disabled',
        version: '1.0.0',
        installers: [{ type: 'download', url: 'https://example.com/cli-exec-policy-disabled.tgz' }],
      }),
      'utf-8',
    );

    const program = new Command();
    registerSkillsCommand(program);

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    process.exitCode = undefined;

    await program.parseAsync(
      [
        'skills',
        'execute',
        'cli-exec-policy-disabled',
        '--json',
        '--execute',
        '--confirm',
        '--runner',
        'noop',
        '-c',
        configPath,
      ],
      { from: 'user' },
    );

    const payload = JSON.parse(String(logSpy.mock.calls[0]?.[0]));
    expect(payload.execution_enabled).toBe(false);
    expect(payload.reason).toBe('execution_policy_disabled');
    expect(payload.results).toEqual([
      {
        installer_type: 'download',
        command: 'download https://example.com/cli-exec-policy-disabled.tgz -> <default destination>',
        status: 'skipped',
        reason: 'execution_policy_disabled',
      },
    ]);

    logSpy.mockRestore();
    process.exitCode = undefined;
    rmSync(root, { recursive: true, force: true });
  });

  it('skills execute JSON uses execution_disabled fallback when --execute is omitted', async () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const configPath = join(root, 'config.yaml');
    const managedDir = join(root, 'managed');
    const bundledDir = join(root, 'bundled');
    const workspaceDir = join(root, 'workspace');
    const skillDir = join(managedDir, 'cli-exec-no-exec');
    mkdirSync(skillDir, { recursive: true });
    mkdirSync(bundledDir, { recursive: true });
    mkdirSync(workspaceDir, { recursive: true });
    writeSkillsCliConfig(configPath, { managedDir, bundledDir, workspaceDir });
    writeFileSync(join(skillDir, 'SKILL.md'), '# Execute Skill\nInstructions');
    writeFileSync(
      join(skillDir, 'manifest.json'),
      JSON.stringify({
        name: 'cli-exec-no-exec',
        description: 'CLI execute no execute',
        version: '1.0.0',
        installers: [{ type: 'download', url: 'https://example.com/cli-exec-no-exec.tgz' }],
      }),
      'utf-8',
    );

    const program = new Command();
    registerSkillsCommand(program);

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    process.exitCode = undefined;

    await program.parseAsync(['skills', 'execute', 'cli-exec-no-exec', '--json', '--confirm', '-c', configPath], {
      from: 'user',
    });

    const payload = JSON.parse(String(logSpy.mock.calls[0]?.[0]));
    expect(payload.execution_enabled).toBe(false);
    expect(payload.reason).toBe('execution_disabled');
    expect(payload.results).toEqual([
      {
        installer_type: 'download',
        command: 'download https://example.com/cli-exec-no-exec.tgz -> <default destination>',
        status: 'skipped',
        reason: 'execution_disabled',
      },
    ]);

    logSpy.mockRestore();
    process.exitCode = undefined;
    rmSync(root, { recursive: true, force: true });
  });

  it('skills execute rejects --execute without --confirm', async () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const configPath = join(root, 'config.yaml');
    const managedDir = join(root, 'managed');
    const bundledDir = join(root, 'bundled');
    const workspaceDir = join(root, 'workspace');
    const skillDir = join(managedDir, 'cli-exec-skill');
    mkdirSync(skillDir, { recursive: true });
    mkdirSync(bundledDir, { recursive: true });
    mkdirSync(workspaceDir, { recursive: true });
    writeSkillsCliConfig(configPath, { managedDir, bundledDir, workspaceDir });
    writeFileSync(join(skillDir, 'SKILL.md'), '# Execute Skill\nInstructions');
    writeFileSync(
      join(skillDir, 'manifest.json'),
      JSON.stringify({
        name: 'cli-exec-skill',
        description: 'CLI execute parse',
        version: '1.0.0',
      }),
      'utf-8',
    );

    const program = new Command();
    registerSkillsCommand(program);

    const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    process.exitCode = undefined;

    await program.parseAsync(['skills', 'execute', 'cli-exec-skill', '--execute', '-c', configPath], { from: 'user' });

    expect(errorSpy).toHaveBeenCalledWith('`--execute` requires `--confirm`. No installer commands were run.');
    expect(logSpy).not.toHaveBeenCalled();
    expect(process.exitCode).toBe(1);

    errorSpy.mockRestore();
    logSpy.mockRestore();
    process.exitCode = undefined;
    rmSync(root, { recursive: true, force: true });
  });

  it('skills execute rejects shell runner when config disallows it', async () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const configPath = join(root, 'config.yaml');
    const managedDir = join(root, 'managed');
    const bundledDir = join(root, 'bundled');
    const workspaceDir = join(root, 'workspace');
    const skillDir = join(managedDir, 'cli-exec-skill');
    mkdirSync(skillDir, { recursive: true });
    mkdirSync(bundledDir, { recursive: true });
    mkdirSync(workspaceDir, { recursive: true });
    writeSkillsCliConfig(configPath, { managedDir, bundledDir, workspaceDir, allowShellRunner: false });
    writeFileSync(join(skillDir, 'SKILL.md'), '# Execute Skill\nInstructions');
    writeFileSync(
      join(skillDir, 'manifest.json'),
      JSON.stringify({
        name: 'cli-exec-skill',
        description: 'CLI execute parse',
        version: '1.0.0',
      }),
      'utf-8',
    );

    const program = new Command();
    registerSkillsCommand(program);

    const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    process.exitCode = undefined;

    await program.parseAsync(['skills', 'execute', 'cli-exec-skill', '--runner', 'shell', '-c', configPath], { from: 'user' });

    expect(errorSpy).toHaveBeenCalledWith("Runner 'shell' is disabled by config. Set skills.allow_shell_runner: true to enable it.");
    expect(logSpy).not.toHaveBeenCalled();
    expect(process.exitCode).toBe(1);

    errorSpy.mockRestore();
    logSpy.mockRestore();
    process.exitCode = undefined;
    rmSync(root, { recursive: true, force: true });
  });

  it('skills execute marks shell execution as allowlist_blocked when command is not allowed', async () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const configPath = join(root, 'config.yaml');
    const managedDir = join(root, 'managed');
    const bundledDir = join(root, 'bundled');
    const workspaceDir = join(root, 'workspace');
    const skillDir = join(managedDir, 'cli-exec-allowlist-blocked');
    mkdirSync(skillDir, { recursive: true });
    mkdirSync(bundledDir, { recursive: true });
    mkdirSync(workspaceDir, { recursive: true });
    writeSkillsCliConfig(configPath, {
      managedDir,
      bundledDir,
      workspaceDir,
      installationExecution: 'enabled',
      allowShellRunner: true,
      shellRunnerAllowlist: ['echo*'],
    });
    writeFileSync(join(skillDir, 'SKILL.md'), '# Execute Skill\nInstructions');
    writeFileSync(
      join(skillDir, 'manifest.json'),
      JSON.stringify({
        name: 'cli-exec-allowlist-blocked',
        description: 'CLI execute allowlist blocked',
        version: '1.0.0',
        installers: [{ type: 'download', url: 'https://example.com/cli-exec-allowlist-blocked.tgz' }],
      }),
      'utf-8',
    );

    const program = new Command();
    registerSkillsCommand(program);

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    process.exitCode = undefined;

    await program.parseAsync(
      [
        'skills',
        'execute',
        'cli-exec-allowlist-blocked',
        '--json',
        '--execute',
        '--confirm',
        '--runner',
        'shell',
        '-c',
        configPath,
      ],
      { from: 'user' },
    );

    const payload = JSON.parse(String(logSpy.mock.calls[0]?.[0]));
    expect(payload.execution_enabled).toBe(true);
    expect(payload.results).toEqual([
      {
        installer_type: 'download',
        command: 'download https://example.com/cli-exec-allowlist-blocked.tgz -> <default destination>',
        status: 'failed',
        reason: 'allowlist_blocked',
      },
    ]);

    logSpy.mockRestore();
    process.exitCode = undefined;
    rmSync(root, { recursive: true, force: true });
  });

  it('skills rollout-status reports governance owner blocker in JSON output', async () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const configPath = join(root, 'config.yaml');
    const managedDir = join(root, 'managed');
    const bundledDir = join(root, 'bundled');
    const workspaceDir = join(root, 'workspace');
    const auditPath = join(root, 'audit.log');
    mkdirSync(managedDir, { recursive: true });
    mkdirSync(bundledDir, { recursive: true });
    mkdirSync(workspaceDir, { recursive: true });
    writeFileSync(auditPath, '', 'utf-8');
    writeSkillsCliConfig(configPath, {
      managedDir,
      bundledDir,
      workspaceDir,
      installationExecution: 'enabled',
      allowShellRunner: true,
      shellRunnerAllowlist: ['npm install*'],
      auditPath,
    });

    const program = new Command();
    registerSkillsCommand(program);

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    process.exitCode = undefined;

    await program.parseAsync(['skills', 'rollout-status', '--json', '-c', configPath], { from: 'user' });

    const payload = JSON.parse(String(logSpy.mock.calls[0]?.[0]));
    expect(payload.recommendation).toBe('locked');
    expect(payload.promotion_policy.recommendation).toBe('not_eligible');
    expect(payload.governance.owner).toBeNull();
    expect(payload.guardrails.blockers).toContain('skills.shell_runner_governance.owner must be set when shell runner is enabled');

    logSpy.mockRestore();
    process.exitCode = undefined;
    rmSync(root, { recursive: true, force: true });
  });

  it('skills rollout-status writes JSON payload to output file', async () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const configPath = join(root, 'config.yaml');
    const managedDir = join(root, 'managed');
    const bundledDir = join(root, 'bundled');
    const workspaceDir = join(root, 'workspace');
    const auditPath = join(root, 'audit.log');
    const outputPath = join(root, 'rollout-status.json');
    mkdirSync(managedDir, { recursive: true });
    mkdirSync(bundledDir, { recursive: true });
    mkdirSync(workspaceDir, { recursive: true });
    writeFileSync(auditPath, '', 'utf-8');
    writeSkillsCliConfig(configPath, {
      managedDir,
      bundledDir,
      workspaceDir,
      installationExecution: 'enabled',
      allowShellRunner: true,
      shellRunnerAllowlist: ['npm install*'],
      shellRunnerGovernanceOwner: 'skills-team',
      auditPath,
    });

    const program = new Command();
    registerSkillsCommand(program);

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    process.exitCode = undefined;

    await program.parseAsync(['skills', 'rollout-status', '--json', '--out', outputPath, '-c', configPath], { from: 'user' });

    expect(existsSync(outputPath)).toBe(true);
    const payload = JSON.parse(readFileSync(outputPath, 'utf-8'));
    expect(payload.governance.owner).toBe('skills-team');
    expect(payload.recommendation).toBe('guarded_observe');
    expect(payload.trend.current.command_result_total).toBe(0);
    expect(payload.promotion_policy.recommendation).toBe('not_eligible');

    logSpy.mockRestore();
    process.exitCode = undefined;
    rmSync(root, { recursive: true, force: true });
  });

  it('skills rollout-status emits dedicated promotion contract JSON with exit code', async () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const configPath = join(root, 'config.yaml');
    const managedDir = join(root, 'managed');
    const bundledDir = join(root, 'bundled');
    const workspaceDir = join(root, 'workspace');
    const auditPath = join(root, 'audit.log');
    mkdirSync(managedDir, { recursive: true });
    mkdirSync(bundledDir, { recursive: true });
    mkdirSync(workspaceDir, { recursive: true });
    writeFileSync(auditPath, '', 'utf-8');
    writeSkillsCliConfig(configPath, {
      managedDir,
      bundledDir,
      workspaceDir,
      installationExecution: 'enabled',
      allowShellRunner: true,
      shellRunnerAllowlist: ['npm install*'],
      shellRunnerGovernanceOwner: 'skills-team',
      auditPath,
    });

    const program = new Command();
    registerSkillsCommand(program);

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    process.exitCode = undefined;

    await program.parseAsync(['skills', 'rollout-status', '--contract', '-c', configPath], { from: 'user' });

    const payload = JSON.parse(String(logSpy.mock.calls[0]?.[0]));
    expect(payload.schema).toBe('skills.rollout.promotion_contract.v1');
    expect(payload.gate.status).toBe('fail');
    expect(payload.gate.exit_code).toBe(1);
    expect(payload.governance.owner).toBe('skills-team');
    expect(process.exitCode).toBe(1);

    logSpy.mockRestore();
    process.exitCode = undefined;
    rmSync(root, { recursive: true, force: true });
  });

  it('skills rollout-status writes dedicated promotion contract to output file', async () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const configPath = join(root, 'config.yaml');
    const managedDir = join(root, 'managed');
    const bundledDir = join(root, 'bundled');
    const workspaceDir = join(root, 'workspace');
    const auditPath = join(root, 'audit.log');
    const outputPath = join(root, 'rollout-contract.json');
    mkdirSync(managedDir, { recursive: true });
    mkdirSync(bundledDir, { recursive: true });
    mkdirSync(workspaceDir, { recursive: true });
    writeFileSync(auditPath, '', 'utf-8');
    writeSkillsCliConfig(configPath, {
      managedDir,
      bundledDir,
      workspaceDir,
      installationExecution: 'enabled',
      allowShellRunner: true,
      shellRunnerAllowlist: ['npm install*'],
      shellRunnerGovernanceOwner: 'skills-team',
      auditPath,
    });

    const program = new Command();
    registerSkillsCommand(program);

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    process.exitCode = undefined;

    await program.parseAsync(['skills', 'rollout-status', '--contract', '--out', outputPath, '-c', configPath], {
      from: 'user',
    });

    expect(existsSync(outputPath)).toBe(true);
    const payload = JSON.parse(readFileSync(outputPath, 'utf-8'));
    expect(payload.schema).toBe('skills.rollout.promotion_contract.v1');
    expect(payload.gate).toBeDefined();
    expect(payload.summary).toBeDefined();

    logSpy.mockRestore();
    process.exitCode = undefined;
    rmSync(root, { recursive: true, force: true });
  });

  it('skills rollout-status includes trend deltas across adjacent windows', async () => {
    const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
    const configPath = join(root, 'config.yaml');
    const managedDir = join(root, 'managed');
    const bundledDir = join(root, 'bundled');
    const workspaceDir = join(root, 'workspace');
    const auditPath = join(root, 'audit.log');
    const outputPath = join(root, 'rollout-trend.json');
    mkdirSync(managedDir, { recursive: true });
    mkdirSync(bundledDir, { recursive: true });
    mkdirSync(workspaceDir, { recursive: true });

    const now = Date.now();
    const oneDay = 24 * 60 * 60 * 1000;
    const events = [
      {
        timestamp: now - oneDay,
        level: 'warn',
        event_type: 'skills.installer.command_result',
        event: {
          skill_name: 'trend-skill',
          phase: 'install',
          installer_type: 'node',
          command: 'sha256:new',
          status: 'failed',
          reason: 'allowlist_blocked',
        },
      },
      {
        timestamp: now - oneDay * 8,
        level: 'info',
        event_type: 'skills.installer.command_result',
        event: {
          skill_name: 'trend-skill',
          phase: 'install',
          installer_type: 'node',
          command: 'legacy-prev',
          status: 'succeeded',
          reason: 'runner_reported_success',
        },
      },
    ];
    writeFileSync(auditPath, `${events.map((event) => JSON.stringify(event)).join('\n')}\n`, 'utf-8');

    writeSkillsCliConfig(configPath, {
      managedDir,
      bundledDir,
      workspaceDir,
      installationExecution: 'enabled',
      allowShellRunner: true,
      shellRunnerAllowlist: ['npm install*'],
      shellRunnerGovernanceOwner: 'skills-team',
      auditPath,
    });

    const program = new Command();
    registerSkillsCommand(program);

    const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    process.exitCode = undefined;

    await program.parseAsync(['skills', 'rollout-status', '--json', '--out', outputPath, '--days', '7', '-c', configPath], {
      from: 'user',
    });

    const payload = JSON.parse(readFileSync(outputPath, 'utf-8'));
    expect(payload.trend.current.command_result_total).toBe(1);
    expect(payload.trend.previous.command_result_total).toBe(1);
    expect(payload.trend.deltas.failures).toBe(1);
    expect(payload.trend.deltas.allowlist_blocks).toBe(1);
    expect(payload.promotion_policy.recommendation).toBe('not_eligible');
    expect(payload.promotion_policy.blockers).toContain('failures increased by 1 vs previous window');

    logSpy.mockRestore();
    process.exitCode = undefined;
    rmSync(root, { recursive: true, force: true });
  });
});
