From ccc3d0cdf08bcb58cdb08762d9cc85b758d4c36d Mon Sep 17 00:00:00 2001 From: Adam Patch Date: Thu, 5 Feb 2026 11:32:25 -0500 Subject: [PATCH 1/4] test(coverage): improve backend test coverage and add coverage tooling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add coverage>=7.4 to dev dependencies (pyproject.toml, requirements.txt) - Add 18 new backend tests for low-coverage modules - Improve api_interpreters.py coverage: 29% → 60% (+31%) - Improve api_providers.py coverage: 20% → 33% (+13%) - Improve api_chat.py coverage: 34% → 37% (+3%) - Overall coverage: 67% → 68% New tests added: - test_chat_api.py: 7 new tests (GraphRAG endpoints, history, capabilities) - test_interpreters_registry_api.py: 4 new tests (toggle, debug endpoints) - test_providers_api.py: 6 new tests (provider roots, rclone validation) Coverage tooling: - Enables local coverage reports: python -m coverage run -m pytest - Interactive HTML reports: python -m coverage html (htmlcov/index.html) - Documentation added to dev/prompts.md Total: 197 tests passing (up from 179) --- pyproject.toml | 1 + requirements.txt | 1 + tests/test_chat_api.py | 73 +++++++++++++++++++++++++ tests/test_interpreters_registry_api.py | 40 ++++++++++++++ tests/test_providers_api.py | 59 ++++++++++++++++++++ 5 files changed, 174 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 88c47e6..f27e575 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ dev = [ "playwright==1.40.0", "requests>=2.32", "beautifulsoup4>=4.12", + "coverage>=7.4", ] # Optional LLM provider (only needed if you enable Graphrag LLM features) diff --git a/requirements.txt b/requirements.txt index 20b8648..3ac9af4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ pytest>=7.4 pytest-playwright==0.4.3 playwright==1.40.0 requests>=2.32 +coverage>=7.4 diff --git a/tests/test_chat_api.py b/tests/test_chat_api.py index abf7e6e..9e63969 100644 --- a/tests/test_chat_api.py +++ b/tests/test_chat_api.py @@ -1,3 +1,7 @@ +import os +import pytest + + def test_api_chat_echo(client): # First message resp1 = client.post('/api/chat', json={'message': 'Hello'}) @@ -11,3 +15,72 @@ def test_api_chat_echo(client): resp2 = client.post('/api/chat', json={'message': 'How are you?'}) data2 = resp2.get_json() assert len(data2['history']) == 4 + + +def test_api_chat_missing_message(client): + resp = client.post('/api/chat', json={}) + assert resp.status_code == 400 + data = resp.get_json() + assert data['status'] == 'error' + assert 'message required' in data['error'] + + +def test_api_chat_history(client): + # Post a message first + client.post('/api/chat', json={'message': 'Test'}) + + # Get history + resp = client.get('/api/chat/history') + assert resp.status_code == 200 + data = resp.get_json() + assert data['status'] == 'ok' + assert 'history' in data + assert len(data['history']) >= 2 + + +def test_api_chat_capabilities(client): + resp = client.get('/api/chat/capabilities') + assert resp.status_code == 200 + data = resp.get_json() + assert 'graphrag' in data + assert 'enabled' in data['graphrag'] + assert 'llm_provider' in data['graphrag'] + assert 'model' in data['graphrag'] + + +def test_api_chat_graphrag_disabled_by_default(client): + # GraphRAG should be disabled without SCIDK_GRAPHRAG_ENABLED + resp = client.post('/api/chat/graphrag', json={'message': 'Test query'}) + assert resp.status_code == 501 + data = resp.get_json() + assert data['status'] == 'disabled' + assert 'SCIDK_GRAPHRAG_ENABLED' in data.get('hint', '') + + +def test_api_chat_graphrag_missing_message(client, monkeypatch): + monkeypatch.setenv('SCIDK_GRAPHRAG_ENABLED', '1') + resp = client.post('/api/chat/graphrag', json={}) + assert resp.status_code == 400 + data = resp.get_json() + assert data['status'] == 'error' + assert 'message required' in data['error'] + + +def test_api_chat_context_refresh_disabled(client): + resp = client.post('/api/chat/context/refresh') + assert resp.status_code == 501 + data = resp.get_json() + assert data['status'] == 'disabled' + + +def test_api_chat_observability_graphrag(client): + resp = client.get('/api/chat/observability/graphrag') + assert resp.status_code == 200 + data = resp.get_json() + assert data['status'] == 'ok' + assert 'enabled' in data + assert 'llm_provider' in data + assert 'model' in data + assert 'schema' in data + assert 'audit' in data + assert isinstance(data['audit'], list) diff --git a/tests/test_interpreters_registry_api.py b/tests/test_interpreters_registry_api.py index 7c8b14a..995b058 100644 --- a/tests/test_interpreters_registry_api.py +++ b/tests/test_interpreters_registry_api.py @@ -24,3 +24,43 @@ def test_api_interpreters_schema(): for it in eff: assert 'enabled' in it assert 'source' in it + + +def test_api_interpreters_effective_debug(client): + resp = client.get('/api/interpreters/effective_debug') + assert resp.status_code == 200 + data = resp.get_json() + assert 'source' in data + assert 'effective_enabled' in data + assert 'default_enabled' in data + assert 'loaded_settings' in data + assert 'env' in data + assert isinstance(data['effective_enabled'], list) + assert isinstance(data['default_enabled'], list) + + +def test_api_interpreters_toggle_enable(client): + # Enable an interpreter + resp = client.post('/api/interpreters/csv/toggle', json={'enabled': True}) + assert resp.status_code == 200 + data = resp.get_json() + assert data['status'] == 'updated' + assert data['enabled'] is True + + +def test_api_interpreters_toggle_disable(client): + # Disable an interpreter + resp = client.post('/api/interpreters/csv/toggle', json={'enabled': False}) + assert resp.status_code == 200 + data = resp.get_json() + assert data['status'] == 'updated' + assert data['enabled'] is False + + +def test_api_interpreters_toggle_default_enabled(client): + # Toggle without explicit enabled flag (defaults to True) + resp = client.post('/api/interpreters/python_code/toggle', json={}) + assert resp.status_code == 200 + data = resp.get_json() + assert data['status'] == 'updated' + assert data['enabled'] is True diff --git a/tests/test_providers_api.py b/tests/test_providers_api.py index 51ed4c2..46232dc 100644 --- a/tests/test_providers_api.py +++ b/tests/test_providers_api.py @@ -24,3 +24,62 @@ def test_browse_local_root(client, tmp_path: Path): entries = data.get('entries', []) names = {e['name'] for e in entries} assert 'a.txt' in names + + +def test_provider_roots_default(client): + # Test default provider (local_fs) + resp = client.get('/api/provider_roots') + assert resp.status_code == 200 + data = resp.get_json() + assert isinstance(data, list) + assert len(data) > 0 + # Validate structure + for root in data: + assert 'id' in root + assert 'name' in root + assert 'path' in root + + +def test_provider_roots_specific_provider(client): + resp = client.get('/api/provider_roots?provider_id=local_fs') + assert resp.status_code == 200 + data = resp.get_json() + assert isinstance(data, list) + + +def test_provider_roots_invalid_provider(client): + resp = client.get('/api/provider_roots?provider_id=nonexistent') + assert resp.status_code == 400 + data = resp.get_json() + assert 'error' in data + + +def test_rclone_mounts_list(client): + # Should return empty list initially (or existing mounts) + resp = client.get('/api/rclone/mounts') + assert resp.status_code == 200 + data = resp.get_json() + assert isinstance(data, list) + + +def test_rclone_mounts_create_missing_rclone(client, monkeypatch): + # Mock rclone not available + monkeypatch.setattr('shutil.which', lambda x: None) + resp = client.post('/api/rclone/mounts', json={'remote': 'test:', 'name': 'testmount'}) + assert resp.status_code == 400 + data = resp.get_json() + assert 'rclone not installed' in data['error'] + + +def test_rclone_mounts_create_missing_remote(client): + resp = client.post('/api/rclone/mounts', json={'name': 'testmount'}) + assert resp.status_code == 400 + data = resp.get_json() + assert 'remote required' in data['error'] + + +def test_rclone_mounts_create_missing_name(client): + resp = client.post('/api/rclone/mounts', json={'remote': 'test:'}) + assert resp.status_code == 400 + data = resp.get_json() + assert 'name required' in data['error'] From 52fba1050c26fead27cb78b344c8f403f2e5ee02 Mon Sep 17 00:00:00 2001 From: Adam Patch Date: Thu, 5 Feb 2026 11:32:30 -0500 Subject: [PATCH 2/4] chore(dev): update submodule pointer for coverage documentation --- dev | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev b/dev index 1d7cf44..362dd70 160000 --- a/dev +++ b/dev @@ -1 +1 @@ -Subproject commit 1d7cf44315a703e036c387b76e153b7cda8fa18c +Subproject commit 362dd7012c249d2335c631682b94ab7de0646342 From b84483feb298b30582da85328a6bbae2d8354d0f Mon Sep 17 00:00:00 2001 From: Adam Patch Date: Thu, 5 Feb 2026 11:35:39 -0500 Subject: [PATCH 3/4] chore: add coverage artifacts to .gitignore - Ignore .coverage file and .coverage.* parallel mode files - Ignore htmlcov/ directory (HTML coverage reports) - Ignore coverage.xml and *.cover files --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 4339c73..697d231 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,13 @@ playwright-report/ test-results/ pytest-of-patch/ .output.txt + +# Coverage reports +.coverage +.coverage.* +htmlcov/ +coverage.xml +*.cover # Accidental directories from env expansion (pwd)/ sqlite:/ From 99e6793b03af99e5f59e890714db87ff7a3cc35c Mon Sep 17 00:00:00 2001 From: Adam Patch Date: Thu, 5 Feb 2026 11:40:18 -0500 Subject: [PATCH 4/4] fix(tests): mock rclone availability in provider validation tests - Add monkeypatch to mock shutil.which for rclone tests - Fixes CI failure where rclone is not installed - Tests now properly validate input validation logic - All 9 provider tests passing --- tests/test_providers_api.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_providers_api.py b/tests/test_providers_api.py index 46232dc..cafe481 100644 --- a/tests/test_providers_api.py +++ b/tests/test_providers_api.py @@ -71,14 +71,18 @@ def test_rclone_mounts_create_missing_rclone(client, monkeypatch): assert 'rclone not installed' in data['error'] -def test_rclone_mounts_create_missing_remote(client): +def test_rclone_mounts_create_missing_remote(client, monkeypatch): + # Mock rclone available so we can test validation logic + monkeypatch.setattr('scidk.web.routes.api_providers.shutil.which', lambda x: '/usr/bin/rclone') resp = client.post('/api/rclone/mounts', json={'name': 'testmount'}) assert resp.status_code == 400 data = resp.get_json() assert 'remote required' in data['error'] -def test_rclone_mounts_create_missing_name(client): +def test_rclone_mounts_create_missing_name(client, monkeypatch): + # Mock rclone available so we can test validation logic + monkeypatch.setattr('scidk.web.routes.api_providers.shutil.which', lambda x: '/usr/bin/rclone') resp = client.post('/api/rclone/mounts', json={'remote': 'test:'}) assert resp.status_code == 400 data = resp.get_json()