项目文件夹

文件
wehub-resource-sync 2cab53bc94
Test Vector Database Adaptors / Test MCP Vector DB Tools (push) Has been cancelled
Tests / Code Quality (Ruff & Mypy) (push) Has been cancelled
Tests / Fast Unit Tests (parallel) (macos-latest, 3.11) (push) Has been cancelled
Tests / Fast Unit Tests (parallel) (macos-latest, 3.12) (push) Has been cancelled
Tests / Tests (push) Has been cancelled
Docker Publish / Build and Push Docker Images (map[description:Skill Seekers CLI - Convert documentation to AI skills dockerfile:Dockerfile name:skill-seekers]) (push) Has been cancelled
Docker Publish / Build and Push Docker Images (map[description:Skill Seekers MCP Server - 25 tools for AI assistants dockerfile:Dockerfile.mcp name:skill-seekers-mcp]) (push) Has been cancelled
Docker Publish / Test Docker Images (push) Has been cancelled
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.10) (push) Has been cancelled
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.11) (push) Has been cancelled
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.12) (push) Has been cancelled
Tests / Serial / Integration / E2E Tests (push) Has been cancelled
Tests / MCP Server Tests (push) Has been cancelled
Test Vector Database Adaptors / Test chroma Adaptor (push) Has been cancelled
Test Vector Database Adaptors / Test faiss Adaptor (push) Has been cancelled
Test Vector Database Adaptors / Test qdrant Adaptor (push) Has been cancelled
Test Vector Database Adaptors / Test weaviate Adaptor (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:46:28 +08:00

669 行
27 KiB
Python

#!/usr/bin/env python3
"""Tests for MarketplacePublisher class (skill publishing to plugin repos)"""
import json
import os
from unittest.mock import MagicMock, patch
import pytest
from skill_seekers.services.marketplace_publisher import MarketplacePublisher
@pytest.fixture
def temp_config_dir(tmp_path):
config_dir = tmp_path / "config"
config_dir.mkdir()
return config_dir
@pytest.fixture
def skill_dir(tmp_path):
sd = tmp_path / "test-skill"
sd.mkdir()
(sd / "SKILL.md").write_text(
"---\nname: test-skill\ndescription: A test skill for unit testing.\n---\n\n"
"# Test Skill\n\nThis is a test skill.\n"
)
refs = sd / "references" / "documentation"
refs.mkdir(parents=True)
(refs / "index.md").write_text("# Documentation\n\nTest docs.\n")
return sd
@pytest.fixture
def skill_dir_no_frontmatter(tmp_path):
sd = tmp_path / "plain-skill"
sd.mkdir()
(sd / "SKILL.md").write_text("# Plain Skill\n\nNo frontmatter here.\n")
return sd
@pytest.fixture
def second_skill_dir(tmp_path):
sd = tmp_path / "second-skill"
sd.mkdir()
(sd / "SKILL.md").write_text(
"---\nname: second-skill\ndescription: Another test skill.\n---\n\n# Second Skill\n"
)
return sd
def _make_bare_remote(tmp_path):
"""Create a marketplace repo and a bare clone of it to act as the remote."""
import git as gitmodule
working_path = tmp_path / "working"
working_path.mkdir()
repo = gitmodule.Repo.init(working_path, initial_branch="main")
repo.config_writer().set_value("user", "name", "Test").release()
repo.config_writer().set_value("user", "email", "t@t.com").release()
mp_dir = working_path / ".claude-plugin"
mp_dir.mkdir()
with open(mp_dir / "marketplace.json", "w") as f:
json.dump({"$schema": "", "name": "t", "description": "", "owner": {}, "plugins": []}, f)
(working_path / "plugins").mkdir()
repo.index.add([".claude-plugin/marketplace.json"])
repo.index.commit("Initial commit")
bare_repo_path = tmp_path / "remote.git"
gitmodule.Repo.clone_from(str(working_path), str(bare_repo_path), bare=True)
return bare_repo_path
def _make_manager(tmp_path, git_url):
from skill_seekers.services.marketplace_manager import MarketplaceManager
config_dir = tmp_path / "config"
config_dir.mkdir()
manager = MarketplaceManager(config_dir=str(config_dir))
manager.add_marketplace(
name="local-test",
git_url=git_url,
token_env="DUMMY_TOKEN",
branch="main",
author={"name": "Test", "email": "t@t.com"},
)
return manager
@pytest.fixture
def mock_marketplace_repo(tmp_path):
import git
repo_path = tmp_path / "marketplace_repo"
repo_path.mkdir()
repo = git.Repo.init(repo_path)
mp_dir = repo_path / ".claude-plugin"
mp_dir.mkdir()
mp_json = {
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
"name": "test-marketplace",
"description": "Test marketplace",
"owner": {"name": "Test", "email": "test@example.com"},
"plugins": [
{
"name": "existing-plugin",
"description": "An existing plugin",
"author": {"name": "Test", "email": "test@example.com"},
"source": "./plugins/existing-plugin",
"category": "development",
}
],
}
with open(mp_dir / "marketplace.json", "w") as f:
json.dump(mp_json, f, indent=2)
(repo_path / "plugins").mkdir()
repo.index.add([".claude-plugin/marketplace.json"])
repo.index.commit("Initial commit")
return repo_path
class TestReadFrontmatter:
def test_read_frontmatter_valid(self, skill_dir):
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
fm = publisher._read_frontmatter(skill_dir / "SKILL.md")
assert fm["name"] == "test-skill"
assert fm["description"] == "A test skill for unit testing."
def test_read_frontmatter_no_frontmatter(self, skill_dir_no_frontmatter):
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
fm = publisher._read_frontmatter(skill_dir_no_frontmatter / "SKILL.md")
assert fm == {}
def test_read_frontmatter_empty_file(self, tmp_path):
(tmp_path / "SKILL.md").write_text("")
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
assert publisher._read_frontmatter(tmp_path / "SKILL.md") == {}
class TestCopySkillToPlugin:
def test_copy_creates_correct_structure(self, skill_dir, tmp_path):
plugin_dir = tmp_path / "plugin_output"
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
publisher._copy_skill_to_plugin(skill_dir, plugin_dir, "test-skill")
assert (plugin_dir / "skills" / "test-skill" / "SKILL.md").exists()
assert (
plugin_dir / "skills" / "test-skill" / "references" / "documentation" / "index.md"
).exists()
def test_copy_skill_md_content_preserved(self, skill_dir, tmp_path):
plugin_dir = tmp_path / "plugin_output"
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
publisher._copy_skill_to_plugin(skill_dir, plugin_dir, "test-skill")
original = (skill_dir / "SKILL.md").read_text()
copied = (plugin_dir / "skills" / "test-skill" / "SKILL.md").read_text()
assert original == copied
def test_copy_without_references(self, tmp_path):
skill_dir = tmp_path / "skill-no-refs"
skill_dir.mkdir()
(skill_dir / "SKILL.md").write_text("# Skill\n")
plugin_dir = tmp_path / "plugin_output"
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
publisher._copy_skill_to_plugin(skill_dir, plugin_dir, "test-skill")
assert (plugin_dir / "skills" / "test-skill" / "SKILL.md").exists()
assert not (plugin_dir / "skills" / "test-skill" / "references").exists()
class TestGeneratePluginJson:
def test_generate_plugin_json(self):
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
result = publisher._generate_plugin_json(
"test-skill", "A test skill", {"name": "Test", "email": "test@example.com"}
)
assert result == {
"name": "test-skill",
"description": "A test skill",
"author": {"name": "Test", "email": "test@example.com"},
}
class TestUpdateMarketplaceJson:
def test_update_appends_new_plugin(self, mock_marketplace_repo):
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
author = {"name": "Test", "email": "test@example.com"}
publisher._update_marketplace_json(
mock_marketplace_repo, "new-plugin", "New plugin", author, "development"
)
with open(mock_marketplace_repo / ".claude-plugin" / "marketplace.json") as f:
data = json.load(f)
assert len(data["plugins"]) == 2
assert "new-plugin" in [p["name"] for p in data["plugins"]]
def test_update_existing_plugin(self, mock_marketplace_repo):
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
author = {"name": "Test", "email": "test@example.com"}
publisher._update_marketplace_json(
mock_marketplace_repo, "existing-plugin", "Updated", author, "tools"
)
with open(mock_marketplace_repo / ".claude-plugin" / "marketplace.json") as f:
data = json.load(f)
assert len(data["plugins"]) == 1
assert data["plugins"][0]["description"] == "Updated"
def test_update_sorts_plugins_alphabetically(self, mock_marketplace_repo):
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
author = {"name": "Test", "email": "test@example.com"}
publisher._update_marketplace_json(
mock_marketplace_repo, "aaa-plugin", "First", author, "dev"
)
with open(mock_marketplace_repo / ".claude-plugin" / "marketplace.json") as f:
data = json.load(f)
names = [p["name"] for p in data["plugins"]]
assert names == sorted(names)
def test_update_creates_marketplace_json_if_missing(self, tmp_path):
repo_path = tmp_path / "empty_repo"
repo_path.mkdir()
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
author = {"name": "Test", "email": "test@example.com"}
publisher._update_marketplace_json(repo_path, "new-plugin", "Desc", author, "development")
assert (repo_path / ".claude-plugin" / "marketplace.json").exists()
class TestPublishErrors:
def test_publish_missing_skill_md(self, tmp_path):
empty_dir = tmp_path / "empty"
empty_dir.mkdir()
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
publisher.git_repo = MagicMock()
with pytest.raises(FileNotFoundError, match="SKILL.md not found"):
publisher.publish(skill_dir=empty_dir, marketplace_name="test")
@patch.dict(os.environ, {}, clear=True)
def test_publish_missing_token(self, skill_dir, temp_config_dir):
from skill_seekers.services.marketplace_manager import MarketplaceManager
manager = MarketplaceManager(config_dir=str(temp_config_dir))
manager.add_marketplace(
name="test", git_url="https://github.com/test/repo.git", token_env="NONEXISTENT_TOKEN"
)
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
publisher.git_repo = MagicMock()
with (
patch(
"skill_seekers.services.marketplace_publisher.MarketplaceManager",
return_value=manager,
),
pytest.raises(RuntimeError, match="Set NONEXISTENT_TOKEN"),
):
publisher.publish(skill_dir=skill_dir, marketplace_name="test")
def test_publish_plugin_already_exists(self, skill_dir, tmp_path, temp_config_dir):
import git as gitmodule
from skill_seekers.services.marketplace_manager import MarketplaceManager
manager = MarketplaceManager(config_dir=str(temp_config_dir))
manager.add_marketplace(
name="test", git_url="https://github.com/test/repo.git", token_env="TEST_TOKEN"
)
# Create a cached repo without .git so publish() takes the clone path
cache_dir = tmp_path / "cache"
cache_dir.mkdir()
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
publisher.git_repo = MagicMock()
publisher.git_repo.cache_dir = cache_dir
publisher.git_repo.inject_token.return_value = "https://fake@github.com/test/repo.git"
# Mock clone_from to create the dir with existing plugin
def fake_clone(_url, path, **_kwargs):
from pathlib import Path
p = Path(path)
p.mkdir(parents=True, exist_ok=True)
(p / "plugins" / "test-skill").mkdir(parents=True)
r = gitmodule.Repo.init(p, initial_branch="main")
r.create_remote("origin", _url)
return r
with (
patch.dict(os.environ, {"TEST_TOKEN": "fake-token"}),
patch(
"skill_seekers.services.marketplace_publisher.MarketplaceManager",
return_value=manager,
),
patch.object(gitmodule.Repo, "clone_from", side_effect=fake_clone),
pytest.raises(ValueError, match="already exists"),
):
publisher.publish(skill_dir=skill_dir, marketplace_name="test")
def test_publish_marketplace_not_found(self, skill_dir, temp_config_dir):
from skill_seekers.services.marketplace_manager import MarketplaceManager
manager = MarketplaceManager(config_dir=str(temp_config_dir))
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
publisher.git_repo = MagicMock()
with (
patch(
"skill_seekers.services.marketplace_publisher.MarketplaceManager",
return_value=manager,
),
pytest.raises(KeyError, match="not found"),
):
publisher.publish(skill_dir=skill_dir, marketplace_name="nonexistent")
class TestValidateSkillName:
"""Test skill name validation to prevent path traversal."""
def test_valid_names(self):
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
assert publisher._validate_skill_name("react") == "react"
assert publisher._validate_skill_name("spine-unity") == "spine-unity"
assert publisher._validate_skill_name("my_skill_v2") == "my_skill_v2"
assert publisher._validate_skill_name("skill.v1") == "skill.v1"
def test_path_traversal_rejected(self):
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
with pytest.raises(ValueError, match="Invalid skill name"):
publisher._validate_skill_name("../../etc/passwd")
with pytest.raises(ValueError, match="Invalid skill name"):
publisher._validate_skill_name("../escape")
def test_empty_name_rejected(self):
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
with pytest.raises(ValueError, match="Invalid skill name"):
publisher._validate_skill_name("")
def test_slash_rejected(self):
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
with pytest.raises(ValueError, match="Invalid skill name"):
publisher._validate_skill_name("path/traversal")
def test_special_chars_rejected(self):
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
with pytest.raises(ValueError, match="Invalid skill name"):
publisher._validate_skill_name("skill;rm -rf")
class TestPublishSuccess:
"""Test publish() success path using a local bare git repo."""
def test_publish_success_flow(self, skill_dir, tmp_path):
"""Full success path: clone → copy → commit → push."""
import git as gitmodule
# Create a working repo with initial marketplace structure, then bare-clone it
working_path = tmp_path / "working"
working_path.mkdir()
repo = gitmodule.Repo.init(working_path, initial_branch="main")
repo.config_writer().set_value("user", "name", "Test").release()
repo.config_writer().set_value("user", "email", "test@test.com").release()
mp_dir = working_path / ".claude-plugin"
mp_dir.mkdir()
mp_json = {
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
"name": "test",
"description": "Test",
"owner": {"name": "Test", "email": "test@test.com"},
"plugins": [],
}
with open(mp_dir / "marketplace.json", "w") as f:
json.dump(mp_json, f)
(working_path / "plugins").mkdir()
repo.index.add([".claude-plugin/marketplace.json"])
repo.index.commit("Initial commit")
# Create bare clone as the "remote"
bare_repo_path = tmp_path / "remote.git"
gitmodule.Repo.clone_from(str(working_path), str(bare_repo_path), bare=True)
# Register marketplace with file:// URL
config_dir = tmp_path / "config"
config_dir.mkdir()
from skill_seekers.services.marketplace_manager import MarketplaceManager
manager = MarketplaceManager(config_dir=str(config_dir))
manager.add_marketplace(
name="local-test",
git_url=f"file://{bare_repo_path}",
token_env="DUMMY_TOKEN",
branch="main",
author={"name": "Test Author", "email": "test@example.com"},
)
# Create publisher with custom cache dir
cache_dir = tmp_path / "cache"
cache_dir.mkdir()
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
from skill_seekers.services.git_repo import GitConfigRepo
publisher.git_repo = GitConfigRepo(cache_dir=str(cache_dir))
# Publish — file:// URLs don't need real tokens but we need the env var set
with (
patch.dict(os.environ, {"DUMMY_TOKEN": "not-needed-for-file-protocol"}),
patch(
"skill_seekers.services.marketplace_publisher.MarketplaceManager",
return_value=manager,
),
):
result = publisher.publish(
skill_dir=skill_dir,
marketplace_name="local-test",
category="testing",
)
# Verify result
assert result["success"] is True
assert result["plugin_path"] == "plugins/test-skill"
assert result["branch"] == "main"
assert len(result["commit_sha"]) == 7
# Verify files in the cached clone
cached_repo = cache_dir / "marketplace_local-test"
assert (
cached_repo / "plugins" / "test-skill" / "skills" / "test-skill" / "SKILL.md"
).exists()
assert (cached_repo / "plugins" / "test-skill" / ".claude-plugin" / "plugin.json").exists()
# Verify marketplace.json was updated
with open(cached_repo / ".claude-plugin" / "marketplace.json") as f:
data = json.load(f)
plugin_names = [p["name"] for p in data["plugins"]]
assert "test-skill" in plugin_names
def test_publish_with_force_overwrites(self, skill_dir, tmp_path):
"""Test that force=True overwrites an existing plugin."""
import git as gitmodule
working_path = tmp_path / "working"
working_path.mkdir()
repo = gitmodule.Repo.init(working_path, initial_branch="main")
repo.config_writer().set_value("user", "name", "Test").release()
repo.config_writer().set_value("user", "email", "t@t.com").release()
mp_dir = working_path / ".claude-plugin"
mp_dir.mkdir()
with open(mp_dir / "marketplace.json", "w") as f:
json.dump(
{"$schema": "", "name": "t", "description": "", "owner": {}, "plugins": []}, f
)
(working_path / "plugins" / "test-skill" / ".claude-plugin").mkdir(parents=True)
repo.index.add([".claude-plugin/marketplace.json"])
repo.index.commit("Initial")
bare_repo_path = tmp_path / "remote.git"
gitmodule.Repo.clone_from(str(working_path), str(bare_repo_path), bare=True)
config_dir = tmp_path / "config"
config_dir.mkdir()
from skill_seekers.services.marketplace_manager import MarketplaceManager
manager = MarketplaceManager(config_dir=str(config_dir))
manager.add_marketplace(
name="local-test",
git_url=f"file://{bare_repo_path}",
token_env="DUMMY_TOKEN",
branch="main",
author={"name": "Test", "email": "t@t.com"},
)
cache_dir = tmp_path / "cache"
cache_dir.mkdir()
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
from skill_seekers.services.git_repo import GitConfigRepo
publisher.git_repo = GitConfigRepo(cache_dir=str(cache_dir))
with (
patch.dict(os.environ, {"DUMMY_TOKEN": "x"}),
patch(
"skill_seekers.services.marketplace_publisher.MarketplaceManager",
return_value=manager,
),
):
result = publisher.publish(
skill_dir=skill_dir,
marketplace_name="local-test",
category="testing",
force=True,
)
assert result["success"] is True
class TestPublishCachedRepo:
"""Regression tests for repeated publishes against the persistent cache."""
def _make_publisher(self, tmp_path, mock_git_repo=False):
from skill_seekers.services.git_repo import GitConfigRepo
cache_dir = tmp_path / "cache"
cache_dir.mkdir()
publisher = MarketplacePublisher.__new__(MarketplacePublisher)
if mock_git_repo:
publisher.git_repo = MagicMock()
publisher.git_repo.cache_dir = cache_dir
else:
publisher.git_repo = GitConfigRepo(cache_dir=str(cache_dir))
return publisher, cache_dir
def test_second_publish_pulls_via_token_url(self, skill_dir, second_skill_dir, tmp_path):
"""Regression: the cached-repo pull must use the token-injected URL.
After the first publish, origin in the cached repo is deliberately reset
to the tokenless git_url. Here the tokenless URL is unreachable (like a
private repo without credentials), so pulling through origin fails —
the second publish only succeeds if the pull goes through the token URL.
"""
import git as gitmodule
bare_repo_path = _make_bare_remote(tmp_path)
tokenless_url = f"file://{tmp_path}/nonexistent-private.git"
manager = _make_manager(tmp_path, tokenless_url)
publisher, cache_dir = self._make_publisher(tmp_path, mock_git_repo=True)
# The "token URL" is the only one that actually reaches the remote
publisher.git_repo.inject_token.return_value = f"file://{bare_repo_path}"
with (
patch.dict(os.environ, {"DUMMY_TOKEN": "x"}),
patch(
"skill_seekers.services.marketplace_publisher.MarketplaceManager",
return_value=manager,
),
):
first = publisher.publish(skill_dir=skill_dir, marketplace_name="local-test")
# First publish strips the token: origin is now the unreachable URL
cached = gitmodule.Repo(cache_dir / "marketplace_local-test")
assert cached.remotes.origin.url == tokenless_url
second = publisher.publish(skill_dir=second_skill_dir, marketplace_name="local-test")
assert first["success"] is True
assert second["success"] is True
bare = gitmodule.Repo(bare_repo_path)
names = bare.git.ls_tree("--name-only", "main", "plugins/").splitlines()
assert "plugins/second-skill" in names
def test_create_branch_publish_restores_base_branch(self, skill_dir, tmp_path):
"""Regression: create_branch=True must not leave the persistent cache
checked out on the skill/<name> feature branch."""
import git as gitmodule
bare_repo_path = _make_bare_remote(tmp_path)
manager = _make_manager(tmp_path, f"file://{bare_repo_path}")
publisher, cache_dir = self._make_publisher(tmp_path)
with (
patch.dict(os.environ, {"DUMMY_TOKEN": "x"}),
patch(
"skill_seekers.services.marketplace_publisher.MarketplaceManager",
return_value=manager,
),
):
result = publisher.publish(
skill_dir=skill_dir, marketplace_name="local-test", create_branch=True
)
assert result["success"] is True
assert result["branch"] == "skill/test-skill"
cached = gitmodule.Repo(cache_dir / "marketplace_local-test")
assert cached.active_branch.name == "main"
def test_publish_after_create_branch_lands_on_base_branch(
self, skill_dir, second_skill_dir, tmp_path
):
"""Regression: a default-branch publish after a create_branch publish must
push the new skill to the remote base branch (previously it 'succeeded'
while committing on the stale feature branch and pushing nothing)."""
import git as gitmodule
bare_repo_path = _make_bare_remote(tmp_path)
manager = _make_manager(tmp_path, f"file://{bare_repo_path}")
publisher, _cache_dir = self._make_publisher(tmp_path)
with (
patch.dict(os.environ, {"DUMMY_TOKEN": "x"}),
patch(
"skill_seekers.services.marketplace_publisher.MarketplaceManager",
return_value=manager,
),
):
publisher.publish(
skill_dir=skill_dir, marketplace_name="local-test", create_branch=True
)
second = publisher.publish(skill_dir=second_skill_dir, marketplace_name="local-test")
assert second["success"] is True
assert second["branch"] == "main"
bare = gitmodule.Repo(bare_repo_path)
names = bare.git.ls_tree("--name-only", "main", "plugins/").splitlines()
assert "plugins/second-skill" in names
# The commit reported by the second publish is the new remote main head
assert bare.commit("main").hexsha[:7] == second["commit_sha"]
def test_create_branch_republish_after_remote_branch_deleted(self, skill_dir, tmp_path):
"""Regression: republishing the same skill with create_branch=True (e.g.
after the PR branch was merged and deleted) must not crash on the
leftover local branch in the persistent cache."""
import git as gitmodule
bare_repo_path = _make_bare_remote(tmp_path)
manager = _make_manager(tmp_path, f"file://{bare_repo_path}")
publisher, _cache_dir = self._make_publisher(tmp_path)
with (
patch.dict(os.environ, {"DUMMY_TOKEN": "x"}),
patch(
"skill_seekers.services.marketplace_publisher.MarketplaceManager",
return_value=manager,
),
):
publisher.publish(
skill_dir=skill_dir, marketplace_name="local-test", create_branch=True
)
# Simulate the feature branch being merged + deleted on the remote
bare = gitmodule.Repo(bare_repo_path)
bare.git.branch("-D", "skill/test-skill")
result = publisher.publish(
skill_dir=skill_dir, marketplace_name="local-test", create_branch=True
)
assert result["success"] is True
assert result["branch"] == "skill/test-skill"
def test_create_branch_republish_with_remote_branch_still_present(self, skill_dir, tmp_path):
"""Regression: republishing with create_branch=True while the remote
skill/<name> branch still exists (force update before merge, or retry)
rebuilds the branch from base, so the push legitimately diverges from
the remote branch and must be forced — a plain push is rejected
non-fast-forward."""
import git as gitmodule
bare_repo_path = _make_bare_remote(tmp_path)
manager = _make_manager(tmp_path, f"file://{bare_repo_path}")
publisher, _cache_dir = self._make_publisher(tmp_path)
with (
patch.dict(os.environ, {"DUMMY_TOKEN": "x"}),
patch(
"skill_seekers.services.marketplace_publisher.MarketplaceManager",
return_value=manager,
),
):
publisher.publish(
skill_dir=skill_dir, marketplace_name="local-test", create_branch=True
)
# Change the skill so the second commit's tree (and SHA) differs —
# identical same-second commits would push as a no-op
# "up-to-date" and hide the non-fast-forward path.
(skill_dir / "SKILL.md").write_text(
"---\nname: test-skill\ndescription: A test skill, v2.\n---\n\n# Test Skill v2\n"
)
result = publisher.publish(
skill_dir=skill_dir, marketplace_name="local-test", create_branch=True, force=True
)
assert result["success"] is True
assert result["branch"] == "skill/test-skill"
# The remote branch must point at the SECOND publish's commit.
remote = gitmodule.Repo(bare_repo_path)
blob = remote.commit("skill/test-skill").tree[
"plugins/test-skill/skills/test-skill/SKILL.md"
]
assert "v2" in blob.data_stream.read().decode()