chore: remainder — backend test infra, agent config, docker, i18n, frontend ui
- Backend: alembic env, config manager/models, dependencies, translate plugin - Backend tests: async_sync_regression, integration tests, git services, test_agent - Docker: docker-compose.yml updates - Agent: qa-tester.md update, semantics-testing SKILL.md update - Frontend: TopNavbar, sidebarNavigation, FeaturesSettings, FeatureGate - i18n: assistant.json en/ru locale updates - New: frontend/src/lib/components/agent/ directory
This commit is contained in:
299
backend/tests/integration/test_task_manager_integration.py
Normal file
299
backend/tests/integration/test_task_manager_integration.py
Normal file
@@ -0,0 +1,299 @@
|
||||
# #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
|
||||
Reference in New Issue
Block a user