cc-os/plugins/cc-architect/scripts/plugin_config/test/test_validate.rb

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