honcho/tests/integration/test_multi_agent_coordinati...

494 lines
18 KiB
Python

"""Integration tests for multi-agent coordination in reasoning dreams.
These tests verify how agents interact and coordinate during complex reasoning
workflows, including concurrent processing, hypothesis evolution, and error recovery.
"""
import pytest
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src import models
from src.agents.dreamer.reasoning import process_reasoning_dream
from tests.utils.reasoning_test_helpers import (
create_test_observations,
create_test_peer,
create_test_workspace,
)
class TestConcurrentDreamProcessing:
"""Test concurrent dream processing for multiple peer pairs."""
@pytest.mark.asyncio
async def test_multiple_observer_observed_pairs(
self, db_session: AsyncSession, mock_all_reasoning_agents
):
"""Test that reasoning dreams for different peer pairs don't interfere.
Scenario: Multiple (observer, observed) pairs in same workspace
Expected: Each pair processes independently without cross-contamination
"""
# Setup: Create workspace with multiple observers and observed peers
workspace_name = await create_test_workspace(db_session)
# Create 2 observers
observer1 = await create_test_peer(db_session, workspace_name, "assistant_1")
observer2 = await create_test_peer(db_session, workspace_name, "assistant_2")
# Create 2 observed users
observed1 = await create_test_peer(db_session, workspace_name, "user_1")
observed2 = await create_test_peer(db_session, workspace_name, "user_2")
# Create observations for both pairs
obs1 = await create_test_observations(
db_session,
workspace_name=workspace_name,
observer=observer1,
observed=observed1,
count=10,
content_prefix="User 1 prefers detailed explanations",
)
obs2 = await create_test_observations(
db_session,
workspace_name=workspace_name,
observer=observer2,
observed=observed2,
count=10,
content_prefix="User 2 prefers concise summaries",
)
# Execute reasoning dreams for both pairs
metrics1 = await process_reasoning_dream(
db=db_session,
workspace_name=workspace_name,
observer=observer1,
observed=observed1,
)
metrics2 = await process_reasoning_dream(
db=db_session,
workspace_name=workspace_name,
observer=observer2,
observed=observed2,
)
# Verify both dreams executed successfully
assert metrics1.hypotheses_generated > 0
assert metrics2.hypotheses_generated > 0
# Verify hypotheses are correctly scoped to their peer pairs
hypotheses1_stmt = select(models.Hypothesis).where(
models.Hypothesis.workspace_name == workspace_name,
models.Hypothesis.observer == observer1,
models.Hypothesis.observed == observed1,
)
result1 = await db_session.execute(hypotheses1_stmt)
hypotheses1 = list(result1.scalars().all())
hypotheses2_stmt = select(models.Hypothesis).where(
models.Hypothesis.workspace_name == workspace_name,
models.Hypothesis.observer == observer2,
models.Hypothesis.observed == observed2,
)
result2 = await db_session.execute(hypotheses2_stmt)
hypotheses2 = list(result2.scalars().all())
# Verify no cross-contamination
assert len(hypotheses1) > 0
assert len(hypotheses2) > 0
# Verify all hypotheses for pair 1 have correct observer/observed
for h in hypotheses1:
assert h.observer == observer1
assert h.observed == observed1
assert h.observer != observer2
assert h.observed != observed2
# Verify all hypotheses for pair 2 have correct observer/observed
for h in hypotheses2:
assert h.observer == observer2
assert h.observed == observed2
assert h.observer != observer1
assert h.observed != observed1
@pytest.mark.asyncio
async def test_multiple_workspaces_isolation(
self, db_session: AsyncSession, mock_all_reasoning_agents
):
"""Test that reasoning dreams in different workspaces are isolated.
Scenario: Same peer names in different workspaces
Expected: Complete isolation between workspaces
"""
# Setup: Create two workspaces with same peer names
workspace1 = await create_test_workspace(db_session)
workspace2 = await create_test_workspace(db_session)
# Use identical peer names in both workspaces
observer = "assistant"
observed = "user_123"
# Create peers in both workspaces
await create_test_peer(db_session, workspace1, observer)
await create_test_peer(db_session, workspace1, observed)
await create_test_peer(db_session, workspace2, observer)
await create_test_peer(db_session, workspace2, observed)
# Create observations in both workspaces
await create_test_observations(
db_session, workspace1, observer, observed, count=10
)
await create_test_observations(
db_session, workspace2, observer, observed, count=10
)
# Execute reasoning dreams in both workspaces
metrics1 = await process_reasoning_dream(
db=db_session,
workspace_name=workspace1,
observer=observer,
observed=observed,
)
metrics2 = await process_reasoning_dream(
db=db_session,
workspace_name=workspace2,
observer=observer,
observed=observed,
)
# Verify both executed successfully
assert metrics1.hypotheses_generated > 0
assert metrics2.hypotheses_generated > 0
# Verify workspace isolation
hypotheses1_stmt = select(models.Hypothesis).where(
models.Hypothesis.workspace_name == workspace1
)
result1 = await db_session.execute(hypotheses1_stmt)
hypotheses1 = list(result1.scalars().all())
hypotheses2_stmt = select(models.Hypothesis).where(
models.Hypothesis.workspace_name == workspace2
)
result2 = await db_session.execute(hypotheses2_stmt)
hypotheses2 = list(result2.scalars().all())
# Verify no overlap
workspace1_ids = {h.id for h in hypotheses1}
workspace2_ids = {h.id for h in hypotheses2}
assert len(workspace1_ids & workspace2_ids) == 0, "Hypotheses leaked between workspaces"
class TestHypothesisEvolution:
"""Test hypothesis evolution across multiple reasoning dreams."""
@pytest.mark.asyncio
async def test_hypothesis_confidence_updates(
self, db_session: AsyncSession, mock_all_reasoning_agents
):
"""Test that hypothesis confidence evolves based on prediction outcomes.
Scenario: Multiple dreams with predictions testing hypotheses
Expected: Hypothesis confidence and tier updated based on results
"""
# Setup
workspace_name = await create_test_workspace(db_session)
observer = await create_test_peer(db_session, workspace_name, "assistant")
observed = await create_test_peer(db_session, workspace_name, "user_123")
# Create initial observations
await create_test_observations(
db_session, workspace_name, observer, observed, count=10
)
# First dream - creates hypotheses
metrics1 = await process_reasoning_dream(
db=db_session,
workspace_name=workspace_name,
observer=observer,
observed=observed,
)
assert metrics1.hypotheses_generated > 0
# Get initial hypothesis
hypotheses_stmt = select(models.Hypothesis).where(
models.Hypothesis.workspace_name == workspace_name,
models.Hypothesis.observer == observer,
models.Hypothesis.observed == observed,
)
result = await db_session.execute(hypotheses_stmt)
hypotheses = list(result.scalars().all())
initial_hypothesis = hypotheses[0]
initial_confidence = initial_hypothesis.confidence
initial_tier = initial_hypothesis.tier
# Add more observations
await create_test_observations(
db_session, workspace_name, observer, observed, count=5
)
# Second dream - tests existing hypotheses
metrics2 = await process_reasoning_dream(
db=db_session,
workspace_name=workspace_name,
observer=observer,
observed=observed,
)
# Verify hypothesis was tested
assert metrics2.predictions_generated > 0
# Reload hypothesis to check for updates
await db_session.refresh(initial_hypothesis)
# Verify predictions exist for this hypothesis
predictions_stmt = select(models.Prediction).where(
models.Prediction.hypothesis_id == initial_hypothesis.id
)
result = await db_session.execute(predictions_stmt)
predictions_for_hypothesis = list(result.scalars().all())
assert len(predictions_for_hypothesis) > 0, (
"Hypothesis should have predictions created for testing"
)
@pytest.mark.asyncio
async def test_hypothesis_superseding(
self, db_session: AsyncSession, mock_all_reasoning_agents
):
"""Test that better hypotheses can supersede older ones.
Scenario: New hypothesis with higher confidence
Expected: Old hypothesis marked as superseded, new one becomes active
"""
# Setup
workspace_name = await create_test_workspace(db_session)
observer = await create_test_peer(db_session, workspace_name, "assistant")
observed = await create_test_peer(db_session, workspace_name, "user_123")
# Create observations
await create_test_observations(
db_session, workspace_name, observer, observed, count=10
)
# First dream
metrics1 = await process_reasoning_dream(
db=db_session,
workspace_name=workspace_name,
observer=observer,
observed=observed,
)
assert metrics1.hypotheses_generated > 0
# Get initial hypotheses
hypotheses_stmt = select(models.Hypothesis).where(
models.Hypothesis.workspace_name == workspace_name,
models.Hypothesis.status == "active",
)
result = await db_session.execute(hypotheses_stmt)
active_hypotheses_before = len(list(result.scalars().all()))
# Add more observations (might trigger new hypotheses)
await create_test_observations(
db_session, workspace_name, observer, observed, count=10
)
# Second dream
metrics2 = await process_reasoning_dream(
db=db_session,
workspace_name=workspace_name,
observer=observer,
observed=observed,
)
# Check hypothesis evolution
all_hypotheses_stmt = select(models.Hypothesis).where(
models.Hypothesis.workspace_name == workspace_name,
)
result = await db_session.execute(all_hypotheses_stmt)
all_hypotheses = list(result.scalars().all())
# Verify we have hypotheses in different states
statuses = {h.status for h in all_hypotheses}
assert "active" in statuses, "Should have active hypotheses"
class TestPredictionRetesting:
"""Test prediction retesting workflows."""
@pytest.mark.asyncio
async def test_undertested_hypothesis_retesting(
self, db_session: AsyncSession, mock_all_reasoning_agents
):
"""Test that under-tested hypotheses are retested in subsequent dreams.
Scenario: Hypothesis with few tested predictions
Expected: Retesting tasks enqueued for under-tested hypotheses
"""
# Setup
workspace_name = await create_test_workspace(db_session)
observer = await create_test_peer(db_session, workspace_name, "assistant")
observed = await create_test_peer(db_session, workspace_name, "user_123")
# Create observations
await create_test_observations(
db_session, workspace_name, observer, observed, count=10
)
# Execute dream
metrics = await process_reasoning_dream(
db=db_session,
workspace_name=workspace_name,
observer=observer,
observed=observed,
min_observations_threshold=5,
)
# Verify reasoning occurred
assert metrics.hypotheses_generated > 0
assert metrics.predictions_generated > 0
# Check if retesting was tracked
# Note: In mock environment, retesting count may be 0 if all hypotheses
# have sufficient predictions
assert metrics.hypotheses_retested >= 0
@pytest.mark.asyncio
async def test_blind_predictions_unblinding(
self, db_session: AsyncSession, mock_all_reasoning_agents
):
"""Test that blind predictions are eventually unblinded.
Scenario: Predictions created as blind
Expected: All predictions remain blind until unblinding process runs
"""
# Setup
workspace_name = await create_test_workspace(db_session)
observer = await create_test_peer(db_session, workspace_name, "assistant")
observed = await create_test_peer(db_session, workspace_name, "user_123")
# Create observations
await create_test_observations(
db_session, workspace_name, observer, observed, count=10
)
# Execute dream
metrics = await process_reasoning_dream(
db=db_session,
workspace_name=workspace_name,
observer=observer,
observed=observed,
)
# Get predictions
predictions_stmt = select(models.Prediction).where(
models.Prediction.workspace_name == workspace_name,
)
result = await db_session.execute(predictions_stmt)
predictions = list(result.scalars().all())
if len(predictions) > 0:
# Verify all predictions created as blind
for prediction in predictions:
assert prediction.is_blind is True, (
f"Prediction {prediction.id} should be blind initially"
)
class TestErrorRecovery:
"""Test error recovery and rollback scenarios."""
@pytest.mark.asyncio
async def test_empty_workspace_handling(
self, db_session: AsyncSession, mock_all_reasoning_agents
):
"""Test graceful handling of empty workspace.
Scenario: Workspace with no observations
Expected: Dream completes without errors, no reasoning artifacts created
"""
# Setup: Create workspace but NO observations
workspace_name = await create_test_workspace(db_session)
observer = await create_test_peer(db_session, workspace_name, "assistant")
observed = await create_test_peer(db_session, workspace_name, "user_123")
# Execute dream with no observations
metrics = await process_reasoning_dream(
db=db_session,
workspace_name=workspace_name,
observer=observer,
observed=observed,
min_observations_threshold=5,
)
# Verify graceful handling
assert metrics.hypotheses_generated == 0
assert metrics.predictions_generated == 0
assert metrics.predictions_falsified == 0
assert metrics.predictions_unfalsified == 0
assert metrics.inductions_created == 0
@pytest.mark.asyncio
async def test_insufficient_observations_handling(
self, db_session: AsyncSession, mock_all_reasoning_agents
):
"""Test handling when observations below threshold.
Scenario: Observations exist but below threshold
Expected: Dream completes without reasoning
"""
# Setup
workspace_name = await create_test_workspace(db_session)
observer = await create_test_peer(db_session, workspace_name, "assistant")
observed = await create_test_peer(db_session, workspace_name, "user_123")
# Create observations BELOW threshold
await create_test_observations(
db_session, workspace_name, observer, observed, count=3
)
# Execute dream with threshold of 5
metrics = await process_reasoning_dream(
db=db_session,
workspace_name=workspace_name,
observer=observer,
observed=observed,
min_observations_threshold=5,
)
# Verify no reasoning occurred
assert metrics.hypotheses_generated == 0
assert metrics.predictions_generated == 0
@pytest.mark.asyncio
async def test_missing_peer_handling(
self, db_session: AsyncSession, mock_all_reasoning_agents
):
"""Test error handling for non-existent peers.
Scenario: Observer or observed peer doesn't exist
Expected: Graceful error or empty result
"""
# Setup
workspace_name = await create_test_workspace(db_session)
# Create only observer, not observed
observer = await create_test_peer(db_session, workspace_name, "assistant")
# Try to execute dream with non-existent observed peer
metrics = await process_reasoning_dream(
db=db_session,
workspace_name=workspace_name,
observer=observer,
observed="nonexistent_user", # This peer doesn't exist
min_observations_threshold=5,
)
# Verify graceful handling (no crash, but no work done)
assert metrics.hypotheses_generated == 0