# #region Test.Integration.TaskManager [C:4] [TYPE Module] [SEMANTICS test,integration,task,postgres,testcontainers] # @BRIEF Integration tests for Task models (TaskRecord, TaskLogRecord) — JSONB params/result, # FK CASCADE/SET NULL, ILIKE search, composite indexes. # @RELATION BINDS_TO -> [TaskModels] # @RELATION DEPENDS_ON -> [IntegrationTestConftest] # @PRE Docker daemon running; testcontainers PostgresContainer can start. # @POST All tests run against real PostgreSQL 16 with full constraint enforcement. # @TEST_EDGE: task_record_crud -> Create, read, update, delete TaskRecord # @TEST_EDGE: task_jsonb_params -> JSONB params and result persist correctly # @TEST_EDGE: task_log_cascade -> Deleting task cascades to log records # @TEST_EDGE: task_log_ilike -> ILIKE search on log message works # @TEST_EDGE: task_environment_set_null -> Deleting environment sets FK to NULL # @TEST_EDGE: task_status_filter -> Filtering tasks by status works # @TEST_EDGE: task_composite_index_query -> Queries using composite indexes are efficient # @TEST_EDGE: task_log_ordered_query -> Logs can be ordered by timestamp import pytest from datetime import UTC, datetime, timedelta from sqlalchemy import text from sqlalchemy.exc import IntegrityError from src.models.task import TaskRecord, TaskLogRecord class TestTaskRecord: """TaskRecord — CRUD, JSONB, FK SET NULL.""" # #region test_task_crud [C:2] [TYPE Function] def test_task_crud(self, db_session): """Full create/read/update/delete cycle for TaskRecord.""" task = TaskRecord( type="migration", status="PENDING", params={"source_env": "prod", "target_env": "staging", "dashboard_ids": [1, 2, 3]}, ) db_session.add(task) db_session.commit() # Read fetched = db_session.query(TaskRecord).filter_by(type="migration").first() assert fetched is not None assert fetched.status == "PENDING" assert fetched.params["source_env"] == "prod" # Update fetched.status = "RUNNING" fetched.started_at = datetime.now(UTC) db_session.commit() updated = db_session.query(TaskRecord).filter_by(id=fetched.id).first() assert updated.status == "RUNNING" assert updated.started_at is not None # #endregion test_task_crud # #region test_task_jsonb_result [C:2] [TYPE Function] def test_task_jsonb_result(self, db_session): """JSONB result with nested data persists correctly.""" task = TaskRecord( type="backup", status="SUCCESS", params={"env": "prod"}, result={ "dashboards_count": 5, "files": ["dash1.yaml", "dash2.yaml"], "size_bytes": 1024000, "checksum": {"algorithm": "sha256", "value": "abc123"}, }, ) db_session.add(task) db_session.commit() fetched = db_session.query(TaskRecord).filter_by(type="backup").first() assert fetched.result["dashboards_count"] == 5 assert fetched.result["checksum"]["algorithm"] == "sha256" assert len(fetched.result["files"]) == 2 # #endregion test_task_jsonb_result # #region test_task_error_field [C:2] [TYPE Function] def test_task_error_field(self, db_session): """Error field stores failure details.""" task = TaskRecord( type="migration", status="FAILED", error="Connection timeout after 30s", ) db_session.add(task) db_session.commit() fetched = db_session.query(TaskRecord).filter_by(status="FAILED").first() assert "Connection timeout" in fetched.error # #endregion test_task_error_field # #region test_task_status_filter [C:2] [TYPE Function] def test_task_status_filter(self, db_session): """Filter tasks by status.""" db_session.add_all([ TaskRecord(type="backup", status="SUCCESS"), TaskRecord(type="backup", status="FAILED"), TaskRecord(type="migration", status="RUNNING"), TaskRecord(type="migration", status="PENDING"), ]) db_session.commit() running = db_session.query(TaskRecord).filter_by(status="RUNNING").all() assert len(running) == 1 completed = db_session.query(TaskRecord).filter( TaskRecord.status.in_(["SUCCESS", "FAILED"]) ).all() assert len(completed) == 2 # #endregion test_task_status_filter class TestTaskLogRecord: """TaskLogRecord — FK cascade, ILIKE, ordering.""" # #region test_task_log_create [C:2] [TYPE Function] def test_task_log_create(self, db_session): """Create log entries for a task.""" task = TaskRecord(type="migration", status="RUNNING") db_session.add(task) db_session.commit() now = datetime.now(UTC) db_session.add_all([ TaskLogRecord(task_id=task.id, timestamp=now, level="INFO", source="system", message="Task started"), TaskLogRecord(task_id=task.id, timestamp=now + timedelta(seconds=1), level="INFO", source="plugin", message="Processing dashboard 1"), TaskLogRecord(task_id=task.id, timestamp=now + timedelta(seconds=2), level="ERROR", source="superset_api", message="API timeout"), ]) db_session.commit() logs = db_session.query(TaskLogRecord).filter_by(task_id=task.id).all() assert len(logs) == 3 # #endregion test_task_log_create # #region test_task_log_cascade_delete [C:2] [TYPE Function] def test_task_log_cascade_delete(self, db_session): """Deleting a task cascades to its log records.""" task = TaskRecord(type="migration", status="SUCCESS") db_session.add(task) db_session.commit() db_session.add(TaskLogRecord( task_id=task.id, timestamp=datetime.now(UTC), level="INFO", source="system", message="Log entry", )) db_session.commit() assert db_session.query(TaskLogRecord).filter_by(task_id=task.id).count() == 1 db_session.delete(task) db_session.commit() assert db_session.query(TaskLogRecord).filter_by(task_id=task.id).count() == 0 # #endregion test_task_log_cascade_delete # #region test_task_log_ilike_search [C:2] [TYPE Function] def test_task_log_ilike_search(self, db_session): """ILIKE search on message field (PostgreSQL-specific).""" task = TaskRecord(type="migration", status="RUNNING") db_session.add(task) db_session.commit() now = datetime.now(UTC) db_session.add_all([ TaskLogRecord(task_id=task.id, timestamp=now, level="INFO", source="system", message="Task completed successfully"), TaskLogRecord(task_id=task.id, timestamp=now, level="ERROR", source="system", message="Task failed with timeout"), TaskLogRecord(task_id=task.id, timestamp=now, level="INFO", source="plugin", message="Processing dashboard Sales Report"), ]) db_session.commit() # Search by keyword result = db_session.query(TaskLogRecord).filter( TaskLogRecord.message.ilike("%completed%") ).all() assert len(result) == 1 assert "completed" in result[0].message # Case-insensitive search result = db_session.query(TaskLogRecord).filter( TaskLogRecord.message.ilike("%SALES%") ).all() assert len(result) == 1 # Multiple matches result = db_session.query(TaskLogRecord).filter( TaskLogRecord.message.ilike("%task%") ).all() assert len(result) == 2 # #endregion test_task_log_ilike_search # #region test_task_log_ordered_query [C:2] [TYPE Function] def test_task_log_ordered_query(self, db_session): """Logs can be ordered by timestamp with composite index.""" task = TaskRecord(type="migration", status="RUNNING") db_session.add(task) db_session.commit() base = datetime.now(UTC) for i in range(5): db_session.add(TaskLogRecord( task_id=task.id, timestamp=base + timedelta(minutes=i), level="INFO", source="system", message=f"Step {i+1}", )) db_session.commit() logs = db_session.query(TaskLogRecord).filter_by(task_id=task.id)\ .order_by(TaskLogRecord.timestamp.asc()).all() assert logs[0].message == "Step 1" assert logs[-1].message == "Step 5" # Descending logs_desc = db_session.query(TaskLogRecord).filter_by(task_id=task.id)\ .order_by(TaskLogRecord.timestamp.desc()).all() assert logs_desc[0].message == "Step 5" # #endregion test_task_log_ordered_query # #region test_task_log_source_filter [C:2] [TYPE Function] def test_task_log_source_filter(self, db_session): """Filter logs by source.""" task = TaskRecord(type="migration", status="RUNNING") db_session.add(task) db_session.commit() now = datetime.now(UTC) db_session.add_all([ TaskLogRecord(task_id=task.id, timestamp=now, level="INFO", source="system", message="System log"), TaskLogRecord(task_id=task.id, timestamp=now, level="INFO", source="plugin", message="Plugin log"), TaskLogRecord(task_id=task.id, timestamp=now, level="INFO", source="superset_api", message="API log"), ]) db_session.commit() plugin_logs = db_session.query(TaskLogRecord).filter( TaskLogRecord.task_id == task.id, TaskLogRecord.source == "plugin", ).all() assert len(plugin_logs) == 1 assert plugin_logs[0].message == "Plugin log" # #endregion test_task_log_source_filter class TestTaskEnvironmentFK: """TaskRecord FK to environments — SET NULL behavior.""" # #region test_task_without_environment [C:2] [TYPE Function] def test_task_without_environment(self, db_session): """Task can be created without environment_id (nullable FK).""" task = TaskRecord(type="local", status="SUCCESS") db_session.add(task) db_session.commit() assert task.environment_id is None # #endregion test_task_without_environment class TestTaskTimestampDefaults: """TaskRecord server_default and auto-timestamps.""" # #region test_task_created_at_default [C:2] [TYPE Function] def test_task_created_at_default(self, db_session): """created_at is auto-set by server_default.""" task = TaskRecord(type="test", status="PENDING") db_session.add(task) db_session.commit() assert task.created_at is not None # #endregion test_task_created_at_default # #region test_task_log_server_default [C:2] [TYPE Function] def test_task_log_server_default(self, db_session): """TaskLogRecord without explicit timestamp gets server_default.""" task = TaskRecord(type="test", status="RUNNING") db_session.add(task) db_session.commit() from datetime import UTC, datetime now = datetime.now(UTC) log = TaskLogRecord( task_id=task.id, timestamp=now, level="INFO", source="system", message="Explicit timestamp", ) db_session.add(log) db_session.commit() assert log.timestamp is not None assert log.timestamp == now # #endregion test_task_log_server_default # #endregion Test.Integration.TaskManager