Files
skill-seekers-reference/tests/test_generate_router_github.py
2026-01-17 17:48:15 +00:00

517 lines
19 KiB
Python

"""
Tests for Phase 4: Router Generation with GitHub Integration
Tests the enhanced router generator that integrates GitHub insights:
- Enhanced topic definition using issue labels (2x weight)
- Router template with repository stats and top issues
- Sub-skill templates with "Common Issues" section
- GitHub issue linking
"""
import json
from skill_seekers.cli.generate_router import RouterGenerator
from skill_seekers.cli.github_fetcher import CodeStream, DocsStream, InsightsStream, ThreeStreamData
class TestRouterGeneratorBasic:
"""Test basic router generation without GitHub streams (backward compat)."""
def test_router_generator_init(self, tmp_path):
"""Test router generator initialization."""
# Create test configs
config1 = {
"name": "test-oauth",
"description": "OAuth authentication",
"base_url": "https://example.com",
"categories": {"authentication": ["auth", "oauth"]},
}
config2 = {
"name": "test-async",
"description": "Async operations",
"base_url": "https://example.com",
"categories": {"async": ["async", "await"]},
}
config_path1 = tmp_path / "config1.json"
config_path2 = tmp_path / "config2.json"
with open(config_path1, "w") as f:
json.dump(config1, f)
with open(config_path2, "w") as f:
json.dump(config2, f)
# Create generator
generator = RouterGenerator([str(config_path1), str(config_path2)])
assert generator.router_name == "test"
assert len(generator.configs) == 2
assert generator.github_streams is None
def test_infer_router_name(self, tmp_path):
"""Test router name inference from sub-skill names."""
config1 = {"name": "fastmcp-oauth", "base_url": "https://example.com"}
config2 = {"name": "fastmcp-async", "base_url": "https://example.com"}
config_path1 = tmp_path / "config1.json"
config_path2 = tmp_path / "config2.json"
with open(config_path1, "w") as f:
json.dump(config1, f)
with open(config_path2, "w") as f:
json.dump(config2, f)
generator = RouterGenerator([str(config_path1), str(config_path2)])
assert generator.router_name == "fastmcp"
def test_extract_routing_keywords_basic(self, tmp_path):
"""Test basic keyword extraction without GitHub."""
config = {
"name": "test-oauth",
"base_url": "https://example.com",
"categories": {"authentication": ["auth", "oauth"], "tokens": ["token", "jwt"]},
}
config_path = tmp_path / "config.json"
with open(config_path, "w") as f:
json.dump(config, f)
generator = RouterGenerator([str(config_path)])
routing = generator.extract_routing_keywords()
assert "test-oauth" in routing
keywords = routing["test-oauth"]
assert "authentication" in keywords
assert "tokens" in keywords
assert "oauth" in keywords # From name
class TestRouterGeneratorWithGitHub:
"""Test router generation with GitHub streams (Phase 4)."""
def test_router_with_github_metadata(self, tmp_path):
"""Test router generator with GitHub metadata."""
config = {
"name": "test-oauth",
"description": "OAuth skill",
"base_url": "https://github.com/test/repo",
"categories": {"oauth": ["oauth", "auth"]},
}
config_path = tmp_path / "config.json"
with open(config_path, "w") as f:
json.dump(config, f)
# Create GitHub streams
code_stream = CodeStream(directory=tmp_path, files=[])
docs_stream = DocsStream(
readme="# Test Project\n\nA test OAuth library.", contributing=None, docs_files=[]
)
insights_stream = InsightsStream(
metadata={
"stars": 1234,
"forks": 56,
"language": "Python",
"description": "OAuth helper",
},
common_problems=[
{
"title": "OAuth fails on redirect",
"number": 42,
"state": "open",
"comments": 15,
"labels": ["bug", "oauth"],
}
],
known_solutions=[],
top_labels=[{"label": "oauth", "count": 20}, {"label": "bug", "count": 10}],
)
github_streams = ThreeStreamData(code_stream, docs_stream, insights_stream)
# Create generator with GitHub streams
generator = RouterGenerator([str(config_path)], github_streams=github_streams)
assert generator.github_metadata is not None
assert generator.github_metadata["stars"] == 1234
assert generator.github_docs is not None
assert generator.github_docs["readme"].startswith("# Test Project")
assert generator.github_issues is not None
def test_extract_keywords_with_github_labels(self, tmp_path):
"""Test keyword extraction with GitHub issue labels (2x weight)."""
config = {
"name": "test-oauth",
"base_url": "https://example.com",
"categories": {"oauth": ["oauth", "auth"]},
}
config_path = tmp_path / "config.json"
with open(config_path, "w") as f:
json.dump(config, f)
# Create GitHub streams with top labels
code_stream = CodeStream(directory=tmp_path, files=[])
docs_stream = DocsStream(readme=None, contributing=None, docs_files=[])
insights_stream = InsightsStream(
metadata={},
common_problems=[],
known_solutions=[],
top_labels=[
{"label": "oauth", "count": 50}, # Matches 'oauth' keyword
{"label": "authentication", "count": 30}, # Related
{"label": "bug", "count": 20}, # Not related
],
)
github_streams = ThreeStreamData(code_stream, docs_stream, insights_stream)
generator = RouterGenerator([str(config_path)], github_streams=github_streams)
routing = generator.extract_routing_keywords()
keywords = routing["test-oauth"]
# 'oauth' label should appear twice (2x weight)
oauth_count = keywords.count("oauth")
assert oauth_count >= 4 # Base 'oauth' from categories + name + 2x from label
def test_generate_skill_md_with_github(self, tmp_path):
"""Test SKILL.md generation with GitHub metadata."""
config = {
"name": "test-oauth",
"description": "OAuth authentication skill",
"base_url": "https://github.com/test/oauth",
"categories": {"oauth": ["oauth"]},
}
config_path = tmp_path / "config.json"
with open(config_path, "w") as f:
json.dump(config, f)
# Create GitHub streams
code_stream = CodeStream(directory=tmp_path, files=[])
docs_stream = DocsStream(
readme="# OAuth Library\n\nQuick start: Install with pip install oauth",
contributing=None,
docs_files=[],
)
insights_stream = InsightsStream(
metadata={
"stars": 5000,
"forks": 200,
"language": "Python",
"description": "OAuth 2.0 library",
},
common_problems=[
{
"title": "Redirect URI mismatch",
"number": 100,
"state": "open",
"comments": 25,
"labels": ["bug", "oauth"],
},
{
"title": "Token refresh fails",
"number": 95,
"state": "open",
"comments": 18,
"labels": ["oauth"],
},
],
known_solutions=[],
top_labels=[],
)
github_streams = ThreeStreamData(code_stream, docs_stream, insights_stream)
generator = RouterGenerator([str(config_path)], github_streams=github_streams)
skill_md = generator.generate_skill_md()
# Check GitHub metadata section
assert "⭐ 5,000" in skill_md
assert "Python" in skill_md
assert "OAuth 2.0 library" in skill_md
# Check Quick Start from README
assert "## Quick Start" in skill_md
assert "OAuth Library" in skill_md
# Check that issue was converted to question in Examples section (Fix 1)
assert "## Common Issues" in skill_md or "## Examples" in skill_md
assert (
"how do i handle redirect uri mismatch" in skill_md.lower()
or "how do i fix redirect uri mismatch" in skill_md.lower()
)
# Note: Issue #100 may appear in Common Issues or as converted question in Examples
def test_generate_skill_md_without_github(self, tmp_path):
"""Test SKILL.md generation without GitHub (backward compat)."""
config = {
"name": "test-oauth",
"description": "OAuth skill",
"base_url": "https://example.com",
"categories": {"oauth": ["oauth"]},
}
config_path = tmp_path / "config.json"
with open(config_path, "w") as f:
json.dump(config, f)
# No GitHub streams
generator = RouterGenerator([str(config_path)])
skill_md = generator.generate_skill_md()
# Should not have GitHub-specific sections
assert "" not in skill_md
assert "Repository Info" not in skill_md
assert "Quick Start (from README)" not in skill_md
assert "Common Issues (from GitHub)" not in skill_md
# Should have basic sections
assert "When to Use This Skill" in skill_md
assert "How It Works" in skill_md
class TestSubSkillIssuesSection:
"""Test sub-skill issue section generation (Phase 4)."""
def test_generate_subskill_issues_section(self, tmp_path):
"""Test generation of issues section for sub-skills."""
config = {
"name": "test-oauth",
"base_url": "https://example.com",
"categories": {"oauth": ["oauth"]},
}
config_path = tmp_path / "config.json"
with open(config_path, "w") as f:
json.dump(config, f)
# Create GitHub streams with issues
code_stream = CodeStream(directory=tmp_path, files=[])
docs_stream = DocsStream(readme=None, contributing=None, docs_files=[])
insights_stream = InsightsStream(
metadata={},
common_problems=[
{
"title": "OAuth redirect fails",
"number": 50,
"state": "open",
"comments": 20,
"labels": ["oauth", "bug"],
},
{
"title": "Token expiration issue",
"number": 45,
"state": "open",
"comments": 15,
"labels": ["oauth"],
},
],
known_solutions=[
{
"title": "Fixed OAuth flow",
"number": 40,
"state": "closed",
"comments": 10,
"labels": ["oauth"],
}
],
top_labels=[],
)
github_streams = ThreeStreamData(code_stream, docs_stream, insights_stream)
generator = RouterGenerator([str(config_path)], github_streams=github_streams)
# Generate issues section for oauth topic
issues_section = generator.generate_subskill_issues_section("test-oauth", ["oauth"])
# Check content
assert "Common Issues (from GitHub)" in issues_section
assert "OAuth redirect fails" in issues_section
assert "Issue #50" in issues_section
assert "20 comments" in issues_section
assert "🔴" in issues_section # Open issue icon
assert "" in issues_section # Closed issue icon
def test_generate_subskill_issues_no_matches(self, tmp_path):
"""Test issues section when no issues match the topic."""
config = {
"name": "test-async",
"base_url": "https://example.com",
"categories": {"async": ["async"]},
}
config_path = tmp_path / "config.json"
with open(config_path, "w") as f:
json.dump(config, f)
# Create GitHub streams with oauth issues (not async)
code_stream = CodeStream(directory=tmp_path, files=[])
docs_stream = DocsStream(readme=None, contributing=None, docs_files=[])
insights_stream = InsightsStream(
metadata={},
common_problems=[
{
"title": "OAuth fails",
"number": 1,
"state": "open",
"comments": 5,
"labels": ["oauth"],
}
],
known_solutions=[],
top_labels=[],
)
github_streams = ThreeStreamData(code_stream, docs_stream, insights_stream)
generator = RouterGenerator([str(config_path)], github_streams=github_streams)
# Generate issues section for async topic (no matches)
issues_section = generator.generate_subskill_issues_section("test-async", ["async"])
# Unmatched issues go to 'other' category, so section is generated
assert "Common Issues (from GitHub)" in issues_section
assert "Other" in issues_section # Unmatched issues
assert "OAuth fails" in issues_section # The oauth issue
class TestIntegration:
"""Integration tests for Phase 4."""
def test_full_router_generation_with_github(self, tmp_path):
"""Test complete router generation workflow with GitHub streams."""
# Create multiple sub-skill configs
config1 = {
"name": "fastmcp-oauth",
"description": "OAuth authentication in FastMCP",
"base_url": "https://github.com/test/fastmcp",
"categories": {"oauth": ["oauth", "auth"]},
}
config2 = {
"name": "fastmcp-async",
"description": "Async operations in FastMCP",
"base_url": "https://github.com/test/fastmcp",
"categories": {"async": ["async", "await"]},
}
config_path1 = tmp_path / "config1.json"
config_path2 = tmp_path / "config2.json"
with open(config_path1, "w") as f:
json.dump(config1, f)
with open(config_path2, "w") as f:
json.dump(config2, f)
# Create comprehensive GitHub streams
code_stream = CodeStream(directory=tmp_path, files=[])
docs_stream = DocsStream(
readme="# FastMCP\n\nFast MCP server framework.\n\n## Installation\n\n```bash\npip install fastmcp\n```",
contributing="# Contributing\n\nPull requests welcome!",
docs_files=[
{"path": "docs/oauth.md", "content": "# OAuth Guide"},
{"path": "docs/async.md", "content": "# Async Guide"},
],
)
insights_stream = InsightsStream(
metadata={
"stars": 10000,
"forks": 500,
"language": "Python",
"description": "Fast MCP server framework",
},
common_problems=[
{
"title": "OAuth setup fails",
"number": 150,
"state": "open",
"comments": 30,
"labels": ["bug", "oauth"],
},
{
"title": "Async deadlock",
"number": 142,
"state": "open",
"comments": 25,
"labels": ["async", "bug"],
},
{
"title": "Token refresh issue",
"number": 130,
"state": "open",
"comments": 20,
"labels": ["oauth"],
},
],
known_solutions=[
{
"title": "Fixed OAuth redirect",
"number": 120,
"state": "closed",
"comments": 15,
"labels": ["oauth"],
},
{
"title": "Resolved async race",
"number": 110,
"state": "closed",
"comments": 12,
"labels": ["async"],
},
],
top_labels=[
{"label": "oauth", "count": 45},
{"label": "async", "count": 38},
{"label": "bug", "count": 30},
],
)
github_streams = ThreeStreamData(code_stream, docs_stream, insights_stream)
# Create router generator
generator = RouterGenerator(
[str(config_path1), str(config_path2)], github_streams=github_streams
)
# Generate SKILL.md
skill_md = generator.generate_skill_md()
# Verify all Phase 4 enhancements present
# 1. Repository metadata
assert "⭐ 10,000" in skill_md
assert "Python" in skill_md
assert "Fast MCP server framework" in skill_md
# 2. Quick start from README
assert "## Quick Start" in skill_md
assert "pip install fastmcp" in skill_md
# 3. Sub-skills listed
assert "fastmcp-oauth" in skill_md
assert "fastmcp-async" in skill_md
# 4. Examples section with converted questions (Fix 1)
assert "## Examples" in skill_md
# Issues converted to natural questions
assert (
"how do i fix oauth setup" in skill_md.lower()
or "how do i handle oauth setup" in skill_md.lower()
)
assert (
"how do i handle async deadlock" in skill_md.lower()
or "how do i fix async deadlock" in skill_md.lower()
)
# Common Issues section may still exist with other issues
# Note: Issue numbers may appear in Common Issues or Common Patterns sections
# 5. Routing keywords include GitHub labels (2x weight)
routing = generator.extract_routing_keywords()
oauth_keywords = routing["fastmcp-oauth"]
async_keywords = routing["fastmcp-async"]
# Labels should be included with 2x weight
assert oauth_keywords.count("oauth") >= 2
assert async_keywords.count("async") >= 2
# Generate config
router_config = generator.create_router_config()
assert router_config["name"] == "fastmcp"
assert router_config["_router"] is True
assert len(router_config["_sub_skills"]) == 2