307 lines
11 KiB
Ruby
307 lines
11 KiB
Ruby
# 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 <root>/.claude-plugin/marketplace.json, and plugins
|
|
# live under <root>/plugins/<name> instead of directly under <root>.
|
|
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
|