Files
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

530 lines
21 KiB
Python

#!/usr/bin/env python3
"""Tests for ConfigPublisher class (config publishing to source repos)."""
import json
import os
from unittest.mock import MagicMock, patch
import pytest
from skill_seekers.services.config_publisher import ConfigPublisher, detect_category
def _get_default_branch(repo_path):
"""Get the default branch name of a git repo (master or main)."""
import git
repo = git.Repo(repo_path)
return repo.active_branch.name
def _init_repo_with_main_branch(path):
"""Initialize a git repo ensuring the branch is named 'main'."""
import git
repo = git.Repo.init(path)
repo.config_writer().set_value("user", "name", "Test").release()
repo.config_writer().set_value("user", "email", "test@test.com").release()
# Create initial commit on whatever default branch
(path / "README.md").write_text("# Init\n")
repo.index.add(["README.md"])
repo.index.commit("Initial commit")
# Rename branch to 'main' if needed
if repo.active_branch.name != "main":
repo.git.branch("-m", repo.active_branch.name, "main")
return repo
class TestDetectCategory:
"""Test detect_category() keyword scoring."""
def test_game_engine_detected(self):
config = {"name": "godot-4", "description": "Godot game engine config"}
assert detect_category(config) == "game-engines"
def test_web_framework_detected(self):
config = {"name": "react-config", "description": "React web framework setup"}
assert detect_category(config) == "web-frameworks"
def test_ai_ml_detected(self):
config = {"name": "pytorch-training", "description": "PyTorch model training config"}
assert detect_category(config) == "ai-ml"
def test_database_detected(self):
config = {"name": "postgres-setup", "description": "PostgreSQL database config"}
assert detect_category(config) == "databases"
def test_devops_detected(self):
config = {"name": "docker-compose", "description": "Docker container orchestration"}
assert detect_category(config) == "devops"
def test_cloud_detected(self):
config = {"name": "aws-deployment", "description": "AWS cloud deployment config"}
assert detect_category(config) == "cloud"
def test_mobile_detected(self):
config = {"name": "flutter-app", "description": "Flutter mobile application config"}
assert detect_category(config) == "mobile"
def test_testing_detected(self):
config = {"name": "pytest-setup", "description": "Pytest testing framework"}
assert detect_category(config) == "testing"
def test_unknown_returns_custom(self):
config = {"name": "my-random-thing", "description": "Something unrelated"}
assert detect_category(config) == "custom"
def test_empty_config_returns_custom(self):
config = {}
assert detect_category(config) == "custom"
def test_name_only_matching(self):
config = {"name": "tailwind-theme"}
assert detect_category(config) == "css-frameworks"
def test_description_only_matching(self):
config = {"name": "my-config", "description": "Uses kubernetes for orchestration"}
assert detect_category(config) == "devops"
def test_highest_score_wins(self):
# "react" and "vue" both in web-frameworks, so web-frameworks should score higher
config = {"name": "react-vue-toolkit", "description": "React and Vue comparison"}
assert detect_category(config) == "web-frameworks"
def test_security_detected(self):
config = {"name": "oauth-setup", "description": "OAuth and JWT authentication"}
assert detect_category(config) == "security"
def test_messaging_detected(self):
config = {"name": "kafka-config", "description": "Apache Kafka messaging setup"}
assert detect_category(config) == "messaging"
class TestPublishErrors:
"""Test ConfigPublisher.publish() error cases."""
def test_publish_missing_config_file(self, tmp_path):
publisher = ConfigPublisher.__new__(ConfigPublisher)
publisher.git_repo = MagicMock()
with pytest.raises(FileNotFoundError, match="Config file not found"):
publisher.publish(
config_path=tmp_path / "nonexistent.json",
source_name="test-source",
)
def test_publish_missing_name_field(self, tmp_path):
config_file = tmp_path / "bad_config.json"
config_file.write_text(json.dumps({"description": "No name field"}))
publisher = ConfigPublisher.__new__(ConfigPublisher)
publisher.git_repo = MagicMock()
with pytest.raises(ValueError, match="must have a 'name' field"):
publisher.publish(
config_path=config_file,
source_name="test-source",
)
@patch.dict(os.environ, {}, clear=True)
def test_publish_missing_token(self, tmp_path):
config_file = tmp_path / "config.json"
config_file.write_text(json.dumps({"name": "test-config"}))
# Create a mock source that returns proper data
mock_source = {
"name": "test-source",
"git_url": "https://github.com/test/repo.git",
"branch": "main",
"token_env": "NONEXISTENT_TOKEN",
}
mock_manager = MagicMock()
mock_manager.get_source.return_value = mock_source
mock_manager.list_sources.return_value = [mock_source]
publisher = ConfigPublisher.__new__(ConfigPublisher)
publisher.git_repo = MagicMock()
with (
patch("skill_seekers.services.source_manager.SourceManager", return_value=mock_manager),
patch("skill_seekers.cli.config_validator.validate_config", return_value=None),
pytest.raises(RuntimeError, match="NONEXISTENT_TOKEN"),
):
publisher.publish(config_path=config_file, source_name="test-source")
def test_publish_source_not_found(self, tmp_path):
config_file = tmp_path / "config.json"
config_file.write_text(json.dumps({"name": "test-config"}))
mock_manager = MagicMock()
# Real SourceManager.get_source RAISES KeyError on a miss (it never
# returns falsy); publish() must translate that into a helpful ValueError.
mock_manager.get_source.side_effect = KeyError("Source 'nonexistent' not found")
mock_manager.list_sources.return_value = []
publisher = ConfigPublisher.__new__(ConfigPublisher)
publisher.git_repo = MagicMock()
with (
patch("skill_seekers.services.source_manager.SourceManager", return_value=mock_manager),
patch("skill_seekers.cli.config_validator.validate_config", return_value=None),
pytest.raises(ValueError, match="not found"),
):
publisher.publish(config_path=config_file, source_name="nonexistent")
def test_publish_duplicate_without_force(self, tmp_path):
"""Config already exists in target repo and force=False should raise."""
import git as gitmodule
config_file = tmp_path / "config.json"
config_file.write_text(json.dumps({"name": "existing-config"}))
# Create a working repo with existing config
working_path = tmp_path / "working"
working_path.mkdir()
repo = _init_repo_with_main_branch(working_path)
# Add existing config
config_dir_in_repo = working_path / "configs" / "custom"
config_dir_in_repo.mkdir(parents=True)
(config_dir_in_repo / "existing-config.json").write_text(
json.dumps({"name": "existing-config"})
)
repo.index.add(["configs/custom/existing-config.json"])
repo.index.commit("Add existing config")
bare_repo_path = tmp_path / "remote.git"
gitmodule.Repo.clone_from(str(working_path), str(bare_repo_path), bare=True)
# Mock source manager
mock_source = {
"name": "test-source",
"git_url": f"file://{bare_repo_path}",
"branch": "main",
"token_env": "DUMMY_TOKEN",
}
mock_manager = MagicMock()
mock_manager.get_source.return_value = mock_source
cache_dir = tmp_path / "cache"
cache_dir.mkdir()
publisher = ConfigPublisher.__new__(ConfigPublisher)
from skill_seekers.services.git_repo import GitConfigRepo
publisher.git_repo = GitConfigRepo(cache_dir=str(cache_dir))
with (
patch.dict(os.environ, {"DUMMY_TOKEN": "fake-token"}),
patch("skill_seekers.services.source_manager.SourceManager", return_value=mock_manager),
patch("skill_seekers.cli.config_validator.validate_config", return_value=None),
pytest.raises(ValueError, match="already exists"),
):
publisher.publish(
config_path=config_file,
source_name="test-source",
category="custom",
force=False,
)
class TestPublishSuccess:
"""Test ConfigPublisher.publish() success path using a local bare git repo."""
def test_publish_happy_path(self, tmp_path):
"""Full success path: clone -> copy -> commit -> push."""
import git as gitmodule
# Create config file to publish
config_file = tmp_path / "my-config.json"
config_data = {"name": "my-config", "description": "A test config for pytest"}
config_file.write_text(json.dumps(config_data))
# Create working repo with 'main' branch, then bare-clone as "remote"
working_path = tmp_path / "working"
working_path.mkdir()
_init_repo_with_main_branch(working_path)
bare_repo_path = tmp_path / "remote.git"
gitmodule.Repo.clone_from(str(working_path), str(bare_repo_path), bare=True)
# Mock source manager
mock_source = {
"name": "local-test",
"git_url": f"file://{bare_repo_path}",
"branch": "main",
"token_env": "DUMMY_TOKEN",
}
mock_manager = MagicMock()
mock_manager.get_source.return_value = mock_source
# Create publisher with custom cache dir
cache_dir = tmp_path / "cache"
cache_dir.mkdir()
publisher = ConfigPublisher.__new__(ConfigPublisher)
from skill_seekers.services.git_repo import GitConfigRepo
publisher.git_repo = GitConfigRepo(cache_dir=str(cache_dir))
with (
patch.dict(os.environ, {"DUMMY_TOKEN": "not-needed-for-file-protocol"}),
patch("skill_seekers.services.source_manager.SourceManager", return_value=mock_manager),
patch("skill_seekers.cli.config_validator.validate_config", return_value=None),
):
result = publisher.publish(
config_path=config_file,
source_name="local-test",
category="testing",
)
# Verify result
assert result["success"] is True
assert result["config_name"] == "my-config"
assert result["config_path"] == "configs/testing/my-config.json"
assert result["source"] == "local-test"
assert result["category"] == "testing"
assert len(result["commit_sha"]) == 8
assert result["branch"] == "main"
# MCP-07: a brand-new config is an "add".
assert "add" in result["message"]
# Verify the file exists in the cached clone
cached_repo = cache_dir / "source_local-test"
assert (cached_repo / "configs" / "testing" / "my-config.json").exists()
# Verify the config content was preserved
with open(cached_repo / "configs" / "testing" / "my-config.json") as f:
saved = json.load(f)
assert saved["name"] == "my-config"
def test_publish_force_overwrite(self, tmp_path):
"""Test that force=True overwrites an existing config."""
import git as gitmodule
config_file = tmp_path / "overwrite-config.json"
config_data = {"name": "overwrite-config", "description": "Updated version"}
config_file.write_text(json.dumps(config_data))
# Create working repo with existing config
working_path = tmp_path / "working"
working_path.mkdir()
repo = _init_repo_with_main_branch(working_path)
# Pre-populate with existing config
configs_dir = working_path / "configs" / "custom"
configs_dir.mkdir(parents=True)
(configs_dir / "overwrite-config.json").write_text(
json.dumps({"name": "overwrite-config", "description": "Old version"})
)
repo.index.add(["configs/custom/overwrite-config.json"])
repo.index.commit("Add existing config")
bare_repo_path = tmp_path / "remote.git"
gitmodule.Repo.clone_from(str(working_path), str(bare_repo_path), bare=True)
mock_source = {
"name": "local-test",
"git_url": f"file://{bare_repo_path}",
"branch": "main",
"token_env": "DUMMY_TOKEN",
}
mock_manager = MagicMock()
mock_manager.get_source.return_value = mock_source
cache_dir = tmp_path / "cache"
cache_dir.mkdir()
publisher = ConfigPublisher.__new__(ConfigPublisher)
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.source_manager.SourceManager", return_value=mock_manager),
patch("skill_seekers.cli.config_validator.validate_config", return_value=None),
):
result = publisher.publish(
config_path=config_file,
source_name="local-test",
category="custom",
force=True,
)
assert result["success"] is True
assert result["config_name"] == "overwrite-config"
# MCP-07: overwriting a pre-existing config is an "update", not an "add".
assert "update" in result["message"]
# Verify the file has updated content
cached_repo = cache_dir / "source_local-test"
with open(cached_repo / "configs" / "custom" / "overwrite-config.json") as f:
saved = json.load(f)
assert saved["description"] == "Updated version"
def test_publish_auto_detect_category(self, tmp_path):
"""Test that category='auto' auto-detects from config content."""
import git as gitmodule
config_file = tmp_path / "react-config.json"
config_data = {"name": "react-config", "description": "React web framework config"}
config_file.write_text(json.dumps(config_data))
working_path = tmp_path / "working"
working_path.mkdir()
_init_repo_with_main_branch(working_path)
bare_repo_path = tmp_path / "remote.git"
gitmodule.Repo.clone_from(str(working_path), str(bare_repo_path), bare=True)
mock_source = {
"name": "local-test",
"git_url": f"file://{bare_repo_path}",
"branch": "main",
"token_env": "DUMMY_TOKEN",
}
mock_manager = MagicMock()
mock_manager.get_source.return_value = mock_source
cache_dir = tmp_path / "cache"
cache_dir.mkdir()
publisher = ConfigPublisher.__new__(ConfigPublisher)
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.source_manager.SourceManager", return_value=mock_manager),
patch("skill_seekers.cli.config_validator.validate_config", return_value=None),
):
result = publisher.publish(
config_path=config_file,
source_name="local-test",
category="auto",
)
assert result["success"] is True
assert result["category"] == "web-frameworks"
def test_publish_twice_reuses_cache(self, tmp_path):
"""Second publish to the same source hits the cached-repo re-pull path
(regression for ★MCP-13: it previously pulled via a tokenless origin)."""
import git as gitmodule
working_path = tmp_path / "working"
working_path.mkdir()
_init_repo_with_main_branch(working_path)
bare_repo_path = tmp_path / "remote.git"
gitmodule.Repo.clone_from(str(working_path), str(bare_repo_path), bare=True)
mock_source = {
"name": "local-test",
"git_url": f"file://{bare_repo_path}",
"branch": "main",
"token_env": "DUMMY_TOKEN",
}
mock_manager = MagicMock()
mock_manager.get_source.return_value = mock_source
cache_dir = tmp_path / "cache"
cache_dir.mkdir()
publisher = ConfigPublisher.__new__(ConfigPublisher)
from skill_seekers.services.git_repo import GitConfigRepo
publisher.git_repo = GitConfigRepo(cache_dir=str(cache_dir))
def _publish(name):
cfg = tmp_path / f"{name}.json"
cfg.write_text(json.dumps({"name": name, "description": "x"}))
with (
patch.dict(os.environ, {"DUMMY_TOKEN": "x"}),
patch(
"skill_seekers.services.source_manager.SourceManager",
return_value=mock_manager,
),
patch("skill_seekers.cli.config_validator.validate_config", return_value=None),
):
return publisher.publish(
config_path=cfg, source_name="local-test", category="custom"
)
first = _publish("first-config")
second = _publish("second-config") # exercises the cached-repo pull branch
assert first["success"] is True
assert second["success"] is True
cached_repo = cache_dir / "source_local-test"
assert (cached_repo / "configs" / "custom" / "first-config.json").exists()
assert (cached_repo / "configs" / "custom" / "second-config.json").exists()
def test_publish_create_branch_twice_survives_leftover_branch(self, tmp_path):
"""Re-publishing with create_branch=True must not fail on the feature
branch left in the cache by the first publish (checkout -B, not -b)."""
import git as gitmodule
working_path = tmp_path / "working"
working_path.mkdir()
_init_repo_with_main_branch(working_path)
bare_repo_path = tmp_path / "remote.git"
gitmodule.Repo.clone_from(str(working_path), str(bare_repo_path), bare=True)
mock_source = {
"name": "local-test",
"git_url": f"file://{bare_repo_path}",
"branch": "main",
"token_env": "DUMMY_TOKEN",
}
mock_manager = MagicMock()
mock_manager.get_source.return_value = mock_source
cache_dir = tmp_path / "cache"
cache_dir.mkdir()
publisher = ConfigPublisher.__new__(ConfigPublisher)
from skill_seekers.services.git_repo import GitConfigRepo
publisher.git_repo = GitConfigRepo(cache_dir=str(cache_dir))
config_file = tmp_path / "config.json"
config_file.write_text(json.dumps({"name": "branchy-config"}))
with (
patch.dict(os.environ, {"DUMMY_TOKEN": "x"}),
patch("skill_seekers.services.source_manager.SourceManager", return_value=mock_manager),
patch("skill_seekers.cli.config_validator.validate_config", return_value=None),
):
first = publisher.publish(
config_path=config_file,
source_name="local-test",
category="custom",
create_branch=True,
)
# Change the content so the second commit's tree (and SHA) differs
# from the first — otherwise both publishes can produce the SAME
# commit within one second and the push is a no-op "up-to-date",
# hiding the non-fast-forward path this test must exercise.
config_file.write_text(json.dumps({"name": "branchy-config", "description": "v2"}))
# The cached repo is back on main but config/branchy-config still
# exists locally (and on the remote, pointing at the now-orphaned
# first commit) — the retry/force-update path must survive both.
second = publisher.publish(
config_path=config_file,
source_name="local-test",
category="custom",
create_branch=True,
force=True,
)
assert first["success"] is True
assert second["success"] is True
assert first["branch"] == "config/branchy-config"
cached_repo = gitmodule.Repo(cache_dir / "source_local-test")
assert cached_repo.active_branch.name == "main"
# The remote branch must point at the SECOND publish's commit.
remote = gitmodule.Repo(bare_repo_path)
pushed = remote.commit("config/branchy-config")
assert (
pushed.tree["configs/custom/branchy-config.json"].data_stream.read().decode()
== config_file.read_text()
)