202 lines
8.2 KiB
Python
202 lines
8.2 KiB
Python
|
|
"""
|
||
|
|
Scenario A — full integration walk-through (in-memory only, no DB required).
|
||
|
|
|
||
|
|
Demonstrates the degenerate parent_id=dim_id pattern and the misclassification
|
||
|
|
collision pipeline from initial seed through recollection rendering.
|
||
|
|
|
||
|
|
Story:
|
||
|
|
1. World model receives coarse early knowledge: "michigan is in usa"
|
||
|
|
→ stored as michigan ISPART usa in dim:usa (parent_id = dim_id)
|
||
|
|
2. Agent sends prompt: "michigan is a state of USA"
|
||
|
|
→ cue scanner extracts michigan ISA state in dim:usa
|
||
|
|
3. Collision detected: existing ISPART vs incoming ISA in dim:usa → misclassification
|
||
|
|
4. Recollection rendered with [usa?] marker while conflict is pending
|
||
|
|
5. After resolution (simulated): fact moves to correct dimension
|
||
|
|
→ michigan ISA state in dim:type
|
||
|
|
→ michigan ISPART usa in dim:geography
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||
|
|
|
||
|
|
from festinger import cache
|
||
|
|
from festinger.cache import SoasRow, UrdEdge
|
||
|
|
from festinger.cue_scanner import scan_cues
|
||
|
|
from festinger.recollection import (
|
||
|
|
build_recollection_block, inject_recollection, render_hit, query_edges
|
||
|
|
)
|
||
|
|
from festinger.urd_writer import InsertRequest, insert_urd_edge, CollisionInfo
|
||
|
|
from tests.helpers import reset_cache, add_soas, add_urd
|
||
|
|
|
||
|
|
|
||
|
|
def make_mock_pool():
|
||
|
|
conn = AsyncMock()
|
||
|
|
conn.execute = AsyncMock(return_value="INSERT 0 1")
|
||
|
|
conn.fetchrow = AsyncMock(return_value=None)
|
||
|
|
conn.__aenter__ = AsyncMock(return_value=conn)
|
||
|
|
conn.__aexit__ = AsyncMock(return_value=False)
|
||
|
|
pool = MagicMock()
|
||
|
|
pool.acquire = MagicMock(return_value=conn)
|
||
|
|
return pool, conn
|
||
|
|
|
||
|
|
|
||
|
|
class TestScenarioAIntegration:
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
reset_cache()
|
||
|
|
# SOAS vocabulary
|
||
|
|
self.michigan = add_soas(101, "michigan", saliency=1.5, novelty=1.0)
|
||
|
|
self.usa = add_soas(102, "usa", saliency=0.8, novelty=0.8)
|
||
|
|
self.state = add_soas(103, "state", saliency=0.6, novelty=0.5)
|
||
|
|
self.type_dim = add_soas(1, "type", saliency=0.0)
|
||
|
|
self.geo_dim = add_soas(5, "geography", saliency=0.0)
|
||
|
|
|
||
|
|
# Degenerate seed edge: michigan ISPART usa, dim=usa (parent_id = dim_id = 102)
|
||
|
|
self.seed_edge = add_urd(
|
||
|
|
concept_id=101,
|
||
|
|
parent_id=102, # usa
|
||
|
|
dim_id=102, # usa — same as parent_id
|
||
|
|
is_isa=False,
|
||
|
|
confidence=0.85,
|
||
|
|
source="test",
|
||
|
|
)
|
||
|
|
|
||
|
|
# ------------------------------------------------------------------
|
||
|
|
# Step 1: Verify the degenerate seed state
|
||
|
|
# ------------------------------------------------------------------
|
||
|
|
|
||
|
|
def test_seed_edge_has_parent_id_equals_dim_id(self):
|
||
|
|
edge = cache.urd_by_concept_dim.get((101, 102))
|
||
|
|
assert edge is not None, "seed edge must be in cache"
|
||
|
|
assert edge.parent_id == edge.dim_id, (
|
||
|
|
f"degenerate edge requires parent_id == dim_id, "
|
||
|
|
f"got parent_id={edge.parent_id}, dim_id={edge.dim_id}"
|
||
|
|
)
|
||
|
|
assert edge.is_isa is False
|
||
|
|
|
||
|
|
def test_seed_recollection_renders_correctly(self):
|
||
|
|
edges = query_edges(101, confidence_floor=0.5, recency_days=90)
|
||
|
|
assert len(edges) == 1
|
||
|
|
line = render_hit("michigan", edges, concept_id=101)
|
||
|
|
# dim token = "usa", parent token = "usa"
|
||
|
|
assert "[usa] usa" in line
|
||
|
|
|
||
|
|
# ------------------------------------------------------------------
|
||
|
|
# Step 2: Cue scanner extracts the incoming ISA triple
|
||
|
|
# ------------------------------------------------------------------
|
||
|
|
|
||
|
|
def test_cue_scanner_extracts_michigan_isa_state(self):
|
||
|
|
prompt = "michigan is a state of USA"
|
||
|
|
triples = scan_cues(prompt)
|
||
|
|
match = next(
|
||
|
|
(t for t in triples if t.subject == "michigan" and t.parent == "state"),
|
||
|
|
None,
|
||
|
|
)
|
||
|
|
assert match is not None, "cue scanner must extract michigan ISA state"
|
||
|
|
assert match.is_isa is True
|
||
|
|
assert match.dimension == "usa" # from "of USA" modifier
|
||
|
|
|
||
|
|
# ------------------------------------------------------------------
|
||
|
|
# Step 3: Incoming ISA in dim:usa collides with existing ISPART in dim:usa
|
||
|
|
# ------------------------------------------------------------------
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_collision_classified_as_misclassification(self):
|
||
|
|
pool, _ = make_mock_pool()
|
||
|
|
captured: list[CollisionInfo] = []
|
||
|
|
|
||
|
|
async def fake_queue(pool, col, priority):
|
||
|
|
captured.append(col)
|
||
|
|
cache.pending_conflicts.add(col.concept_id)
|
||
|
|
|
||
|
|
with patch("festinger.urd_writer._queue_collision", side_effect=fake_queue):
|
||
|
|
req = InsertRequest(
|
||
|
|
concept_id=101, # michigan
|
||
|
|
parent_id=103, # state
|
||
|
|
dim_id=102, # usa — same dim as existing ISPART
|
||
|
|
is_isa=True,
|
||
|
|
confidence=0.85,
|
||
|
|
source="gutask",
|
||
|
|
)
|
||
|
|
collision = await insert_urd_edge(pool, req)
|
||
|
|
|
||
|
|
assert collision is not None
|
||
|
|
assert collision.collision_type == "misclassification", (
|
||
|
|
f"expected misclassification, got {collision.collision_type!r}. "
|
||
|
|
f"existing is_isa={self.seed_edge.is_isa}, incoming is_isa=True"
|
||
|
|
)
|
||
|
|
|
||
|
|
# ------------------------------------------------------------------
|
||
|
|
# Step 4: Recollection renders the [usa?] pending-conflict marker
|
||
|
|
# ------------------------------------------------------------------
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_recollection_shows_pending_marker_after_collision(self):
|
||
|
|
pool, _ = make_mock_pool()
|
||
|
|
|
||
|
|
async def fake_queue(pool, col, priority):
|
||
|
|
cache.pending_conflicts.add(col.concept_id)
|
||
|
|
|
||
|
|
with patch("festinger.urd_writer._queue_collision", side_effect=fake_queue):
|
||
|
|
req = InsertRequest(
|
||
|
|
concept_id=101, parent_id=103, dim_id=102,
|
||
|
|
is_isa=True, confidence=0.85, source="gutask",
|
||
|
|
)
|
||
|
|
await insert_urd_edge(pool, req)
|
||
|
|
|
||
|
|
assert 101 in cache.pending_conflicts
|
||
|
|
block = build_recollection_block([101], confidence_floor=0.5, recency_days=90)
|
||
|
|
assert block is not None
|
||
|
|
assert "[usa?]" in block, (
|
||
|
|
f"recollection must show [usa?] pending marker. Got:\n{block}"
|
||
|
|
)
|
||
|
|
|
||
|
|
# ------------------------------------------------------------------
|
||
|
|
# Step 5: Simulate resolution — move facts to correct dimensions
|
||
|
|
# After nightly job:
|
||
|
|
# michigan ISA state in dim:type (the ISA fact)
|
||
|
|
# michigan ISPART usa in dim:geography (the ISPART fact, moved off degenerate dim)
|
||
|
|
# ------------------------------------------------------------------
|
||
|
|
|
||
|
|
def test_recollection_clean_after_simulated_resolution(self):
|
||
|
|
# Remove the degenerate edge
|
||
|
|
del cache.urd_by_concept_dim[(101, 102)]
|
||
|
|
cache.urd_by_concept[101] = []
|
||
|
|
|
||
|
|
# Insert two correctly-dimensioned edges
|
||
|
|
geo_edge = add_urd(
|
||
|
|
concept_id=101, parent_id=102, dim_id=5, # geography dim
|
||
|
|
is_isa=False, confidence=0.9, source="festinger",
|
||
|
|
)
|
||
|
|
type_edge = add_urd(
|
||
|
|
concept_id=101, parent_id=103, dim_id=1, # type dim
|
||
|
|
is_isa=True, confidence=0.9, source="festinger",
|
||
|
|
)
|
||
|
|
cache.pending_conflicts.discard(101)
|
||
|
|
|
||
|
|
edges = query_edges(101, confidence_floor=0.5, recency_days=90)
|
||
|
|
assert len(edges) == 2
|
||
|
|
|
||
|
|
line = render_hit("michigan", edges, concept_id=101)
|
||
|
|
assert "[geography] usa" in line
|
||
|
|
assert "[type] state" in line
|
||
|
|
assert "?" not in line, "no pending marker after resolution"
|
||
|
|
|
||
|
|
def test_full_recollection_block_after_resolution(self):
|
||
|
|
# Simulate post-resolution state
|
||
|
|
del cache.urd_by_concept_dim[(101, 102)]
|
||
|
|
cache.urd_by_concept[101] = []
|
||
|
|
add_urd(concept_id=101, parent_id=102, dim_id=5, is_isa=False, confidence=0.9, source="festinger")
|
||
|
|
add_urd(concept_id=101, parent_id=103, dim_id=1, is_isa=True, confidence=0.9, source="festinger")
|
||
|
|
cache.pending_conflicts.discard(101)
|
||
|
|
|
||
|
|
block = build_recollection_block([101], confidence_floor=0.5, recency_days=90)
|
||
|
|
assert block is not None
|
||
|
|
assert "<recollection>" in block
|
||
|
|
assert "michigan:" in block
|
||
|
|
assert "[geography] usa" in block
|
||
|
|
assert "[type] state" in block
|
||
|
|
# No pending markers
|
||
|
|
assert "?" not in block
|