New router intelligence modules (26 files): alert_ingest/store, audit_store, architecture_pressure, backlog_generator/store, cost_analyzer, data_governance, dependency_scanner, drift_analyzer, incident_* (5 files), llm_enrichment, platform_priority_digest, provider_budget, release_check_runner, risk_* (6 files), signature_state_store, sofiia_auto_router, tool_governance New services: - sofiia-console: Dockerfile, adapters/, monitor/nodes/ops/voice modules, launchd, react static - memory-service: integration_endpoints, integrations, voice_endpoints, static UI - aurora-service: full app suite (analysis, job_store, orchestrator, reporting, schemas, subagents) - sofiia-supervisor: new supervisor service - aistalk-bridge-lite: Telegram bridge lite - calendar-service: CalDAV calendar service with reminders - mlx-stt-service / mlx-tts-service: Apple Silicon speech services - binance-bot-monitor: market monitor service - node-worker: STT/TTS memory providers New tools (9): agent_email, browser_tool, contract_tool, observability_tool, oncall_tool, pr_reviewer_tool, repo_tool, safe_code_executor, secure_vault New crews: agromatrix_crew (10 modules: depth_classifier, doc_facts, doc_focus, farm_state, light_reply, llm_factory, memory_manager, proactivity, reflection_engine, session_context, style_adapter, telemetry) Tests: 85+ test files for all new modules Made-with: Cursor
439 lines
10 KiB
Python
439 lines
10 KiB
Python
"""
|
|
Tests for Contract Tool (OpenAPI/JSON Schema)
|
|
"""
|
|
|
|
import pytest
|
|
import os
|
|
import sys
|
|
import yaml
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
|
|
from services.router.tool_manager import ToolManager, ToolResult
|
|
|
|
|
|
class TestContractToolLinting:
|
|
"""Test OpenAPI linting functionality"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lint_missing_operation_id(self):
|
|
"""Test detection of missing operationId"""
|
|
tool_mgr = ToolManager({})
|
|
|
|
spec = """
|
|
openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths:
|
|
/users:
|
|
get:
|
|
responses:
|
|
"200":
|
|
description: Success
|
|
"""
|
|
|
|
result = await tool_mgr._contract_tool({
|
|
"action": "lint_openapi",
|
|
"inputs": {
|
|
"format": "openapi_yaml",
|
|
"base": {"source": "text", "value": spec}
|
|
},
|
|
"options": {}
|
|
})
|
|
|
|
assert result.success is True
|
|
assert len(result.result["lint"]) > 0
|
|
assert any("operationId" in i["message"] for i in result.result["lint"])
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lint_good_spec(self):
|
|
"""Test that valid spec passes lint"""
|
|
tool_mgr = ToolManager({})
|
|
|
|
spec = """
|
|
openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths:
|
|
/users:
|
|
get:
|
|
operationId: getUsers
|
|
responses:
|
|
"200":
|
|
description: Success
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
"""
|
|
|
|
result = await tool_mgr._contract_tool({
|
|
"action": "lint_openapi",
|
|
"inputs": {
|
|
"format": "openapi_yaml",
|
|
"base": {"source": "text", "value": spec}
|
|
},
|
|
"options": {}
|
|
})
|
|
|
|
assert result.success is True
|
|
errors = [i for i in result.result["lint"] if i["severity"] == "error"]
|
|
assert len(errors) == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_lint_no_spec(self):
|
|
"""Test error when no spec provided"""
|
|
tool_mgr = ToolManager({})
|
|
|
|
result = await tool_mgr._contract_tool({
|
|
"action": "lint_openapi",
|
|
"inputs": {},
|
|
"options": {}
|
|
})
|
|
|
|
assert result.success is False
|
|
|
|
|
|
class TestContractToolDiff:
|
|
"""Test OpenAPI diff/breaking changes detection"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_endpoint_removed_breaking(self):
|
|
"""Test detection of removed endpoint"""
|
|
tool_mgr = ToolManager({})
|
|
|
|
base_spec = """
|
|
openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths:
|
|
/users:
|
|
get:
|
|
operationId: getUsers
|
|
responses:
|
|
"200":
|
|
description: Success
|
|
"""
|
|
|
|
head_spec = """
|
|
openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths: {}
|
|
"""
|
|
|
|
result = await tool_mgr._contract_tool({
|
|
"action": "diff_openapi",
|
|
"inputs": {
|
|
"format": "openapi_yaml",
|
|
"base": {"source": "text", "value": base_spec},
|
|
"head": {"source": "text", "value": head_spec}
|
|
},
|
|
"options": {}
|
|
})
|
|
|
|
assert result.success is True
|
|
assert len(result.result["breaking"]) > 0
|
|
assert any("endpoint_removed" in i["type"] for i in result.result["breaking"])
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_required_field_added_breaking(self):
|
|
"""Test detection of required field added"""
|
|
tool_mgr = ToolManager({})
|
|
|
|
base_spec = """
|
|
openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths: {}
|
|
components:
|
|
schemas:
|
|
User:
|
|
type: object
|
|
properties:
|
|
name:
|
|
type: string
|
|
"""
|
|
|
|
head_spec = """
|
|
openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths: {}
|
|
components:
|
|
schemas:
|
|
User:
|
|
type: object
|
|
required:
|
|
- email
|
|
properties:
|
|
name:
|
|
type: string
|
|
email:
|
|
type: string
|
|
"""
|
|
|
|
result = await tool_mgr._contract_tool({
|
|
"action": "diff_openapi",
|
|
"inputs": {
|
|
"format": "openapi_yaml",
|
|
"base": {"source": "text", "value": base_spec},
|
|
"head": {"source": "text", "value": head_spec}
|
|
},
|
|
"options": {}
|
|
})
|
|
|
|
assert result.success is True
|
|
assert len(result.result["breaking"]) > 0
|
|
assert any("required_added" in i["type"] for i in result.result["breaking"])
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_optional_field_added_nonbreaking(self):
|
|
"""Test that optional field addition is non-breaking"""
|
|
tool_mgr = ToolManager({})
|
|
|
|
base_spec = """
|
|
openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths: {}
|
|
components:
|
|
schemas:
|
|
User:
|
|
type: object
|
|
properties:
|
|
name:
|
|
type: string
|
|
"""
|
|
|
|
head_spec = """
|
|
openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths: {}
|
|
components:
|
|
schemas:
|
|
User:
|
|
type: object
|
|
properties:
|
|
name:
|
|
type: string
|
|
email:
|
|
type: string
|
|
"""
|
|
|
|
result = await tool_mgr._contract_tool({
|
|
"action": "diff_openapi",
|
|
"inputs": {
|
|
"format": "openapi_yaml",
|
|
"base": {"source": "text", "value": base_spec},
|
|
"head": {"source": "text", "value": head_spec}
|
|
},
|
|
"options": {}
|
|
})
|
|
|
|
assert result.success is True
|
|
# Should have non-breaking, no breaking
|
|
breaking = result.result.get("breaking", [])
|
|
assert len(breaking) == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_enum_narrowed_breaking(self):
|
|
"""Test detection of enum narrowing"""
|
|
tool_mgr = ToolManager({})
|
|
|
|
base_spec = """
|
|
openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths: {}
|
|
components:
|
|
schemas:
|
|
Status:
|
|
type: string
|
|
enum:
|
|
- pending
|
|
- active
|
|
- completed
|
|
"""
|
|
|
|
head_spec = """
|
|
openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths: {}
|
|
components:
|
|
schemas:
|
|
Status:
|
|
type: string
|
|
enum:
|
|
- pending
|
|
- active
|
|
"""
|
|
|
|
result = await tool_mgr._contract_tool({
|
|
"action": "diff_openapi",
|
|
"inputs": {
|
|
"format": "openapi_yaml",
|
|
"base": {"source": "text", "value": base_spec},
|
|
"head": {"source": "text", "value": head_spec}
|
|
},
|
|
"options": {}
|
|
})
|
|
|
|
assert result.success is True
|
|
assert len(result.result["breaking"]) > 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fail_on_breaking(self):
|
|
"""Test fail_on_breaking option"""
|
|
tool_mgr = ToolManager({})
|
|
|
|
base_spec = """
|
|
openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths:
|
|
/users:
|
|
get:
|
|
operationId: getUsers
|
|
responses:
|
|
"200":
|
|
description: Success
|
|
"""
|
|
|
|
head_spec = """
|
|
openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths: {}
|
|
"""
|
|
|
|
result = await tool_mgr._contract_tool({
|
|
"action": "diff_openapi",
|
|
"inputs": {
|
|
"format": "openapi_yaml",
|
|
"base": {"source": "text", "value": base_spec},
|
|
"head": {"source": "text", "value": head_spec}
|
|
},
|
|
"options": {
|
|
"fail_on_breaking": True
|
|
}
|
|
})
|
|
|
|
assert result.success is False
|
|
assert result.error is not None
|
|
|
|
|
|
class TestContractToolLimits:
|
|
"""Test limit enforcement"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_max_chars_limit(self):
|
|
"""Test that max_chars limit is enforced"""
|
|
tool_mgr = ToolManager({})
|
|
|
|
large_spec = "x" * 900000 # 900KB
|
|
|
|
result = await tool_mgr._contract_tool({
|
|
"action": "lint_openapi",
|
|
"inputs": {
|
|
"format": "openapi_yaml",
|
|
"base": {"source": "text", "value": large_spec}
|
|
},
|
|
"options": {
|
|
"max_chars": 800000
|
|
}
|
|
})
|
|
|
|
assert result.success is False
|
|
assert "too large" in result.error.lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_missing_base_or_head(self):
|
|
"""Test error when base or head missing for diff"""
|
|
tool_mgr = ToolManager({})
|
|
|
|
spec = """
|
|
openapi: "3.0.0"
|
|
info:
|
|
title: Test API
|
|
version: "1.0.0"
|
|
paths: {}
|
|
"""
|
|
|
|
result = await tool_mgr._contract_tool({
|
|
"action": "diff_openapi",
|
|
"inputs": {
|
|
"format": "openapi_yaml",
|
|
"base": {"source": "text", "value": spec}
|
|
},
|
|
"options": {}
|
|
})
|
|
|
|
assert result.success is False
|
|
|
|
|
|
class TestContractToolStub:
|
|
"""Test client stub generation"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_generate_python_stub(self):
|
|
"""Test Python client stub generation"""
|
|
tool_mgr = ToolManager({})
|
|
|
|
spec = """
|
|
openapi: "3.0.0"
|
|
info:
|
|
title: User API
|
|
version: "1.0.0"
|
|
paths:
|
|
/users:
|
|
get:
|
|
operationId: getUsers
|
|
summary: Get all users
|
|
responses:
|
|
"200":
|
|
description: Success
|
|
post:
|
|
operationId: createUser
|
|
summary: Create user
|
|
parameters:
|
|
- name: data
|
|
in: body
|
|
required: true
|
|
responses:
|
|
"201":
|
|
description: Created
|
|
"""
|
|
|
|
result = await tool_mgr._contract_tool({
|
|
"action": "generate_client_stub",
|
|
"inputs": {
|
|
"format": "openapi_yaml",
|
|
"base": {"source": "text", "value": spec}
|
|
},
|
|
"options": {}
|
|
})
|
|
|
|
assert result.success is True
|
|
assert "client_stub" in result.result
|
|
assert "def getUsers" in result.result["client_stub"]
|
|
assert "def createUser" in result.result["client_stub"]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|