Files
maskanx_cm_backend/tests/test_phase1_integration.py
T
AFFAANhandClaude Opus 5 19e1e84fb7 Initial commit: MaskanX backend
Independent FastAPI backend for the MaskanX agentic growth platform.

Includes the agent runtime, MCP client integrations (Meta Ads, LinkedIn,
HubSpot, Tavily, Exa, xAI, Citedy, image generation), PostgreSQL storage
for chats and cron jobs, provider and secret management, and the CLI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:28:22 +05:30

242 lines
9.6 KiB
Python

# -*- coding: utf-8 -*-
"""Phase 1 end-to-end integration test.
Tests the full chain: ingest → type classify → store → consolidate → query → prompt cache.
This is the smoke test for all Phase 1 features (1A + 1B + 1C + 1B bridge) working together.
"""
import pytest
from adclaw.memory_agent.consolidate import ConsolidationEngine
from adclaw.memory_agent.embeddings import FakeEmbeddingPipeline
from adclaw.memory_agent.ingest import IngestAgent
from adclaw.memory_agent.models import AOMConfig, Memory
from adclaw.memory_agent.query import QueryAgent
from adclaw.memory_agent.store import MemoryStore
from adclaw.agents.prompt import CachedPromptBuilder, DynamicContext
@pytest.fixture
async def store():
s = MemoryStore(":memory:", dimensions=32)
await s.initialize()
yield s
await s.close()
@pytest.fixture
def embedder():
return FakeEmbeddingPipeline(dimensions=32)
@pytest.fixture
def config():
return AOMConfig(
enabled=True,
embedding_dimensions=32,
importance_threshold=0.1,
consolidation_min_interval_seconds=0,
consolidation_min_new_memories=1,
contradiction_detection_enabled=True,
)
async def _llm_caller(prompt: str) -> str:
"""Fake LLM that handles all prompt types."""
lower = prompt.lower()
if "extract" in lower:
return '{"entities": ["test"], "topics": ["email", "writing"], "importance": 0.7}'
if "contradict" in lower or "winner" in lower:
return "WINNER: B\nREASON: B is newer and more accurate."
if "consolidat" in lower or "synthesiz" in lower or "cluster" in lower:
return "INSIGHT: Combined knowledge about email best practices.\nIMPORTANCE: 0.8"
if "memory" in lower or "question" in lower:
return "Based on memories, always use formal tone and avoid emojis. [Memory #abc]"
return "Test response"
class TestPhase1EndToEnd:
"""Full chain smoke test: ingest → classify → consolidate → query → prompt."""
async def test_full_chain(self, store, embedder, config, tmp_path):
"""The main integration test — exercises all Phase 1 features."""
# ---- Step 1: Ingest memories of different types ----
ingest = IngestAgent(store, embedder, _llm_caller, config)
# Wire consolidation engine notification
engine = ConsolidationEngine(store, embedder, _llm_caller, config)
ingest._on_memory_inserted = engine.notify_new_memory
# Feedback memory
mem_fb = await ingest.ingest(
"Don't use emojis in client emails because they look unprofessional",
skip_llm=True,
)
assert mem_fb.memory_type == "feedback"
assert "feedback_structure" in mem_fb.metadata
# Project memory
mem_proj = await ingest.ingest(
"Campaign launch deadline is Q2 2026, SEO report deliverable",
skip_llm=True,
)
assert mem_proj.memory_type == "project"
# Reference memory
mem_ref = await ingest.ingest(
"Brand guidelines at https://drive.google.com/brand-guide",
skip_llm=True,
)
assert mem_ref.memory_type == "reference"
# User memories
mem_user1 = await ingest.ingest(
"I prefer formal tone in all client communications",
skip_llm=True,
)
assert mem_user1.memory_type == "user"
mem_user2 = await ingest.ingest(
"I always review drafts twice before sending",
skip_llm=True,
)
assert mem_user2.memory_type == "user"
# ---- Step 2: Verify gate logic ----
# 5 memories ingested → engine should have been notified
assert engine._new_memory_count == 5
assert engine._should_run() is True
# ---- Step 3: Run consolidation cycle ----
results = await engine.run_consolidation_cycle()
# Counter reset after successful cycle
assert engine._new_memory_count == 0
# ---- Step 4: Verify store state ----
stats = await store.get_stats()
assert stats["total_memories"] == 5
# ---- Step 5: Ingest contradicting memories (with explicit topics for detection) ----
mem_contra = Memory(
content="The API endpoint is active and working correctly",
topics=["api", "endpoint"],
importance=0.6,
)
await store.insert_memory(mem_contra, embedding=await embedder.embed(mem_contra.content))
mem_contra2 = Memory(
content="The API endpoint is broken and deprecated",
topics=["api", "endpoint"],
importance=0.6,
)
await store.insert_memory(mem_contra2, embedding=await embedder.embed(mem_contra2.content))
# Run contradiction detection
engine.notify_new_memory() # manual notify for the test
await engine._detect_contradictions([mem_contra, mem_contra2])
# One should be superseded (B wins per our fake LLM)
loser = await store.get_memory(mem_contra.id)
assert loser.is_deleted == 1
assert loser.superseded_by == mem_contra2.id
winner = await store.get_memory(mem_contra2.id)
assert winner.is_deleted == 0
# ---- Step 6: Query with feedback boost ----
query_agent = QueryAgent(store, embedder, _llm_caller, config)
result = await query_agent.query("How should I write emails?", skip_synthesis=True)
# Should have results
assert len(result.citations) > 0
# Feedback should be boosted — ranked higher than non-feedback
feedback_citations = [c for c in result.citations if c.memory.memory_type == "feedback"]
if feedback_citations and len(result.citations) >= 2:
fb_idx = next(i for i, c in enumerate(result.citations) if c.memory.memory_type == "feedback")
non_fb_idxs = [i for i, c in enumerate(result.citations) if c.memory.memory_type != "feedback"]
if non_fb_idxs:
assert fb_idx < min(non_fb_idxs), "Feedback should be ranked higher than non-feedback"
# ---- Step 7: Prompt caching ----
(tmp_path / "AGENTS.md").write_text("Marketing agent instructions: write SEO content.")
(tmp_path / "SOUL.md").write_text("I am a helpful marketing assistant.")
builder = CachedPromptBuilder(working_dir=tmp_path)
# First build
dynamic = DynamicContext(
env_context="Platform: linux",
team_summary="3 agents: researcher, writer, SEO specialist",
)
prompt1 = builder.build(dynamic=dynamic)
assert "Marketing agent instructions" in prompt1
assert "Platform: linux" in prompt1
assert "# Team Summary" in prompt1
# Static cache should hit
static1 = builder.static_prompt
static2 = builder.static_prompt
assert static1 is static2 # same object = cache hit
# ---- Step 8: File change invalidates cache ----
(tmp_path / "AGENTS.md").write_text("Updated: focus on email marketing campaigns.")
builder.invalidate()
prompt2 = builder.build(dynamic=dynamic)
assert "email marketing campaigns" in prompt2
assert "Marketing agent instructions" not in prompt2
# ---- Step 9: Verify no memory type mixing in consolidation ----
# The consolidation should not have merged feedback with project memories
consolidations = await store.list_consolidations(limit=50)
for cons in consolidations:
# Get types of source memories
types = set()
for mid in cons.memory_ids:
mem = await store.get_memory(mid)
if mem:
types.add(mem.memory_type)
# All memories in a consolidation should be the same type
assert len(types) <= 1, (
f"Cross-type consolidation detected: types={types}, "
f"memory_ids={cons.memory_ids}"
)
async def test_empty_store_graceful(self, store, embedder, config, tmp_path):
"""All Phase 1 components handle empty store gracefully."""
engine = ConsolidationEngine(store, embedder, _llm_caller, config)
# Gate blocks on empty store
assert engine._should_run() is False
# Consolidation on empty store
engine.notify_new_memory()
results = await engine.run_consolidation_cycle()
assert results == []
# Query on empty store
query_agent = QueryAgent(store, embedder, _llm_caller, config)
result = await query_agent.query("anything")
assert result.answer == "No relevant memories found."
# Prompt builder with no files
builder = CachedPromptBuilder(working_dir=tmp_path)
prompt = builder.build()
# Should return default prompt (no AGENTS.md)
assert "helpful assistant" in prompt.lower()
async def test_ingest_notify_consolidation_wiring(self, store, embedder, config):
"""Verify IngestAgent → ConsolidationEngine notification wiring."""
engine = ConsolidationEngine(store, embedder, _llm_caller, config)
ingest = IngestAgent(store, embedder, _llm_caller, config)
ingest._on_memory_inserted = engine.notify_new_memory
assert engine._new_memory_count == 0
await ingest.ingest("Test memory one", skip_llm=True)
assert engine._new_memory_count == 1
await ingest.ingest("Test memory two", skip_llm=True)
assert engine._new_memory_count == 2
# Dedup should NOT increment counter
await ingest.ingest("Test memory one", skip_llm=True) # duplicate hash
assert engine._new_memory_count == 2 # unchanged