diff --git a/src/routers/workspaces.py b/src/routers/workspaces.py index 90530e92..df3eecea 100644 --- a/src/routers/workspaces.py +++ b/src/routers/workspaces.py @@ -224,11 +224,13 @@ async def schedule_dream( observed = request.observed if request.observed is not None else request.observer dream_type = request.dream_type - # Count documents in the collection + # Explicit-only count, matching dream_scheduler.check_and_schedule_dream and + # execute_dream — document_count becomes last_dream_document_count (the baseline). count_stmt = select(func.count(models.Document.id)).where( models.Document.workspace_name == workspace_id, models.Document.observer == observer, models.Document.observed == observed, + models.Document.level == "explicit", ) document_count = int(await db.scalar(count_stmt) or 0) diff --git a/tests/routes/test_workspaces.py b/tests/routes/test_workspaces.py index 36bc8312..f1ce97d6 100644 --- a/tests/routes/test_workspaces.py +++ b/tests/routes/test_workspaces.py @@ -1,9 +1,12 @@ from typing import Any +from unittest.mock import AsyncMock, patch import pytest from fastapi.testclient import TestClient from nanoid import generate as generate_nanoid +from sqlalchemy.ext.asyncio import AsyncSession +from src import models from src.models import Peer, Workspace @@ -569,3 +572,96 @@ def test_delete_workspace_after_session_deletion(client: TestClient): # Now workspace deletion should succeed response = client.delete(f"/v3/workspaces/{workspace_name}") assert response.status_code == 202 + + +@pytest.mark.asyncio +async def test_schedule_dream_passes_explicit_only_document_count( + client: TestClient, + db_session: AsyncSession, + sample_data: tuple[Workspace, Peer], +): + """POST /schedule_dream must count only explicit-level docs. + + Regression test for the third caller of `enqueue_dream`. The manual + schedule_dream endpoint computes `document_count` and passes it to + `enqueue_dream`, where it becomes `last_dream_document_count` — the + baseline that `check_and_schedule_dream` and `execute_dream` subtract + against to compute the NEW-doc delta. + + Both of those callers filter to `level == "explicit"` (see Loop 2's + fixes). If this route doesn't match, the next auto-scheduled dream + sees a negative/suppressed delta because the baseline was inflated by + deductive/inductive docs. + + Seeds 5 explicit + 10 deductive (total 15) and asserts the count + passed to enqueue_dream is 5, not 15. + """ + workspace, peer = sample_data + + # Seed collection + mixed-level documents. Must commit so the + # endpoint's DB session sees them (separate tracked_db path under + # some configs; safe regardless). + collection = models.Collection( + observer=peer.name, + observed=peer.name, + workspace_name=workspace.name, + internal_metadata={}, + ) + db_session.add(collection) + await db_session.flush() + + for _ in range(5): + db_session.add( + models.Document( + content="explicit doc", + level="explicit", + workspace_name=workspace.name, + observer=peer.name, + observed=peer.name, + ) + ) + for _ in range(10): + db_session.add( + models.Document( + content="deductive doc", + level="deductive", + workspace_name=workspace.name, + observer=peer.name, + observed=peer.name, + ) + ) + await db_session.commit() + + # Capture kwargs passed to enqueue_dream (patched at the router's + # import site — the route does `from src.deriver.enqueue import + # enqueue_dream` at module load, so patching the symbol on the + # router module is what actually intercepts the call). + captured: dict[str, Any] = {} + + async def fake_enqueue_dream(*args: Any, **kwargs: Any) -> None: + captured["args"] = args + captured["kwargs"] = kwargs + + with ( + patch("src.routers.workspaces.settings.DREAM.ENABLED", True), + patch( + "src.routers.workspaces.enqueue_dream", + new=AsyncMock(side_effect=fake_enqueue_dream), + ), + ): + response = client.post( + f"/v3/workspaces/{workspace.name}/schedule_dream", + json={ + "observer": peer.name, + "observed": peer.name, + "dream_type": "omni", + }, + ) + + assert response.status_code == 204, response.text + assert "kwargs" in captured, "enqueue_dream was not called" + assert captured["kwargs"]["document_count"] == 5, ( + "schedule_dream must pass explicit-only count (5), " + f"got {captured['kwargs']['document_count']} — the unfiltered " + "count would be 15 and would inflate last_dream_document_count." + )