fix(dreamer): explicit-only count on manual schedule_dream route

The third caller of enqueue_dream — POST /workspaces/{id}/schedule_dream —
was passing an all-levels document count as the baseline, breaking symmetry
with check_and_schedule_dream and execute_dream after Loop 2's filter fixes.
Filter the manual route's count to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
lilyplasticlabs 2026-04-17 19:13:43 -04:00
parent 0a7bde7341
commit d76627a8a4
2 changed files with 99 additions and 1 deletions

View File

@ -224,11 +224,13 @@ async def schedule_dream(
observed = request.observed if request.observed is not None else request.observer observed = request.observed if request.observed is not None else request.observer
dream_type = request.dream_type 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( count_stmt = select(func.count(models.Document.id)).where(
models.Document.workspace_name == workspace_id, models.Document.workspace_name == workspace_id,
models.Document.observer == observer, models.Document.observer == observer,
models.Document.observed == observed, models.Document.observed == observed,
models.Document.level == "explicit",
) )
document_count = int(await db.scalar(count_stmt) or 0) document_count = int(await db.scalar(count_stmt) or 0)

View File

@ -1,9 +1,12 @@
from typing import Any from typing import Any
from unittest.mock import AsyncMock, patch
import pytest import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from nanoid import generate as generate_nanoid from nanoid import generate as generate_nanoid
from sqlalchemy.ext.asyncio import AsyncSession
from src import models
from src.models import Peer, Workspace from src.models import Peer, Workspace
@ -569,3 +572,96 @@ def test_delete_workspace_after_session_deletion(client: TestClient):
# Now workspace deletion should succeed # Now workspace deletion should succeed
response = client.delete(f"/v3/workspaces/{workspace_name}") response = client.delete(f"/v3/workspaces/{workspace_name}")
assert response.status_code == 202 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."
)