581 lines
24 KiB
Python
581 lines
24 KiB
Python
# #region test_task_persistence [C:2] [TYPE Module]
|
|
# @RELATION BINDS_TO -> SrcRoot
|
|
# @SEMANTICS: test, task, persistence, unit_test
|
|
# @PURPOSE: Unit tests for TaskPersistenceService.
|
|
# @LAYER Tests
|
|
# @TEST_DATA: valid_task -> {"id": "test-uuid-1", "plugin_id": "backup", "status": "PENDING"}
|
|
|
|
from datetime import UTC, datetime
|
|
from unittest.mock import patch
|
|
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
from src.core.task_manager.models import LogEntry, Task, TaskStatus
|
|
from src.core.task_manager.persistence import TaskPersistenceService
|
|
from src.models.mapping import Base, Environment
|
|
from src.models.task import TaskRecord
|
|
|
|
|
|
# #region TestTaskPersistenceHelpers [C:2] [TYPE Class]
|
|
# @RELATION BINDS_TO -> test_task_persistence
|
|
# @PURPOSE: Test suite for TaskPersistenceService static helper methods.
|
|
class TestTaskPersistenceHelpers:
|
|
|
|
# #region test_json_load_if_needed_none [C:2] [TYPE Function]
|
|
# @PURPOSE: Test _json_load_if_needed with None input.
|
|
def test_json_load_if_needed_none(self):
|
|
assert TaskPersistenceService._json_load_if_needed(None) is None
|
|
# #endregion test_json_load_if_needed_none
|
|
|
|
# #region test_json_load_if_needed_dict [C:2] [TYPE Function]
|
|
# @PURPOSE: Test _json_load_if_needed with dict input.
|
|
def test_json_load_if_needed_dict(self):
|
|
data = {"key": "value"}
|
|
assert TaskPersistenceService._json_load_if_needed(data) == data
|
|
# #endregion test_json_load_if_needed_dict
|
|
|
|
# #region test_json_load_if_needed_list [C:2] [TYPE Function]
|
|
# @PURPOSE: Test _json_load_if_needed with list input.
|
|
def test_json_load_if_needed_list(self):
|
|
data = [1, 2, 3]
|
|
assert TaskPersistenceService._json_load_if_needed(data) == data
|
|
# #endregion test_json_load_if_needed_list
|
|
|
|
# #region test_json_load_if_needed_json_string [C:2] [TYPE Function]
|
|
# @PURPOSE: Test _json_load_if_needed with JSON string.
|
|
def test_json_load_if_needed_json_string(self):
|
|
result = TaskPersistenceService._json_load_if_needed('{"key": "value"}')
|
|
assert result == {"key": "value"}
|
|
# #endregion test_json_load_if_needed_json_string
|
|
|
|
# #region test_json_load_if_needed_empty_string [C:2] [TYPE Function]
|
|
# @PURPOSE: Test _json_load_if_needed with empty/null strings.
|
|
def test_json_load_if_needed_empty_string(self):
|
|
assert TaskPersistenceService._json_load_if_needed("") is None
|
|
assert TaskPersistenceService._json_load_if_needed("null") is None
|
|
assert TaskPersistenceService._json_load_if_needed(" null ") is None
|
|
# #endregion test_json_load_if_needed_empty_string
|
|
|
|
# #region test_json_load_if_needed_plain_string [C:2] [TYPE Function]
|
|
# @PURPOSE: Test _json_load_if_needed with non-JSON string.
|
|
def test_json_load_if_needed_plain_string(self):
|
|
result = TaskPersistenceService._json_load_if_needed("not json")
|
|
assert result == "not json"
|
|
# #endregion test_json_load_if_needed_plain_string
|
|
|
|
# #region test_json_load_if_needed_integer [C:2] [TYPE Function]
|
|
# @PURPOSE: Test _json_load_if_needed with integer.
|
|
def test_json_load_if_needed_integer(self):
|
|
assert TaskPersistenceService._json_load_if_needed(42) == 42
|
|
# #endregion test_json_load_if_needed_integer
|
|
|
|
# #region test_parse_datetime_none [C:2] [TYPE Function]
|
|
# @PURPOSE: Test _parse_datetime with None.
|
|
def test_parse_datetime_none(self):
|
|
assert TaskPersistenceService._parse_datetime(None) is None
|
|
# #endregion test_parse_datetime_none
|
|
|
|
# #region test_parse_datetime_datetime_object [C:2] [TYPE Function]
|
|
# @PURPOSE: Test _parse_datetime with datetime object.
|
|
def test_parse_datetime_datetime_object(self):
|
|
dt = datetime(2024, 1, 1, 12, 0, 0)
|
|
assert TaskPersistenceService._parse_datetime(dt) == dt
|
|
# #endregion test_parse_datetime_datetime_object
|
|
|
|
# #region test_parse_datetime_iso_string [C:2] [TYPE Function]
|
|
# @PURPOSE: Test _parse_datetime with ISO string.
|
|
def test_parse_datetime_iso_string(self):
|
|
result = TaskPersistenceService._parse_datetime("2024-01-01T12:00:00")
|
|
assert isinstance(result, datetime)
|
|
assert result.year == 2024
|
|
# #endregion test_parse_datetime_iso_string
|
|
|
|
# #region test_parse_datetime_invalid_string [C:2] [TYPE Function]
|
|
# @PURPOSE: Test _parse_datetime with invalid string.
|
|
def test_parse_datetime_invalid_string(self):
|
|
assert TaskPersistenceService._parse_datetime("not-a-date") is None
|
|
# #endregion test_parse_datetime_invalid_string
|
|
|
|
# #region test_parse_datetime_integer [C:2] [TYPE Function]
|
|
# @PURPOSE: Test _parse_datetime with non-string, non-datetime.
|
|
def test_parse_datetime_integer(self):
|
|
assert TaskPersistenceService._parse_datetime(12345) is None
|
|
# #endregion test_parse_datetime_integer
|
|
|
|
# #endregion TestTaskPersistenceHelpers
|
|
|
|
|
|
# #region TestTaskPersistenceService [C:2] [TYPE Class]
|
|
# @RELATION BINDS_TO -> test_task_persistence
|
|
# @PURPOSE: Test suite for TaskPersistenceService CRUD operations.
|
|
# @TEST_DATA: valid_task -> {"id": "test-uuid-1", "plugin_id": "backup", "status": "PENDING"}
|
|
class TestTaskPersistenceService:
|
|
|
|
# #region setup_class [C:2] [TYPE Function]
|
|
# @PURPOSE: Setup in-memory test database.
|
|
@classmethod
|
|
def setup_class(cls):
|
|
"""Create an in-memory SQLite database for testing."""
|
|
cls.engine = create_engine("sqlite:///:memory:")
|
|
from sqlalchemy import event
|
|
event.listen(cls.engine, "connect", lambda c, _: c.execute("PRAGMA foreign_keys=ON"))
|
|
Base.metadata.create_all(bind=cls.engine)
|
|
cls.TestSessionLocal = sessionmaker(bind=cls.engine)
|
|
cls.service = TaskPersistenceService()
|
|
# #endregion setup_class
|
|
|
|
# #region teardown_class [C:2] [TYPE Function]
|
|
# @PURPOSE: Dispose of test database.
|
|
@classmethod
|
|
def teardown_class(cls):
|
|
cls.engine.dispose()
|
|
# #endregion teardown_class
|
|
|
|
# #region setup_method [C:2] [TYPE Function]
|
|
# @PURPOSE: Clean task_records table before each test.
|
|
def setup_method(self):
|
|
session = self.TestSessionLocal()
|
|
session.query(TaskRecord).delete()
|
|
session.query(Environment).delete()
|
|
session.commit()
|
|
session.close()
|
|
# #endregion setup_method
|
|
|
|
def _patched(self):
|
|
"""Helper: returns a patch context for TasksSessionLocal."""
|
|
return patch(
|
|
"src.core.task_manager.persistence.TasksSessionLocal",
|
|
self.TestSessionLocal
|
|
)
|
|
|
|
def _make_task(self, **kwargs):
|
|
"""Helper: create a Task with test defaults."""
|
|
defaults = {
|
|
"id": "test-uuid-1",
|
|
"plugin_id": "backup",
|
|
"status": TaskStatus.PENDING,
|
|
"params": {"source_env_id": "env-1"},
|
|
}
|
|
defaults.update(kwargs)
|
|
return Task(**defaults)
|
|
|
|
# #region test_persist_task_new [C:2] [TYPE Function]
|
|
# @PURPOSE: Test persisting a new task creates a record.
|
|
# @PRE: Empty database.
|
|
# @POST: TaskRecord exists in database.
|
|
def test_persist_task_new(self):
|
|
"""Test persisting a new task creates a record."""
|
|
task = self._make_task()
|
|
|
|
with self._patched():
|
|
self.service.persist_task(task)
|
|
|
|
session = self.TestSessionLocal()
|
|
record = session.query(TaskRecord).filter_by(id="test-uuid-1").first()
|
|
session.close()
|
|
|
|
assert record is not None
|
|
assert record.type == "backup"
|
|
assert record.status == "PENDING"
|
|
# #endregion test_persist_task_new
|
|
|
|
# #region test_persist_task_update [C:2] [TYPE Function]
|
|
# @PURPOSE: Test updating an existing task.
|
|
# @PRE: Task already persisted.
|
|
# @POST: Task record updated with new status.
|
|
def test_persist_task_update(self):
|
|
"""Test persisting an existing task updates the record."""
|
|
task = self._make_task(status=TaskStatus.PENDING)
|
|
|
|
with self._patched():
|
|
self.service.persist_task(task)
|
|
|
|
# Update status
|
|
task.status = TaskStatus.RUNNING
|
|
task.started_at = datetime.now(UTC)
|
|
|
|
with self._patched():
|
|
self.service.persist_task(task)
|
|
|
|
session = self.TestSessionLocal()
|
|
record = session.query(TaskRecord).filter_by(id="test-uuid-1").first()
|
|
session.close()
|
|
|
|
assert record.status == "RUNNING"
|
|
assert record.started_at is not None
|
|
# #endregion test_persist_task_update
|
|
|
|
# #region test_persist_task_with_logs [C:2] [TYPE Function]
|
|
# @PURPOSE: Test persisting a task with log entries.
|
|
# @PRE: Task has logs attached.
|
|
# @POST: Logs serialized as JSON in task record.
|
|
def test_persist_task_with_logs(self):
|
|
"""Test persisting a task with log entries."""
|
|
task = self._make_task()
|
|
task.logs = [
|
|
LogEntry(message="Step 1", level="INFO", source="plugin"),
|
|
LogEntry(message="Step 2", level="INFO", source="plugin"),
|
|
]
|
|
|
|
with self._patched():
|
|
self.service.persist_task(task)
|
|
|
|
session = self.TestSessionLocal()
|
|
record = session.query(TaskRecord).filter_by(id="test-uuid-1").first()
|
|
session.close()
|
|
|
|
assert record.logs is not None
|
|
assert len(record.logs) == 2
|
|
# #endregion test_persist_task_with_logs
|
|
|
|
# #region test_persist_task_failed_extracts_error [C:2] [TYPE Function]
|
|
# @PURPOSE: Test that FAILED task extracts last error message.
|
|
# @PRE: Task has FAILED status with ERROR logs.
|
|
# @POST: record.error contains last error message.
|
|
def test_persist_task_failed_extracts_error(self):
|
|
"""Test that FAILED tasks extract the last error message."""
|
|
task = self._make_task(status=TaskStatus.FAILED)
|
|
task.logs = [
|
|
LogEntry(message="Started OK", level="INFO", source="plugin"),
|
|
LogEntry(message="Connection failed", level="ERROR", source="plugin"),
|
|
LogEntry(message="Retrying...", level="INFO", source="plugin"),
|
|
LogEntry(message="Fatal: timeout", level="ERROR", source="plugin"),
|
|
]
|
|
|
|
with self._patched():
|
|
self.service.persist_task(task)
|
|
|
|
session = self.TestSessionLocal()
|
|
record = session.query(TaskRecord).filter_by(id="test-uuid-1").first()
|
|
session.close()
|
|
|
|
assert record.error == "Fatal: timeout"
|
|
# #endregion test_persist_task_failed_extracts_error
|
|
|
|
# #region test_persist_tasks_batch [C:2] [TYPE Function]
|
|
# @PURPOSE: Test persisting multiple tasks.
|
|
# @PRE: Empty database.
|
|
# @POST: All task records created.
|
|
def test_persist_tasks_batch(self):
|
|
"""Test persisting multiple tasks at once."""
|
|
tasks = [
|
|
self._make_task(id=f"batch-{i}", plugin_id="migration")
|
|
for i in range(3)
|
|
]
|
|
|
|
with self._patched():
|
|
self.service.persist_tasks(tasks)
|
|
|
|
session = self.TestSessionLocal()
|
|
count = session.query(TaskRecord).count()
|
|
session.close()
|
|
|
|
assert count == 3
|
|
# #endregion test_persist_tasks_batch
|
|
|
|
# #region test_load_tasks [C:2] [TYPE Function]
|
|
# @PURPOSE: Test loading tasks from database.
|
|
# @PRE: Tasks persisted.
|
|
# @POST: Returns list of Task objects with correct data.
|
|
def test_load_tasks(self):
|
|
"""Test loading tasks from database (round-trip)."""
|
|
task = self._make_task(
|
|
status=TaskStatus.SUCCESS,
|
|
started_at=datetime.now(UTC),
|
|
finished_at=datetime.now(UTC),
|
|
)
|
|
task.params = {"key": "value"}
|
|
task.result = {"output": "done"}
|
|
task.logs = [LogEntry(message="Done", level="INFO", source="plugin")]
|
|
|
|
with self._patched():
|
|
self.service.persist_task(task)
|
|
|
|
with self._patched():
|
|
loaded = self.service.load_tasks(limit=10)
|
|
|
|
assert len(loaded) == 1
|
|
assert loaded[0].id == "test-uuid-1"
|
|
assert loaded[0].plugin_id == "backup"
|
|
assert loaded[0].status == TaskStatus.SUCCESS
|
|
assert loaded[0].params == {"key": "value"}
|
|
# #endregion test_load_tasks
|
|
|
|
# #region test_load_tasks_with_status_filter [C:2] [TYPE Function]
|
|
# @PURPOSE: Test loading tasks filtered by status.
|
|
# @PRE: Tasks with different statuses persisted.
|
|
# @POST: Returns only tasks matching status filter.
|
|
def test_load_tasks_with_status_filter(self):
|
|
"""Test loading tasks filtered by status."""
|
|
tasks = [
|
|
self._make_task(id="s1", status=TaskStatus.SUCCESS),
|
|
self._make_task(id="s2", status=TaskStatus.FAILED),
|
|
self._make_task(id="s3", status=TaskStatus.SUCCESS),
|
|
]
|
|
|
|
with self._patched():
|
|
self.service.persist_tasks(tasks)
|
|
|
|
with self._patched():
|
|
failed_tasks = self.service.load_tasks(status=TaskStatus.FAILED)
|
|
|
|
assert len(failed_tasks) == 1
|
|
assert failed_tasks[0].id == "s2"
|
|
assert failed_tasks[0].status == TaskStatus.FAILED
|
|
# #endregion test_load_tasks_with_status_filter
|
|
|
|
# #region test_load_tasks_with_limit [C:2] [TYPE Function]
|
|
# @PURPOSE: Test loading tasks with limit.
|
|
# @PRE: Multiple tasks persisted.
|
|
# @POST: Returns at most `limit` tasks.
|
|
def test_load_tasks_with_limit(self):
|
|
"""Test loading tasks respects limit parameter."""
|
|
tasks = [
|
|
self._make_task(id=f"lim-{i}")
|
|
for i in range(10)
|
|
]
|
|
|
|
with self._patched():
|
|
self.service.persist_tasks(tasks)
|
|
|
|
with self._patched():
|
|
loaded = self.service.load_tasks(limit=3)
|
|
|
|
assert len(loaded) == 3
|
|
# #endregion test_load_tasks_with_limit
|
|
|
|
# #region test_delete_tasks [C:2] [TYPE Function]
|
|
# @PURPOSE: Test deleting tasks by ID list.
|
|
# @PRE: Tasks persisted.
|
|
# @POST: Specified tasks deleted, others remain.
|
|
def test_delete_tasks(self):
|
|
"""Test deleting tasks by ID list."""
|
|
tasks = [
|
|
self._make_task(id="del-1"),
|
|
self._make_task(id="del-2"),
|
|
self._make_task(id="keep-1"),
|
|
]
|
|
|
|
with self._patched():
|
|
self.service.persist_tasks(tasks)
|
|
|
|
with self._patched():
|
|
self.service.delete_tasks(["del-1", "del-2"])
|
|
|
|
session = self.TestSessionLocal()
|
|
remaining = session.query(TaskRecord).all()
|
|
session.close()
|
|
|
|
assert len(remaining) == 1
|
|
assert remaining[0].id == "keep-1"
|
|
# #endregion test_delete_tasks
|
|
|
|
# #region test_delete_tasks_empty_list [C:2] [TYPE Function]
|
|
# @PURPOSE: Test deleting with empty list (no-op).
|
|
# @PRE: None.
|
|
# @POST: No error, no changes.
|
|
def test_delete_tasks_empty_list(self):
|
|
"""Test deleting with empty list is a no-op."""
|
|
with self._patched():
|
|
self.service.delete_tasks([]) # Should not raise
|
|
# #endregion test_delete_tasks_empty_list
|
|
|
|
# #region test_persist_task_with_datetime_in_params [C:2] [TYPE Function]
|
|
# @PURPOSE: Test json_serializable handles datetime in params.
|
|
# @PRE: Task params contain datetime values.
|
|
# @POST: Params serialized correctly.
|
|
def test_persist_task_with_datetime_in_params(self):
|
|
"""Test that datetime values in params are serialized to ISO format."""
|
|
dt = datetime(2024, 6, 15, 10, 30, 0)
|
|
task = self._make_task(params={"timestamp": dt, "name": "test"})
|
|
|
|
with self._patched():
|
|
self.service.persist_task(task)
|
|
|
|
session = self.TestSessionLocal()
|
|
record = session.query(TaskRecord).filter_by(id="test-uuid-1").first()
|
|
session.close()
|
|
|
|
assert record.params is not None
|
|
assert record.params["timestamp"] == "2024-06-15T10:30:00"
|
|
assert record.params["name"] == "test"
|
|
# #endregion test_persist_task_with_datetime_in_params
|
|
|
|
# #region test_persist_task_resolves_environment_slug_to_existing_id [C:2] [TYPE Function]
|
|
# @PURPOSE: Ensure slug-like environment token resolves to environments.id before persisting task.
|
|
# @PRE: environments table contains env with name convertible to provided slug token.
|
|
# @POST: task_records.environment_id stores actual environments.id and does not violate FK.
|
|
def test_persist_task_resolves_environment_slug_to_existing_id(self):
|
|
session = self.TestSessionLocal()
|
|
env = Environment(id="env-uuid-1", name="SS DEV", url="https://example.local", credentials_id="cred-1")
|
|
session.add(env)
|
|
session.commit()
|
|
session.close()
|
|
|
|
task = self._make_task(params={"environment_id": "ss-dev"})
|
|
|
|
with self._patched():
|
|
self.service.persist_task(task)
|
|
|
|
session = self.TestSessionLocal()
|
|
record = session.query(TaskRecord).filter_by(id="test-uuid-1").first()
|
|
session.close()
|
|
|
|
assert record is not None
|
|
assert record.environment_id == "env-uuid-1"
|
|
# #endregion test_persist_task_resolves_environment_slug_to_existing_id
|
|
|
|
# #region test_resolve_environment_id_direct_id_match [C:2] [TYPE Function]
|
|
# @PURPOSE: _resolve_environment_id returns match on direct primary key lookup.
|
|
def test_resolve_environment_id_direct_id_match(self):
|
|
session = self.TestSessionLocal()
|
|
env = Environment(id="direct-id", name="Some Env", url="https://x", credentials_id="cred-1")
|
|
session.add(env)
|
|
session.commit()
|
|
session.close()
|
|
|
|
result = TaskPersistenceService._resolve_environment_id(self.TestSessionLocal(), "direct-id")
|
|
assert result == "direct-id"
|
|
# #endregion test_resolve_environment_id_direct_id_match
|
|
|
|
# #region test_resolve_environment_id_name_match [C:2] [TYPE Function]
|
|
# @PURPOSE: _resolve_environment_id returns match on name lookup.
|
|
def test_resolve_environment_id_name_match(self):
|
|
session = self.TestSessionLocal()
|
|
env = Environment(id="env-name-id", name="Production", url="https://prod", credentials_id="cred-1")
|
|
session.add(env)
|
|
session.commit()
|
|
session.close()
|
|
|
|
result = TaskPersistenceService._resolve_environment_id(self.TestSessionLocal(), "Production")
|
|
assert result == "env-name-id"
|
|
# #endregion test_resolve_environment_id_name_match
|
|
|
|
# #region test_resolve_environment_id_empty_token [C:2] [TYPE Function]
|
|
# @PURPOSE: _resolve_environment_id returns None when normalized token is empty.
|
|
def test_resolve_environment_id_empty_token(self):
|
|
# No envs in DB; value "---" normalizes to "" (all non-alnum chars) → returns None
|
|
result = TaskPersistenceService._resolve_environment_id(self.TestSessionLocal(), "---")
|
|
assert result is None
|
|
# #endregion test_resolve_environment_id_empty_token
|
|
|
|
# #region test_persist_task_with_list_in_params [C:2] [TYPE Function]
|
|
# @PURPOSE: json_serializable handles list values in params.
|
|
def test_persist_task_with_list_in_params(self):
|
|
task = self._make_task(params={"items": [1, 2, {"nested": "val"}]})
|
|
with self._patched():
|
|
self.service.persist_task(task)
|
|
|
|
session = self.TestSessionLocal()
|
|
record = session.query(TaskRecord).filter_by(id="test-uuid-1").first()
|
|
session.close()
|
|
assert record is not None
|
|
assert record.params["items"] == [1, 2, {"nested": "val"}]
|
|
# #endregion test_persist_task_with_list_in_params
|
|
|
|
# #region test_persist_task_with_context_datetime [C:2] [TYPE Function]
|
|
# @PURPOSE: Log context with datetime values gets json_serializable treatment.
|
|
def test_persist_task_with_context_datetime(self):
|
|
from datetime import UTC, datetime
|
|
task = self._make_task()
|
|
dt_val = datetime(2024, 7, 4, 12, 0, 0, tzinfo=UTC)
|
|
task.logs = [
|
|
LogEntry(message="ctx test", level="INFO", source="test", context={"time": dt_val, "val": 42}),
|
|
]
|
|
with self._patched():
|
|
self.service.persist_task(task)
|
|
|
|
session = self.TestSessionLocal()
|
|
record = session.query(TaskRecord).filter_by(id="test-uuid-1").first()
|
|
session.close()
|
|
assert record is not None
|
|
assert len(record.logs) == 1
|
|
assert record.logs[0]["context"]["time"] == "2024-07-04T12:00:00+00:00"
|
|
# #endregion test_persist_task_with_context_datetime
|
|
|
|
# #region test_persist_task_db_error_rollback [C:2] [TYPE Function]
|
|
# @PURPOSE: persist_task rollbacks on database error.
|
|
def test_persist_task_db_error_rollback(self):
|
|
task = self._make_task()
|
|
# Force a DB error by passing params that cause SQLite issue
|
|
with patch.object(self.TestSessionLocal(), "commit", side_effect=RuntimeError("db down")):
|
|
# Repatch TasksSessionLocal to use the broken session
|
|
def broken_session():
|
|
s = self.TestSessionLocal()
|
|
s.commit = lambda: (_ for _ in ()).throw(RuntimeError("db down"))
|
|
return s
|
|
with patch("src.core.task_manager.persistence.TasksSessionLocal", broken_session):
|
|
self.service.persist_task(task) # Should not raise
|
|
|
|
session = self.TestSessionLocal()
|
|
record = session.query(TaskRecord).filter_by(id="test-uuid-1").first()
|
|
session.close()
|
|
assert record is None # rolled back
|
|
# #endregion test_persist_task_db_error_rollback
|
|
|
|
# #region test_load_tasks_with_non_dict_logs [C:2] [TYPE Function]
|
|
# @PURPOSE: Load tasks where log entries are not dicts (skipped gracefully).
|
|
def test_load_tasks_with_non_dict_logs(self):
|
|
task = self._make_task(status=TaskStatus.SUCCESS)
|
|
with self._patched():
|
|
self.service.persist_task(task)
|
|
|
|
# Manually inject non-dict log data
|
|
session = self.TestSessionLocal()
|
|
record = session.query(TaskRecord).filter_by(id="test-uuid-1").first()
|
|
record.logs = [42, "not-a-dict"]
|
|
session.commit()
|
|
session.close()
|
|
|
|
with self._patched():
|
|
loaded = self.service.load_tasks()
|
|
assert len(loaded) == 1
|
|
assert loaded[0].id == "test-uuid-1"
|
|
assert loaded[0].logs == [] # non-dict entries skipped
|
|
# #endregion test_load_tasks_with_non_dict_logs
|
|
|
|
# #region test_load_tasks_corrupt_record_skipped [C:2] [TYPE Function]
|
|
# @PURPOSE: Load tasks gracefully skips records that fail reconstruction.
|
|
def test_load_tasks_corrupt_record_skipped(self):
|
|
session = self.TestSessionLocal()
|
|
# Create a record with invalid status value
|
|
record = TaskRecord(id="corrupt-1", type="test", status="INVALID_STATUS")
|
|
session.add(record)
|
|
session.commit()
|
|
session.close()
|
|
|
|
with self._patched():
|
|
loaded = self.service.load_tasks()
|
|
# Corrupt record should be skipped, no exception raised
|
|
ids = [t.id for t in loaded]
|
|
assert "corrupt-1" not in ids
|
|
# #endregion test_load_tasks_corrupt_record_skipped
|
|
|
|
# #region test_delete_tasks_db_error [C:2] [TYPE Function]
|
|
# @PURPOSE: delete_tasks handles database error gracefully.
|
|
def test_delete_tasks_db_error(self):
|
|
# Pre-create a task
|
|
task = self._make_task(id="del-err")
|
|
with self._patched():
|
|
self.service.persist_task(task)
|
|
|
|
# Force error during delete
|
|
def broken_session():
|
|
s = self.TestSessionLocal()
|
|
original_commit = s.commit
|
|
s.commit = lambda: (_ for _ in ()).throw(RuntimeError("delete failed"))
|
|
return s
|
|
with patch("src.core.task_manager.persistence.TasksSessionLocal", broken_session):
|
|
self.service.delete_tasks(["del-err"]) # Should not raise
|
|
|
|
# Task still exists
|
|
session = self.TestSessionLocal()
|
|
record = session.query(TaskRecord).filter_by(id="del-err").first()
|
|
session.close()
|
|
assert record is not None
|
|
# #endregion test_delete_tasks_db_error
|
|
|
|
# #endregion TestTaskPersistenceService
|
|
# #endregion test_task_persistence
|