test(phase2): add unit tests for core, persistence, plugin, utils

- Add test_handler_helpers.py for plugin handler helpers (7 tests)
- Add test_mgr_methods.py for persistence manager (5 tests)
- Add test_app_config_validation.py for core app config (12 tests)
- Add test_knowledge_service.py for API knowledge service (22 tests)
- Add test_kbmgr.py for RAG knowledge base manager (39 tests)
- Add test_survey_manager.py for survey manager (22 tests)
- Add test_connector_methods.py for plugin connector (24 tests)
- Add test_funcschema.py for utils function schema (9 tests)
- Add test_platform.py for utils platform detection (7 tests)
- Add test_extract_deps.py for plugin deps extraction (7 tests)
- Add test_database_decorator.py for persistence decorator (7 tests)
- Add test_load_config.py for core config loading (19 tests)
- Add COVERAGE_EXCLUSIONS.md documenting external adapter exclusions
- Fix test_chat_session_limit.py path for portability

Coverage: core 28% → 30%, persistence 24% → 24.4%, plugin 27% → 28%
Total: 1082 tests passed, core module coverage 45.5%

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
huanghuoguoguo
2026-05-10 20:43:54 +08:00
parent ea6ed9b7fd
commit 3872e3e1ac
17 changed files with 4041 additions and 444 deletions
+422 -440
View File
@@ -1,524 +1,506 @@
"""Unit tests for core TaskContext, TaskWrapper, and AsyncTaskManager.
Tests cover:
- TaskContext initialization, state tracking, serialization
- TaskWrapper ID generation, to_dict serialization
- AsyncTaskManager task creation, stats, pruning
Note: Uses import_isolation to break circular import chains.
"""
Unit tests for AsyncTaskManager and TaskWrapper.
Tests cover async task lifecycle management:
- Task scheduling and tracking
- Task completion
- Task exception handling
- Task cancellation
- Multiple task isolation
Uses module pre-mocking to break circular import chain.
"""
from __future__ import annotations
import pytest
import asyncio
import sys
import enum
from unittest.mock import MagicMock
from importlib import import_module
from unittest.mock import Mock, MagicMock
from contextlib import contextmanager
from typing import Generator
# Pre-mock app module BEFORE importing taskmgr to break circular chain:
# taskmgr → app → http_controller → groups/knowledge/migration → taskmgr (partial)
class FakeMinimalApp:
"""Minimal app that only provides event_loop."""
class MockLifecycleControlScopeEnum:
"""Mock enum value for LifecycleControlScope with .value attribute."""
def __init__(self, value: str):
self.value = value
def __init__(self, event_loop):
self.event_loop = event_loop
self.instance_config = MagicMock()
self.instance_config.data = {}
# Pre-register mock app module
_mock_app_module = MagicMock()
_mock_app_module.Application = FakeMinimalApp
sys.modules['langbot.pkg.core.app'] = _mock_app_module
# Pre-register mock entities module - use proper Enum
class LifecycleControlScope(enum.Enum):
APPLICATION = 'application'
PLATFORM = 'platform'
PLUGIN = 'plugin'
PROVIDER = 'provider'
_mock_entities_module = MagicMock()
_mock_entities_module.LifecycleControlScope = LifecycleControlScope
sys.modules['langbot.pkg.core.entities'] = _mock_entities_module
def __repr__(self):
return f"LifecycleControlScope.{self.value.upper()}"
def get_taskmgr():
"""Import taskmgr after pre-mocking."""
return import_module('langbot.pkg.core.taskmgr')
class MockLifecycleControlScope:
"""Mock enum for LifecycleControlScope."""
APPLICATION = MockLifecycleControlScopeEnum('application')
PLATFORM = MockLifecycleControlScopeEnum('platform')
PIPELINE = MockLifecycleControlScopeEnum('pipeline')
PLUGIN = MockLifecycleControlScopeEnum('plugin')
def get_entities():
"""Get pre-registered mock entities module."""
return sys.modules['langbot.pkg.core.entities']
@contextmanager
def isolated_taskmgr_import() -> Generator[None, None, None]:
"""Context manager to isolate circular imports for taskmgr testing."""
# Mock modules that cause circular imports
mock_entities = MagicMock()
mock_entities.LifecycleControlScope = MockLifecycleControlScope
mock_app = MagicMock()
mock_importutil = MagicMock()
mock_importutil.import_modules_in_pkg = lambda pkg: None
mock_importutil.import_modules_in_pkgs = lambda pkgs: None
mock_http_controller = MagicMock()
mock_rag_mgr = MagicMock()
mocks = {
'langbot.pkg.core.entities': mock_entities,
'langbot.pkg.core.app': mock_app,
'langbot.pkg.api.http.controller.main': mock_http_controller,
'langbot.pkg.rag.knowledge.kbmgr': mock_rag_mgr,
'langbot.pkg.utils.importutil': mock_importutil,
}
# Save original state
saved = {}
for name in mocks:
if name in sys.modules:
saved[name] = sys.modules[name]
# Clear taskmgr to force re-import
taskmgr_name = 'langbot.pkg.core.taskmgr'
if taskmgr_name in sys.modules:
saved[taskmgr_name] = sys.modules[taskmgr_name]
try:
# Apply mocks
for name, module in mocks.items():
sys.modules[name] = module
# Clear taskmgr
sys.modules.pop(taskmgr_name, None)
yield
finally:
# Restore
for name in mocks:
if name in saved:
sys.modules[name] = saved[name]
else:
sys.modules.pop(name, None)
if taskmgr_name in saved:
sys.modules[taskmgr_name] = saved[taskmgr_name]
else:
sys.modules.pop(taskmgr_name, None)
class TestTaskContextReal:
"""Tests for real TaskContext class (no circular import)."""
def get_taskmgr_classes():
"""Get TaskContext, TaskWrapper, AsyncTaskManager classes."""
with isolated_taskmgr_import():
from langbot.pkg.core.taskmgr import TaskContext, TaskWrapper, AsyncTaskManager
return TaskContext, TaskWrapper, AsyncTaskManager
@pytest.mark.asyncio
async def test_task_context_new(self):
"""TaskContext.new() creates instance."""
taskmgr = get_taskmgr()
ctx = taskmgr.TaskContext.new()
def create_mock_app():
"""Create a mock Application for testing."""
mock_app = Mock()
mock_app.event_loop = asyncio.get_running_loop()
mock_app.instance_config = Mock()
mock_app.instance_config.data = {
'system': {
'task_retention': {
'completed_limit': 200,
}
}
}
return mock_app
class TestTaskContext:
"""Tests for TaskContext class."""
def test_init_default_values(self):
"""Test that TaskContext initializes with default values."""
TaskContext, _, _ = get_taskmgr_classes()
ctx = TaskContext()
assert ctx.current_action == 'default'
assert ctx.log == ''
assert ctx.metadata == {}
@pytest.mark.asyncio
async def test_task_context_trace(self):
"""TaskContext.trace adds formatted log."""
taskmgr = get_taskmgr()
def test_set_current_action(self):
"""Test setting current action."""
TaskContext, _, _ = get_taskmgr_classes()
ctx = TaskContext()
ctx = taskmgr.TaskContext.new()
ctx.trace('test message', action='test_action')
ctx.set_current_action('installing_plugin')
assert ctx.current_action == 'installing_plugin'
assert ctx.current_action == 'test_action'
assert 'test message' in ctx.log
assert 'test_action' in ctx.log
# Contains timestamp format
assert '|' in ctx.log
def test_trace_without_action(self):
"""Test trace method without action override."""
TaskContext, _, _ = get_taskmgr_classes()
ctx = TaskContext()
@pytest.mark.asyncio
async def test_task_context_multiple_traces(self):
"""TaskContext accumulates multiple traces."""
taskmgr = get_taskmgr()
ctx.trace('Starting process')
assert 'Starting process' in ctx.log
assert ctx.current_action == 'default'
ctx = taskmgr.TaskContext.new()
ctx.trace('first')
ctx.trace('second')
def test_trace_with_action_override(self):
"""Test trace method with action override."""
TaskContext, _, _ = get_taskmgr_classes()
ctx = TaskContext()
assert 'first' in ctx.log
assert 'second' in ctx.log
ctx.trace('Downloading', action='download')
assert 'Downloading' in ctx.log
assert ctx.current_action == 'download'
@pytest.mark.asyncio
async def test_task_context_to_dict(self):
"""TaskContext.to_dict returns all fields."""
taskmgr = get_taskmgr()
def test_trace_accumulates_logs(self):
"""Test that trace accumulates log entries."""
TaskContext, _, _ = get_taskmgr_classes()
ctx = TaskContext()
ctx = taskmgr.TaskContext.new()
ctx.trace('log entry')
ctx.trace('Step 1')
ctx.trace('Step 2')
ctx.trace('Step 3')
assert 'Step 1' in ctx.log
assert 'Step 2' in ctx.log
assert 'Step 3' in ctx.log
# Each trace adds a newline
assert ctx.log.count('\n') == 3
def test_to_dict_serialization(self):
"""Test to_dict serialization."""
TaskContext, _, _ = get_taskmgr_classes()
ctx = TaskContext()
ctx.set_current_action('test_action')
ctx.trace('Test message')
ctx.metadata['key'] = 'value'
result = ctx.to_dict()
assert 'current_action' in result
assert 'log' in result
assert 'metadata' in result
assert result['log'] == ctx.log
assert result['current_action'] == 'test_action'
assert 'Test message' in result['log']
assert result['metadata'] == {'key': 'value'}
def test_static_new_factory(self):
"""Test TaskContext.new() factory method."""
TaskContext, _, _ = get_taskmgr_classes()
ctx = TaskContext.new()
assert isinstance(ctx, TaskContext)
assert ctx.current_action == 'default'
def test_static_placeholder_singleton(self):
"""Test TaskContext.placeholder() returns singleton."""
with isolated_taskmgr_import():
from langbot.pkg.core.taskmgr import TaskContext
# Reset global placeholder
import langbot.pkg.core.taskmgr as taskmgr_module
taskmgr_module.placeholder_context = None
ctx1 = TaskContext.placeholder()
ctx2 = TaskContext.placeholder()
assert ctx1 is ctx2
def test_metadata_is_mutable_dict(self):
"""Test that metadata is a mutable dict."""
TaskContext, _, _ = get_taskmgr_classes()
ctx = TaskContext()
ctx.metadata['count'] = 5
ctx.metadata['items'] = ['a', 'b', 'c']
assert ctx.metadata['count'] == 5
assert len(ctx.metadata['items']) == 3
class TestTaskWrapper:
"""Tests for TaskWrapper class."""
@pytest.mark.asyncio
async def test_task_context_set_current_action(self):
"""set_current_action updates action."""
taskmgr = get_taskmgr()
async def test_id_auto_increment(self):
"""Test that task IDs auto-increment."""
TaskContext, TaskWrapper, _ = get_taskmgr_classes()
ctx = taskmgr.TaskContext.new()
ctx.set_current_action('new_action')
# Reset ID index
TaskWrapper._id_index = 0
assert ctx.current_action == 'new_action'
mock_app = create_mock_app()
@pytest.mark.asyncio
async def test_task_context_metadata(self):
"""TaskContext metadata can be set."""
taskmgr = get_taskmgr()
ctx = taskmgr.TaskContext.new()
ctx.metadata['key'] = 'value'
assert ctx.metadata['key'] == 'value'
assert ctx.to_dict()['metadata']['key'] == 'value'
def test_task_context_placeholder_singleton(self):
"""placeholder returns same instance."""
taskmgr = get_taskmgr()
ctx1 = taskmgr.TaskContext.placeholder()
ctx2 = taskmgr.TaskContext.placeholder()
assert ctx1 is ctx2
class TestTaskWrapperReal:
"""Tests for real TaskWrapper class."""
@pytest.mark.asyncio
async def test_task_wrapper_creates_task(self):
"""TaskWrapper creates and wraps asyncio.Task."""
taskmgr = get_taskmgr()
loop = asyncio.get_running_loop()
app = FakeMinimalApp(loop)
async def simple_coro():
return 42
wrapper = taskmgr.TaskWrapper(app, simple_coro(), name='test')
assert wrapper.name == 'test'
assert wrapper.task is not None
assert isinstance(wrapper.task, asyncio.Task)
result = await wrapper.task
assert result == 42
@pytest.mark.asyncio
async def test_task_wrapper_with_custom_context(self):
"""TaskWrapper uses provided TaskContext."""
taskmgr = get_taskmgr()
loop = asyncio.get_running_loop()
app = FakeMinimalApp(loop)
ctx = taskmgr.TaskContext.new()
ctx.set_current_action('custom')
async def coro():
async def dummy_coro():
await asyncio.sleep(0.01)
return 'done'
wrapper = taskmgr.TaskWrapper(app, coro(), context=ctx)
wrapper1 = TaskWrapper(mock_app, dummy_coro())
wrapper2 = TaskWrapper(mock_app, dummy_coro())
assert wrapper.task_context.current_action == 'custom'
assert wrapper1.id == 0
assert wrapper2.id == 1
await wrapper.task
# Clean up
wrapper1.cancel()
wrapper2.cancel()
@pytest.mark.asyncio
async def test_task_wrapper_exception_capture(self):
"""TaskWrapper captures exception from failed task."""
taskmgr = get_taskmgr()
async def test_default_task_type_and_kind(self):
"""Test default task_type and kind values."""
_, TaskWrapper, _ = get_taskmgr_classes()
mock_app = create_mock_app()
loop = asyncio.get_running_loop()
app = FakeMinimalApp(loop)
async def dummy_coro():
return 'done'
wrapper = TaskWrapper(mock_app, dummy_coro())
assert wrapper.task_type == 'system'
assert wrapper.kind == 'system_task'
wrapper.cancel()
@pytest.mark.asyncio
async def test_to_dict_serialization(self):
"""Test TaskWrapper.to_dict serialization."""
_, TaskWrapper, _ = get_taskmgr_classes()
mock_app = create_mock_app()
async def immediate_coro():
return 'result'
wrapper = TaskWrapper(
mock_app, immediate_coro(),
name='test_task',
label='Test Task',
)
# Wait for task to complete
await wrapper.task
result = wrapper.to_dict()
assert result['name'] == 'test_task'
assert result['label'] == 'Test Task'
assert result['task_type'] == 'system'
assert result['runtime']['done'] == True
assert result['runtime']['result'] == 'result'
@pytest.mark.asyncio
async def test_to_dict_with_exception(self):
"""Test TaskWrapper.to_dict when task has exception."""
_, TaskWrapper, _ = get_taskmgr_classes()
mock_app = create_mock_app()
async def failing_coro():
raise ValueError('test error')
raise ValueError('Test error')
wrapper = taskmgr.TaskWrapper(app, failing_coro())
wrapper = TaskWrapper(mock_app, failing_coro())
# Let task complete with exception
await asyncio.sleep(0.01)
# Wait for task to complete
try:
await wrapper.task
except ValueError:
pass
exception = wrapper.assume_exception()
assert exception is not None
assert isinstance(exception, ValueError)
assert 'test error' in str(exception)
result = wrapper.to_dict()
assert result['runtime']['done'] == True
assert result['runtime']['exception'] == 'Test error'
assert 'exception_traceback' in result['runtime']
@pytest.mark.asyncio
async def test_task_wrapper_result_capture(self):
"""TaskWrapper captures result from completed task."""
taskmgr = get_taskmgr()
loop = asyncio.get_running_loop()
app = FakeMinimalApp(loop)
async def coro():
return 'result_value'
wrapper = taskmgr.TaskWrapper(app, coro())
await wrapper.task
result = wrapper.assume_result()
assert result == 'result_value'
@pytest.mark.asyncio
async def test_task_wrapper_cancel(self):
"""TaskWrapper.cancel cancels the task."""
taskmgr = get_taskmgr()
loop = asyncio.get_running_loop()
app = FakeMinimalApp(loop)
async def test_cancel_task(self):
"""Test cancel method cancels the asyncio task."""
_, TaskWrapper, _ = get_taskmgr_classes()
mock_app = create_mock_app()
async def long_coro():
await asyncio.sleep(10)
return 'done'
wrapper = taskmgr.TaskWrapper(app, long_coro())
wrapper = TaskWrapper(mock_app, long_coro())
# Task should be running
assert not wrapper.task.done()
wrapper.cancel()
# Give it a moment to be cancelled
await asyncio.sleep(0.01)
assert wrapper.task.cancelled() or wrapper.task.done()
assert wrapper.task.done()
assert wrapper.task.cancelled()
class TestAsyncTaskManager:
"""Tests for AsyncTaskManager class."""
@pytest.mark.asyncio
async def test_task_wrapper_to_dict(self):
"""TaskWrapper.to_dict serializes task info."""
taskmgr = get_taskmgr()
async def test_create_task_adds_to_list(self):
"""Test that create_task adds task to tasks list."""
_, _, AsyncTaskManager = get_taskmgr_classes()
mock_app = create_mock_app()
loop = asyncio.get_running_loop()
app = FakeMinimalApp(loop)
manager = AsyncTaskManager(mock_app)
async def coro():
return 42
async def dummy_coro():
await asyncio.sleep(0.01)
return 'done'
wrapper = taskmgr.TaskWrapper(app, coro(), name='dict_test', label='Test')
await wrapper.task
result = wrapper.to_dict()
assert result['name'] == 'dict_test'
assert result['label'] == 'Test'
assert 'runtime' in result
assert result['runtime']['done'] is True
@pytest.mark.asyncio
async def test_task_wrapper_id_increment(self):
"""TaskWrapper IDs increment."""
taskmgr = get_taskmgr()
loop = asyncio.get_running_loop()
app = FakeMinimalApp(loop)
async def coro():
return 1
wrapper1 = taskmgr.TaskWrapper(app, coro())
wrapper2 = taskmgr.TaskWrapper(app, coro())
assert wrapper2.id > wrapper1.id
class TestAsyncTaskManagerReal:
"""Tests for real AsyncTaskManager class."""
@pytest.mark.asyncio
async def test_manager_create_task(self):
"""AsyncTaskManager creates and tracks tasks."""
taskmgr = get_taskmgr()
loop = asyncio.get_running_loop()
app = FakeMinimalApp(loop)
manager = taskmgr.AsyncTaskManager(app)
async def coro():
return 'result'
wrapper = manager.create_task(coro(), name='test')
wrapper = manager.create_task(dummy_coro())
assert wrapper in manager.tasks
assert wrapper.name == 'test'
assert len(manager.tasks) == 1
await wrapper.task
wrapper.cancel()
@pytest.mark.asyncio
async def test_manager_create_user_task(self):
"""create_user_task creates user-type task."""
taskmgr = get_taskmgr()
async def test_get_stats_counts_correctly(self):
"""Test get_stats returns correct counts."""
_, _, AsyncTaskManager = get_taskmgr_classes()
mock_app = create_mock_app()
loop = asyncio.get_running_loop()
app = FakeMinimalApp(loop)
manager = AsyncTaskManager(mock_app)
manager = taskmgr.AsyncTaskManager(app)
async def immediate_coro():
return 'done'
async def coro():
return 'user_result'
async def delayed_coro():
await asyncio.sleep(0.1)
return 'done'
wrapper = manager.create_user_task(coro())
# Create tasks
w1 = manager.create_task(immediate_coro())
w2 = manager.create_task(delayed_coro())
# Wait for first to complete
await w1.task
stats = manager.get_stats()
assert stats['total'] == 2
assert stats['completed'] == 1
assert stats['running'] == 1
w2.cancel()
@pytest.mark.asyncio
async def test_get_tasks_dict_filters_by_type(self):
"""Test get_tasks_dict filters by type."""
_, _, AsyncTaskManager = get_taskmgr_classes()
mock_app = create_mock_app()
manager = AsyncTaskManager(mock_app)
async def dummy_coro():
await asyncio.sleep(0.01)
# Create system and user tasks
w1 = manager.create_task(dummy_coro(), task_type='system')
w2 = manager.create_task(dummy_coro(), task_type='user')
w3 = manager.create_task(dummy_coro(), task_type='user')
result = manager.get_tasks_dict(type='user')
assert len(result['tasks']) == 2
for t in result['tasks']:
assert t['task_type'] == 'user'
w1.cancel()
w2.cancel()
w3.cancel()
@pytest.mark.asyncio
async def test_cancel_by_scope(self):
"""Test cancel_by_scope cancels matching tasks."""
_, _, AsyncTaskManager = get_taskmgr_classes()
mock_app = create_mock_app()
manager = AsyncTaskManager(mock_app)
async def long_coro():
await asyncio.sleep(10)
# Create task with APPLICATION scope
w1 = manager.create_task(
long_coro(),
scopes=[MockLifecycleControlScope.APPLICATION]
)
# Create task with different scope
w2 = manager.create_task(
long_coro(),
scopes=[MockLifecycleControlScope.PIPELINE]
)
manager.cancel_by_scope(MockLifecycleControlScope.APPLICATION)
await asyncio.sleep(0.01)
assert w1.task.cancelled() or w1.task.done()
assert not w2.task.done()
w2.cancel()
@pytest.mark.asyncio
async def test_cancel_task_by_id(self):
"""Test cancel_task cancels specific task by ID."""
_, _, AsyncTaskManager = get_taskmgr_classes()
mock_app = create_mock_app()
manager = AsyncTaskManager(mock_app)
async def long_coro():
await asyncio.sleep(10)
w1 = manager.create_task(long_coro())
w2 = manager.create_task(long_coro())
manager.cancel_task(w1.id)
await asyncio.sleep(0.01)
assert w1.task.done()
assert not w2.task.done()
w2.cancel()
@pytest.mark.asyncio
async def test_create_user_task_sets_user_type(self):
"""Test create_user_task sets task_type to 'user'."""
_, _, AsyncTaskManager = get_taskmgr_classes()
mock_app = create_mock_app()
manager = AsyncTaskManager(mock_app)
async def dummy_coro():
await asyncio.sleep(0.01)
wrapper = manager.create_user_task(dummy_coro())
assert wrapper.task_type == 'user'
await wrapper.task
wrapper.cancel()
@pytest.mark.asyncio
async def test_manager_multiple_tasks_isolated(self):
"""Multiple tasks run independently."""
taskmgr = get_taskmgr()
async def test_get_task_by_id(self):
"""Test get_task_by_id returns correct task."""
_, _, AsyncTaskManager = get_taskmgr_classes()
mock_app = create_mock_app()
loop = asyncio.get_running_loop()
app = FakeMinimalApp(loop)
manager = AsyncTaskManager(mock_app)
manager = taskmgr.AsyncTaskManager(app)
results = []
async def task_a():
results.append('a')
async def task_b():
results.append('b')
w1 = manager.create_task(task_a(), name='a')
w2 = manager.create_task(task_b(), name='b')
await asyncio.gather(w1.task, w2.task)
assert 'a' in results
assert 'b' in results
@pytest.mark.asyncio
async def test_manager_get_task_by_id(self):
"""get_task_by_id finds task."""
taskmgr = get_taskmgr()
loop = asyncio.get_running_loop()
app = FakeMinimalApp(loop)
manager = taskmgr.AsyncTaskManager(app)
async def coro():
return 1
wrapper = manager.create_task(coro())
found = manager.get_task_by_id(wrapper.id)
assert found is wrapper
not_found = manager.get_task_by_id(99999)
assert not_found is None
@pytest.mark.asyncio
async def test_manager_cancel_task(self):
"""cancel_task cancels specific task."""
taskmgr = get_taskmgr()
loop = asyncio.get_running_loop()
app = FakeMinimalApp(loop)
manager = taskmgr.AsyncTaskManager(app)
async def long():
await asyncio.sleep(10)
wrapper = manager.create_task(long())
manager.cancel_task(wrapper.id)
await asyncio.sleep(0.01)
assert wrapper.task.cancelled() or wrapper.task.done()
@pytest.mark.asyncio
async def test_manager_cancel_by_scope(self):
"""cancel_by_scope cancels matching scope tasks."""
taskmgr = get_taskmgr()
entities = get_entities()
loop = asyncio.get_running_loop()
app = FakeMinimalApp(loop)
manager = taskmgr.AsyncTaskManager(app)
async def long():
await asyncio.sleep(10)
async def app_long():
await asyncio.sleep(10)
# Create task with PLATFORM scope
platform_wrapper = manager.create_task(
long(),
scopes=[entities.LifecycleControlScope.PLATFORM],
)
# Create task with APPLICATION scope
manager.create_task(
app_long(),
scopes=[entities.LifecycleControlScope.APPLICATION],
)
manager.cancel_by_scope(entities.LifecycleControlScope.PLATFORM)
await asyncio.sleep(0.01)
# Platform task cancelled
assert platform_wrapper.task.cancelled() or platform_wrapper.task.done()
@pytest.mark.asyncio
async def test_manager_get_stats(self):
"""get_stats returns task counts."""
taskmgr = get_taskmgr()
loop = asyncio.get_running_loop()
app = FakeMinimalApp(loop)
manager = taskmgr.AsyncTaskManager(app)
async def quick():
return 1
for _ in range(3):
w = manager.create_task(quick())
await w.task
stats = manager.get_stats()
assert stats['total'] >= 3
assert stats['completed'] >= 3
@pytest.mark.asyncio
async def test_manager_get_tasks_dict(self):
"""get_tasks_dict filters by type."""
taskmgr = get_taskmgr()
loop = asyncio.get_running_loop()
app = FakeMinimalApp(loop)
manager = taskmgr.AsyncTaskManager(app)
async def coro():
return 1
system_w = manager.create_task(coro(), task_type='system')
user_w = manager.create_user_task(coro())
await asyncio.gather(system_w.task, user_w.task)
system_tasks = manager.get_tasks_dict(type='system')
assert all(t['task_type'] == 'system' for t in system_tasks['tasks'])
@pytest.mark.asyncio
async def test_manager_wait_all(self):
"""wait_all waits for all tasks."""
taskmgr = get_taskmgr()
loop = asyncio.get_running_loop()
app = FakeMinimalApp(loop)
manager = taskmgr.AsyncTaskManager(app)
async def delayed():
await asyncio.sleep(0.05)
for _ in range(3):
manager.create_task(delayed())
await manager.wait_all()
stats = manager.get_stats()
assert stats['running'] == 0
class TestTaskPruningReal:
"""Tests for real task pruning behavior."""
@pytest.mark.asyncio
async def test_prune_completed_tasks(self):
"""Completed tasks are pruned when exceeding limit."""
taskmgr = get_taskmgr()
loop = asyncio.get_running_loop()
app = FakeMinimalApp(loop)
app.instance_config.data = {'system': {'task_retention': {'completed_limit': 3}}}
manager = taskmgr.AsyncTaskManager(app)
async def quick():
return 1
# Create more than limit
for _ in range(5):
w = manager.create_task(quick())
await w.task
async def dummy_coro():
await asyncio.sleep(0.01)
# Completed count should be <= limit
completed = sum(1 for w in manager.tasks if w.task.done())
assert completed <= 3
w1 = manager.create_task(dummy_coro())
w2 = manager.create_task(dummy_coro())
found = manager.get_task_by_id(w1.id)
assert found is w1
not_found = manager.get_task_by_id(9999)
assert not_found is None
w1.cancel()
w2.cancel()