- 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
300 lines
12 KiB
Python
300 lines
12 KiB
Python
# #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
|