""" Tests for download tracking models (DownloadTracker, DownloadDuplicates, DownloadAttempt). Tests model structure, column constraints, default values, and __repr__ methods WITHOUT requiring a real database -- uses direct attribute assignment on model instances. """ from sqlalchemy import Boolean, Integer, String, Text, UniqueConstraint from local_deep_research.database.models.download_tracker import ( DownloadAttempt, DownloadDuplicates, DownloadTracker, ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _col(model_cls, name): """Return a Column object from a model's __table__.""" return model_cls.__table__.columns[name] def _make(model_cls, **kwargs): """Instantiate a model via its normal constructor (no DB session needed). SQLAlchemy's declarative ``__init__`` accepts column names as keyword arguments and properly initialises the instance state so that instrumented attribute access (used by ``__repr__``) works correctly. """ return model_cls(**kwargs) # =========================================================================== # DownloadTracker # =========================================================================== class TestDownloadTrackerTablename: """Test 1: __tablename__ is 'download_tracker'.""" def test_tablename(self): assert DownloadTracker.__tablename__ == "download_tracker" class TestDownloadTrackerColumns: """Test 2: Column types and nullable constraints.""" def test_url_column_is_text_and_not_nullable(self): col = _col(DownloadTracker, "url") assert isinstance(col.type, Text) assert col.nullable is False def test_url_hash_is_string64_not_nullable_with_table_level_unique(self): col = _col(DownloadTracker, "url_hash") assert isinstance(col.type, String) assert col.type.length == 64 assert col.nullable is False # Uniqueness lives on a table-level UniqueConstraint named # ``uq_download_tracker_url_hash`` (see model __table_args__) so the # index lands inline in CREATE TABLE — required so SQLCipher accepts # ``url_hash`` as a valid FK target for the child tables. Column-level # ``unique=True``/``index=True`` would emit a separate CREATE UNIQUE # INDEX which SQLCipher does not recognise as the FK target. unique_cols = { tuple(c.name for c in constraint.columns) for constraint in DownloadTracker.__table__.constraints if isinstance(constraint, UniqueConstraint) } assert ("url_hash",) in unique_cols def test_first_resource_id_is_integer_not_nullable(self): col = _col(DownloadTracker, "first_resource_id") assert isinstance(col.type, Integer) assert col.nullable is False def test_first_resource_id_has_foreign_key(self): col = _col(DownloadTracker, "first_resource_id") fk_targets = [fk.target_fullname for fk in col.foreign_keys] assert "research_resources.id" in fk_targets def test_file_hash_is_nullable(self): col = _col(DownloadTracker, "file_hash") assert isinstance(col.type, String) assert col.type.length == 64 assert col.nullable is True assert col.index is True def test_file_path_is_nullable_text(self): col = _col(DownloadTracker, "file_path") assert isinstance(col.type, Text) assert col.nullable is True def test_file_name_is_nullable_string255_indexed(self): col = _col(DownloadTracker, "file_name") assert isinstance(col.type, String) assert col.type.length == 255 assert col.nullable is True assert col.index is True def test_file_size_is_nullable_integer(self): col = _col(DownloadTracker, "file_size") assert isinstance(col.type, Integer) assert col.nullable is True def test_is_downloaded_is_boolean_not_nullable_indexed(self): col = _col(DownloadTracker, "is_downloaded") assert isinstance(col.type, Boolean) assert col.nullable is False assert col.index is True def test_is_accessible_is_boolean(self): col = _col(DownloadTracker, "is_accessible") assert isinstance(col.type, Boolean) def test_downloaded_at_is_nullable(self): col = _col(DownloadTracker, "downloaded_at") assert col.nullable is True def test_first_seen_is_not_nullable(self): col = _col(DownloadTracker, "first_seen") assert col.nullable is False def test_last_checked_is_not_nullable(self): col = _col(DownloadTracker, "last_checked") assert col.nullable is False def test_library_document_id_is_nullable_with_fk(self): col = _col(DownloadTracker, "library_document_id") # documents.id is String(36) UUID; column type matches PK type so # SQLite doesn't silently coerce via type-affinity (which produced # the original mismatched-FK bug, see #3697). assert isinstance(col.type, String) assert col.type.length == 36 assert col.nullable is True fk_targets = [fk.target_fullname for fk in col.foreign_keys] assert "documents.id" in fk_targets def test_id_is_primary_key_autoincrement(self): col = _col(DownloadTracker, "id") assert col.primary_key is True assert col.autoincrement is True class TestDownloadTrackerReprDownloaded: """Test 3: __repr__ shows 'downloaded' when is_downloaded=True.""" def test_repr_downloaded(self): obj = _make( DownloadTracker, url_hash="abcdef1234567890", is_downloaded=True, ) result = repr(obj) assert "downloaded" in result assert "not downloaded" not in result class TestDownloadTrackerReprNotDownloaded: """Test 4: __repr__ shows 'not downloaded' when is_downloaded=False.""" def test_repr_not_downloaded(self): obj = _make( DownloadTracker, url_hash="abcdef1234567890", is_downloaded=False, ) result = repr(obj) assert "not downloaded" in result class TestDownloadTrackerReprTruncation: """Test 5: __repr__ truncates url_hash to first 8 characters.""" def test_repr_truncates_url_hash(self): full_hash = "a1b2c3d4e5f6g7h8i9j0" obj = _make( DownloadTracker, url_hash=full_hash, is_downloaded=False, ) result = repr(obj) assert "a1b2c3d4..." in result # The full hash should NOT appear in the repr assert full_hash not in result class TestDownloadTrackerDefaultIsDownloaded: """Test 6: Default is_downloaded is False.""" def test_default_is_downloaded(self): col = _col(DownloadTracker, "is_downloaded") assert col.default is not None assert col.default.arg is False class TestDownloadTrackerDefaultIsAccessible: """Test 7: Default is_accessible is True.""" def test_default_is_accessible(self): col = _col(DownloadTracker, "is_accessible") assert col.default is not None assert col.default.arg is True # =========================================================================== # DownloadDuplicates # =========================================================================== class TestDownloadDuplicatesTablename: """Test 8: __tablename__ is 'download_duplicates'.""" def test_tablename(self): assert DownloadDuplicates.__tablename__ == "download_duplicates" class TestDownloadDuplicatesUniqueConstraint: """Test 9: Has UniqueConstraint on (url_hash, resource_id).""" def test_unique_constraint_exists(self): table = DownloadDuplicates.__table__ unique_constraints = [ c for c in table.constraints if c.__class__.__name__ == "UniqueConstraint" ] # Find the constraint named 'uix_url_resource' matching = [ c for c in unique_constraints if c.name == "uix_url_resource" ] assert len(matching) == 1 col_names = {col.name for col in matching[0].columns} assert col_names == {"url_hash", "resource_id"} class TestDownloadDuplicatesRepr: """Test 10: __repr__ shows url_hash and resource_id.""" def test_repr_content(self): obj = _make( DownloadDuplicates, url_hash="deadbeef12345678", resource_id=42, ) result = repr(obj) assert "deadbeef..." in result assert "resource_id=42" in result assert "DownloadDuplicates" in result class TestDownloadDuplicatesCompositeIndex: """Test 11: Has composite index on (research_id, url_hash).""" def test_composite_index_exists(self): table = DownloadDuplicates.__table__ index_names = {idx.name for idx in table.indexes} assert "idx_research_duplicates" in index_names # Verify the columns in the index target_index = next( idx for idx in table.indexes if idx.name == "idx_research_duplicates" ) col_names = [col.name for col in target_index.columns] assert "research_id" in col_names assert "url_hash" in col_names class TestDownloadDuplicatesColumns: """Additional column validation for DownloadDuplicates.""" def test_url_hash_not_nullable_with_fk(self): col = _col(DownloadDuplicates, "url_hash") assert col.nullable is False fk_targets = [fk.target_fullname for fk in col.foreign_keys] assert "download_tracker.url_hash" in fk_targets def test_resource_id_not_nullable_with_fk(self): col = _col(DownloadDuplicates, "resource_id") assert col.nullable is False fk_targets = [fk.target_fullname for fk in col.foreign_keys] assert "research_resources.id" in fk_targets def test_research_id_not_nullable_indexed(self): col = _col(DownloadDuplicates, "research_id") assert isinstance(col.type, String) assert col.type.length == 36 assert col.nullable is False assert col.index is True def test_added_at_not_nullable(self): col = _col(DownloadDuplicates, "added_at") assert col.nullable is False # =========================================================================== # DownloadAttempt # =========================================================================== class TestDownloadAttemptTablename: """Test 12: __tablename__ is 'download_attempts'.""" def test_tablename(self): assert DownloadAttempt.__tablename__ == "download_attempts" class TestDownloadAttemptReprSuccess: """Test 13: __repr__ shows 'success' when succeeded=True.""" def test_repr_success(self): obj = _make( DownloadAttempt, attempt_number=1, succeeded=True, status_code=200, error_type=None, ) result = repr(obj) assert "success" in result assert "failed" not in result assert "attempt=1" in result class TestDownloadAttemptReprFailedWithStatusCode: """Test 14: __repr__ shows 'failed (404)' when succeeded=False, status_code=404.""" def test_repr_failed_status_code(self): obj = _make( DownloadAttempt, attempt_number=2, succeeded=False, status_code=404, error_type=None, ) result = repr(obj) assert "failed (404)" in result assert "attempt=2" in result class TestDownloadAttemptReprFailedWithErrorType: """Test 15: __repr__ shows 'failed (timeout)' when no status_code, error_type='timeout'.""" def test_repr_failed_error_type(self): obj = _make( DownloadAttempt, attempt_number=3, succeeded=False, status_code=None, error_type="timeout", ) result = repr(obj) assert "failed (timeout)" in result assert "attempt=3" in result class TestDownloadAttemptReprFailedNoInfo: """Test 16: __repr__ shows 'failed (None)' when no status_code or error_type.""" def test_repr_failed_none(self): obj = _make( DownloadAttempt, attempt_number=1, succeeded=False, status_code=None, error_type=None, ) result = repr(obj) assert "failed (None)" in result class TestDownloadAttemptDefaultSucceeded: """Test 17: Default succeeded is False.""" def test_default_succeeded(self): col = _col(DownloadAttempt, "succeeded") assert col.default is not None assert col.default.arg is False class TestDownloadAttemptColumns: """Additional column validation for DownloadAttempt.""" def test_url_hash_not_nullable_with_fk(self): col = _col(DownloadAttempt, "url_hash") assert col.nullable is False assert col.index is True fk_targets = [fk.target_fullname for fk in col.foreign_keys] assert "download_tracker.url_hash" in fk_targets def test_attempt_number_not_nullable(self): col = _col(DownloadAttempt, "attempt_number") assert isinstance(col.type, Integer) assert col.nullable is False def test_status_code_nullable(self): col = _col(DownloadAttempt, "status_code") assert isinstance(col.type, Integer) assert col.nullable is True def test_error_type_nullable_string100(self): col = _col(DownloadAttempt, "error_type") assert isinstance(col.type, String) assert col.type.length == 100 assert col.nullable is True def test_error_message_nullable_text(self): col = _col(DownloadAttempt, "error_message") assert isinstance(col.type, Text) assert col.nullable is True def test_attempted_at_not_nullable(self): col = _col(DownloadAttempt, "attempted_at") assert col.nullable is False def test_duration_ms_nullable(self): col = _col(DownloadAttempt, "duration_ms") assert isinstance(col.type, Integer) assert col.nullable is True def test_succeeded_not_nullable(self): col = _col(DownloadAttempt, "succeeded") assert isinstance(col.type, Boolean) assert col.nullable is False def test_bytes_downloaded_nullable(self): col = _col(DownloadAttempt, "bytes_downloaded") assert isinstance(col.type, Integer) assert col.nullable is True def test_id_primary_key(self): col = _col(DownloadAttempt, "id") assert col.primary_key is True