7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
292 lines
11 KiB
Python
292 lines
11 KiB
Python
"""Tests for database initialize module functions."""
|
|
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import Mock, patch
|
|
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import Session
|
|
|
|
from local_deep_research.database.models import Base
|
|
|
|
|
|
class TestCheckDatabaseSchema:
|
|
"""Tests for check_database_schema function."""
|
|
|
|
def test_returns_dict_with_tables_key(self):
|
|
"""check_database_schema returns dict with 'tables' key."""
|
|
from local_deep_research.database.initialize import (
|
|
check_database_schema,
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
db_path = Path(temp_dir) / "test.db"
|
|
engine = create_engine(f"sqlite:///{db_path}")
|
|
try:
|
|
# Create tables
|
|
Base.metadata.create_all(engine)
|
|
|
|
result = check_database_schema(engine)
|
|
|
|
assert isinstance(result, dict)
|
|
assert "tables" in result
|
|
finally:
|
|
engine.dispose()
|
|
|
|
def test_lists_existing_tables(self):
|
|
"""check_database_schema lists existing tables."""
|
|
from local_deep_research.database.initialize import (
|
|
check_database_schema,
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
db_path = Path(temp_dir) / "test.db"
|
|
engine = create_engine(f"sqlite:///{db_path}")
|
|
try:
|
|
# Create tables
|
|
Base.metadata.create_all(engine)
|
|
|
|
result = check_database_schema(engine)
|
|
|
|
# Should have tables dict
|
|
assert isinstance(result["tables"], dict)
|
|
finally:
|
|
engine.dispose()
|
|
|
|
def test_lists_missing_tables(self):
|
|
"""check_database_schema identifies missing tables."""
|
|
from local_deep_research.database.initialize import (
|
|
check_database_schema,
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
db_path = Path(temp_dir) / "test.db"
|
|
engine = create_engine(f"sqlite:///{db_path}")
|
|
try:
|
|
# Don't create any tables
|
|
result = check_database_schema(engine)
|
|
|
|
assert "missing_tables" in result
|
|
assert isinstance(result["missing_tables"], list)
|
|
finally:
|
|
engine.dispose()
|
|
|
|
def test_detects_news_tables(self):
|
|
"""check_database_schema detects news tables presence."""
|
|
from local_deep_research.database.initialize import (
|
|
check_database_schema,
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
db_path = Path(temp_dir) / "test.db"
|
|
engine = create_engine(f"sqlite:///{db_path}")
|
|
try:
|
|
# Create tables
|
|
Base.metadata.create_all(engine)
|
|
|
|
result = check_database_schema(engine)
|
|
|
|
assert "has_news_tables" in result
|
|
assert isinstance(result["has_news_tables"], bool)
|
|
finally:
|
|
engine.dispose()
|
|
|
|
def test_returns_columns_for_each_table(self):
|
|
"""check_database_schema returns column names for existing tables."""
|
|
from local_deep_research.database.initialize import (
|
|
check_database_schema,
|
|
)
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
db_path = Path(temp_dir) / "test.db"
|
|
engine = create_engine(f"sqlite:///{db_path}")
|
|
try:
|
|
# Create tables
|
|
Base.metadata.create_all(engine)
|
|
|
|
result = check_database_schema(engine)
|
|
|
|
# Each table in tables dict should have a list of columns
|
|
for table_name, columns in result["tables"].items():
|
|
assert isinstance(columns, list)
|
|
finally:
|
|
engine.dispose()
|
|
|
|
|
|
class TestInitializeDefaultSettings:
|
|
"""Tests for _initialize_default_settings function."""
|
|
|
|
def test_calls_settings_manager(self):
|
|
"""_initialize_default_settings calls SettingsManager methods."""
|
|
from local_deep_research.database.initialize import (
|
|
_initialize_default_settings,
|
|
)
|
|
|
|
mock_session = Mock(spec=Session)
|
|
|
|
with patch(
|
|
"local_deep_research.settings.manager.SettingsManager"
|
|
) as MockSettingsManager:
|
|
mock_settings_mgr = Mock()
|
|
mock_settings_mgr.db_version_matches_package.return_value = False
|
|
MockSettingsManager.return_value = mock_settings_mgr
|
|
|
|
_initialize_default_settings(mock_session)
|
|
|
|
MockSettingsManager.assert_called_once_with(mock_session)
|
|
mock_settings_mgr.db_version_matches_package.assert_called_once()
|
|
mock_settings_mgr.load_from_defaults_file.assert_called_once()
|
|
mock_settings_mgr.update_db_version.assert_called_once()
|
|
|
|
def test_skips_when_version_matches(self):
|
|
"""_initialize_default_settings skips update when version matches."""
|
|
from local_deep_research.database.initialize import (
|
|
_initialize_default_settings,
|
|
)
|
|
|
|
mock_session = Mock(spec=Session)
|
|
|
|
with patch(
|
|
"local_deep_research.settings.manager.SettingsManager"
|
|
) as MockSettingsManager:
|
|
mock_settings_mgr = Mock()
|
|
mock_settings_mgr.db_version_matches_package.return_value = True
|
|
MockSettingsManager.return_value = mock_settings_mgr
|
|
|
|
_initialize_default_settings(mock_session)
|
|
|
|
# Should not call load_from_defaults_file
|
|
mock_settings_mgr.load_from_defaults_file.assert_not_called()
|
|
|
|
def test_handles_errors_gracefully(self):
|
|
"""_initialize_default_settings swallows SettingsManager errors.
|
|
|
|
Background: PR #2235 originally tried to make DB errors propagate
|
|
through this code path. PR #2118 (Feb 22 2026, commit 76524cc4de)
|
|
walked that change back because masking failures here was causing
|
|
runtime bugs / CI failure masking — startup must be resilient
|
|
even when the user's settings DB is corrupt or missing. The
|
|
current contract is: SettingsManager errors during initial
|
|
defaults seeding are logged-and-swallowed, not raised. This test
|
|
pins that contract.
|
|
|
|
PUNCHLIST historically flagged this as H5_SWALLOWS_ERROR. That
|
|
flag is a false positive against the current SUT — see
|
|
settings/manager.py:780-786 for the catch-and-log site.
|
|
"""
|
|
# audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: FIX).
|
|
from local_deep_research.database.initialize import (
|
|
_initialize_default_settings,
|
|
)
|
|
|
|
mock_session = Mock(spec=Session)
|
|
|
|
with patch(
|
|
"local_deep_research.settings.manager.SettingsManager"
|
|
) as MockSettingsManager:
|
|
MockSettingsManager.side_effect = Exception("Settings error")
|
|
|
|
# Must not raise — startup-resilience contract per PR #2118.
|
|
_initialize_default_settings(mock_session)
|
|
|
|
|
|
class TestInitializeDatabase:
|
|
"""Tests for initialize_database function."""
|
|
|
|
def test_creates_all_tables(self):
|
|
"""initialize_database creates all tables from Base.metadata."""
|
|
from local_deep_research.database.initialize import initialize_database
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
db_path = Path(temp_dir) / "test.db"
|
|
engine = create_engine(f"sqlite:///{db_path}")
|
|
try:
|
|
initialize_database(engine)
|
|
|
|
# Verify tables were created
|
|
from sqlalchemy import inspect
|
|
|
|
inspector = inspect(engine)
|
|
tables = inspector.get_table_names()
|
|
|
|
# Should have at least some tables
|
|
assert len(tables) > 0
|
|
finally:
|
|
engine.dispose()
|
|
|
|
def test_calls_run_migrations(self):
|
|
"""initialize_database calls run_migrations."""
|
|
from local_deep_research.database.initialize import initialize_database
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
db_path = Path(temp_dir) / "test.db"
|
|
engine = create_engine(f"sqlite:///{db_path}")
|
|
try:
|
|
with patch(
|
|
"local_deep_research.database.initialize.run_migrations"
|
|
) as mock_migrations:
|
|
initialize_database(engine)
|
|
|
|
mock_migrations.assert_called_once_with(engine)
|
|
finally:
|
|
engine.dispose()
|
|
|
|
def test_initializes_settings_when_session_provided(self):
|
|
"""initialize_database initializes settings when session provided."""
|
|
from local_deep_research.database.initialize import initialize_database
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
db_path = Path(temp_dir) / "test.db"
|
|
engine = create_engine(f"sqlite:///{db_path}")
|
|
try:
|
|
mock_session = Mock(spec=Session)
|
|
|
|
with patch(
|
|
"local_deep_research.database.initialize._initialize_default_settings"
|
|
) as mock_init_settings:
|
|
initialize_database(engine, db_session=mock_session)
|
|
|
|
mock_init_settings.assert_called_once_with(mock_session)
|
|
finally:
|
|
engine.dispose()
|
|
|
|
def test_skips_settings_when_no_session(self):
|
|
"""initialize_database skips settings init when no session provided."""
|
|
from local_deep_research.database.initialize import initialize_database
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
db_path = Path(temp_dir) / "test.db"
|
|
engine = create_engine(f"sqlite:///{db_path}")
|
|
try:
|
|
with patch(
|
|
"local_deep_research.database.initialize._initialize_default_settings"
|
|
) as mock_init_settings:
|
|
initialize_database(engine)
|
|
|
|
mock_init_settings.assert_not_called()
|
|
finally:
|
|
engine.dispose()
|
|
|
|
def test_handles_checkfirst_for_existing_tables(self):
|
|
"""initialize_database uses checkfirst=True for existing tables."""
|
|
from local_deep_research.database.initialize import initialize_database
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
db_path = Path(temp_dir) / "test.db"
|
|
engine = create_engine(f"sqlite:///{db_path}")
|
|
try:
|
|
# Create tables first
|
|
Base.metadata.create_all(engine)
|
|
|
|
# Run initialize again - should not fail
|
|
initialize_database(engine)
|
|
|
|
# Verify tables still exist
|
|
from sqlalchemy import inspect
|
|
|
|
inspector = inspect(engine)
|
|
tables = inspector.get_table_names()
|
|
assert len(tables) > 0
|
|
finally:
|
|
engine.dispose()
|