# frozen_string_literal: true require_relative 'test_helper' require 'open3' class TestValidate < Minitest::Test include TestHelpers def setup setup_temp_dirs @plugin_dir = File.join(@tmpdir, 'my-plugin') end def teardown teardown_temp_dirs end def build_valid_plugin(name: 'my-plugin', description: 'Does a thing', skills: nil) extra = skills ? { 'skills' => skills } : {} create_plugin_json(@plugin_dir, name: name, description: description, extra: extra) create_skill(@plugin_dir, 'my-skill') create_marketplace(plugins: [ { 'name' => name, 'source' => "./#{File.basename(@plugin_dir)}", 'description' => description } ]) end def make_validate_op(overrides = {}) params = { plugin_path: @plugin_dir, marketplace_file: marketplace_path }.merge(overrides) PluginConfig::Operations::Validate.new(params) end # ─── Happy path ─────────────────────────────────────────────────────────── def test_valid_plugin_passes_all_checks build_valid_plugin result = make_validate_op.call assert_equal 'ok', result[:status] assert result[:valid] assert_empty result[:issues] result[:checks].each_value do |check| assert_equal 'pass', check[:status], "Expected pass, got #{check.inspect}" end end # ─── Failure modes ──────────────────────────────────────────────────────── def test_missing_plugin_json_fails FileUtils.mkdir_p(@plugin_dir) create_marketplace(plugins: []) result = make_validate_op.call refute result[:valid] assert_equal 'issues_found', result[:status] assert_equal 'fail', result[:checks][:plugin_json][:status] end def test_invalid_json_plugin_json_fails path = File.join(@plugin_dir, '.claude-plugin', 'plugin.json') FileUtils.mkdir_p(File.dirname(path)) File.write(path, '{ not valid json') create_marketplace(plugins: []) result = make_validate_op.call refute result[:valid] assert_equal 'fail', result[:checks][:plugin_json][:status] end def test_missing_required_field_fails create_plugin_json(@plugin_dir, name: 'my-plugin', description: '') create_skill(@plugin_dir, 'my-skill') create_marketplace(plugins: [{ 'name' => 'my-plugin', 'source' => './my-plugin', 'description' => '' }]) result = make_validate_op.call refute result[:valid] assert_equal 'fail', result[:checks][:required_fields][:status] end def test_name_mismatch_with_directory_fails build_valid_plugin(name: 'totally-different-name') result = make_validate_op.call refute result[:valid] assert_equal 'fail', result[:checks][:name_matches_directory][:status] end def test_missing_marketplace_entry_fails create_plugin_json(@plugin_dir, name: 'my-plugin', description: 'Does a thing') create_skill(@plugin_dir, 'my-skill') create_marketplace(plugins: []) # no entry for my-plugin result = make_validate_op.call refute result[:valid] assert_equal 'fail', result[:checks][:marketplace_entry][:status] end def test_description_drift_fails create_plugin_json(@plugin_dir, name: 'my-plugin', description: 'Does a thing.') create_skill(@plugin_dir, 'my-skill') create_marketplace(plugins: [ { 'name' => 'my-plugin', 'source' => './my-plugin', 'description' => 'Does a thing' } ]) result = make_validate_op.call refute result[:valid] assert_equal 'fail', result[:checks][:description_matches][:status] end def test_missing_skill_md_fails create_plugin_json(@plugin_dir, name: 'my-plugin', description: 'Does a thing') create_skill(@plugin_dir, 'my-skill', with_skill_md: false) create_marketplace(plugins: [ { 'name' => 'my-plugin', 'source' => './my-plugin', 'description' => 'Does a thing' } ]) result = make_validate_op.call refute result[:valid] assert_equal 'fail', result[:checks][:skill_md_present][:status] end def test_skills_array_mismatch_fails build_valid_plugin(skills: %w[my-skill a-second-skill-not-on-disk]) result = make_validate_op.call refute result[:valid] assert_equal 'fail', result[:checks][:skills_array_matches][:status] end def test_wrong_marketplace_source_fails create_plugin_json(@plugin_dir, name: 'my-plugin', description: 'Does a thing') create_skill(@plugin_dir, 'my-skill') create_marketplace(plugins: [ { 'name' => 'my-plugin', 'source' => './wrong-name', 'description' => 'Does a thing' } ]) result = make_validate_op.call refute result[:valid] assert_equal 'fail', result[:checks][:marketplace_source][:status] end def test_marketplace_source_resolves_path_for_nested_repo_layout # marketplace.json lives at /.claude-plugin/marketplace.json, and plugins # live under /plugins/ instead of directly under . root = File.join(@tmpdir, 'nested-repo') plugin_dir = File.join(root, 'plugins', 'my-plugin') create_plugin_json(plugin_dir, name: 'my-plugin', description: 'Does a thing') create_skill(plugin_dir, 'my-skill') nested_marketplace_dir = File.join(root, '.claude-plugin') FileUtils.mkdir_p(nested_marketplace_dir) nested_marketplace_path = File.join(nested_marketplace_dir, 'marketplace.json') File.write(nested_marketplace_path, JSON.pretty_generate( 'name' => 'test-marketplace', 'plugins' => [ { 'name' => 'my-plugin', 'source' => './plugins/my-plugin', 'description' => 'Does a thing' } ] )) result = PluginConfig::Operations::Validate.new( plugin_path: plugin_dir, marketplace_file: nested_marketplace_path ).call assert_equal 'pass', result[:checks][:marketplace_source][:status], result[:checks][:marketplace_source].inspect end # ─── Cross-repo: graceful skip when no marketplace can be resolved ──────── def test_no_marketplace_resolvable_skips_marketplace_checks_without_failing create_plugin_json(@plugin_dir, name: 'my-plugin', description: 'Does a thing') create_skill(@plugin_dir, 'my-skill') # No marketplace_file override, and no default marketplace.json anywhere near @plugin_dir. result = PluginConfig::Operations::Validate.new(plugin_path: @plugin_dir).call assert_equal 'ok', result[:status] assert result[:valid] %i[marketplace_entry description_matches marketplace_source].each do |key| assert_equal 'skipped', result[:checks][key][:status], "Expected #{key} to be skipped, got #{result[:checks][key].inspect}" end %i[plugin_json required_fields name_matches_directory skill_md_present skills_array_matches].each do |key| assert_equal 'pass', result[:checks][key][:status] end end def test_explicit_missing_marketplace_file_still_fails create_plugin_json(@plugin_dir, name: 'my-plugin', description: 'Does a thing') create_skill(@plugin_dir, 'my-skill') nonexistent = File.join(@tmpdir, 'does-not-exist.json') result = PluginConfig::Operations::Validate.new( plugin_path: @plugin_dir, marketplace_file: nonexistent ).call refute result[:valid] assert_equal 'fail', result[:checks][:marketplace_entry][:status] end def test_default_marketplace_resolution_used_when_present create_plugin_json(@plugin_dir, name: 'my-plugin', description: 'Does a thing') create_skill(@plugin_dir, 'my-skill') default_dir = File.join(@tmpdir, '.claude-plugin') FileUtils.mkdir_p(default_dir) File.write(File.join(default_dir, 'marketplace.json'), JSON.pretty_generate( 'name' => 'test-marketplace', 'plugins' => [ { 'name' => 'my-plugin', 'source' => './my-plugin', 'description' => 'Does a thing' } ] )) result = PluginConfig::Operations::Validate.new(plugin_path: @plugin_dir).call assert_equal 'ok', result[:status] assert result[:valid] result[:checks].each_value { |check| assert_equal 'pass', check[:status] } end end # ─── Entry point (real exit code contract) ─────────────────────────────────── class TestValidateEntryPoint < Minitest::Test include TestHelpers SCRIPT_PATH = File.expand_path('../plugin_config.rb', __dir__) def setup setup_temp_dirs @plugin_dir = File.join(@tmpdir, 'my-plugin') end def teardown teardown_temp_dirs end def run_script(*args) Open3.capture3('ruby', SCRIPT_PATH, *args) end def test_valid_plugin_exits_0 create_plugin_json(@plugin_dir, name: 'my-plugin', description: 'Does a thing') create_skill(@plugin_dir, 'my-skill') create_marketplace(plugins: [ { 'name' => 'my-plugin', 'source' => './my-plugin', 'description' => 'Does a thing' } ]) stdout, stderr, status = run_script( 'validate', "--plugin_path=#{@plugin_dir}", "--marketplace_file=#{marketplace_path}" ) assert status.success?, "Expected exit 0, got #{status.exitstatus}. stderr: #{stderr}" parsed = JSON.parse(stdout) assert_equal 'ok', parsed['status'] end def test_invalid_plugin_exits_1 create_marketplace(plugins: []) # no plugin.json created at all stdout, _stderr, status = run_script( 'validate', "--plugin_path=#{@plugin_dir}", "--marketplace_file=#{marketplace_path}" ) assert_equal 1, status.exitstatus parsed = JSON.parse(stdout) assert_equal 'issues_found', parsed['status'] end def test_validate_is_cwd_independent_with_absolute_plugin_path_and_default_marketplace create_plugin_json(@plugin_dir, name: 'my-plugin', description: 'Does a thing') create_skill(@plugin_dir, 'my-skill') default_dir = File.join(@tmpdir, '.claude-plugin') FileUtils.mkdir_p(default_dir) File.write(File.join(default_dir, 'marketplace.json'), JSON.pretty_generate( 'name' => 'test-marketplace', 'plugins' => [ { 'name' => 'my-plugin', 'source' => './my-plugin', 'description' => 'Does a thing' } ] )) other_cwd = Dir.mktmpdir('elsewhere') begin stdout, stderr, status = Dir.chdir(other_cwd) do run_script('validate', "--plugin_path=#{File.expand_path(@plugin_dir)}") end assert status.success?, "Expected exit 0, got #{status.exitstatus}. stderr: #{stderr}" parsed = JSON.parse(stdout) assert_equal 'ok', parsed['status'] assert parsed['valid'] ensure FileUtils.rm_rf(other_cwd) end end end