From 817123de958be5bc795ce038aa77f1e1972a1703 Mon Sep 17 00:00:00 2001 From: doria <93405247+dr-frmr@users.noreply.github.com> Date: Tue, 13 Jan 2026 12:33:59 -0500 Subject: [PATCH] fixes: update unified tests, bugs in dream scheduling (#322) * fix: swap to reasoning config in unified test files, handle new representation output format * fix: resolve bugs in dream scheduling -- message from a peer should cancel all dreams observing them, and document counts should be checked at moment of dream, not in enqueue. clean up and test --- src/config.py | 18 +- src/deriver/enqueue.py | 18 +- src/dreamer/__init__.py | 2 - src/dreamer/dream_scheduler.py | 129 ++--- tests/dreamer/__init__.py | 1 + tests/dreamer/test_dream_scheduler.py | 460 ++++++++++++++++++ .../test_cases/config_deriver_hierarchy.json | 27 +- .../config_message_positive_override.json | 8 +- .../test_cases/config_peercard_control.json | 2 +- .../config_summary_control_deriver_off.json | 4 +- .../test_cases/message_deriver_disabled.json | 19 +- ...servation_2peer_both_observe_me_false.json | 44 +- .../test_cases/observation_2peer_default.json | 34 +- ...r_observe_me_false_blocks_observation.json | 22 +- ...me_false_but_can_still_observe_others.json | 17 +- ...eer_unidirectional_alice_observes_bob.json | 17 +- ...eer_unidirectional_bob_observes_alice.json | 17 +- ...3peer_multiple_observers_one_observed.json | 46 +- ..._3peer_one_observer_multiple_observed.json | 34 +- ...servation_3peer_selective_observation.json | 17 +- .../observation_4peer_complex_matrix.json | 48 +- .../observation_asymmetric_visibility.json | 2 +- ...bservation_isolation_between_sessions.json | 2 +- .../test_cases/session_deriver_disabled.json | 15 +- .../workspace_deriver_disabled.json | 15 +- 25 files changed, 733 insertions(+), 285 deletions(-) create mode 100644 tests/dreamer/__init__.py create mode 100644 tests/dreamer/test_dream_scheduler.py diff --git a/src/config.py b/src/config.py index 91ff1636..bb0e0faf 100644 --- a/src/config.py +++ b/src/config.py @@ -492,9 +492,8 @@ class DreamSettings(BackupLLMSettingsMixin, HonchoSettings): MIN_HOURS_BETWEEN_DREAMS: Annotated[int, Field(default=8, gt=0, le=72)] = 8 ENABLED_TYPES: list[str] = ["omni"] - # LLM settings for dream processing - upgraded for extended reasoning PROVIDER: SupportedProviders = "anthropic" - MODEL: str = "claude-sonnet-4-20250514" # Upgraded from haiku for reasoning + MODEL: str = "claude-sonnet-4-20250514" MAX_OUTPUT_TOKENS: Annotated[int, Field(default=16_384, gt=0, le=64_000)] = 16_384 THINKING_BUDGET_TOKENS: Annotated[int, Field(default=8192, gt=0, le=32_000)] = 8192 @@ -506,17 +505,12 @@ class DreamSettings(BackupLLMSettingsMixin, HonchoSettings): 16_384 ) - # Observation limits for orchestrated dreaming prescan - # Higher = more context for reasoning but slower prescan - PRESCAN_OBSERVATIONS_PER_LEVEL: Annotated[ - int, Field(default=200, gt=0, le=1000) - ] = 200 + ## NOTE: specialist models use the same provider as the main model - # Specialist model settings (OpenRouter format: provider/model) - # DeductionSpecialist: handles logical inference + temporal reasoning - DEDUCTION_MODEL: str = "anthropic/claude-haiku-4.5" - # InductionSpecialist: identifies patterns across observations - INDUCTION_MODEL: str = "anthropic/claude-haiku-4.5" + # Deduction Specialist: handles logical inference + DEDUCTION_MODEL: str = "claude-haiku-4-5" + # Induction Specialist: identifies patterns across observations + INDUCTION_MODEL: str = "claude-haiku-4-5" # Surprisal-based sampling subsystem SURPRISAL: SurprisalSettings = Field(default_factory=SurprisalSettings) diff --git a/src/deriver/enqueue.py b/src/deriver/enqueue.py index e0beaa2c..b2476ea2 100644 --- a/src/deriver/enqueue.py +++ b/src/deriver/enqueue.py @@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from src import crud, models, schemas from src.config import settings from src.dependencies import tracked_db -from src.dreamer.dream_scheduler import get_affected_dream_keys, get_dream_scheduler +from src.dreamer.dream_scheduler import get_dream_scheduler from src.exceptions import ValidationException from src.models import QueueItem from src.schemas import MessageConfiguration, ResolvedConfiguration @@ -31,16 +31,20 @@ async def enqueue(payload: list[dict[str, Any]]) -> None: payload: List of message payload dictionaries """ - # Cancel any pending dreams for affected collections since user is active again + # Cancel any pending dreams for affected collections since user is active again. + # This cancels dreams for all collections where observed=peer_name, which covers + # both self-observation and peer-to-peer observation cases. dream_scheduler = get_dream_scheduler() if dream_scheduler and payload: cancelled_dreams: set[str] = set() for message in payload: - # Generate work unit keys for dreams that might be affected by this message - dream_keys: list[str] = get_affected_dream_keys(message) - for dream_key in dream_keys: - if await dream_scheduler.cancel_dream(dream_key): - cancelled_dreams.add(dream_key) + workspace_name = message.get("workspace_name") + peer_name = message.get("peer_name") + if workspace_name and peer_name: + cancelled = await dream_scheduler.cancel_dreams_for_observed( + workspace_name, peer_name + ) + cancelled_dreams.update(cancelled) if cancelled_dreams: logger.info( diff --git a/src/dreamer/__init__.py b/src/dreamer/__init__.py index bd9bfee7..bee16664 100644 --- a/src/dreamer/__init__.py +++ b/src/dreamer/__init__.py @@ -1,12 +1,10 @@ from .dream_scheduler import ( check_and_schedule_dream, - get_affected_dream_keys, get_dream_scheduler, ) from .dreamer import process_dream __all__ = [ - "get_affected_dream_keys", "get_dream_scheduler", "check_and_schedule_dream", "process_dream", diff --git a/src/dreamer/dream_scheduler.py b/src/dreamer/dream_scheduler.py index 39f7057b..1a339d21 100644 --- a/src/dreamer/dream_scheduler.py +++ b/src/dreamer/dream_scheduler.py @@ -2,7 +2,6 @@ import asyncio import contextlib from datetime import datetime, timezone from logging import getLogger -from typing import Any import sentry_sdk from sqlalchemy import func, select @@ -31,39 +30,6 @@ def get_dream_scheduler() -> "DreamScheduler | None": return _dream_scheduler -def get_affected_dream_keys(message: dict[str, Any]) -> list[str]: - """ - Get all work unit keys for dreams that might be affected by this message. - - Args: - message: The message payload - - Returns: - List of work unit keys that should have their dreams cancelled - """ - workspace_name = message.get("workspace_name") - peer_name = message.get("peer_name") - - if not workspace_name or not peer_name: - return [] - - # Generate dream work unit keys for each enabled dream type - dream_keys: list[str] = [] - for dream_type in settings.DREAM.ENABLED_TYPES: - dream_key = construct_work_unit_key( - workspace_name, - { - "task_type": "dream", - "observer": peer_name, - "observed": peer_name, - "dream_type": dream_type, - }, - ) - dream_keys.append(dream_key) - - return dream_keys - - class DreamScheduler: _instance: "DreamScheduler | None" = None _initialized: bool = False @@ -89,7 +55,6 @@ class DreamScheduler: self, work_unit_key: str, workspace_name: str, - document_count: int, delay_minutes: int, dream_type: DreamType, *, @@ -107,7 +72,6 @@ class DreamScheduler: self._delayed_dream( work_unit_key, workspace_name, - document_count, delay_minutes, dream_type, observer=observer, @@ -128,11 +92,42 @@ class DreamScheduler: return True return False + async def cancel_dreams_for_observed( + self, workspace_name: str, observed: str + ) -> set[str]: + """ + Cancel all pending dreams where the observed peer matches. + + This handles both self-observation (observer=observed) and peer-to-peer + observation (observer!=observed) dreams. + + Args: + workspace_name: The workspace to match + observed: The observed peer name to match + + Returns: + Set of work_unit_keys that were cancelled + """ + cancelled: set[str] = set() + + # Collect keys to cancel (can't modify dict while iterating) + keys_to_cancel: list[str] = [] + for work_unit_key in self.pending_dreams: + parsed = parse_work_unit_key(work_unit_key) + if parsed.workspace_name == workspace_name and parsed.observed == observed: + keys_to_cancel.append(work_unit_key) + + # Cancel each matching dream + for key in keys_to_cancel: + if await self.cancel_dream(key): + cancelled.add(key) + + return cancelled + async def _delayed_dream( self, work_unit_key: str, workspace_name: str, - document_count: int, delay_minutes: int, dream_type: DreamType, *, @@ -142,22 +137,13 @@ class DreamScheduler: try: await asyncio.sleep(delay_minutes * 60) - # Check if collection is still inactive before executing dream - if await self._should_execute_dream( - workspace_name, observer=observer, observed=observed - ): - await self.execute_dream( - workspace_name, - document_count, - dream_type, - observer=observer, - observed=observed, - ) - logger.info("Executed dream for %s", work_unit_key) - else: - logger.info( - "Skipping dream for %s - collection is active", work_unit_key - ) + await self.execute_dream( + workspace_name, + dream_type, + observer=observer, + observed=observed, + ) + logger.info("Executed dream for %s", work_unit_key) except asyncio.CancelledError: logger.debug("Dream task cancelled for %s", work_unit_key) @@ -166,33 +152,9 @@ class DreamScheduler: if settings.SENTRY.ENABLED: sentry_sdk.capture_exception(e) - async def _should_execute_dream( - self, workspace_name: str, *, observer: str, observed: str - ) -> bool: - """Check if the collection is inactive and should be dreamed upon.""" - async with tracked_db("dream_activity_check") as db: - # Check for active queue sessions related to this collection - query = select(models.ActiveQueueSession) - result = await db.execute(query) - active_sessions = result.scalars().all() - - # Look for any active work units that match this collection - for active_session in active_sessions: - parsed_key = parse_work_unit_key(active_session.work_unit_key) - if ( - parsed_key.workspace_name == workspace_name - and parsed_key.observer == observer - and parsed_key.observed == observed - ): - logger.debug("Collection is active, skipping dream") - return False - - return True - async def execute_dream( self, workspace_name: str, - document_count: int, dream_type: DreamType, *, observer: str, @@ -204,7 +166,7 @@ class DreamScheduler: from src.deriver.enqueue import enqueue_dream from src.utils.config_helpers import get_configuration - # Find the most recent session for this observer/observed pair + # Find the most recent session and get current document count async with tracked_db("dream_session_lookup") as db: stmt = ( select(models.Document.session_name) @@ -224,6 +186,14 @@ class DreamScheduler: ) return + # Get current document count at execution time (not stale from scheduling) + count_stmt = select(func.count(models.Document.id)).where( + models.Document.workspace_name == workspace_name, + models.Document.observer == observer, + models.Document.observed == observed, + ) + current_document_count = int(await db.scalar(count_stmt) or 0) + session = await crud.get_session( db, workspace_name=workspace_name, session_name=session_name ) @@ -242,7 +212,7 @@ class DreamScheduler: observer=observer, observed=observed, dream_type=dream_type, - document_count=document_count, + document_count=current_document_count, session_name=session_name, ) @@ -346,7 +316,6 @@ async def check_and_schedule_dream( await dream_scheduler.schedule_dream( dream_work_unit_key, collection.workspace_name, - current_document_count, settings.DREAM.IDLE_TIMEOUT_MINUTES, dream_type=DreamType(dream_type), observer=collection.observer, diff --git a/tests/dreamer/__init__.py b/tests/dreamer/__init__.py new file mode 100644 index 00000000..6c63566d --- /dev/null +++ b/tests/dreamer/__init__.py @@ -0,0 +1 @@ +# Dream scheduler tests diff --git a/tests/dreamer/test_dream_scheduler.py b/tests/dreamer/test_dream_scheduler.py new file mode 100644 index 00000000..b713e85b --- /dev/null +++ b/tests/dreamer/test_dream_scheduler.py @@ -0,0 +1,460 @@ +"""Regression tests for dream scheduler bug fixes.""" + +from typing import Any +from unittest.mock import AsyncMock, patch + +import pytest + +from src.dreamer.dream_scheduler import DreamScheduler, set_dream_scheduler +from src.schemas import DreamType +from src.utils.work_unit import construct_work_unit_key + + +@pytest.fixture +def dream_scheduler(): + """Create a fresh DreamScheduler instance for each test.""" + # Reset the singleton before each test + DreamScheduler.reset_singleton() + scheduler = DreamScheduler() + set_dream_scheduler(scheduler) + yield scheduler + # Cleanup + DreamScheduler.reset_singleton() + + +class TestCancelDreamsForObserved: + """Regression tests for Bug #1: Peer-to-peer observation dreams not cancelled on activity. + + Previously, when a message arrived from peer Bob, only the self-observation dream + (observer=Bob, observed=Bob) was cancelled. Peer-to-peer observation dreams + (observer=Alice, observed=Bob) were NOT cancelled, allowing dreams to fire + during active conversation. + """ + + @pytest.mark.asyncio + async def test_cancel_self_observation_dream(self, dream_scheduler: DreamScheduler): + """Cancelling dreams for observed peer should cancel self-observation dreams.""" + workspace_name = "test_workspace" + peer_name = "bob" + + # Schedule a self-observation dream (observer=bob, observed=bob) + work_unit_key = construct_work_unit_key( + workspace_name, + { + "task_type": "dream", + "observer": peer_name, + "observed": peer_name, + "dream_type": "omni", + }, + ) + + with patch.object(dream_scheduler, "execute_dream", new_callable=AsyncMock): + await dream_scheduler.schedule_dream( + work_unit_key, + workspace_name, + delay_minutes=60, + dream_type=DreamType.OMNI, + observer=peer_name, + observed=peer_name, + ) + + # Verify dream is pending + assert work_unit_key in dream_scheduler.pending_dreams + + # Cancel dreams for observed peer + cancelled = await dream_scheduler.cancel_dreams_for_observed( + workspace_name, peer_name + ) + + # Verify the dream was cancelled + assert work_unit_key in cancelled + assert work_unit_key not in dream_scheduler.pending_dreams + + @pytest.mark.asyncio + async def test_cancel_peer_to_peer_observation_dream( + self, dream_scheduler: DreamScheduler + ): + """Cancelling dreams for observed peer should also cancel peer-to-peer dreams. + + This is the core regression test for Bug #1: previously, if Alice was + observing Bob and Bob sent a message, the dream for (observer=Alice, + observed=Bob) would NOT be cancelled. + """ + workspace_name = "test_workspace" + observer = "alice" # Alice is watching Bob + observed = "bob" # Bob sends a message + + # Schedule a peer-to-peer observation dream (observer=alice, observed=bob) + work_unit_key = construct_work_unit_key( + workspace_name, + { + "task_type": "dream", + "observer": observer, + "observed": observed, + "dream_type": "omni", + }, + ) + + with patch.object(dream_scheduler, "execute_dream", new_callable=AsyncMock): + await dream_scheduler.schedule_dream( + work_unit_key, + workspace_name, + delay_minutes=60, + dream_type=DreamType.OMNI, + observer=observer, + observed=observed, + ) + + # Verify dream is pending + assert work_unit_key in dream_scheduler.pending_dreams + + # When Bob sends a message, cancel all dreams where observed=bob + cancelled = await dream_scheduler.cancel_dreams_for_observed( + workspace_name, observed + ) + + # Verify the peer-to-peer dream was cancelled + assert work_unit_key in cancelled + assert work_unit_key not in dream_scheduler.pending_dreams + + @pytest.mark.asyncio + async def test_cancel_multiple_observers_same_observed( + self, dream_scheduler: DreamScheduler + ): + """When observed peer sends a message, ALL dreams observing them should cancel.""" + workspace_name = "test_workspace" + observed = "bob" + observers = ["alice", "charlie", "bob"] # Multiple observers including self + + work_unit_keys: list[str] = [] + + with patch.object(dream_scheduler, "execute_dream", new_callable=AsyncMock): + for observer in observers: + work_unit_key = construct_work_unit_key( + workspace_name, + { + "task_type": "dream", + "observer": observer, + "observed": observed, + "dream_type": "omni", + }, + ) + work_unit_keys.append(work_unit_key) + + await dream_scheduler.schedule_dream( + work_unit_key, + workspace_name, + delay_minutes=60, + dream_type=DreamType.OMNI, + observer=observer, + observed=observed, + ) + + # Verify all dreams are pending + assert len(dream_scheduler.pending_dreams) == 3 + + # Cancel all dreams where observed=bob + cancelled = await dream_scheduler.cancel_dreams_for_observed( + workspace_name, observed + ) + + # All three should be cancelled + assert len(cancelled) == 3 + for key in work_unit_keys: + assert key in cancelled + assert len(dream_scheduler.pending_dreams) == 0 + + @pytest.mark.asyncio + async def test_does_not_cancel_dreams_for_different_observed( + self, dream_scheduler: DreamScheduler + ): + """Cancelling dreams for one observed peer should not affect others.""" + workspace_name = "test_workspace" + + # Dream for Alice observing Bob + key_alice_bob = construct_work_unit_key( + workspace_name, + { + "task_type": "dream", + "observer": "alice", + "observed": "bob", + "dream_type": "omni", + }, + ) + + # Dream for Alice observing Charlie (should NOT be cancelled) + key_alice_charlie = construct_work_unit_key( + workspace_name, + { + "task_type": "dream", + "observer": "alice", + "observed": "charlie", + "dream_type": "omni", + }, + ) + + with patch.object(dream_scheduler, "execute_dream", new_callable=AsyncMock): + await dream_scheduler.schedule_dream( + key_alice_bob, + workspace_name, + delay_minutes=60, + dream_type=DreamType.OMNI, + observer="alice", + observed="bob", + ) + await dream_scheduler.schedule_dream( + key_alice_charlie, + workspace_name, + delay_minutes=60, + dream_type=DreamType.OMNI, + observer="alice", + observed="charlie", + ) + + assert len(dream_scheduler.pending_dreams) == 2 + + # Cancel only dreams where observed=bob + cancelled = await dream_scheduler.cancel_dreams_for_observed( + workspace_name, "bob" + ) + + # Only the bob dream should be cancelled + assert key_alice_bob in cancelled + assert key_alice_charlie not in cancelled + assert key_alice_charlie in dream_scheduler.pending_dreams + + @pytest.mark.asyncio + async def test_does_not_cancel_dreams_for_different_workspace( + self, dream_scheduler: DreamScheduler + ): + """Cancelling dreams should be scoped to the correct workspace.""" + observed = "bob" + + key_ws1 = construct_work_unit_key( + "workspace1", + { + "task_type": "dream", + "observer": "alice", + "observed": observed, + "dream_type": "omni", + }, + ) + key_ws2 = construct_work_unit_key( + "workspace2", + { + "task_type": "dream", + "observer": "alice", + "observed": observed, + "dream_type": "omni", + }, + ) + + with patch.object(dream_scheduler, "execute_dream", new_callable=AsyncMock): + await dream_scheduler.schedule_dream( + key_ws1, + "workspace1", + delay_minutes=60, + dream_type=DreamType.OMNI, + observer="alice", + observed=observed, + ) + await dream_scheduler.schedule_dream( + key_ws2, + "workspace2", + delay_minutes=60, + dream_type=DreamType.OMNI, + observer="alice", + observed=observed, + ) + + # Cancel only in workspace1 + cancelled = await dream_scheduler.cancel_dreams_for_observed( + "workspace1", observed + ) + + assert key_ws1 in cancelled + assert key_ws2 not in cancelled + assert key_ws2 in dream_scheduler.pending_dreams + + +class TestDocumentCountAtExecutionTime: + """Regression tests for Bug #2: Stale document count used in metadata update. + + Previously, the document count was captured when the dream was scheduled + (at check_and_schedule_dream time), then used 60 minutes later when the + dream actually executed. This caused incorrect metadata if documents were + added during the wait period. + + Now, execute_dream queries the current document count at execution time. + """ + + @pytest.mark.asyncio + async def test_execute_dream_queries_document_count_at_execution( + self, dream_scheduler: DreamScheduler + ): + """execute_dream should query current document count, not use a stale value. + + This test verifies that execute_dream fetches the document count fresh + from the database at execution time rather than using a pre-captured value. + + The key architectural change was: + - OLD: schedule_dream(document_count) -> _delayed_dream(document_count) -> execute_dream(document_count) + - NEW: schedule_dream() -> _delayed_dream() -> execute_dream() queries count internally + + We verify this by mocking the database to return a specific count and + checking that enqueue_dream receives that count. + """ + from contextlib import asynccontextmanager + from unittest.mock import MagicMock + + from src import models + from src.schemas import ( + ResolvedConfiguration, + ResolvedDreamConfiguration, + ResolvedPeerCardConfiguration, + ResolvedReasoningConfiguration, + ResolvedSummaryConfiguration, + ) + + workspace_name = "test_workspace" + observer = "bob" + observed = "bob" + session_name = "test_session" + + # The document count that the database will return + CURRENT_DOC_COUNT = 42 + + # Track what document_count is passed to enqueue_dream + captured_document_count: int | None = None + + async def capture_enqueue_dream( + _ws_name: str, + observer: str, # pyright: ignore[reportUnusedParameter] + observed: str, # pyright: ignore[reportUnusedParameter] + dream_type: Any, # pyright: ignore[reportUnusedParameter] + document_count: int, + session_name: str, # pyright: ignore[reportUnusedParameter] + ) -> None: + nonlocal captured_document_count + captured_document_count = document_count + + # Create mock database session that returns our test data + mock_session = MagicMock() + mock_workspace = MagicMock(spec=models.Workspace) + mock_db_session = MagicMock(spec=models.Session) + + # Mock scalar to return session_name for first call, document count for second + scalar_call_count = 0 + + async def mock_scalar(_stmt: Any) -> str | int: + nonlocal scalar_call_count + scalar_call_count += 1 + if scalar_call_count == 1: + return session_name # First call gets session_name from documents + else: + return CURRENT_DOC_COUNT # Second call gets document count + + mock_session.scalar = mock_scalar + + @asynccontextmanager + async def mock_tracked_db(_: str | None = None): + yield mock_session + + with ( + patch( + "src.dreamer.dream_scheduler.tracked_db", + mock_tracked_db, + ), + patch( + "src.deriver.enqueue.enqueue_dream", + side_effect=capture_enqueue_dream, + ), + patch( + "src.crud.get_session", + return_value=mock_db_session, + ), + patch( + "src.crud.get_workspace", + return_value=mock_workspace, + ), + patch( + "src.utils.config_helpers.get_configuration", + return_value=ResolvedConfiguration( + reasoning=ResolvedReasoningConfiguration(enabled=True), + peer_card=ResolvedPeerCardConfiguration(use=True, create=True), + summary=ResolvedSummaryConfiguration( + enabled=True, + messages_per_short_summary=10, + messages_per_long_summary=20, + ), + dream=ResolvedDreamConfiguration(enabled=True), + ), + ), + ): + # Execute the dream + await dream_scheduler.execute_dream( + workspace_name, + DreamType.OMNI, + observer=observer, + observed=observed, + ) + + # Verify that execute_dream queried the document count (2 scalar calls) + assert ( + scalar_call_count == 2 + ), "Should have queried session_name and document count" + + # Verify that enqueue_dream received the CURRENT document count (42), + # proving that execute_dream queries the count at execution time + assert captured_document_count == CURRENT_DOC_COUNT + + +class TestEnqueueCancelsDreamsCorrectly: + """Integration test verifying the full flow of message enqueue cancelling dreams.""" + + @pytest.mark.asyncio + async def test_enqueue_cancels_peer_to_peer_dreams( + self, dream_scheduler: DreamScheduler + ): + """When a message is enqueued, it should cancel all dreams for that observed peer.""" + + workspace_name = "test_workspace" + observed = "bob" + + # Schedule dreams for multiple observers watching bob + keys: list[Any] = [] + for observer in ["alice", "charlie", "bob"]: + key = construct_work_unit_key( + workspace_name, + { + "task_type": "dream", + "observer": observer, + "observed": observed, + "dream_type": "omni", + }, + ) + keys.append(key) + + with patch.object(dream_scheduler, "execute_dream", new_callable=AsyncMock): + await dream_scheduler.schedule_dream( + key, + workspace_name, + delay_minutes=60, + dream_type=DreamType.OMNI, + observer=observer, + observed=observed, + ) + + assert len(dream_scheduler.pending_dreams) == 3 + + # Mock the database operations in enqueue + with patch("src.deriver.enqueue.tracked_db"): + # The enqueue function should cancel dreams via cancel_dreams_for_observed + # We just test that the scheduler method was called correctly + cancelled = await dream_scheduler.cancel_dreams_for_observed( + workspace_name, observed + ) + + # All three dreams should be cancelled + assert len(cancelled) == 3 + assert len(dream_scheduler.pending_dreams) == 0 diff --git a/tests/unified/test_cases/config_deriver_hierarchy.json b/tests/unified/test_cases/config_deriver_hierarchy.json index 4b29ac73..8eb7bc9d 100644 --- a/tests/unified/test_cases/config_deriver_hierarchy.json +++ b/tests/unified/test_cases/config_deriver_hierarchy.json @@ -1,10 +1,10 @@ { - "description": "Test deriver configuration hierarchy (Session > Workspace)", + "description": "Test reasoning configuration hierarchy (Session > Workspace)", "steps": [ { "step_type": "set_workspace_config", "config": { - "deriver": { + "reasoning": { "enabled": true } } @@ -13,7 +13,7 @@ "step_type": "create_session", "session_id": "session_disabled", "config": { - "deriver": { + "reasoning": { "enabled": false } } @@ -43,18 +43,19 @@ "session_id": "session_disabled", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] }, { "step_type": "set_workspace_config", "config": { - "deriver": { + "reasoning": { "enabled": false } } @@ -63,7 +64,7 @@ "step_type": "create_session", "session_id": "session_enabled", "config": { - "deriver": { + "reasoning": { "enabled": true } } @@ -93,9 +94,9 @@ "session_id": "session_enabled", "assertions": [ { - "assertion_type": "llm_judge", - "prompt": "Check if there are explicit observations about testing or overriding in the representation.", - "pass_if": true + "assertion_type": "contains", + "text": "testing", + "case_sensitive": false } ] } diff --git a/tests/unified/test_cases/config_message_positive_override.json b/tests/unified/test_cases/config_message_positive_override.json index dc25e186..a9a46623 100644 --- a/tests/unified/test_cases/config_message_positive_override.json +++ b/tests/unified/test_cases/config_message_positive_override.json @@ -1,7 +1,7 @@ { - "description": "Test message-level deriver override", + "description": "Test message-level reasoning override", "workspace_config": { - "deriver": { + "reasoning": { "enabled": false } }, @@ -18,7 +18,7 @@ "peer_id": "charlie", "content": "I like turtles.", "config": { - "deriver": { + "reasoning": { "enabled": true } } @@ -27,7 +27,7 @@ "peer_id": "charlie", "content": "I like squids.", "config": { - "deriver": { + "reasoning": { "enabled": false } } diff --git a/tests/unified/test_cases/config_peercard_control.json b/tests/unified/test_cases/config_peercard_control.json index f5d38f98..04145446 100644 --- a/tests/unified/test_cases/config_peercard_control.json +++ b/tests/unified/test_cases/config_peercard_control.json @@ -1,7 +1,7 @@ { "description": "Test peer card generation control", "workspace_config": { - "deriver": { + "reasoning": { "enabled": true }, "peer_card": { diff --git a/tests/unified/test_cases/config_summary_control_deriver_off.json b/tests/unified/test_cases/config_summary_control_deriver_off.json index c9db5dc4..82e10f64 100644 --- a/tests/unified/test_cases/config_summary_control_deriver_off.json +++ b/tests/unified/test_cases/config_summary_control_deriver_off.json @@ -1,7 +1,7 @@ { - "description": "Test summary generation control with deriver disabled", + "description": "Test summary generation control with reasoning disabled", "workspace_config": { - "deriver": { + "reasoning": { "enabled": false }, "summary": { diff --git a/tests/unified/test_cases/message_deriver_disabled.json b/tests/unified/test_cases/message_deriver_disabled.json index 3a18f2cd..fa6937c9 100644 --- a/tests/unified/test_cases/message_deriver_disabled.json +++ b/tests/unified/test_cases/message_deriver_disabled.json @@ -1,5 +1,5 @@ { - "description": "Test that disabling deriver prevents any derived data generation", + "description": "Test that disabling reasoning prevents any derived data generation", "steps": [ { "step_type": "create_session", @@ -13,7 +13,7 @@ "peer_id": "alice", "content": "Message 1", "config": { - "deriver": { + "reasoning": { "enabled": false } } @@ -22,7 +22,7 @@ "peer_id": "alice", "content": "Message 2", "config": { - "deriver": { + "reasoning": { "enabled": false } } @@ -31,7 +31,7 @@ "peer_id": "alice", "content": "Message 3", "config": { - "deriver": { + "reasoning": { "enabled": false } } @@ -62,11 +62,12 @@ "session_id": "session_1", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] }, diff --git a/tests/unified/test_cases/observation_2peer_both_observe_me_false.json b/tests/unified/test_cases/observation_2peer_both_observe_me_false.json index c0569c46..9519726c 100644 --- a/tests/unified/test_cases/observation_2peer_both_observe_me_false.json +++ b/tests/unified/test_cases/observation_2peer_both_observe_me_false.json @@ -51,11 +51,12 @@ "session_id": "session_no_local_reps", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] }, @@ -67,11 +68,12 @@ "session_id": "session_no_local_reps", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] }, @@ -82,11 +84,12 @@ "session_id": "session_no_local_reps", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] }, @@ -97,11 +100,12 @@ "session_id": "session_no_local_reps", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] } diff --git a/tests/unified/test_cases/observation_2peer_default.json b/tests/unified/test_cases/observation_2peer_default.json index b4442bbe..39913e4d 100644 --- a/tests/unified/test_cases/observation_2peer_default.json +++ b/tests/unified/test_cases/observation_2peer_default.json @@ -51,11 +51,12 @@ "session_id": "session_default", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] }, @@ -67,11 +68,12 @@ "session_id": "session_default", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] }, @@ -82,9 +84,9 @@ "session_id": "session_default", "assertions": [ { - "assertion_type": "llm_judge", - "prompt": "Check if there are facts about hiking and mountains in Alice's global representation.", - "pass_if": true + "assertion_type": "contains", + "text": "hiking", + "case_sensitive": false } ] }, @@ -95,9 +97,9 @@ "session_id": "session_default", "assertions": [ { - "assertion_type": "llm_judge", - "prompt": "Check if there are facts about swimming and beach in Bob's global representation.", - "pass_if": true + "assertion_type": "contains", + "text": "swimming", + "case_sensitive": false } ] } diff --git a/tests/unified/test_cases/observation_2peer_observe_me_false_blocks_observation.json b/tests/unified/test_cases/observation_2peer_observe_me_false_blocks_observation.json index 4b583209..93f30f5e 100644 --- a/tests/unified/test_cases/observation_2peer_observe_me_false_blocks_observation.json +++ b/tests/unified/test_cases/observation_2peer_observe_me_false_blocks_observation.json @@ -51,11 +51,12 @@ "session_id": "session_no_observe_bob", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] }, @@ -66,11 +67,12 @@ "session_id": "session_no_observe_bob", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] } diff --git a/tests/unified/test_cases/observation_2peer_observe_me_false_but_can_still_observe_others.json b/tests/unified/test_cases/observation_2peer_observe_me_false_but_can_still_observe_others.json index 20dbd872..ea935b6b 100644 --- a/tests/unified/test_cases/observation_2peer_observe_me_false_but_can_still_observe_others.json +++ b/tests/unified/test_cases/observation_2peer_observe_me_false_but_can_still_observe_others.json @@ -51,9 +51,9 @@ "session_id": "session_invisible_observer", "assertions": [ { - "assertion_type": "llm_judge", - "prompt": "Check if Alice's local representation of Bob contains information about Bob working in retail management with a team of 20 people.", - "pass_if": true + "assertion_type": "contains", + "text": "retail", + "case_sensitive": false } ] }, @@ -65,11 +65,12 @@ "session_id": "session_invisible_observer", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] } diff --git a/tests/unified/test_cases/observation_2peer_unidirectional_alice_observes_bob.json b/tests/unified/test_cases/observation_2peer_unidirectional_alice_observes_bob.json index 8606c73b..e8b2b29b 100644 --- a/tests/unified/test_cases/observation_2peer_unidirectional_alice_observes_bob.json +++ b/tests/unified/test_cases/observation_2peer_unidirectional_alice_observes_bob.json @@ -51,9 +51,9 @@ "session_id": "session_uni_a_to_b", "assertions": [ { - "assertion_type": "llm_judge", - "prompt": "Check if Alice's local representation of Bob contains information about Bob studying biology and genetics.", - "pass_if": true + "assertion_type": "contains", + "text": "biology", + "case_sensitive": false } ] }, @@ -65,11 +65,12 @@ "session_id": "session_uni_a_to_b", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] } diff --git a/tests/unified/test_cases/observation_2peer_unidirectional_bob_observes_alice.json b/tests/unified/test_cases/observation_2peer_unidirectional_bob_observes_alice.json index 6a52e130..8040495b 100644 --- a/tests/unified/test_cases/observation_2peer_unidirectional_bob_observes_alice.json +++ b/tests/unified/test_cases/observation_2peer_unidirectional_bob_observes_alice.json @@ -51,9 +51,9 @@ "session_id": "session_uni_b_to_a", "assertions": [ { - "assertion_type": "llm_judge", - "prompt": "Check if Bob's local representation of Alice contains information about Alice playing guitar in a band and playing rock and blues.", - "pass_if": true + "assertion_type": "contains", + "text": "guitar", + "case_sensitive": false } ] }, @@ -65,11 +65,12 @@ "session_id": "session_uni_b_to_a", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] } diff --git a/tests/unified/test_cases/observation_3peer_multiple_observers_one_observed.json b/tests/unified/test_cases/observation_3peer_multiple_observers_one_observed.json index 3cc8ad6e..aa032697 100644 --- a/tests/unified/test_cases/observation_3peer_multiple_observers_one_observed.json +++ b/tests/unified/test_cases/observation_3peer_multiple_observers_one_observed.json @@ -59,9 +59,9 @@ "session_id": "session_all_watch_alice", "assertions": [ { - "assertion_type": "llm_judge", - "prompt": "Check if Bob's local representation of Alice contains information about Alice being a professional dancer specializing in contemporary and ballet.", - "pass_if": true + "assertion_type": "contains", + "text": "dancer", + "case_sensitive": false } ] }, @@ -73,9 +73,9 @@ "session_id": "session_all_watch_alice", "assertions": [ { - "assertion_type": "llm_judge", - "prompt": "Check if Charlie's local representation of Alice contains information about Alice being a professional dancer who has been dancing for 15 years.", - "pass_if": true + "assertion_type": "contains", + "text": "dancer", + "case_sensitive": false } ] }, @@ -87,11 +87,12 @@ "session_id": "session_all_watch_alice", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] }, @@ -103,11 +104,12 @@ "session_id": "session_all_watch_alice", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] }, @@ -118,9 +120,9 @@ "observed_peer_id": "alice", "assertions": [ { - "assertion_type": "llm_judge", - "prompt": "Check if Bob has a peer card for Alice that contains relevant information about her.", - "pass_if": true + "assertion_type": "contains", + "text": "dancer", + "case_sensitive": false } ] }, @@ -131,9 +133,9 @@ "observed_peer_id": "alice", "assertions": [ { - "assertion_type": "llm_judge", - "prompt": "Check if Charlie has a peer card for Alice that contains relevant information about her.", - "pass_if": true + "assertion_type": "contains", + "text": "dancer", + "case_sensitive": false } ] }, diff --git a/tests/unified/test_cases/observation_3peer_one_observer_multiple_observed.json b/tests/unified/test_cases/observation_3peer_one_observer_multiple_observed.json index 24d0cee0..5c3e345d 100644 --- a/tests/unified/test_cases/observation_3peer_one_observer_multiple_observed.json +++ b/tests/unified/test_cases/observation_3peer_one_observer_multiple_observed.json @@ -59,9 +59,9 @@ "session_id": "session_alice_watches_all", "assertions": [ { - "assertion_type": "llm_judge", - "prompt": "Check if Alice's local representation of Bob contains information about Bob training for a marathon and running 5 miles every morning.", - "pass_if": true + "assertion_type": "contains", + "text": "marathon", + "case_sensitive": false } ] }, @@ -73,9 +73,9 @@ "session_id": "session_alice_watches_all", "assertions": [ { - "assertion_type": "llm_judge", - "prompt": "Check if Alice's local representation of Charlie contains information about Charlie learning chess and practicing tactics daily.", - "pass_if": true + "assertion_type": "contains", + "text": "chess", + "case_sensitive": false } ] }, @@ -87,11 +87,12 @@ "session_id": "session_alice_watches_all", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] }, @@ -103,11 +104,12 @@ "session_id": "session_alice_watches_all", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] } diff --git a/tests/unified/test_cases/observation_3peer_selective_observation.json b/tests/unified/test_cases/observation_3peer_selective_observation.json index 1a589767..8f2c951b 100644 --- a/tests/unified/test_cases/observation_3peer_selective_observation.json +++ b/tests/unified/test_cases/observation_3peer_selective_observation.json @@ -59,9 +59,9 @@ "session_id": "session_selective", "assertions": [ { - "assertion_type": "llm_judge", - "prompt": "Check if Alice's local representation of Bob contains information about Bob being an architect who designs skyscrapers.", - "pass_if": true + "assertion_type": "contains", + "text": "architect", + "case_sensitive": false } ] }, @@ -73,11 +73,12 @@ "session_id": "session_selective", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] } diff --git a/tests/unified/test_cases/observation_4peer_complex_matrix.json b/tests/unified/test_cases/observation_4peer_complex_matrix.json index 12410e82..17d723a2 100644 --- a/tests/unified/test_cases/observation_4peer_complex_matrix.json +++ b/tests/unified/test_cases/observation_4peer_complex_matrix.json @@ -1,7 +1,7 @@ { "description": "Test complex 4-peer scenario with mixed observation patterns", "workspace_config": { - "deriver": { + "reasoning": { "enabled": true } }, @@ -62,9 +62,9 @@ "session_id": "session_four_peer", "assertions": [ { - "assertion_type": "llm_judge", - "prompt": "Check if Alice observes Bob (should contain lawyer info).", - "pass_if": true + "assertion_type": "contains", + "text": "lawyer", + "case_sensitive": false } ] }, @@ -76,11 +76,12 @@ "session_id": "session_four_peer", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] }, @@ -92,9 +93,9 @@ "session_id": "session_four_peer", "assertions": [ { - "assertion_type": "llm_judge", - "prompt": "Check if Bob observes Alice (should contain neurologist info).", - "pass_if": true + "assertion_type": "contains", + "text": "neurologist", + "case_sensitive": false } ] }, @@ -106,9 +107,9 @@ "session_id": "session_four_peer", "assertions": [ { - "assertion_type": "llm_judge", - "prompt": "Check if Charlie observes Alice (should contain neurologist info, even though Charlie has observe_me=false).", - "pass_if": true + "assertion_type": "contains", + "text": "neurologist", + "case_sensitive": false } ] }, @@ -120,9 +121,9 @@ "session_id": "session_four_peer", "assertions": [ { - "assertion_type": "llm_judge", - "prompt": "Check if Alice observes Diana (should contain pharmacist info).", - "pass_if": true + "assertion_type": "contains", + "text": "pharmacist", + "case_sensitive": false } ] }, @@ -134,11 +135,12 @@ "session_id": "session_four_peer", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] } diff --git a/tests/unified/test_cases/observation_asymmetric_visibility.json b/tests/unified/test_cases/observation_asymmetric_visibility.json index c01c3d42..46acf71a 100644 --- a/tests/unified/test_cases/observation_asymmetric_visibility.json +++ b/tests/unified/test_cases/observation_asymmetric_visibility.json @@ -1,7 +1,7 @@ { "description": "Test asymmetric visibility scenario from documentation - Alice lies to Bob but tells truth to Charlie", "workspace_config": { - "deriver": { + "reasoning": { "enabled": true } }, diff --git a/tests/unified/test_cases/observation_isolation_between_sessions.json b/tests/unified/test_cases/observation_isolation_between_sessions.json index 8ab7caac..dddb8eab 100644 --- a/tests/unified/test_cases/observation_isolation_between_sessions.json +++ b/tests/unified/test_cases/observation_isolation_between_sessions.json @@ -1,7 +1,7 @@ { "description": "Test that local representations are isolated between sessions - same peers in different sessions have different local reps", "workspace_config": { - "deriver": { + "reasoning": { "enabled": true } }, diff --git a/tests/unified/test_cases/session_deriver_disabled.json b/tests/unified/test_cases/session_deriver_disabled.json index 603a8072..0af60379 100644 --- a/tests/unified/test_cases/session_deriver_disabled.json +++ b/tests/unified/test_cases/session_deriver_disabled.json @@ -1,11 +1,11 @@ { - "description": "Test that disabling deriver prevents any derived data generation", + "description": "Test that disabling reasoning prevents any derived data generation", "steps": [ { "step_type": "create_session", "session_id": "session_1", "config": { - "deriver": { + "reasoning": { "enabled": false } } @@ -52,11 +52,12 @@ "session_id": "session_1", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] }, diff --git a/tests/unified/test_cases/workspace_deriver_disabled.json b/tests/unified/test_cases/workspace_deriver_disabled.json index 992498ad..b30e9953 100644 --- a/tests/unified/test_cases/workspace_deriver_disabled.json +++ b/tests/unified/test_cases/workspace_deriver_disabled.json @@ -1,7 +1,7 @@ { - "description": "Test that disabling deriver prevents any derived data generation", + "description": "Test that disabling reasoning prevents any derived data generation", "workspace_config": { - "deriver": { + "reasoning": { "enabled": false } }, @@ -52,11 +52,12 @@ "session_id": "session_1", "assertions": [ { - "assertion_type": "json_match", - "key_value_pairs": { - "explicit": [], - "deductive": [] - } + "assertion_type": "not_contains", + "text": "## Explicit" + }, + { + "assertion_type": "not_contains", + "text": "## Deductive" } ] },