2cab53bc94
Docker Publish / Build and Push Docker Images (map[description:Skill Seekers CLI - Convert documentation to AI skills dockerfile:Dockerfile name:skill-seekers]) (push) Waiting to run
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) Waiting to run
Docker Publish / Test Docker Images (push) Blocked by required conditions
Test Vector Database Adaptors / Test chroma Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test faiss Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test qdrant Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test weaviate Adaptor (push) Waiting to run
Test Vector Database Adaptors / Test MCP Vector DB Tools (push) Waiting to run
Tests / Code Quality (Ruff & Mypy) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (macos-latest, 3.11) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (macos-latest, 3.12) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.10) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.11) (push) Waiting to run
Tests / Fast Unit Tests (parallel) (ubuntu-latest, 3.12) (push) Waiting to run
Tests / Tests (push) Blocked by required conditions
Tests / Serial / Integration / E2E Tests (push) Blocked by required conditions
Tests / MCP Server Tests (push) Blocked by required conditions
573 lines
20 KiB
Python
573 lines
20 KiB
Python
"""
|
|
Tests for Phase 3: Enhanced Source Merging with GitHub Streams
|
|
|
|
Tests the multi-layer merging architecture:
|
|
- Layer 1: C3.x code (ground truth)
|
|
- Layer 2: HTML docs (official intent)
|
|
- Layer 3: GitHub docs (README/CONTRIBUTING)
|
|
- Layer 4: GitHub insights (issues)
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
import skill_seekers.cli.merge_sources as merge_sources_module
|
|
from skill_seekers.cli.conflict_detector import Conflict
|
|
from skill_seekers.cli.github_fetcher import CodeStream, DocsStream, InsightsStream, ThreeStreamData
|
|
from skill_seekers.cli.merge_sources import (
|
|
AIEnhancedMerger,
|
|
ClaudeEnhancedMerger,
|
|
RuleBasedMerger,
|
|
_match_issues_to_apis,
|
|
categorize_issues_by_topic,
|
|
generate_hybrid_content,
|
|
)
|
|
|
|
|
|
class TestIssueCategorization:
|
|
"""Test issue categorization by topic."""
|
|
|
|
def test_categorize_issues_basic(self):
|
|
"""Test basic issue categorization."""
|
|
problems = [
|
|
{
|
|
"title": "OAuth setup fails",
|
|
"labels": ["bug", "oauth"],
|
|
"number": 1,
|
|
"state": "open",
|
|
"comments": 10,
|
|
},
|
|
{
|
|
"title": "Testing framework issue",
|
|
"labels": ["testing"],
|
|
"number": 2,
|
|
"state": "open",
|
|
"comments": 5,
|
|
},
|
|
]
|
|
solutions = [
|
|
{
|
|
"title": "Fixed OAuth redirect",
|
|
"labels": ["oauth"],
|
|
"number": 3,
|
|
"state": "closed",
|
|
"comments": 3,
|
|
}
|
|
]
|
|
|
|
topics = ["oauth", "testing", "async"]
|
|
|
|
categorized = categorize_issues_by_topic(problems, solutions, topics)
|
|
|
|
assert "oauth" in categorized
|
|
assert len(categorized["oauth"]) == 2 # 1 problem + 1 solution
|
|
assert "testing" in categorized
|
|
assert len(categorized["testing"]) == 1
|
|
|
|
def test_categorize_issues_keyword_matching(self):
|
|
"""Test keyword matching in titles and labels."""
|
|
problems = [
|
|
{
|
|
"title": "Database connection timeout",
|
|
"labels": ["db"],
|
|
"number": 1,
|
|
"state": "open",
|
|
"comments": 7,
|
|
}
|
|
]
|
|
solutions = []
|
|
|
|
topics = ["database"]
|
|
|
|
categorized = categorize_issues_by_topic(problems, solutions, topics)
|
|
|
|
# Should match 'database' topic due to 'db' in labels
|
|
assert "database" in categorized or "other" in categorized
|
|
|
|
def test_categorize_issues_multi_keyword_topic(self):
|
|
"""Test topics with multiple keywords."""
|
|
problems = [
|
|
{
|
|
"title": "Async API call fails",
|
|
"labels": ["async", "api"],
|
|
"number": 1,
|
|
"state": "open",
|
|
"comments": 8,
|
|
}
|
|
]
|
|
solutions = []
|
|
|
|
topics = ["async api"]
|
|
|
|
categorized = categorize_issues_by_topic(problems, solutions, topics)
|
|
|
|
# Should match due to both 'async' and 'api' in labels
|
|
assert "async api" in categorized
|
|
assert len(categorized["async api"]) == 1
|
|
|
|
def test_categorize_issues_no_match_goes_to_other(self):
|
|
"""Test that unmatched issues go to 'other' category."""
|
|
problems = [
|
|
{
|
|
"title": "Random issue",
|
|
"labels": ["misc"],
|
|
"number": 1,
|
|
"state": "open",
|
|
"comments": 5,
|
|
}
|
|
]
|
|
solutions = []
|
|
|
|
topics = ["oauth", "testing"]
|
|
|
|
categorized = categorize_issues_by_topic(problems, solutions, topics)
|
|
|
|
assert "other" in categorized
|
|
assert len(categorized["other"]) == 1
|
|
|
|
def test_categorize_issues_empty_lists(self):
|
|
"""Test categorization with empty input."""
|
|
categorized = categorize_issues_by_topic([], [], ["oauth"])
|
|
|
|
# Should return empty dict (no categories with issues)
|
|
assert len(categorized) == 0
|
|
|
|
|
|
class TestHybridContent:
|
|
"""Test hybrid content generation."""
|
|
|
|
def test_generate_hybrid_content_basic(self):
|
|
"""Test basic hybrid content generation."""
|
|
api_data = {
|
|
"apis": {"oauth_login": {"name": "oauth_login", "status": "matched"}},
|
|
"summary": {"total_apis": 1},
|
|
}
|
|
|
|
github_docs = {
|
|
"readme": "# Project README",
|
|
"contributing": None,
|
|
"docs_files": [{"path": "docs/oauth.md", "content": "OAuth guide"}],
|
|
}
|
|
|
|
github_insights = {
|
|
"metadata": {
|
|
"stars": 1234,
|
|
"forks": 56,
|
|
"language": "Python",
|
|
"description": "Test project",
|
|
},
|
|
"common_problems": [
|
|
{
|
|
"title": "OAuth fails",
|
|
"number": 42,
|
|
"state": "open",
|
|
"comments": 10,
|
|
"labels": ["bug"],
|
|
}
|
|
],
|
|
"known_solutions": [
|
|
{
|
|
"title": "Fixed OAuth",
|
|
"number": 35,
|
|
"state": "closed",
|
|
"comments": 5,
|
|
"labels": ["bug"],
|
|
}
|
|
],
|
|
"top_labels": [{"label": "bug", "count": 10}, {"label": "enhancement", "count": 5}],
|
|
}
|
|
|
|
conflicts = []
|
|
|
|
hybrid = generate_hybrid_content(api_data, github_docs, github_insights, conflicts)
|
|
|
|
# Check structure
|
|
assert "api_reference" in hybrid
|
|
assert "github_context" in hybrid
|
|
assert "conflict_summary" in hybrid
|
|
assert "issue_links" in hybrid
|
|
|
|
# Check GitHub docs layer
|
|
assert hybrid["github_context"]["docs"]["readme"] == "# Project README"
|
|
assert hybrid["github_context"]["docs"]["docs_files_count"] == 1
|
|
|
|
# Check GitHub insights layer
|
|
assert hybrid["github_context"]["metadata"]["stars"] == 1234
|
|
assert hybrid["github_context"]["metadata"]["language"] == "Python"
|
|
assert hybrid["github_context"]["issues"]["common_problems_count"] == 1
|
|
assert hybrid["github_context"]["issues"]["known_solutions_count"] == 1
|
|
assert len(hybrid["github_context"]["issues"]["top_problems"]) == 1
|
|
assert len(hybrid["github_context"]["top_labels"]) == 2
|
|
|
|
def test_generate_hybrid_content_with_conflicts(self):
|
|
"""Test hybrid content with conflicts."""
|
|
api_data = {"apis": {}, "summary": {}}
|
|
github_docs = None
|
|
github_insights = None
|
|
|
|
conflicts = [
|
|
Conflict(
|
|
api_name="test_api",
|
|
type="signature_mismatch",
|
|
severity="medium",
|
|
difference="Parameter count differs",
|
|
docs_info={"parameters": ["a", "b"]},
|
|
code_info={"parameters": ["a", "b", "c"]},
|
|
),
|
|
Conflict(
|
|
api_name="test_api_2",
|
|
type="missing_in_docs",
|
|
severity="low",
|
|
difference="API not documented",
|
|
docs_info=None,
|
|
code_info={"name": "test_api_2"},
|
|
),
|
|
]
|
|
|
|
hybrid = generate_hybrid_content(api_data, github_docs, github_insights, conflicts)
|
|
|
|
# Check conflict summary
|
|
assert hybrid["conflict_summary"]["total_conflicts"] == 2
|
|
assert hybrid["conflict_summary"]["by_type"]["signature_mismatch"] == 1
|
|
assert hybrid["conflict_summary"]["by_type"]["missing_in_docs"] == 1
|
|
assert hybrid["conflict_summary"]["by_severity"]["medium"] == 1
|
|
assert hybrid["conflict_summary"]["by_severity"]["low"] == 1
|
|
|
|
def test_generate_hybrid_content_no_github_data(self):
|
|
"""Test hybrid content with no GitHub data."""
|
|
api_data = {"apis": {}, "summary": {}}
|
|
|
|
hybrid = generate_hybrid_content(api_data, None, None, [])
|
|
|
|
# Should still have structure, but no GitHub context
|
|
assert "api_reference" in hybrid
|
|
assert "github_context" in hybrid
|
|
assert hybrid["github_context"] == {}
|
|
assert hybrid["conflict_summary"]["total_conflicts"] == 0
|
|
|
|
|
|
class TestIssueToAPIMatching:
|
|
"""Test matching issues to APIs."""
|
|
|
|
def test_match_issues_to_apis_basic(self):
|
|
"""Test basic issue to API matching."""
|
|
apis = {"oauth_login": {"name": "oauth_login"}, "async_fetch": {"name": "async_fetch"}}
|
|
|
|
problems = [
|
|
{
|
|
"title": "OAuth login fails",
|
|
"number": 42,
|
|
"state": "open",
|
|
"comments": 10,
|
|
"labels": ["bug", "oauth"],
|
|
}
|
|
]
|
|
|
|
solutions = [
|
|
{
|
|
"title": "Fixed async fetch timeout",
|
|
"number": 35,
|
|
"state": "closed",
|
|
"comments": 5,
|
|
"labels": ["async"],
|
|
}
|
|
]
|
|
|
|
issue_links = _match_issues_to_apis(apis, problems, solutions)
|
|
|
|
# Should match oauth issue to oauth_login API
|
|
assert "oauth_login" in issue_links
|
|
assert len(issue_links["oauth_login"]) == 1
|
|
assert issue_links["oauth_login"][0]["number"] == 42
|
|
|
|
# Should match async issue to async_fetch API
|
|
assert "async_fetch" in issue_links
|
|
assert len(issue_links["async_fetch"]) == 1
|
|
assert issue_links["async_fetch"][0]["number"] == 35
|
|
|
|
def test_match_issues_to_apis_no_matches(self):
|
|
"""Test when no issues match any APIs."""
|
|
apis = {"database_connect": {"name": "database_connect"}}
|
|
|
|
problems = [
|
|
{
|
|
"title": "Random unrelated issue",
|
|
"number": 1,
|
|
"state": "open",
|
|
"comments": 5,
|
|
"labels": ["misc"],
|
|
}
|
|
]
|
|
|
|
issue_links = _match_issues_to_apis(apis, problems, [])
|
|
|
|
# Should be empty - no matches
|
|
assert len(issue_links) == 0
|
|
|
|
def test_match_issues_to_apis_dotted_names(self):
|
|
"""Test matching with dotted API names."""
|
|
apis = {"module.oauth.login": {"name": "module.oauth.login"}}
|
|
|
|
problems = [
|
|
{
|
|
"title": "OAuth module fails",
|
|
"number": 42,
|
|
"state": "open",
|
|
"comments": 10,
|
|
"labels": ["oauth"],
|
|
}
|
|
]
|
|
|
|
issue_links = _match_issues_to_apis(apis, problems, [])
|
|
|
|
# Should match due to 'oauth' keyword
|
|
assert "module.oauth.login" in issue_links
|
|
assert len(issue_links["module.oauth.login"]) == 1
|
|
|
|
|
|
class TestRuleBasedMergerWithGitHubStreams:
|
|
"""Test RuleBasedMerger with GitHub streams."""
|
|
|
|
def test_merger_with_github_streams(self, tmp_path):
|
|
"""Test merger with three-stream GitHub data."""
|
|
docs_data = {"pages": []}
|
|
github_data = {"apis": {}}
|
|
conflicts = []
|
|
|
|
# Create three-stream data
|
|
code_stream = CodeStream(directory=tmp_path, files=[])
|
|
docs_stream = DocsStream(
|
|
readme="# README",
|
|
contributing="# Contributing",
|
|
docs_files=[{"path": "docs/guide.md", "content": "Guide content"}],
|
|
)
|
|
insights_stream = InsightsStream(
|
|
metadata={"stars": 1234, "forks": 56, "language": "Python"},
|
|
common_problems=[
|
|
{"title": "Bug 1", "number": 1, "state": "open", "comments": 10, "labels": ["bug"]}
|
|
],
|
|
known_solutions=[
|
|
{"title": "Fix 1", "number": 2, "state": "closed", "comments": 5, "labels": ["bug"]}
|
|
],
|
|
top_labels=[{"label": "bug", "count": 10}],
|
|
)
|
|
github_streams = ThreeStreamData(code_stream, docs_stream, insights_stream)
|
|
|
|
# Create merger with streams
|
|
merger = RuleBasedMerger(docs_data, github_data, conflicts, github_streams)
|
|
|
|
assert merger.github_streams is not None
|
|
assert merger.github_docs is not None
|
|
assert merger.github_insights is not None
|
|
assert merger.github_docs["readme"] == "# README"
|
|
assert merger.github_insights["metadata"]["stars"] == 1234
|
|
|
|
def test_merger_merge_all_with_streams(self, tmp_path):
|
|
"""Test merge_all() with GitHub streams."""
|
|
docs_data = {"pages": []}
|
|
github_data = {"apis": {}}
|
|
conflicts = []
|
|
|
|
# Create three-stream data
|
|
code_stream = CodeStream(directory=tmp_path, files=[])
|
|
docs_stream = DocsStream(readme="# README", contributing=None, docs_files=[])
|
|
insights_stream = InsightsStream(
|
|
metadata={"stars": 500}, common_problems=[], known_solutions=[], top_labels=[]
|
|
)
|
|
github_streams = ThreeStreamData(code_stream, docs_stream, insights_stream)
|
|
|
|
# Create and run merger
|
|
merger = RuleBasedMerger(docs_data, github_data, conflicts, github_streams)
|
|
result = merger.merge_all()
|
|
|
|
# Check result has GitHub context
|
|
assert "github_context" in result
|
|
assert "conflict_summary" in result
|
|
assert "issue_links" in result
|
|
assert result["github_context"]["metadata"]["stars"] == 500
|
|
|
|
def test_merger_without_streams_backward_compat(self):
|
|
"""Test backward compatibility without GitHub streams."""
|
|
docs_data = {"pages": []}
|
|
github_data = {"apis": {}}
|
|
conflicts = []
|
|
|
|
# Create merger without streams (old API)
|
|
merger = RuleBasedMerger(docs_data, github_data, conflicts)
|
|
|
|
assert merger.github_streams is None
|
|
assert merger.github_docs is None
|
|
assert merger.github_insights is None
|
|
|
|
# Should still work
|
|
result = merger.merge_all()
|
|
assert "apis" in result
|
|
assert "summary" in result
|
|
# Should not have GitHub context
|
|
assert "github_context" not in result
|
|
|
|
|
|
class TestIntegration:
|
|
"""Integration tests for Phase 3."""
|
|
|
|
def test_full_pipeline_with_streams(self, tmp_path):
|
|
"""Test complete pipeline with three-stream data."""
|
|
# Create minimal test data
|
|
docs_data = {"pages": []}
|
|
github_data = {"apis": {}}
|
|
|
|
# Create three-stream data
|
|
code_stream = CodeStream(directory=tmp_path, files=[])
|
|
docs_stream = DocsStream(
|
|
readme="# Test Project\n\nA test project.",
|
|
contributing="# Contributing\n\nPull requests welcome.",
|
|
docs_files=[
|
|
{"path": "docs/quickstart.md", "content": "# Quick Start"},
|
|
{"path": "docs/api.md", "content": "# API Reference"},
|
|
],
|
|
)
|
|
insights_stream = InsightsStream(
|
|
metadata={
|
|
"stars": 2500,
|
|
"forks": 123,
|
|
"language": "Python",
|
|
"description": "Test framework",
|
|
},
|
|
common_problems=[
|
|
{
|
|
"title": "Installation fails on Windows",
|
|
"number": 150,
|
|
"state": "open",
|
|
"comments": 25,
|
|
"labels": ["bug", "windows"],
|
|
},
|
|
{
|
|
"title": "Memory leak in async mode",
|
|
"number": 142,
|
|
"state": "open",
|
|
"comments": 18,
|
|
"labels": ["bug", "async"],
|
|
},
|
|
],
|
|
known_solutions=[
|
|
{
|
|
"title": "Fixed config loading",
|
|
"number": 130,
|
|
"state": "closed",
|
|
"comments": 8,
|
|
"labels": ["bug"],
|
|
},
|
|
{
|
|
"title": "Resolved OAuth timeout",
|
|
"number": 125,
|
|
"state": "closed",
|
|
"comments": 12,
|
|
"labels": ["oauth"],
|
|
},
|
|
],
|
|
top_labels=[
|
|
{"label": "bug", "count": 45},
|
|
{"label": "enhancement", "count": 20},
|
|
{"label": "question", "count": 15},
|
|
],
|
|
)
|
|
github_streams = ThreeStreamData(code_stream, docs_stream, insights_stream)
|
|
|
|
# Create merger and merge
|
|
merger = RuleBasedMerger(docs_data, github_data, [], github_streams)
|
|
result = merger.merge_all()
|
|
|
|
# Verify all layers present
|
|
assert "apis" in result # Layer 1 & 2: Code + Docs
|
|
assert "github_context" in result # Layer 3 & 4: GitHub docs + insights
|
|
|
|
# Verify Layer 3: GitHub docs
|
|
gh_context = result["github_context"]
|
|
assert gh_context["docs"]["readme"] == "# Test Project\n\nA test project."
|
|
assert gh_context["docs"]["contributing"] == "# Contributing\n\nPull requests welcome."
|
|
assert gh_context["docs"]["docs_files_count"] == 2
|
|
|
|
# Verify Layer 4: GitHub insights
|
|
assert gh_context["metadata"]["stars"] == 2500
|
|
assert gh_context["metadata"]["language"] == "Python"
|
|
assert gh_context["issues"]["common_problems_count"] == 2
|
|
assert gh_context["issues"]["known_solutions_count"] == 2
|
|
assert len(gh_context["issues"]["top_problems"]) == 2
|
|
assert len(gh_context["issues"]["top_solutions"]) == 2
|
|
assert len(gh_context["top_labels"]) == 3
|
|
|
|
# Verify conflict summary
|
|
assert "conflict_summary" in result
|
|
assert result["conflict_summary"]["total_conflicts"] == 0
|
|
|
|
|
|
class TestMainModuleExecution:
|
|
"""Regression tests for `python -m skill_seekers.cli.merge_sources`.
|
|
|
|
The ClaudeEnhancedMerger backward-compat alias is bound below the
|
|
`if __name__ == "__main__"` block, so when merge_sources() referenced
|
|
the alias it raised NameError during module-body execution
|
|
(`--mode ai-enhanced` / `--mode claude-enhanced` via -m or as a script).
|
|
"""
|
|
|
|
def test_backward_compat_alias_still_exported(self):
|
|
"""External importers keep the ClaudeEnhancedMerger alias."""
|
|
assert ClaudeEnhancedMerger is AIEnhancedMerger
|
|
|
|
@pytest.mark.parametrize("mode", ["ai-enhanced", "claude-enhanced"])
|
|
def test_module_execution_ai_mode_no_nameerror(self, tmp_path, mode):
|
|
"""-m execution with an AI mode must not crash with NameError."""
|
|
docs_path = tmp_path / "docs.json"
|
|
github_path = tmp_path / "github.json"
|
|
out_path = tmp_path / "merged.json"
|
|
docs_path.write_text(json.dumps({"pages": []}))
|
|
github_path.write_text(json.dumps({"apis": {}}))
|
|
|
|
env = os.environ.copy()
|
|
# Keep the AI merge offline and deterministic: no API keys forces
|
|
# LOCAL mode, and the recursion guard makes _call_local return None
|
|
# without spawning an agent, so merge_all() falls back to the
|
|
# rule-based merger.
|
|
for var in (
|
|
"ANTHROPIC_API_KEY",
|
|
"ANTHROPIC_AUTH_TOKEN",
|
|
"GOOGLE_API_KEY",
|
|
"OPENAI_API_KEY",
|
|
"MOONSHOT_API_KEY",
|
|
):
|
|
env.pop(var, None)
|
|
env["SKILL_SEEKER_ENHANCE_ACTIVE"] = "1"
|
|
# Run the module from this checkout, not a stale installed copy.
|
|
src_dir = str(Path(merge_sources_module.__file__).resolve().parents[2])
|
|
env["PYTHONPATH"] = os.pathsep.join(p for p in (src_dir, env.get("PYTHONPATH")) if p)
|
|
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-m",
|
|
"skill_seekers.cli.merge_sources",
|
|
str(docs_path),
|
|
str(github_path),
|
|
"--output",
|
|
str(out_path),
|
|
"--mode",
|
|
mode,
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=120,
|
|
env=env,
|
|
)
|
|
|
|
assert "NameError" not in result.stderr, result.stderr
|
|
assert result.returncode == 0, result.stderr
|
|
merged = json.loads(out_path.read_text())
|
|
# AI agent was blocked, so the run must have completed via the
|
|
# rule-based fallback rather than crashing.
|
|
assert merged["merge_mode"] == "rule-based"
|