Claude Code only caches the plugin directory when installed from a
marketplace, not parent directories. This broke the shared mcp-servers/
architecture because relative paths like ../../mcp-servers/ resolved
to non-existent locations in the cache.
Changes:
- Move gitea and wikijs MCP servers into plugins/projman/mcp-servers/
- Move netbox MCP server into plugins/cmdb-assistant/mcp-servers/
- Update .mcp.json files to use ${CLAUDE_PLUGIN_ROOT}/mcp-servers/
- Update setup.sh to handle new bundled structure
- Add netbox.env config template to setup.sh
- Update CLAUDE.md and CANONICAL-PATHS.md documentation
This ensures plugins work correctly when installed and cached.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
356 lines
11 KiB
Python
356 lines
11 KiB
Python
"""
|
|
Tests for Wiki.js GraphQL client.
|
|
"""
|
|
import pytest
|
|
from unittest.mock import AsyncMock, patch, MagicMock
|
|
from mcp_server.wikijs_client import WikiJSClient
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
"""Create WikiJSClient instance for testing"""
|
|
return WikiJSClient(
|
|
api_url="http://wiki.test.com/graphql",
|
|
api_token="test_token_123",
|
|
base_path="/test-company",
|
|
project="projects/test-project"
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def company_client():
|
|
"""Create WikiJSClient in company mode"""
|
|
return WikiJSClient(
|
|
api_url="http://wiki.test.com/graphql",
|
|
api_token="test_token_123",
|
|
base_path="/test-company",
|
|
project=None # Company mode
|
|
)
|
|
|
|
|
|
def test_client_initialization(client):
|
|
"""Test client initializes with correct settings"""
|
|
assert client.api_url == "http://wiki.test.com/graphql"
|
|
assert client.api_token == "test_token_123"
|
|
assert client.base_path == "/test-company"
|
|
assert client.project == "projects/test-project"
|
|
assert client.mode == 'project'
|
|
|
|
|
|
def test_company_mode_initialization(company_client):
|
|
"""Test client initializes in company mode"""
|
|
assert company_client.mode == 'company'
|
|
assert company_client.project is None
|
|
|
|
|
|
def test_get_full_path_project_mode(client):
|
|
"""Test path construction in project mode"""
|
|
path = client._get_full_path("documentation/api")
|
|
assert path == "/test-company/projects/test-project/documentation/api"
|
|
|
|
|
|
def test_get_full_path_company_mode(company_client):
|
|
"""Test path construction in company mode"""
|
|
path = company_client._get_full_path("shared/architecture")
|
|
assert path == "/test-company/shared/architecture"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_pages(client):
|
|
"""Test searching pages"""
|
|
mock_response = {
|
|
'data': {
|
|
'pages': {
|
|
'search': {
|
|
'results': [
|
|
{
|
|
'id': 1,
|
|
'path': '/test-company/projects/test-project/doc1',
|
|
'title': 'Document 1',
|
|
'tags': ['api', 'documentation']
|
|
},
|
|
{
|
|
'id': 2,
|
|
'path': '/test-company/projects/test-project/doc2',
|
|
'title': 'Document 2',
|
|
'tags': ['guide', 'tutorial']
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_instance = MagicMock()
|
|
mock_instance.__aenter__.return_value = mock_instance
|
|
mock_instance.__aexit__.return_value = None
|
|
mock_instance.post = AsyncMock(return_value=MagicMock(
|
|
json=lambda: mock_response,
|
|
raise_for_status=lambda: None
|
|
))
|
|
mock_client.return_value = mock_instance
|
|
|
|
results = await client.search_pages("documentation")
|
|
|
|
assert len(results) == 2
|
|
assert results[0]['title'] == 'Document 1'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_page(client):
|
|
"""Test getting a specific page"""
|
|
mock_response = {
|
|
'data': {
|
|
'pages': {
|
|
'single': {
|
|
'id': 1,
|
|
'path': '/test-company/projects/test-project/doc1',
|
|
'title': 'Document 1',
|
|
'content': '# Test Content',
|
|
'tags': ['api'],
|
|
'isPublished': True
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_instance = MagicMock()
|
|
mock_instance.__aenter__.return_value = mock_instance
|
|
mock_instance.__aexit__.return_value = None
|
|
mock_instance.post = AsyncMock(return_value=MagicMock(
|
|
json=lambda: mock_response,
|
|
raise_for_status=lambda: None
|
|
))
|
|
mock_client.return_value = mock_instance
|
|
|
|
page = await client.get_page("doc1")
|
|
|
|
assert page is not None
|
|
assert page['title'] == 'Document 1'
|
|
assert page['content'] == '# Test Content'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_page(client):
|
|
"""Test creating a new page"""
|
|
mock_response = {
|
|
'data': {
|
|
'pages': {
|
|
'create': {
|
|
'responseResult': {
|
|
'succeeded': True,
|
|
'errorCode': None,
|
|
'message': 'Page created successfully'
|
|
},
|
|
'page': {
|
|
'id': 1,
|
|
'path': '/test-company/projects/test-project/new-doc',
|
|
'title': 'New Document'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_instance = MagicMock()
|
|
mock_instance.__aenter__.return_value = mock_instance
|
|
mock_instance.__aexit__.return_value = None
|
|
mock_instance.post = AsyncMock(return_value=MagicMock(
|
|
json=lambda: mock_response,
|
|
raise_for_status=lambda: None
|
|
))
|
|
mock_client.return_value = mock_instance
|
|
|
|
page = await client.create_page(
|
|
path="new-doc",
|
|
title="New Document",
|
|
content="# Content",
|
|
tags=["test"]
|
|
)
|
|
|
|
assert page['id'] == 1
|
|
assert page['title'] == 'New Document'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_page(client):
|
|
"""Test updating a page"""
|
|
mock_response = {
|
|
'data': {
|
|
'pages': {
|
|
'update': {
|
|
'responseResult': {
|
|
'succeeded': True,
|
|
'message': 'Page updated'
|
|
},
|
|
'page': {
|
|
'id': 1,
|
|
'path': '/test-company/projects/test-project/doc1',
|
|
'title': 'Updated Title'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_instance = MagicMock()
|
|
mock_instance.__aenter__.return_value = mock_instance
|
|
mock_instance.__aexit__.return_value = None
|
|
mock_instance.post = AsyncMock(return_value=MagicMock(
|
|
json=lambda: mock_response,
|
|
raise_for_status=lambda: None
|
|
))
|
|
mock_client.return_value = mock_instance
|
|
|
|
page = await client.update_page(
|
|
page_id=1,
|
|
title="Updated Title"
|
|
)
|
|
|
|
assert page['title'] == 'Updated Title'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_pages(client):
|
|
"""Test listing pages"""
|
|
mock_response = {
|
|
'data': {
|
|
'pages': {
|
|
'list': [
|
|
{'id': 1, 'path': '/test-company/projects/test-project/doc1', 'title': 'Doc 1'},
|
|
{'id': 2, 'path': '/test-company/projects/test-project/doc2', 'title': 'Doc 2'},
|
|
{'id': 3, 'path': '/test-company/other-project/doc3', 'title': 'Doc 3'}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_instance = MagicMock()
|
|
mock_instance.__aenter__.return_value = mock_instance
|
|
mock_instance.__aexit__.return_value = None
|
|
mock_instance.post = AsyncMock(return_value=MagicMock(
|
|
json=lambda: mock_response,
|
|
raise_for_status=lambda: None
|
|
))
|
|
mock_client.return_value = mock_instance
|
|
|
|
# List all pages in current project
|
|
pages = await client.list_pages("")
|
|
|
|
# Should only return pages from test-project
|
|
assert len(pages) == 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_lesson(client):
|
|
"""Test creating a lesson learned"""
|
|
mock_response = {
|
|
'data': {
|
|
'pages': {
|
|
'create': {
|
|
'responseResult': {
|
|
'succeeded': True,
|
|
'message': 'Lesson created'
|
|
},
|
|
'page': {
|
|
'id': 1,
|
|
'path': '/test-company/projects/test-project/lessons-learned/sprints/test-lesson',
|
|
'title': 'Test Lesson'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_instance = MagicMock()
|
|
mock_instance.__aenter__.return_value = mock_instance
|
|
mock_instance.__aexit__.return_value = None
|
|
mock_instance.post = AsyncMock(return_value=MagicMock(
|
|
json=lambda: mock_response,
|
|
raise_for_status=lambda: None
|
|
))
|
|
mock_client.return_value = mock_instance
|
|
|
|
lesson = await client.create_lesson(
|
|
title="Test Lesson",
|
|
content="# Lesson Content",
|
|
tags=["testing", "sprint-16"],
|
|
category="sprints"
|
|
)
|
|
|
|
assert lesson['id'] == 1
|
|
assert 'lessons-learned' in lesson['path']
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_lessons(client):
|
|
"""Test searching lessons learned"""
|
|
mock_response = {
|
|
'data': {
|
|
'pages': {
|
|
'search': {
|
|
'results': [
|
|
{
|
|
'id': 1,
|
|
'path': '/test-company/projects/test-project/lessons-learned/sprints/lesson1',
|
|
'title': 'Lesson 1',
|
|
'tags': ['testing']
|
|
},
|
|
{
|
|
'id': 2,
|
|
'path': '/test-company/projects/test-project/documentation/doc1',
|
|
'title': 'Doc 1',
|
|
'tags': ['guide']
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_instance = MagicMock()
|
|
mock_instance.__aenter__.return_value = mock_instance
|
|
mock_instance.__aexit__.return_value = None
|
|
mock_instance.post = AsyncMock(return_value=MagicMock(
|
|
json=lambda: mock_response,
|
|
raise_for_status=lambda: None
|
|
))
|
|
mock_client.return_value = mock_instance
|
|
|
|
lessons = await client.search_lessons(query="testing")
|
|
|
|
# Should only return lessons-learned pages
|
|
assert len(lessons) == 1
|
|
assert 'lessons-learned' in lessons[0]['path']
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graphql_error_handling(client):
|
|
"""Test handling of GraphQL errors"""
|
|
mock_response = {
|
|
'errors': [
|
|
{'message': 'Page not found'},
|
|
{'message': 'Invalid query'}
|
|
]
|
|
}
|
|
|
|
with patch('httpx.AsyncClient') as mock_client:
|
|
mock_instance = MagicMock()
|
|
mock_instance.__aenter__.return_value = mock_instance
|
|
mock_instance.__aexit__.return_value = None
|
|
mock_instance.post = AsyncMock(return_value=MagicMock(
|
|
json=lambda: mock_response,
|
|
raise_for_status=lambda: None
|
|
))
|
|
mock_client.return_value = mock_instance
|
|
|
|
with pytest.raises(ValueError, match="GraphQL errors"):
|
|
await client.search_pages("test")
|