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:/ diff --git a/dev b/dev index 1d7cf44..362dd70 160000 --- a/dev +++ b/dev @@ -1 +1 @@ -Subproject commit 1d7cf44315a703e036c387b76e153b7cda8fa18c +Subproject commit 362dd7012c249d2335c631682b94ab7de0646342 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..cafe481 100644 --- a/tests/test_providers_api.py +++ b/tests/test_providers_api.py @@ -24,3 +24,66 @@ 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, 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, 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() + assert 'name required' in data['error']