From 88a7ebcaa3999bb33a6cf14d669be270c735a2fb Mon Sep 17 00:00:00 2001 From: Andrew Park Date: Fri, 17 Apr 2026 11:02:21 -0700 Subject: [PATCH] test: add live marker, CI coverage gate, rust bridge boundary tests (#256) --- .github/workflows/ci.yml | 15 +++- tests/connectors/test_live_smoke.py | 3 + tests/core/test_rust_bridge.py | 118 ++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6848d32a..632cf242 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,20 @@ jobs: run: uv run maturin develop --manifest-path rust/crates/openjarvis-python/Cargo.toml - name: Run tests - run: uv run pytest tests/ -v --tb=short -m "not live and not cloud" + run: | + uv run pytest tests/ -v --tb=short -m "not live and not cloud" \ + --cov=openjarvis \ + --cov-report=term-missing \ + --cov-report=xml \ + --cov-fail-under=60 + + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-xml + path: coverage.xml + if-no-files-found: warn rust: runs-on: ubuntu-latest diff --git a/tests/connectors/test_live_smoke.py b/tests/connectors/test_live_smoke.py index a1c290aa..9c4754f6 100644 --- a/tests/connectors/test_live_smoke.py +++ b/tests/connectors/test_live_smoke.py @@ -9,6 +9,8 @@ from __future__ import annotations import tempfile from pathlib import Path +import pytest + from openjarvis.connectors.obsidian import ObsidianConnector from openjarvis.connectors.pipeline import IngestionPipeline from openjarvis.connectors.store import KnowledgeStore @@ -19,6 +21,7 @@ from openjarvis.tools.knowledge_search import KnowledgeSearchTool DOCS_DIR = Path(__file__).resolve().parents[2] / "docs" +@pytest.mark.live def test_live_obsidian_full_pipeline() -> None: """Smoke test: index real .md files, then search them.""" assert DOCS_DIR.is_dir(), f"Docs dir not found: {DOCS_DIR}" diff --git a/tests/core/test_rust_bridge.py b/tests/core/test_rust_bridge.py index 893250de..876ae827 100644 --- a/tests/core/test_rust_bridge.py +++ b/tests/core/test_rust_bridge.py @@ -4,6 +4,8 @@ from __future__ import annotations import json +import pytest + class TestGetRustModule: """Test get_rust_module() returns the Rust extension module.""" @@ -127,6 +129,122 @@ class TestRetrievalResultsFromJson: assert results[0].metadata == {"nested": True} +class TestConverterInvalidInputs: + """Boundary behavior: malformed JSON and missing/unexpected fields.""" + + def test_scan_result_malformed_json_raises(self): + from openjarvis._rust_bridge import scan_result_from_json + + with pytest.raises(json.JSONDecodeError): + scan_result_from_json("{not json") + + def test_scan_result_missing_findings_key_is_clean(self): + from openjarvis._rust_bridge import scan_result_from_json + + result = scan_result_from_json("{}") + assert result.clean + assert result.findings == [] + + def test_scan_result_unknown_threat_level_raises(self): + from openjarvis._rust_bridge import scan_result_from_json + + data = { + "findings": [ + { + "pattern_name": "x", + "matched_text": "y", + "threat_level": "not-a-level", + "start": 0, + "end": 1, + "description": "", + }, + ], + } + with pytest.raises(ValueError): + scan_result_from_json(json.dumps(data)) + + def test_scan_result_finding_missing_fields_uses_defaults(self): + from openjarvis._rust_bridge import scan_result_from_json + + result = scan_result_from_json('{"findings": [{}]}') + assert not result.clean + finding = result.findings[0] + assert finding.pattern_name == "" + assert finding.matched_text == "" + assert finding.threat_level.value == "low" + assert finding.start == 0 + assert finding.end == 0 + + def test_injection_result_malformed_json_raises(self): + from openjarvis._rust_bridge import injection_result_from_json + + with pytest.raises(json.JSONDecodeError): + injection_result_from_json("") + + def test_injection_result_unknown_top_level_threat_defaults_to_low(self): + from openjarvis._rust_bridge import injection_result_from_json + + data = { + "is_clean": True, + "findings": [], + "threat_level": "bogus", + } + result = injection_result_from_json(json.dumps(data)) + assert result.threat_level.value == "low" + + def test_injection_result_missing_keys_uses_defaults(self): + from openjarvis._rust_bridge import injection_result_from_json + + result = injection_result_from_json("{}") + assert result.is_clean is True + assert result.findings == [] + assert result.threat_level.value == "low" + + def test_retrieval_results_malformed_json_raises(self): + from openjarvis._rust_bridge import retrieval_results_from_json + + with pytest.raises(json.JSONDecodeError): + retrieval_results_from_json("not-json") + + def test_retrieval_results_metadata_invalid_json_string_falls_back_to_empty(self): + from openjarvis._rust_bridge import retrieval_results_from_json + + data = [ + { + "content": "c", + "score": 0.1, + "source": "s", + "metadata": "{not valid json", + }, + ] + results = retrieval_results_from_json(json.dumps(data)) + assert results[0].metadata == {} + + def test_retrieval_results_missing_fields_uses_defaults(self): + from openjarvis._rust_bridge import retrieval_results_from_json + + results = retrieval_results_from_json("[{}]") + assert len(results) == 1 + assert results[0].content == "" + assert results[0].score == 0.0 + assert results[0].source == "" + assert results[0].metadata == {} + + def test_retrieval_results_score_string_coerced_to_float(self): + from openjarvis._rust_bridge import retrieval_results_from_json + + data = [{"content": "x", "score": "2.5", "source": "", "metadata": {}}] + results = retrieval_results_from_json(json.dumps(data)) + assert results[0].score == 2.5 + + def test_retrieval_results_non_iterable_top_level_raises(self): + from openjarvis._rust_bridge import retrieval_results_from_json + + # Top-level must be a list; a bare number has no iteration. + with pytest.raises(TypeError): + retrieval_results_from_json("42") + + class TestRustBackedModules: """Test that Rust-backed modules work correctly."""