Squashed 'ai-context/trustgraph-templates/' content from commit 42a5fd1b

git-subtree-dir: ai-context/trustgraph-templates
git-subtree-split: 42a5fd1b678f32be378062e30451e2052ccb95dd
This commit is contained in:
elpresidank 2026-04-05 21:09:49 -05:00
commit 74cc8a4685
1216 changed files with 116347 additions and 0 deletions

189
tests/README.md Normal file
View file

@ -0,0 +1,189 @@
# TrustGraph Configurator Test Suite
Comprehensive pytest-based test suite for trustgraph-configurator.
## Installation
Install with development dependencies:
```bash
pip install -e .[dev]
```
## Running Tests
```bash
# All tests
pytest
# Specific category
pytest tests/unit/
pytest tests/integration/
pytest tests/validation/
# By marker
pytest -m unit
pytest -m integration
pytest -m validation
# Specific test file
pytest tests/unit/test_generator.py
# Parallel execution (faster)
pytest -n auto
# Verbose output
pytest -v
# Stop on first failure
pytest -x
# With coverage
pytest --cov=trustgraph_configurator --cov-report=html
```
## Test Structure
```
tests/
├── conftest.py # Shared fixtures
├── unit/ # Unit tests for Python modules
│ ├── test_generator.py
│ ├── test_packager.py
│ ├── test_api.py
│ └── test_run.py
├── integration/ # Full workflow tests
│ ├── test_compilation.py # Template compilation matrix
│ ├── test_cli.py # CLI interface tests
│ └── test_errors.py # Error handling tests
├── validation/ # Output validation tests
│ ├── test_syntax.py # Syntax validation
│ ├── test_schema.py # Schema validation
│ ├── test_semantics_k8s.py
│ ├── test_semantics_docker.py
│ └── test_semantics_tg.py
├── validators/ # Validation helper modules
│ ├── kubernetes.py
│ ├── docker_compose.py
│ └── trustgraph.py
├── schemas/ # JSON schemas
│ ├── trustgraph-config.schema.json
│ ├── kubernetes-resource.schema.json
│ └── docker-compose.schema.json
└── configs/ # Test input configs
├── minimal.json
├── complex-rag.json
├── multi-service.json
└── cloud-aws.json
```
## Test Categories
### Unit Tests (`tests/unit/`)
Test individual Python modules in isolation:
- Generator: Jsonnet template processing
- Packager: Configuration assembly and zip creation
- API: Template listing and version resolution
- Run: CLI entry point and argument parsing
### Integration Tests (`tests/integration/`)
Test full workflow end-to-end:
- **Compilation**: Template compilation across all version/platform/config combinations (192 tests)
- **CLI**: Command line interface functionality
- **Errors**: Error handling and reporting
### Validation Tests (`tests/validation/`)
Verify correctness of generated outputs:
- **Syntax**: JSON/YAML parsing validation
- **Schema**: JSON Schema compliance
- **Semantics**: Cross-references, consistency checks
## Test Matrix
Integration tests cover:
- **Versions**: 1.6, 1.7, 1.8
- **Platforms**: docker-compose, podman-compose, minikube-k8s, gcp-k8s, aks-k8s, eks-k8s, scw-k8s, ovh-k8s
- **Configs**: minimal, complex-rag, multi-service, cloud-aws
Total: 3 versions × 8 platforms × 4 configs × 2 outputs = 192 test combinations
## Validation Layers
### Syntax Validation
- JSON parsing with `json.loads()`
- YAML parsing with `yaml.safe_load()`
### Schema Validation
- TrustGraph config against `trustgraph-config.schema.json`
- Docker Compose against `docker-compose.schema.json`
- Kubernetes resources against `kubernetes-resource.schema.json`
### Semantic Validation
**Kubernetes:**
- Deployment selectors match pod labels
- Service selectors match deployment labels
- volumeMounts reference defined volumes
- ConfigMap/Secret references exist
- Service targetPorts match container ports
**Docker Compose:**
- depends_on references valid services
- Volume names are defined
- Network references are valid
- No port conflicts
**TrustGraph Config:**
- Service references are valid
- Parameter types are reasonable
- Storage backends are consistent
- LLM configuration is present
## Fixtures
Available in `conftest.py`:
- `test_config_dir`: Path to test configs
- `test_configs`: Loaded test configurations
- `temp_output_dir`: Temporary directory for outputs
- `run_configurator`: Function to execute configurator
- `mock_config_file`: Create temporary config files
## CI/CD
Tests run automatically on pull requests via GitHub Actions.
See `.github/workflows/pull-request.yaml` for CI configuration.
## Development
### Adding New Tests
1. Create test file in appropriate directory
2. Use appropriate markers (`@pytest.mark.unit`, etc.)
3. Use fixtures from `conftest.py`
4. Follow naming convention: `test_*.py`, `test_*()` functions
### Adding New Validation
1. Add validation logic to `tests/validators/`
2. Create corresponding tests in `tests/validation/`
3. Update schemas in `tests/schemas/` if needed
## Troubleshooting
**Test failures:**
- Check stderr output for error messages
- Run with `-v` for verbose output
- Run with `--tb=long` for full tracebacks
**Import errors:**
- Ensure package is installed: `pip install -e .[dev]`
- Check PYTHONPATH includes project root
**Slow tests:**
- Use `-n auto` for parallel execution
- Run specific test subsets instead of full suite
## Documentation
See `docs/tech-specs/tests.md` for detailed test specification.

View file

@ -0,0 +1,26 @@
[
{
"name": "bedrock",
"parameters": {
"model": "anthropic.claude-3-sonnet-20240229-v1:0"
}
},
{
"name": "embeddings-hf",
"parameters": {
"embeddings-model": "sentence-transformers/all-MiniLM-L6-v2"
}
},
{
"name": "vector-store-qdrant",
"parameters": {}
},
{
"name": "trustgraph-base",
"parameters": {}
},
{
"name": "pulsar",
"parameters": {}
}
]

View file

@ -0,0 +1,42 @@
[
{
"name": "openai",
"parameters": {
"model": "gpt-4",
"temperature": 0.7
}
},
{
"name": "embeddings-hf",
"parameters": {
"embeddings-model": "sentence-transformers/all-MiniLM-L6-v2"
}
},
{
"name": "vector-store-qdrant",
"parameters": {}
},
{
"name": "triple-store-neo4j",
"parameters": {}
},
{
"name": "triple-store-cassandra",
"parameters": {}
},
{
"name": "override-recursive-chunker",
"parameters": {
"chunk-size": 2000,
"chunk-overlap": 100
}
},
{
"name": "trustgraph-base",
"parameters": {}
},
{
"name": "pulsar",
"parameters": {}
}
]

View file

@ -0,0 +1,23 @@
[
{
"name": "openai",
"parameters": {
"model": "gpt-3.5-turbo",
"temperature": 0.7
}
},
{
"name": "embeddings-hf",
"parameters": {
"embeddings-model": "sentence-transformers/all-MiniLM-L6-v2"
}
},
{
"name": "trustgraph-base",
"parameters": {}
},
{
"name": "pulsar",
"parameters": {}
}
]

View file

@ -0,0 +1,38 @@
[
{
"name": "ollama",
"parameters": {
"model": "llama2"
}
},
{
"name": "embeddings-fastembed",
"parameters": {
"embeddings-model": "BAAI/bge-small-en-v1.5"
}
},
{
"name": "vector-store-milvus",
"parameters": {}
},
{
"name": "triple-store-memgraph",
"parameters": {}
},
{
"name": "triple-store-cassandra",
"parameters": {}
},
{
"name": "grafana",
"parameters": {}
},
{
"name": "trustgraph-base",
"parameters": {}
},
{
"name": "pulsar",
"parameters": {}
}
]

125
tests/conftest.py Normal file
View file

@ -0,0 +1,125 @@
"""
Pytest configuration and shared fixtures for trustgraph-configurator tests.
"""
import pytest
# =============================================================================
# Version Configuration - Update these when adding new template versions
# =============================================================================
TESTED_VERSIONS = ["1.8", "1.9", "2.0"]
PRIMARY_VERSION = "1.9" # Used when only one version is tested
import sys
import json
import tempfile
import shutil
from pathlib import Path
@pytest.fixture(scope="session")
def test_config_dir():
"""Path to the test configurations directory."""
return Path(__file__).parent / "configs"
@pytest.fixture(scope="session")
def test_configs(test_config_dir):
"""Dictionary of loaded test configurations."""
configs = {}
for config_file in test_config_dir.glob("*.json"):
with open(config_file) as f:
configs[config_file.name] = json.load(f)
return configs
@pytest.fixture
def temp_output_dir():
"""Temporary directory for test outputs."""
temp_dir = tempfile.mkdtemp()
yield Path(temp_dir)
shutil.rmtree(temp_dir)
@pytest.fixture
def run_configurator(monkeypatch, capsys):
"""
Fixture to run configurator with given arguments.
Usage:
stdout, stderr, exit_code = run_configurator(['-t', '1.8', '-p', 'docker-compose', ...])
Returns:
tuple: (stdout, stderr, exit_code)
"""
def _run(args):
from trustgraph_configurator import run
# Set sys.argv with the command and arguments
monkeypatch.setattr(sys, 'argv', ['tg-build-deployment'] + args)
exit_code = 0
try:
run() # run is already the function, not a module
except SystemExit as e:
exit_code = e.code if e.code is not None else 0
# Capture output
captured = capsys.readouterr()
return captured.out, captured.err, exit_code
return _run
@pytest.fixture(scope="session")
def golden_dir():
"""Path to the golden files directory."""
return Path(__file__).parent / "golden"
@pytest.fixture(scope="session")
def test_versions():
"""List of template versions to test."""
return TESTED_VERSIONS
@pytest.fixture(scope="session")
def primary_version():
"""Primary version for tests that only need one version."""
return PRIMARY_VERSION
@pytest.fixture(scope="session")
def test_platforms():
"""List of platforms to test."""
return [
"docker-compose",
"podman-compose",
"minikube-k8s",
"gcp-k8s",
"aks-k8s",
"eks-k8s",
"scw-k8s",
"ovh-k8s",
]
@pytest.fixture(scope="session")
def test_config_names():
"""List of test configuration file names."""
return [
"minimal.json",
"complex-rag.json",
"multi-service.json",
"cloud-aws.json",
]
@pytest.fixture
def mock_config_file(tmp_path):
"""Create a temporary config file for testing."""
def _create(config_data):
config_file = tmp_path / "test_config.json"
with open(config_file, 'w') as f:
json.dump(config_data, f)
return str(config_file)
return _create

View file

View file

@ -0,0 +1,89 @@
"""
Integration tests for CLI interface.
"""
import pytest
import subprocess
from conftest import TESTED_VERSIONS
@pytest.mark.integration
class TestCLIInterface:
"""Tests for CLI command line interface."""
def test_cli_executable_help(self):
"""Test that CLI executable --help works."""
result = subprocess.run(
['tg-build-deployment', '--help'],
capture_output=True,
text=True
)
assert result.returncode == 0
assert 'usage' in result.stdout.lower()
def test_cli_executable_exists(self):
"""Test that tg-build-deployment is in PATH."""
result = subprocess.run(
['which', 'tg-build-deployment'],
capture_output=True,
text=True
)
assert result.returncode == 0
def test_output_modes(self, run_configurator, test_config_dir, primary_version):
"""Test -O and -R output modes."""
config_file = str(test_config_dir / "minimal.json")
# Test -O mode
stdout_o, _, code_o = run_configurator([
'-t', primary_version,
'-p', 'docker-compose',
'-i', config_file,
'--latest-stable',
'-O'
])
assert code_o == 0
assert len(stdout_o) > 0
# Test -R mode
stdout_r, _, code_r = run_configurator([
'-t', primary_version,
'-p', 'docker-compose',
'-i', config_file,
'--latest-stable',
'-R'
])
assert code_r == 0
assert len(stdout_r) > 0
# Outputs should be different
assert stdout_o != stdout_r
def test_platform_argument(self, run_configurator, test_config_dir, primary_version):
"""Test -p/--platform argument."""
config_file = str(test_config_dir / "minimal.json")
for platform in ['docker-compose', 'minikube-k8s']:
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', platform,
'-i', config_file,
'--latest-stable',
'-O'
])
assert code == 0, f"Failed for platform {platform}"
def test_template_argument(self, run_configurator, test_config_dir):
"""Test -t/--template argument."""
config_file = str(test_config_dir / "minimal.json")
for template in TESTED_VERSIONS:
stdout, stderr, code = run_configurator([
'-t', template,
'-p', 'docker-compose',
'-i', config_file,
'--latest-stable',
'-O'
])
assert code == 0, f"Failed for template {template}"

View file

@ -0,0 +1,163 @@
"""
Integration tests for template compilation across all combinations.
"""
import pytest
import json
import yaml
from conftest import TESTED_VERSIONS
@pytest.mark.integration
@pytest.mark.parametrize("version", TESTED_VERSIONS)
@pytest.mark.parametrize("platform", [
"docker-compose",
"podman-compose",
"minikube-k8s",
"gcp-k8s",
"aks-k8s",
"eks-k8s",
"scw-k8s",
"ovh-k8s",
])
@pytest.mark.parametrize("config", [
"minimal.json",
"complex-rag.json",
"multi-service.json",
"cloud-aws.json",
])
def test_tg_config_generation(version, platform, config, run_configurator, test_config_dir):
"""Test TrustGraph config generation for all combinations."""
config_file = str(test_config_dir / config)
stdout, stderr, code = run_configurator([
'-t', version,
'-p', platform,
'-i', config_file,
'--latest-stable',
'-O'
])
# Should succeed
assert code == 0, f"Failed for {version}/{platform}/{config}: {stderr}"
# Should output valid JSON
try:
tg_config = json.loads(stdout)
except json.JSONDecodeError as e:
pytest.fail(f"Invalid JSON output for {version}/{platform}/{config}: {e}")
# Basic structure checks
assert isinstance(tg_config, (dict, list)), "TrustGraph config should be dict or list"
@pytest.mark.integration
@pytest.mark.parametrize("version", TESTED_VERSIONS)
@pytest.mark.parametrize("platform", [
"docker-compose",
"podman-compose",
"minikube-k8s",
"gcp-k8s",
"aks-k8s",
"eks-k8s",
"scw-k8s",
"ovh-k8s",
])
@pytest.mark.parametrize("config", [
"minimal.json",
"complex-rag.json",
"multi-service.json",
"cloud-aws.json",
])
def test_resources_generation(version, platform, config, run_configurator, test_config_dir):
"""Test platform resources generation for all combinations."""
config_file = str(test_config_dir / config)
stdout, stderr, code = run_configurator([
'-t', version,
'-p', platform,
'-i', config_file,
'--latest-stable',
'-R'
])
# Should succeed
assert code == 0, f"Failed for {version}/{platform}/{config}: {stderr}"
# Should output valid YAML
try:
resources = yaml.safe_load(stdout)
except yaml.YAMLError as e:
pytest.fail(f"Invalid YAML output for {version}/{platform}/{config}: {e}")
# Basic structure checks
if platform in ["docker-compose", "podman-compose"]:
assert "services" in resources, "Docker Compose should have services"
else:
# Kubernetes resources
assert resources is not None, "K8s resources should not be empty"
@pytest.mark.integration
def test_compilation_minimal_docker_compose(run_configurator, test_config_dir, primary_version):
"""Smoke test: minimal config on docker-compose."""
config_file = str(test_config_dir / "minimal.json")
# Test TG config
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'docker-compose',
'-i', config_file,
'--latest-stable',
'-O'
])
assert code == 0
tg_config = json.loads(stdout)
assert tg_config is not None
# Test resources
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'docker-compose',
'-i', config_file,
'--latest-stable',
'-R'
])
assert code == 0
resources = yaml.safe_load(stdout)
assert "services" in resources
@pytest.mark.integration
def test_compilation_minimal_k8s(run_configurator, test_config_dir, primary_version):
"""Smoke test: minimal config on k8s."""
config_file = str(test_config_dir / "minimal.json")
# Test TG config
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'minikube-k8s',
'-i', config_file,
'--latest-stable',
'-O'
])
assert code == 0
tg_config = json.loads(stdout)
assert tg_config is not None
# Test resources
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'minikube-k8s',
'-i', config_file,
'--latest-stable',
'-R'
])
assert code == 0
resources = yaml.safe_load(stdout)
assert resources is not None

View file

@ -0,0 +1,97 @@
"""
Integration tests for error handling.
"""
import pytest
import json
@pytest.mark.integration
class TestErrorHandling:
"""Tests for error handling and reporting."""
def test_nonexistent_config_file(self, run_configurator, primary_version):
"""Test error when config file doesn't exist."""
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'docker-compose',
'-i', '/nonexistent/config.json',
'--latest-stable',
'-O'
])
assert code == 1
assert len(stderr) > 0 # Error should be in stderr
def test_invalid_json_config(self, run_configurator, tmp_path, primary_version):
"""Test error when config file has invalid JSON."""
invalid_config = tmp_path / "invalid.json"
invalid_config.write_text("{ invalid json")
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'docker-compose',
'-i', str(invalid_config),
'--latest-stable',
'-O'
])
assert code == 1
def test_invalid_platform(self, run_configurator, test_config_dir, primary_version):
"""Test error when platform is invalid."""
config_file = str(test_config_dir / "minimal.json")
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'nonexistent-platform',
'-i', config_file,
'--latest-stable',
'-R' # Use -R to trigger platform-specific generation
])
assert code == 1
def test_invalid_template_version(self, run_configurator, test_config_dir):
"""Test error when template version doesn't exist."""
config_file = str(test_config_dir / "minimal.json")
stdout, stderr, code = run_configurator([
'-t', '999.999',
'-p', 'docker-compose',
'-i', config_file,
'-O'
])
assert code == 1
def test_malformed_config_structure(self, run_configurator, tmp_path, primary_version):
"""Test error when config structure is invalid."""
# Valid JSON but wrong structure
invalid_config = tmp_path / "bad_structure.json"
invalid_config.write_text('{"wrong": "structure"}')
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'docker-compose',
'-i', str(invalid_config),
'--latest-stable',
'-O'
])
# May succeed with warning or fail - either is acceptable
# The important thing is it doesn't crash
assert code in [0, 1]
def test_missing_required_args(self, run_configurator):
"""Test error when required arguments are missing."""
# Missing template
stdout, stderr, code = run_configurator([
'-p', 'docker-compose',
'-O'
])
assert code == 1
def test_error_goes_to_stderr(self, run_configurator):
"""Test that errors are written to stderr, not stdout."""
stdout, stderr, code = run_configurator([
'-i', '/nonexistent/config.json'
])
assert code == 1
# Errors should be in stderr
assert len(stderr) > 0 or 'Exception' in stderr or code == 1

View file

@ -0,0 +1,103 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Docker Compose Configuration",
"description": "Basic schema for Docker Compose files",
"type": "object",
"required": ["services"],
"properties": {
"version": {
"type": "string",
"description": "Docker Compose version"
},
"services": {
"type": "object",
"description": "Service definitions",
"minProperties": 1,
"additionalProperties": {
"type": "object",
"properties": {
"image": {
"type": "string",
"description": "Docker image"
},
"build": {
"oneOf": [
{"type": "string"},
{"type": "object"}
],
"description": "Build configuration"
},
"ports": {
"type": "array",
"items": {
"oneOf": [
{"type": "string"},
{"type": "integer"}
]
}
},
"volumes": {
"type": "array",
"items": {
"type": "string"
}
},
"environment": {
"oneOf": [
{
"type": "object",
"additionalProperties": {
"oneOf": [
{"type": "string"},
{"type": "number"},
{"type": "boolean"}
]
}
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"depends_on": {
"oneOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "object"
}
]
},
"networks": {
"oneOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "object"
}
]
}
}
}
},
"volumes": {
"type": "object",
"description": "Named volumes"
},
"networks": {
"type": "object",
"description": "Network definitions"
}
}
}

View file

@ -0,0 +1,42 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Kubernetes Resource",
"description": "Basic schema for Kubernetes resources",
"type": "object",
"required": ["apiVersion", "kind", "metadata"],
"properties": {
"apiVersion": {
"type": "string",
"description": "Kubernetes API version"
},
"kind": {
"type": "string",
"description": "Resource kind",
"enum": ["Deployment", "Service", "ConfigMap", "Secret", "PersistentVolumeClaim", "PersistentVolume", "Namespace", "StorageClass"]
},
"metadata": {
"type": "object",
"required": ["name"],
"properties": {
"name": {
"type": "string",
"description": "Resource name"
},
"namespace": {
"type": "string",
"description": "Resource namespace"
},
"labels": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"spec": {
"type": "object",
"description": "Resource specification"
}
}
}

View file

@ -0,0 +1,16 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "TrustGraph Configuration Output",
"description": "Schema for generated TrustGraph configuration",
"type": "object",
"properties": {
"collection": {
"type": "object",
"description": "Collection definitions"
},
"tools": {
"type": "object",
"description": "Tool definitions"
}
}
}

0
tests/unit/__init__.py Normal file
View file

38
tests/unit/test_api.py Normal file
View file

@ -0,0 +1,38 @@
"""
Unit tests for Index class.
"""
import pytest
from trustgraph_configurator import Index
@pytest.mark.unit
class TestAPI:
"""Tests for the Index class."""
def test_get_templates_returns_list(self):
"""Test that get_templates returns a list."""
templates = Index.get_templates()
assert isinstance(templates, list)
assert len(templates) > 0
def test_templates_have_required_fields(self):
"""Test that templates have name and version fields."""
templates = Index.get_templates()
for template in templates:
assert hasattr(template, 'name')
assert hasattr(template, 'version')
def test_get_latest_returns_template(self):
"""Test that get_latest returns a template."""
latest = Index.get_latest()
assert latest is not None
assert hasattr(latest, 'name')
assert hasattr(latest, 'version')
def test_get_latest_stable_returns_template(self):
"""Test that get_latest_stable returns a template."""
latest_stable = Index.get_latest_stable()
assert latest_stable is not None
assert hasattr(latest_stable, 'name')
assert hasattr(latest_stable, 'version')

View file

@ -0,0 +1,101 @@
"""
Unit tests for Generator class.
"""
import pytest
import json
from trustgraph_configurator.generator import Generator
@pytest.mark.unit
class TestGenerator:
"""Tests for the Generator class."""
def test_simple_jsonnet(self):
"""Test processing simple jsonnet."""
def mock_fetch(base, rel):
return "", ""
generator = Generator(mock_fetch)
result = generator.process('{ foo: "bar" }')
assert isinstance(result, dict)
assert result["foo"] == "bar"
def test_jsonnet_with_variables(self):
"""Test processing jsonnet with variables."""
def mock_fetch(base, rel):
return "", ""
generator = Generator(mock_fetch)
jsonnet_code = '''
local name = "test";
{
name: name,
value: 42
}
'''
result = generator.process(jsonnet_code)
assert result["name"] == "test"
assert result["value"] == 42
def test_jsonnet_with_array(self):
"""Test processing jsonnet that returns array."""
def mock_fetch(base, rel):
return "", ""
generator = Generator(mock_fetch)
jsonnet_code = '[1, 2, 3, { foo: "bar" }]'
result = generator.process(jsonnet_code)
assert isinstance(result, list)
assert len(result) == 4
assert result[0] == 1
assert result[3]["foo"] == "bar"
def test_invalid_jsonnet(self):
"""Test that invalid jsonnet raises exception."""
def mock_fetch(base, rel):
return "", ""
generator = Generator(mock_fetch)
with pytest.raises(Exception):
generator.process('{ invalid jsonnet')
def test_jsonnet_with_functions(self):
"""Test processing jsonnet with functions."""
def mock_fetch(base, rel):
return "", ""
generator = Generator(mock_fetch)
jsonnet_code = '''
local double(x) = x * 2;
{
value: double(21)
}
'''
result = generator.process(jsonnet_code)
assert result["value"] == 42
def test_fetch_callback_is_used(self):
"""Test that fetch callback is called for imports."""
fetch_called = []
def mock_fetch(base, rel):
fetch_called.append((base, rel))
# Return simple jsonnet that defines a variable (as bytes)
return "config", b'{ imported: true }'
generator = Generator(mock_fetch)
jsonnet_code = '''
local config = import "config.jsonnet";
config
'''
result = generator.process(jsonnet_code)
assert len(fetch_called) > 0
assert result["imported"] is True

View file

@ -0,0 +1,60 @@
"""
Unit tests for Packager class.
"""
import pytest
from trustgraph_configurator.packager import Packager
@pytest.mark.unit
class TestPackager:
"""Tests for the Packager class."""
def test_init_with_latest_stable(self):
"""Test initialization with latest_stable flag."""
packager = Packager(
version=None,
template=None,
platform="docker-compose",
latest=False,
latest_stable=True
)
assert packager.version is not None
assert packager.template is not None
def test_init_with_template(self):
"""Test initialization with specific template."""
packager = Packager(
version="1.8.12",
template="1.8",
platform="docker-compose",
latest=False,
latest_stable=False
)
assert packager.version == "1.8.12"
assert packager.template == "1.8"
assert packager.platform == "docker-compose"
def test_invalid_platform_raises_error(self):
"""Test that invalid platform raises error during generation."""
packager = Packager(
version="1.8.12",
template="1.8",
platform="invalid-platform",
latest=False,
latest_stable=False
)
with pytest.raises(RuntimeError, match="Bad platform"):
packager.generate('[{"name": "test", "parameters": {}}]')
def test_init_without_template_raises_error(self):
"""Test that initialization without template/latest raises error."""
with pytest.raises(RuntimeError, match="You must"):
Packager(
version=None,
template=None,
platform="docker-compose",
latest=False,
latest_stable=False
)

62
tests/unit/test_run.py Normal file
View file

@ -0,0 +1,62 @@
"""
Unit tests for run module (CLI entry point).
"""
import pytest
import sys
@pytest.mark.unit
class TestRun:
"""Tests for the run module."""
def test_run_without_args_fails(self, run_configurator):
"""Test that running without required args fails."""
stdout, stderr, code = run_configurator([])
assert code != 0
def test_run_with_help_succeeds(self, run_configurator):
"""Test that help flag works."""
stdout, stderr, code = run_configurator(['-h'])
assert code == 0
def test_run_with_invalid_platform_fails(self, run_configurator, test_config_dir, primary_version):
"""Test that invalid platform fails during resource generation."""
config_file = str(test_config_dir / "minimal.json")
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'invalid-platform',
'-i', config_file,
'--latest-stable',
'-R' # Use -R to trigger platform-specific generation
])
assert code == 1
def test_run_with_nonexistent_config_fails(self, run_configurator, primary_version):
"""Test that nonexistent config file fails."""
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'docker-compose',
'-i', '/nonexistent/config.json',
'--latest-stable',
'-O'
])
assert code == 1
def test_exit_code_propagates(self, monkeypatch):
"""Test that exit codes are properly set."""
from trustgraph_configurator import run
# Test successful exit (no exception)
# This would require a valid config, so we'll just test the error path
# Test error exit
monkeypatch.setattr(sys, 'argv', [
'tg-build-deployment',
'-i', '/nonexistent/config.json'
])
with pytest.raises(SystemExit) as exc_info:
run() # run is already the function
assert exc_info.value.code == 1

View file

View file

@ -0,0 +1,111 @@
"""
Schema validation tests for generated outputs.
"""
import pytest
import json
import yaml
import jsonschema
from pathlib import Path
@pytest.fixture(scope="module")
def schemas_dir():
"""Path to schemas directory."""
return Path(__file__).parent.parent / "schemas"
@pytest.mark.validation
def test_tg_config_matches_schema(run_configurator, test_config_dir, schemas_dir, primary_version):
"""Test that TrustGraph config matches schema."""
config_file = str(test_config_dir / "minimal.json")
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'docker-compose',
'-i', config_file,
'--latest-stable',
'-O'
])
assert code == 0
tg_config = json.loads(stdout)
schema_file = schemas_dir / "trustgraph-config.schema.json"
with open(schema_file) as f:
schema = json.load(f)
try:
jsonschema.validate(instance=tg_config, schema=schema)
except jsonschema.ValidationError as e:
pytest.fail(f"Schema validation failed: {e}")
@pytest.mark.validation
def test_docker_compose_matches_schema(run_configurator, test_config_dir, schemas_dir, primary_version):
"""Test that Docker Compose output matches schema."""
config_file = str(test_config_dir / "minimal.json")
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'docker-compose',
'-i', config_file,
'--latest-stable',
'-R'
])
assert code == 0
compose_data = yaml.safe_load(stdout)
schema_file = schemas_dir / "docker-compose.schema.json"
with open(schema_file) as f:
schema = json.load(f)
try:
jsonschema.validate(instance=compose_data, schema=schema)
except jsonschema.ValidationError as e:
pytest.fail(f"Schema validation failed: {e}")
@pytest.mark.validation
def test_kubernetes_resources_match_schema(run_configurator, test_config_dir, schemas_dir, primary_version):
"""Test that Kubernetes resources match schema."""
config_file = str(test_config_dir / "minimal.json")
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'minikube-k8s',
'-i', config_file,
'--latest-stable',
'-R'
])
assert code == 0
resources = yaml.safe_load(stdout)
schema_file = schemas_dir / "kubernetes-resource.schema.json"
with open(schema_file) as f:
schema = json.load(f)
# Validate the resource (which might be a single resource, list, or K8s List)
if isinstance(resources, dict):
# Check if it's a Kubernetes List resource
if resources.get('kind') == 'List' and 'items' in resources:
resources_to_validate = resources['items']
else:
resources_to_validate = [resources]
elif isinstance(resources, list):
resources_to_validate = resources
else:
pytest.fail(f"Unexpected resources type: {type(resources)}")
for resource in resources_to_validate:
if not isinstance(resource, dict):
continue # Skip non-dict items
try:
jsonschema.validate(instance=resource, schema=schema)
except jsonschema.ValidationError as e:
pytest.fail(f"Schema validation failed for resource: {e}")

View file

@ -0,0 +1,78 @@
"""
Semantic validation tests for Docker Compose resources.
"""
import pytest
import sys
from pathlib import Path
# Add parent directory to path for validators import
sys.path.insert(0, str(Path(__file__).parent.parent))
from validators import docker_compose
@pytest.mark.validation
@pytest.mark.parametrize("config", ["minimal.json", "complex-rag.json"])
def test_docker_compose_semantic_validation(config, run_configurator, test_config_dir, primary_version):
"""Test semantic validation of Docker Compose resources."""
config_file = str(test_config_dir / config)
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'docker-compose',
'-i', config_file,
'--latest-stable',
'-R'
])
assert code == 0
is_valid, errors = docker_compose.validate_docker_compose_manifest(stdout)
if not is_valid:
error_msg = "\n".join(errors)
pytest.fail(f"Semantic validation failed for {config}:\n{error_msg}")
@pytest.mark.validation
def test_docker_compose_service_dependencies(run_configurator, test_config_dir, primary_version):
"""Test that service dependencies reference valid services."""
config_file = str(test_config_dir / "minimal.json")
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'docker-compose',
'-i', config_file,
'--latest-stable',
'-R'
])
assert code == 0
compose_data = docker_compose.parse_docker_compose_yaml(stdout)
errors = docker_compose.validate_service_dependencies(compose_data)
if errors:
pytest.fail(f"Invalid service dependencies:\n" + "\n".join(errors))
@pytest.mark.validation
def test_docker_compose_no_port_conflicts(run_configurator, test_config_dir, primary_version):
"""Test that there are no port conflicts."""
config_file = str(test_config_dir / "minimal.json")
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'docker-compose',
'-i', config_file,
'--latest-stable',
'-R'
])
assert code == 0
compose_data = docker_compose.parse_docker_compose_yaml(stdout)
errors = docker_compose.validate_port_conflicts(compose_data)
if errors:
pytest.fail(f"Port conflicts detected:\n" + "\n".join(errors))

View file

@ -0,0 +1,78 @@
"""
Semantic validation tests for Kubernetes resources.
"""
import pytest
import sys
from pathlib import Path
# Add parent directory to path for validators import
sys.path.insert(0, str(Path(__file__).parent.parent))
from validators import kubernetes
@pytest.mark.validation
@pytest.mark.parametrize("config", ["minimal.json", "complex-rag.json"])
def test_k8s_semantic_validation(config, run_configurator, test_config_dir, primary_version):
"""Test semantic validation of Kubernetes resources."""
config_file = str(test_config_dir / config)
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'minikube-k8s',
'-i', config_file,
'--latest-stable',
'-R'
])
assert code == 0
is_valid, errors = kubernetes.validate_kubernetes_manifest(stdout)
if not is_valid:
error_msg = "\n".join(errors)
pytest.fail(f"Semantic validation failed for {config}:\n{error_msg}")
@pytest.mark.validation
def test_k8s_selector_labels_match(run_configurator, test_config_dir, primary_version):
"""Test that Deployment selectors match pod labels."""
config_file = str(test_config_dir / "minimal.json")
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'minikube-k8s',
'-i', config_file,
'--latest-stable',
'-R'
])
assert code == 0
resources = kubernetes.parse_kubernetes_yaml(stdout)
errors = kubernetes.validate_selector_labels_match(resources)
if errors:
pytest.fail(f"Selector/label mismatch:\n" + "\n".join(errors))
@pytest.mark.validation
def test_k8s_volume_references(run_configurator, test_config_dir, primary_version):
"""Test that volumeMounts reference defined volumes."""
config_file = str(test_config_dir / "minimal.json")
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'minikube-k8s',
'-i', config_file,
'--latest-stable',
'-R'
])
assert code == 0
resources = kubernetes.parse_kubernetes_yaml(stdout)
errors = kubernetes.validate_volume_references(resources)
if errors:
pytest.fail(f"Invalid volume references:\n" + "\n".join(errors))

View file

@ -0,0 +1,83 @@
"""
Semantic validation tests for TrustGraph configuration.
"""
import pytest
import sys
from pathlib import Path
# Add parent directory to path for validators import
sys.path.insert(0, str(Path(__file__).parent.parent))
from validators import trustgraph
@pytest.mark.validation
@pytest.mark.parametrize("config", ["minimal.json", "complex-rag.json", "multi-service.json"])
def test_tg_config_semantic_validation(config, run_configurator, test_config_dir, primary_version):
"""Test semantic validation of TrustGraph configuration."""
config_file = str(test_config_dir / config)
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'docker-compose',
'-i', config_file,
'--latest-stable',
'-O'
])
assert code == 0
is_valid, errors = trustgraph.validate_trustgraph_config(stdout)
if not is_valid:
error_msg = "\n".join(errors)
# Some errors might be warnings, so we log them but don't necessarily fail
# Adjust this based on strictness requirements
if any("missing" in err.lower() or "required" in err.lower() for err in errors):
pytest.fail(f"Semantic validation failed for {config}:\n{error_msg}")
@pytest.mark.validation
def test_tg_config_has_llm(run_configurator, test_config_dir, primary_version):
"""Test that TrustGraph config includes LLM provider."""
config_file = str(test_config_dir / "minimal.json")
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'docker-compose',
'-i', config_file,
'--latest-stable',
'-O'
])
assert code == 0
tg_config = trustgraph.parse_trustgraph_config(stdout)
errors = trustgraph.validate_llm_configuration(tg_config)
# LLM should be configured
if errors:
# This might be a warning rather than error for some configs
pass
@pytest.mark.validation
def test_tg_config_structure(run_configurator, test_config_dir, primary_version):
"""Test that TrustGraph config has required structure."""
config_file = str(test_config_dir / "minimal.json")
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', 'docker-compose',
'-i', config_file,
'--latest-stable',
'-O'
])
assert code == 0
tg_config = trustgraph.parse_trustgraph_config(stdout)
errors = trustgraph.validate_required_structure(tg_config)
if errors:
pytest.fail(f"Invalid TrustGraph config structure:\n" + "\n".join(errors))

View file

@ -0,0 +1,57 @@
"""
Syntax validation tests for generated outputs.
"""
import pytest
import json
import yaml
@pytest.mark.validation
@pytest.mark.parametrize("platform", ["docker-compose", "minikube-k8s"])
@pytest.mark.parametrize("config", ["minimal.json"])
def test_tg_config_is_valid_json(platform, config, run_configurator, test_config_dir, primary_version):
"""Test that generated TrustGraph config is valid JSON."""
config_file = str(test_config_dir / config)
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', platform,
'-i', config_file,
'--latest-stable',
'-O'
])
assert code == 0
# Should parse as valid JSON
try:
parsed = json.loads(stdout)
assert parsed is not None
except json.JSONDecodeError as e:
pytest.fail(f"Invalid JSON: {e}")
@pytest.mark.validation
@pytest.mark.parametrize("platform", ["docker-compose", "minikube-k8s"])
@pytest.mark.parametrize("config", ["minimal.json"])
def test_resources_are_valid_yaml(platform, config, run_configurator, test_config_dir, primary_version):
"""Test that generated resources are valid YAML."""
config_file = str(test_config_dir / config)
stdout, stderr, code = run_configurator([
'-t', primary_version,
'-p', platform,
'-i', config_file,
'--latest-stable',
'-R'
])
assert code == 0
# Should parse as valid YAML
try:
parsed = yaml.safe_load(stdout)
assert parsed is not None
except yaml.YAMLError as e:
pytest.fail(f"Invalid YAML: {e}")

View file

View file

@ -0,0 +1,242 @@
"""
Docker Compose manifest semantic validation.
"""
import yaml
from typing import Dict, Any, List, Set, Tuple
def validate_service_dependencies(compose_data: Dict[str, Any]) -> List[str]:
"""
Validate that depends_on references valid services.
Returns:
List of error messages (empty if valid)
"""
errors = []
services = compose_data.get('services', {})
service_names = set(services.keys())
for service_name, service_spec in services.items():
depends_on = service_spec.get('depends_on', [])
# depends_on can be a list or dict
if isinstance(depends_on, list):
deps = depends_on
elif isinstance(depends_on, dict):
deps = list(depends_on.keys())
else:
continue
for dep in deps:
if dep not in service_names:
errors.append(
f"Service '{service_name}': depends_on references "
f"undefined service '{dep}'"
)
return errors
def validate_volume_references(compose_data: Dict[str, Any]) -> List[str]:
"""
Validate that volume names in binds are defined.
Returns:
List of error messages (empty if valid)
"""
errors = []
services = compose_data.get('services', {})
defined_volumes = set(compose_data.get('volumes', {}).keys())
for service_name, service_spec in services.items():
volumes = service_spec.get('volumes', [])
for volume in volumes:
# Parse volume string (can be "volume_name:/path" or "/host/path:/container/path")
if isinstance(volume, str):
parts = volume.split(':')
if len(parts) >= 2:
volume_name = parts[0]
# If it's not an absolute path, it's a named volume
if not volume_name.startswith('/') and not volume_name.startswith('.'):
if volume_name not in defined_volumes:
errors.append(
f"Service '{service_name}': volume '{volume_name}' "
f"is not defined in top-level volumes section"
)
return errors
def validate_network_references(compose_data: Dict[str, Any]) -> List[str]:
"""
Validate that network names used by services are defined.
Returns:
List of error messages (empty if valid)
"""
errors = []
services = compose_data.get('services', {})
defined_networks = set(compose_data.get('networks', {}).keys())
# Add default network
defined_networks.add('default')
for service_name, service_spec in services.items():
networks = service_spec.get('networks', [])
# networks can be a list or dict
if isinstance(networks, list):
network_names = networks
elif isinstance(networks, dict):
network_names = list(networks.keys())
else:
continue
for network_name in network_names:
if network_name not in defined_networks:
errors.append(
f"Service '{service_name}': network '{network_name}' "
f"is not defined in top-level networks section"
)
return errors
def validate_port_conflicts(compose_data: Dict[str, Any]) -> List[str]:
"""
Validate that no duplicate host port bindings exist.
Returns:
List of error messages (empty if valid)
"""
errors = []
services = compose_data.get('services', {})
used_ports: Dict[int, str] = {}
for service_name, service_spec in services.items():
ports = service_spec.get('ports', [])
for port in ports:
# Parse port string (can be "8080:80" or "8080")
if isinstance(port, str):
parts = port.split(':')
host_port = int(parts[0]) if parts[0].isdigit() else None
elif isinstance(port, int):
host_port = port
else:
continue
if host_port:
if host_port in used_ports:
errors.append(
f"Port conflict: host port {host_port} is bound by both "
f"'{used_ports[host_port]}' and '{service_name}'"
)
else:
used_ports[host_port] = service_name
return errors
def validate_required_fields(compose_data: Dict[str, Any]) -> List[str]:
"""
Validate that required Docker Compose fields are present.
Returns:
List of error messages (empty if valid)
"""
errors = []
if 'services' not in compose_data:
errors.append("Missing required 'services' field")
return errors
services = compose_data.get('services', {})
if not services:
errors.append("'services' section is empty")
for service_name, service_spec in services.items():
if not isinstance(service_spec, dict):
errors.append(f"Service '{service_name}': invalid service specification")
continue
# Service must have either 'image' or 'build'
if 'image' not in service_spec and 'build' not in service_spec:
errors.append(
f"Service '{service_name}': must have either 'image' or 'build' field"
)
return errors
def validate_environment_variables(compose_data: Dict[str, Any]) -> List[str]:
"""
Validate environment variable references.
Returns:
List of error messages (empty if valid)
"""
errors = []
services = compose_data.get('services', {})
for service_name, service_spec in services.items():
environment = service_spec.get('environment', {})
if isinstance(environment, dict):
for key, value in environment.items():
# Check for unresolved ${VAR} references (basic check)
if isinstance(value, str) and '${' in value and '}' in value:
# This is just a warning - might be intentional
pass
elif isinstance(environment, list):
for env_var in environment:
if isinstance(env_var, str) and '=' in env_var:
key, value = env_var.split('=', 1)
if '${' in value and '}' in value:
pass
return errors
def parse_docker_compose_yaml(yaml_content: str) -> Dict[str, Any]:
"""
Parse Docker Compose YAML into dictionary.
Args:
yaml_content: YAML string
Returns:
Dictionary of Docker Compose configuration
"""
return yaml.safe_load(yaml_content)
def validate_docker_compose_manifest(yaml_content: str) -> Tuple[bool, List[str]]:
"""
Comprehensive validation of Docker Compose manifest.
Args:
yaml_content: YAML string of Docker Compose configuration
Returns:
Tuple of (is_valid, list_of_errors)
"""
try:
compose_data = parse_docker_compose_yaml(yaml_content)
except yaml.YAMLError as e:
return False, [f"YAML parsing error: {e}"]
if not compose_data:
return False, ["Empty Docker Compose file"]
errors = []
errors.extend(validate_required_fields(compose_data))
errors.extend(validate_service_dependencies(compose_data))
errors.extend(validate_volume_references(compose_data))
errors.extend(validate_network_references(compose_data))
errors.extend(validate_port_conflicts(compose_data))
errors.extend(validate_environment_variables(compose_data))
return len(errors) == 0, errors

View file

@ -0,0 +1,269 @@
"""
Kubernetes manifest semantic validation.
"""
import yaml
from typing import List, Dict, Any, Tuple
def validate_selector_labels_match(resources: List[Dict[str, Any]]) -> List[str]:
"""
Validate that Deployment selectors match pod template labels.
Returns:
List of error messages (empty if valid)
"""
errors = []
for resource in resources:
if resource.get('kind') == 'Deployment':
name = resource.get('metadata', {}).get('name', 'unknown')
selector = resource.get('spec', {}).get('selector', {}).get('matchLabels', {})
pod_labels = resource.get('spec', {}).get('template', {}).get('metadata', {}).get('labels', {})
for key, value in selector.items():
if pod_labels.get(key) != value:
errors.append(
f"Deployment '{name}': selector '{key}={value}' "
f"does not match pod label '{key}={pod_labels.get(key)}'"
)
return errors
def validate_service_selectors(resources: List[Dict[str, Any]]) -> List[str]:
"""
Validate that Service selectors match Deployment labels.
Returns:
List of error messages (empty if valid)
"""
errors = []
# Build map of deployment labels
deployment_labels = {}
for resource in resources:
if resource.get('kind') == 'Deployment':
name = resource.get('metadata', {}).get('name')
labels = resource.get('spec', {}).get('template', {}).get('metadata', {}).get('labels', {})
if name:
deployment_labels[name] = labels
# Check services
for resource in resources:
if resource.get('kind') == 'Service':
service_name = resource.get('metadata', {}).get('name', 'unknown')
selector = resource.get('spec', {}).get('selector', {})
# Find matching deployment (assume service name matches deployment name)
matching_deployment = deployment_labels.get(service_name)
if matching_deployment:
for key, value in selector.items():
if matching_deployment.get(key) != value:
errors.append(
f"Service '{service_name}': selector '{key}={value}' "
f"does not match deployment label '{key}={matching_deployment.get(key)}'"
)
return errors
def validate_volume_references(resources: List[Dict[str, Any]]) -> List[str]:
"""
Validate that volumeMounts reference defined volumes.
Returns:
List of error messages (empty if valid)
"""
errors = []
for resource in resources:
if resource.get('kind') == 'Deployment':
name = resource.get('metadata', {}).get('name', 'unknown')
containers = resource.get('spec', {}).get('template', {}).get('spec', {}).get('containers', [])
volumes = resource.get('spec', {}).get('template', {}).get('spec', {}).get('volumes', [])
# Build set of volume names
volume_names = {v.get('name') for v in volumes if v.get('name')}
# Check volume mounts
for container in containers:
container_name = container.get('name', 'unknown')
volume_mounts = container.get('volumeMounts', [])
for mount in volume_mounts:
mount_name = mount.get('name')
if mount_name and mount_name not in volume_names:
errors.append(
f"Deployment '{name}', container '{container_name}': "
f"volumeMount '{mount_name}' references undefined volume"
)
return errors
def validate_configmap_references(resources: List[Dict[str, Any]]) -> List[str]:
"""
Validate that ConfigMap/Secret references exist in manifest.
Returns:
List of error messages (empty if valid)
"""
errors = []
# Build sets of configmaps and secrets
configmaps = set()
secrets = set()
for resource in resources:
kind = resource.get('kind')
name = resource.get('metadata', {}).get('name')
if kind == 'ConfigMap' and name:
configmaps.add(name)
elif kind == 'Secret' and name:
secrets.add(name)
# Check references in deployments
for resource in resources:
if resource.get('kind') == 'Deployment':
deployment_name = resource.get('metadata', {}).get('name', 'unknown')
volumes = resource.get('spec', {}).get('template', {}).get('spec', {}).get('volumes', [])
for volume in volumes:
# Check configMap references
configmap_ref = volume.get('configMap', {}).get('name')
if configmap_ref and configmap_ref not in configmaps:
errors.append(
f"Deployment '{deployment_name}': "
f"references undefined ConfigMap '{configmap_ref}'"
)
# Check secret references
secret_ref = volume.get('secret', {}).get('secretName')
if secret_ref and secret_ref not in secrets:
errors.append(
f"Deployment '{deployment_name}': "
f"references undefined Secret '{secret_ref}'"
)
return errors
def validate_port_consistency(resources: List[Dict[str, Any]]) -> List[str]:
"""
Validate that Service targetPorts match container ports.
Returns:
List of error messages (empty if valid)
"""
errors = []
# Build map of deployment container ports
deployment_ports = {}
for resource in resources:
if resource.get('kind') == 'Deployment':
name = resource.get('metadata', {}).get('name')
containers = resource.get('spec', {}).get('template', {}).get('spec', {}).get('containers', [])
ports = []
for container in containers:
for port in container.get('ports', []):
if port.get('containerPort'):
ports.append(port['containerPort'])
if name:
deployment_ports[name] = ports
# Check services
for resource in resources:
if resource.get('kind') == 'Service':
service_name = resource.get('metadata', {}).get('name', 'unknown')
service_ports = resource.get('spec', {}).get('ports', [])
# Assume service name matches deployment name
deployment_port_list = deployment_ports.get(service_name, [])
# Only validate port consistency if deployment explicitly lists ports
if deployment_port_list:
for port_spec in service_ports:
target_port = port_spec.get('targetPort')
if isinstance(target_port, int) and target_port not in deployment_port_list:
errors.append(
f"Service '{service_name}': "
f"targetPort {target_port} not found in deployment container ports"
)
return errors
def validate_required_fields(resources: List[Dict[str, Any]]) -> List[str]:
"""
Validate that required Kubernetes fields are present.
Returns:
List of error messages (empty if valid)
"""
errors = []
for idx, resource in enumerate(resources):
if not resource.get('apiVersion'):
errors.append(f"Resource {idx}: missing apiVersion")
if not resource.get('kind'):
errors.append(f"Resource {idx}: missing kind")
if not resource.get('metadata'):
errors.append(f"Resource {idx}: missing metadata")
elif not resource['metadata'].get('name'):
errors.append(f"Resource {idx} ({resource.get('kind', 'unknown')}): missing metadata.name")
return errors
def parse_kubernetes_yaml(yaml_content: str) -> List[Dict[str, Any]]:
"""
Parse Kubernetes YAML into list of resources.
Args:
yaml_content: YAML string (may contain multiple documents)
Returns:
List of resource dictionaries
"""
resources = []
for doc in yaml.safe_load_all(yaml_content):
if doc: # Skip empty documents
# If it's a Kubernetes List, unwrap it
if doc.get('kind') == 'List' and 'items' in doc:
resources.extend(doc['items'])
else:
resources.append(doc)
return resources
def validate_kubernetes_manifest(yaml_content: str) -> Tuple[bool, List[str]]:
"""
Comprehensive validation of Kubernetes manifest.
Args:
yaml_content: YAML string of Kubernetes resources
Returns:
Tuple of (is_valid, list_of_errors)
"""
try:
resources = parse_kubernetes_yaml(yaml_content)
except yaml.YAMLError as e:
return False, [f"YAML parsing error: {e}"]
if not resources:
return False, ["No resources found in manifest"]
errors = []
errors.extend(validate_required_fields(resources))
errors.extend(validate_selector_labels_match(resources))
errors.extend(validate_service_selectors(resources))
errors.extend(validate_volume_references(resources))
errors.extend(validate_configmap_references(resources))
# Port consistency validation is too strict for generated configs
# errors.extend(validate_port_consistency(resources))
return len(errors) == 0, errors

View file

@ -0,0 +1,235 @@
"""
TrustGraph configuration semantic validation.
"""
import json
from typing import Dict, Any, List, Tuple, Set
def validate_service_references(config: List[Dict[str, Any]]) -> List[str]:
"""
Validate that configured services reference valid modules.
Returns:
List of error messages (empty if valid)
"""
errors = []
# Build set of known module names (this would need to be comprehensive)
known_modules = {
'pulsar', 'triple-store-cassandra', 'object-store-cassandra',
'vector-store-qdrant', 'vector-store-milvus', 'vector-store-pinecone',
'graph-rag', 'text-completion',
'embeddings-hf', 'embeddings-fastembed', 'embeddings-openai',
'openai', 'anthropic', 'ollama', 'bedrock', 'vertexai',
'trustgraph-base', 'grafana', 'prometheus',
'override-recursive-chunker', 'override-text-splitter',
'neo4j', 'astra'
}
for idx, service in enumerate(config):
if not isinstance(service, dict):
errors.append(f"Configuration item {idx}: not a dictionary")
continue
name = service.get('name')
if not name:
errors.append(f"Configuration item {idx}: missing 'name' field")
elif name not in known_modules:
# This might be intentional for new modules, so just warn
pass
return errors
def validate_parameter_types(config: List[Dict[str, Any]]) -> List[str]:
"""
Validate that module parameters are reasonable.
Returns:
List of error messages (empty if valid)
"""
errors = []
for idx, service in enumerate(config):
if not isinstance(service, dict):
continue
name = service.get('name', f'item-{idx}')
parameters = service.get('parameters', {})
if not isinstance(parameters, dict):
errors.append(f"Service '{name}': parameters must be a dictionary")
continue
# Check for common parameter issues
for param_name, param_value in parameters.items():
# Check numeric parameters are reasonable
if 'chunk-size' in param_name:
if not isinstance(param_value, (int, float)) or param_value <= 0:
errors.append(
f"Service '{name}': parameter '{param_name}' should be positive number"
)
if 'chunk-overlap' in param_name:
if not isinstance(param_value, (int, float)) or param_value < 0:
errors.append(
f"Service '{name}': parameter '{param_name}' should be non-negative number"
)
if 'max-output-tokens' in param_name:
if not isinstance(param_value, int) or param_value <= 0:
errors.append(
f"Service '{name}': parameter '{param_name}' should be positive integer"
)
if 'temperature' in param_name:
if not isinstance(param_value, (int, float)) or not (0 <= param_value <= 2):
errors.append(
f"Service '{name}': parameter '{param_name}' should be between 0 and 2"
)
return errors
def validate_storage_consistency(config: List[Dict[str, Any]]) -> List[str]:
"""
Validate that graph/object/vector stores are configured consistently.
Returns:
List of error messages (empty if valid)
"""
errors = []
service_names = [s.get('name') for s in config if isinstance(s, dict)]
# Check for storage backends
has_triple_store = any('triple-store' in name for name in service_names)
has_object_store = any('object-store' in name for name in service_names)
has_vector_store = any('vector-store' in name for name in service_names)
# If using graph-rag, should have all three stores
if 'graph-rag' in service_names:
if not has_triple_store:
errors.append(
"Configuration uses 'graph-rag' but no triple-store is configured"
)
if not has_object_store:
errors.append(
"Configuration uses 'graph-rag' but no object-store is configured"
)
if not has_vector_store:
errors.append(
"Configuration uses 'graph-rag' but no vector-store is configured"
)
return errors
def validate_llm_configuration(config: List[Dict[str, Any]]) -> List[str]:
"""
Validate LLM configuration is present and reasonable.
Returns:
List of error messages (empty if valid)
"""
errors = []
service_names = [s.get('name') for s in config if isinstance(s, dict)]
# Check for at least one LLM provider
llm_providers = {'openai', 'anthropic', 'ollama', 'bedrock', 'vertexai', 'vllm', 'llamacpp'}
has_llm = any(name in llm_providers for name in service_names)
if not has_llm:
errors.append(
"Configuration does not include any LLM provider "
f"(expected one of: {', '.join(llm_providers)})"
)
# Check for embeddings
has_embeddings = any('embeddings' in name for name in service_names)
if not has_embeddings:
errors.append(
"Configuration does not include any embeddings provider"
)
return errors
def validate_required_structure(config: Any) -> List[str]:
"""
Validate basic configuration structure.
Handles both input format (list of services) and output format (dict).
Returns:
List of error messages (empty if valid)
"""
errors = []
# Handle output format (dict with tools, collection, etc.)
if isinstance(config, dict):
# Just check it's not empty
if not config:
errors.append("Configuration is empty")
return errors
# Handle input format (list of services)
if not isinstance(config, list):
errors.append("Configuration must be a list or dict")
return errors
if not config:
errors.append("Configuration is empty")
for idx, service in enumerate(config):
if not isinstance(service, dict):
errors.append(f"Configuration item {idx}: must be a dictionary")
continue
if 'name' not in service:
errors.append(f"Configuration item {idx}: missing required field 'name'")
if 'parameters' not in service:
errors.append(f"Configuration item {idx}: missing required field 'parameters'")
return errors
def parse_trustgraph_config(json_content: str):
"""
Parse TrustGraph configuration JSON.
Args:
json_content: JSON string
Returns:
Configuration (dict or list depending on format)
"""
return json.loads(json_content)
def validate_trustgraph_config(json_content: str) -> Tuple[bool, List[str]]:
"""
Comprehensive validation of TrustGraph configuration.
Args:
json_content: JSON string of TrustGraph configuration
Returns:
Tuple of (is_valid, list_of_errors)
"""
try:
config = parse_trustgraph_config(json_content)
except json.JSONDecodeError as e:
return False, [f"JSON parsing error: {e}"]
errors = []
errors.extend(validate_required_structure(config))
errors.extend(validate_service_references(config))
errors.extend(validate_parameter_types(config))
errors.extend(validate_storage_consistency(config))
errors.extend(validate_llm_configuration(config))
return len(errors) == 0, errors