feat(consolidation): debounced gate for task body consolidation
New services/consolidation.py module with maybe_consolidate() — the debounced trigger gate. Two reasons: - log_added: gated by DEFAULT_LOG_THRESHOLD (3) counted since the task's consolidated_at timestamp. - task_closed: bypasses the count gate; fires whenever status flips to done/cancelled. Both reasons gated by the auto_consolidate_tasks user setting (default on). Per-task asyncio.Lock prevents two simultaneous passes for the same task. consolidate_task is a stub here — full implementation in the next commit.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
"""Tests for the task-body consolidation pipeline (gate + full pass).
|
||||
|
||||
Design: docs/superpowers/specs/2026-05-13-task-as-durable-record-design.md
|
||||
"""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
|
||||
# ── Gate logic ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_maybe_consolidate_below_threshold_does_not_fire():
|
||||
"""log_added with only 2 logs since last pass → no consolidation."""
|
||||
from fabledassistant.services import consolidation
|
||||
|
||||
with patch.object(
|
||||
consolidation, "_logs_since_last_consolidation",
|
||||
new=AsyncMock(return_value=2),
|
||||
), patch.object(
|
||||
consolidation, "consolidate_task", new=AsyncMock(),
|
||||
) as mock_consolidate, patch.object(
|
||||
consolidation, "_auto_consolidate_enabled", new=AsyncMock(return_value=True),
|
||||
):
|
||||
await consolidation.maybe_consolidate(
|
||||
user_id=1, task_id=42, reason="log_added",
|
||||
)
|
||||
|
||||
mock_consolidate.assert_not_awaited()
|
||||
|
||||
|
||||
async def test_maybe_consolidate_at_threshold_fires():
|
||||
"""log_added with N logs since last pass → consolidation scheduled."""
|
||||
from fabledassistant.services import consolidation
|
||||
|
||||
# asyncio.create_task is the actual scheduler; replace it so the test
|
||||
# doesn't need an event-loop background task to settle.
|
||||
with patch.object(
|
||||
consolidation, "_logs_since_last_consolidation",
|
||||
new=AsyncMock(return_value=3),
|
||||
), patch.object(
|
||||
consolidation, "_auto_consolidate_enabled", new=AsyncMock(return_value=True),
|
||||
), patch(
|
||||
"fabledassistant.services.consolidation.asyncio.create_task",
|
||||
) as mock_create_task:
|
||||
await consolidation.maybe_consolidate(
|
||||
user_id=1, task_id=42, reason="log_added",
|
||||
)
|
||||
|
||||
mock_create_task.assert_called_once()
|
||||
|
||||
|
||||
async def test_maybe_consolidate_task_closed_always_fires():
|
||||
"""task_closed bypasses the log-count gate."""
|
||||
from fabledassistant.services import consolidation
|
||||
|
||||
with patch.object(
|
||||
consolidation, "_logs_since_last_consolidation",
|
||||
new=AsyncMock(return_value=0),
|
||||
), patch.object(
|
||||
consolidation, "_auto_consolidate_enabled", new=AsyncMock(return_value=True),
|
||||
), patch(
|
||||
"fabledassistant.services.consolidation.asyncio.create_task",
|
||||
) as mock_create_task:
|
||||
await consolidation.maybe_consolidate(
|
||||
user_id=1, task_id=42, reason="task_closed",
|
||||
)
|
||||
|
||||
mock_create_task.assert_called_once()
|
||||
|
||||
|
||||
async def test_maybe_consolidate_setting_off_blocks_both_reasons():
|
||||
"""auto_consolidate_tasks=false → neither trigger fires."""
|
||||
from fabledassistant.services import consolidation
|
||||
|
||||
with patch.object(
|
||||
consolidation, "_logs_since_last_consolidation",
|
||||
new=AsyncMock(return_value=5),
|
||||
), patch.object(
|
||||
consolidation, "_auto_consolidate_enabled", new=AsyncMock(return_value=False),
|
||||
), patch(
|
||||
"fabledassistant.services.consolidation.asyncio.create_task",
|
||||
) as mock_create_task:
|
||||
await consolidation.maybe_consolidate(
|
||||
user_id=1, task_id=42, reason="log_added",
|
||||
)
|
||||
await consolidation.maybe_consolidate(
|
||||
user_id=1, task_id=42, reason="task_closed",
|
||||
)
|
||||
|
||||
mock_create_task.assert_not_called()
|
||||
|
||||
|
||||
async def test_maybe_consolidate_unknown_reason_is_noop():
|
||||
"""Unknown reasons get logged and skipped — not raised."""
|
||||
from fabledassistant.services import consolidation
|
||||
|
||||
with patch.object(
|
||||
consolidation, "_logs_since_last_consolidation",
|
||||
new=AsyncMock(return_value=99),
|
||||
), patch.object(
|
||||
consolidation, "_auto_consolidate_enabled", new=AsyncMock(return_value=True),
|
||||
), patch(
|
||||
"fabledassistant.services.consolidation.asyncio.create_task",
|
||||
) as mock_create_task:
|
||||
await consolidation.maybe_consolidate(
|
||||
user_id=1, task_id=42, reason="some_other_reason",
|
||||
)
|
||||
|
||||
mock_create_task.assert_not_called()
|
||||
Reference in New Issue
Block a user