588 lines
20 KiB
Python
588 lines
20 KiB
Python
import datetime
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from nanoid import generate as generate_nanoid
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from src import crud, models, schemas
|
|
from src.config import settings
|
|
from src.crud.representation import (
|
|
get_peer_card,
|
|
get_working_representation,
|
|
get_working_representation_data,
|
|
set_peer_card,
|
|
set_working_representation,
|
|
)
|
|
from src.exceptions import ResourceNotFoundException
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_peer_card_get_set_roundtrip(
|
|
db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer]
|
|
):
|
|
"""Roundtrip set/get for peer card on a valid peer."""
|
|
workspace, peer = sample_data
|
|
|
|
# Initially absent
|
|
assert await get_peer_card(db_session, workspace.name, peer.name) is None
|
|
|
|
# Set and read back
|
|
value_1 = ["Initial peer card text"]
|
|
await set_peer_card(db_session, workspace.name, peer.name, value_1)
|
|
assert await get_peer_card(db_session, workspace.name, peer.name) == value_1
|
|
|
|
# Update and read back
|
|
value_2 = ["Updated peer card text", "Another line"]
|
|
await set_peer_card(db_session, workspace.name, peer.name, value_2)
|
|
assert await get_peer_card(db_session, workspace.name, peer.name) == value_2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_set_peer_card_missing_peer_raises(
|
|
db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer]
|
|
):
|
|
"""Setting a peer card for a non-existent peer should raise ResourceNotFoundException."""
|
|
workspace, _existing_peer = sample_data
|
|
with pytest.raises(ResourceNotFoundException):
|
|
await set_peer_card(db_session, workspace.name, "missing-peer", ["card"])
|
|
|
|
|
|
async def _create_session_with_peers(
|
|
db_session: AsyncSession, workspace: models.Workspace
|
|
) -> tuple[models.Session, models.Peer, models.Peer]:
|
|
"""Create a session with two peers and return (session, observer, observed)."""
|
|
observer = models.Peer(name=str(generate_nanoid()), workspace_name=workspace.name)
|
|
observed = models.Peer(name=str(generate_nanoid()), workspace_name=workspace.name)
|
|
db_session.add_all([observer, observed])
|
|
await db_session.flush()
|
|
|
|
session_name = str(generate_nanoid())
|
|
session = await crud.get_or_create_session(
|
|
db_session,
|
|
schemas.SessionCreate(
|
|
name=session_name,
|
|
peers={
|
|
observer.name: schemas.SessionPeerConfig(),
|
|
observed.name: schemas.SessionPeerConfig(),
|
|
},
|
|
),
|
|
workspace.name,
|
|
)
|
|
await db_session.commit()
|
|
return session, observer, observed
|
|
|
|
|
|
def _make_wr_payload(
|
|
*,
|
|
explicit: list[str],
|
|
deductive: list[dict[str, Any]] | None = None,
|
|
thinking: str | None = None,
|
|
message_id: str = "m-new",
|
|
created_at: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Build a structured working representation dict payload."""
|
|
return {
|
|
"final_observations": {
|
|
"explicit": explicit,
|
|
"deductive": deductive or [],
|
|
"thinking": thinking,
|
|
},
|
|
"message_id": message_id,
|
|
"created_at": created_at
|
|
or datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_working_representation_self_merge_and_trim(
|
|
db_session: AsyncSession,
|
|
sample_data: tuple[models.Workspace, models.Peer],
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
):
|
|
"""Merging a self working representation appends and trims to 25, and updates metadata fields."""
|
|
workspace, peer = sample_data
|
|
|
|
# Align limit with current implementation and future config usage
|
|
monkeypatch.setattr(
|
|
settings.DERIVER, "WORKING_REPRESENTATION_MAX_OBSERVATIONS", 25, raising=False
|
|
)
|
|
LIMIT = settings.DERIVER.WORKING_REPRESENTATION_MAX_OBSERVATIONS
|
|
|
|
# Create a session with the single peer
|
|
session = await crud.get_or_create_session(
|
|
db_session,
|
|
schemas.SessionCreate(
|
|
name=str(generate_nanoid()), peers={peer.name: schemas.SessionPeerConfig()}
|
|
),
|
|
workspace.name,
|
|
)
|
|
await db_session.commit()
|
|
|
|
# Existing explicit: 24 items 0..23
|
|
existing = _make_wr_payload(
|
|
explicit=[f"E{i}" for i in range(24)],
|
|
deductive=[{"conclusion": f"D{i}", "premises": [f"P{i}"]} for i in range(10)],
|
|
thinking="old-think",
|
|
message_id="m-old",
|
|
created_at=datetime.datetime(
|
|
2024, 1, 1, tzinfo=datetime.timezone.utc
|
|
).isoformat(),
|
|
)
|
|
await set_working_representation(
|
|
db_session, existing, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
|
|
# New explicit: 3 items 24..26, deductive two new items
|
|
new_payload = _make_wr_payload(
|
|
explicit=["E24", "E25", "E26"],
|
|
deductive=[
|
|
{"conclusion": "D_new1", "premises": []},
|
|
{"conclusion": "D_new2", "premises": ["PX"]},
|
|
],
|
|
thinking="new-think",
|
|
message_id="m-new",
|
|
created_at=datetime.datetime(
|
|
2025, 1, 1, tzinfo=datetime.timezone.utc
|
|
).isoformat(),
|
|
)
|
|
await set_working_representation(
|
|
db_session, new_payload, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
|
|
# Verify merged raw data
|
|
raw = await get_working_representation_data(
|
|
db_session, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
assert isinstance(raw, dict)
|
|
final = raw["final_observations"]
|
|
|
|
# Explicit should be last LIMIT of 24+3 (drop oldest overflow)
|
|
explicit = final["explicit"]
|
|
total_explicit = 24 + 3
|
|
expected_len = min(LIMIT, total_explicit)
|
|
dropped = max(0, total_explicit - LIMIT)
|
|
assert len(explicit) == expected_len
|
|
assert explicit[0] == f"E{dropped}"
|
|
assert explicit[-1] == "E26"
|
|
|
|
# Deductive should be appended and capped to LIMIT
|
|
deductive = final["deductive"]
|
|
assert len(deductive) == min(LIMIT, 12)
|
|
assert deductive[-2]["conclusion"] == "D_new1"
|
|
assert deductive[-1]["conclusion"] == "D_new2"
|
|
|
|
# Thinking and metadata should reflect latest
|
|
assert final.get("thinking") == "new-think"
|
|
assert raw.get("message_id") == "m-new"
|
|
created_at_value = raw.get("created_at")
|
|
assert created_at_value is not None
|
|
assert created_at_value.startswith("2025-01-01")
|
|
|
|
# Formatted string getter returns sections and bullets
|
|
formatted = await get_working_representation(
|
|
db_session, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
assert "EXPLICIT OBSERVATIONS:" in formatted
|
|
assert "DEDUCTIVE OBSERVATIONS:" in formatted
|
|
assert "- E26" in formatted
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_working_representation_directional_merge_and_keys(
|
|
db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer]
|
|
):
|
|
"""Directional working representations are stored under observer_observed key and merge correctly."""
|
|
workspace, _ = sample_data
|
|
session, observer, observed = await _create_session_with_peers(
|
|
db_session, workspace
|
|
)
|
|
|
|
# Store initial
|
|
first = _make_wr_payload(explicit=["A", "B"], thinking="first")
|
|
await set_working_representation(
|
|
db_session, first, workspace.name, observer.name, observed.name, session.name
|
|
)
|
|
|
|
# Merge second
|
|
second = _make_wr_payload(
|
|
explicit=["C"],
|
|
deductive=[{"conclusion": "Z", "premises": ["p1", "p2"]}],
|
|
thinking="second",
|
|
)
|
|
await set_working_representation(
|
|
db_session, second, workspace.name, observer.name, observed.name, session.name
|
|
)
|
|
|
|
# Fetch raw and assert structure
|
|
raw = await get_working_representation_data(
|
|
db_session, workspace.name, observer.name, observed.name, session.name
|
|
)
|
|
assert isinstance(raw, dict)
|
|
final = raw["final_observations"]
|
|
assert final["explicit"] == ["A", "B", "C"]
|
|
assert final["deductive"][-1]["conclusion"] == "Z"
|
|
assert final.get("thinking") == "second"
|
|
|
|
# Validate it's stored under the derived key in SessionPeer.internal_metadata
|
|
derived_key = f"{observer.name}_{observed.name}"
|
|
stmt = select(models.SessionPeer).where(
|
|
models.SessionPeer.peer_name == observer.name,
|
|
models.SessionPeer.session_name == session.name,
|
|
models.SessionPeer.workspace_name == workspace.name,
|
|
)
|
|
result = await db_session.execute(stmt)
|
|
sp = result.scalar_one()
|
|
assert derived_key in sp.internal_metadata
|
|
assert sp.internal_metadata[derived_key]["final_observations"]["explicit"] == [
|
|
"A",
|
|
"B",
|
|
"C",
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wr_string_roundtrip_then_structured_overrides(
|
|
db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer]
|
|
):
|
|
"""String WR stores as-is and later structured merge does not incorporate old string content."""
|
|
workspace, peer = sample_data
|
|
session = await crud.get_or_create_session(
|
|
db_session,
|
|
schemas.SessionCreate(
|
|
name=str(generate_nanoid()), peers={peer.name: schemas.SessionPeerConfig()}
|
|
),
|
|
workspace.name,
|
|
)
|
|
await db_session.commit()
|
|
|
|
# Store legacy string representation first
|
|
raw_string = "legacy working rep text"
|
|
await set_working_representation(
|
|
db_session, raw_string, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
|
|
# Ensure get returns the same string
|
|
assert (
|
|
await get_working_representation(
|
|
db_session, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
== raw_string
|
|
)
|
|
|
|
# Now store structured representation; merge should ignore old string and just store new structured
|
|
structured = _make_wr_payload(explicit=["X", "Y"], deductive=[])
|
|
await set_working_representation(
|
|
db_session, structured, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
|
|
raw = await get_working_representation_data(
|
|
db_session, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
assert isinstance(raw, dict)
|
|
assert raw["final_observations"]["explicit"] == ["X", "Y"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wr_missing_levels_and_empty_new_lists(
|
|
db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer]
|
|
):
|
|
"""Merge handles missing levels and empty new lists; result formatting is empty string."""
|
|
workspace, peer = sample_data
|
|
session = await crud.get_or_create_session(
|
|
db_session,
|
|
schemas.SessionCreate(
|
|
name=str(generate_nanoid()), peers={peer.name: schemas.SessionPeerConfig()}
|
|
),
|
|
workspace.name,
|
|
)
|
|
await db_session.commit()
|
|
|
|
# Existing has only deductive; explicit missing
|
|
existing = {
|
|
"final_observations": {
|
|
"deductive": [{"conclusion": "old", "premises": ["p"]}],
|
|
},
|
|
"message_id": "m-old",
|
|
"created_at": datetime.datetime(
|
|
2024, 1, 2, tzinfo=datetime.timezone.utc
|
|
).isoformat(),
|
|
}
|
|
await set_working_representation(
|
|
db_session, existing, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
|
|
# New has empty lists and no thinking
|
|
new_payload = _make_wr_payload(
|
|
explicit=[], deductive=[], thinking=None, message_id="m-new"
|
|
)
|
|
await set_working_representation(
|
|
db_session, new_payload, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
|
|
raw = await get_working_representation_data(
|
|
db_session, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
assert isinstance(raw, dict)
|
|
final = raw["final_observations"]
|
|
# Deductive remains as old (no additions), explicit remains missing/empty and thinking None
|
|
assert final["deductive"] == [{"conclusion": "old", "premises": ["p"]}]
|
|
assert final["explicit"] == []
|
|
assert final.get("thinking") is None
|
|
# Formatter includes the remaining deductive observation
|
|
formatted = await get_working_representation(
|
|
db_session, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
assert "DEDUCTIVE OBSERVATIONS:" in formatted
|
|
assert "- old (based on: p)" in formatted
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wr_trim_boundary_exact_25_plus_one(
|
|
db_session: AsyncSession,
|
|
sample_data: tuple[models.Workspace, models.Peer],
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
):
|
|
"""Exactly 25 existing + 1 new yields last 25, preserving order and including the new last element."""
|
|
workspace, peer = sample_data
|
|
monkeypatch.setattr(
|
|
settings.DERIVER, "WORKING_REPRESENTATION_MAX_OBSERVATIONS", 25, raising=False
|
|
)
|
|
LIMIT = settings.DERIVER.WORKING_REPRESENTATION_MAX_OBSERVATIONS
|
|
session = await crud.get_or_create_session(
|
|
db_session,
|
|
schemas.SessionCreate(
|
|
name=str(generate_nanoid()), peers={peer.name: schemas.SessionPeerConfig()}
|
|
),
|
|
workspace.name,
|
|
)
|
|
await db_session.commit()
|
|
|
|
existing = _make_wr_payload(
|
|
explicit=[f"E{i}" for i in range(LIMIT)],
|
|
deductive=[{"conclusion": f"D{i}", "premises": []} for i in range(LIMIT)],
|
|
)
|
|
await set_working_representation(
|
|
db_session, existing, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
|
|
new_payload = _make_wr_payload(
|
|
explicit=[f"E{LIMIT}"],
|
|
deductive=[{"conclusion": f"D{LIMIT}", "premises": []}],
|
|
thinking="t2",
|
|
message_id="m2",
|
|
)
|
|
await set_working_representation(
|
|
db_session, new_payload, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
|
|
raw = await get_working_representation_data(
|
|
db_session, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
assert isinstance(raw, dict)
|
|
final = raw["final_observations"]
|
|
assert final["explicit"] == [f"E{i}" for i in range(1, LIMIT + 1)]
|
|
assert final["explicit"][-1] == f"E{LIMIT}"
|
|
assert len(final["deductive"]) == LIMIT
|
|
assert final["deductive"][-1]["conclusion"] == f"D{LIMIT}"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wr_formatting_rules_mixed_observation_types(
|
|
db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer]
|
|
):
|
|
"""Formatting includes section headers, bullets, premise display, and content fallback."""
|
|
workspace, peer = sample_data
|
|
session = await crud.get_or_create_session(
|
|
db_session,
|
|
schemas.SessionCreate(
|
|
name=str(generate_nanoid()), peers={peer.name: schemas.SessionPeerConfig()}
|
|
),
|
|
workspace.name,
|
|
)
|
|
await db_session.commit()
|
|
|
|
payload = {
|
|
"final_observations": {
|
|
"explicit": ["likes pizza", {"content": "runs daily"}],
|
|
"deductive": [
|
|
{
|
|
"conclusion": "is healthy",
|
|
"premises": ["runs daily", "eats veggies"],
|
|
},
|
|
{"content": "fallback without conclusion"},
|
|
],
|
|
"thinking": "t",
|
|
},
|
|
"message_id": "m1",
|
|
"created_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
}
|
|
await set_working_representation(
|
|
db_session, payload, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
|
|
formatted = await get_working_representation(
|
|
db_session, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
# Section headers present
|
|
assert formatted.splitlines()[0] == "EXPLICIT OBSERVATIONS:"
|
|
assert "DEDUCTIVE OBSERVATIONS:" in formatted
|
|
# Bullets present for strings and dict content fallback
|
|
assert "- likes pizza" in formatted
|
|
assert "- runs daily" in formatted
|
|
# Deductive with premises shows based on
|
|
assert "is healthy (based on: runs daily; eats veggies)" in formatted
|
|
# Deductive dict without conclusion falls back to content
|
|
assert "- fallback without conclusion" in formatted
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wr_legacy_key_fallback_for_self_representation(
|
|
db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer]
|
|
):
|
|
"""If only the legacy key is present, data retrieval falls back appropriately."""
|
|
workspace, peer = sample_data
|
|
session = await crud.get_or_create_session(
|
|
db_session,
|
|
schemas.SessionCreate(
|
|
name=str(generate_nanoid()), peers={peer.name: schemas.SessionPeerConfig()}
|
|
),
|
|
workspace.name,
|
|
)
|
|
await db_session.commit()
|
|
|
|
# Manually write legacy key on SessionPeer
|
|
stmt = select(models.SessionPeer).where(
|
|
models.SessionPeer.peer_name == peer.name,
|
|
models.SessionPeer.session_name == session.name,
|
|
models.SessionPeer.workspace_name == workspace.name,
|
|
)
|
|
result = await db_session.execute(stmt)
|
|
sp: models.SessionPeer = result.scalar_one()
|
|
# Assign a new dict so JSON mutation is tracked & persisted
|
|
sp.internal_metadata = {
|
|
**(sp.internal_metadata or {}),
|
|
"global_representation": "legacy-global",
|
|
}
|
|
await db_session.commit()
|
|
|
|
# Retrieval should see legacy value
|
|
raw = await get_working_representation_data(
|
|
db_session, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
assert raw == "legacy-global"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wr_empty_both_levels_formats_empty_string(
|
|
db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer]
|
|
):
|
|
"""When both explicit and deductive are empty after merge, formatted string is empty."""
|
|
workspace, peer = sample_data
|
|
session = await crud.get_or_create_session(
|
|
db_session,
|
|
schemas.SessionCreate(
|
|
name=str(generate_nanoid()), peers={peer.name: schemas.SessionPeerConfig()}
|
|
),
|
|
workspace.name,
|
|
)
|
|
await db_session.commit()
|
|
|
|
empty_payload = _make_wr_payload(explicit=[], deductive=[], thinking=None)
|
|
await set_working_representation(
|
|
db_session, empty_payload, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
|
|
formatted = await get_working_representation(
|
|
db_session, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
assert formatted == ""
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wr_existing_dict_without_final_observations(
|
|
db_session: AsyncSession, sample_data: tuple[models.Workspace, models.Peer]
|
|
):
|
|
"""Existing dict lacking final_observations merges cleanly with new structured payload."""
|
|
workspace, peer = sample_data
|
|
session = await crud.get_or_create_session(
|
|
db_session,
|
|
schemas.SessionCreate(
|
|
name=str(generate_nanoid()), peers={peer.name: schemas.SessionPeerConfig()}
|
|
),
|
|
workspace.name,
|
|
)
|
|
await db_session.commit()
|
|
|
|
await set_working_representation(
|
|
db_session,
|
|
{"some": "field"},
|
|
workspace.name,
|
|
peer.name,
|
|
peer.name,
|
|
session.name,
|
|
)
|
|
new_payload = _make_wr_payload(
|
|
explicit=["n1"], deductive=[{"conclusion": "c1", "premises": []}]
|
|
)
|
|
await set_working_representation(
|
|
db_session, new_payload, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
|
|
raw = await get_working_representation_data(
|
|
db_session, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
assert isinstance(raw, dict)
|
|
final = raw["final_observations"]
|
|
assert final["explicit"] == ["n1"]
|
|
assert final["deductive"][0]["conclusion"] == "c1"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wr_deductive_trim_when_no_new_items(
|
|
db_session: AsyncSession,
|
|
sample_data: tuple[models.Workspace, models.Peer],
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
):
|
|
"""If existing deductive > 25 and no new entries, merge still trims to last 25."""
|
|
workspace, peer = sample_data
|
|
monkeypatch.setattr(
|
|
settings.DERIVER, "WORKING_REPRESENTATION_MAX_OBSERVATIONS", 25, raising=False
|
|
)
|
|
LIMIT = settings.DERIVER.WORKING_REPRESENTATION_MAX_OBSERVATIONS
|
|
session = await crud.get_or_create_session(
|
|
db_session,
|
|
schemas.SessionCreate(
|
|
name=str(generate_nanoid()), peers={peer.name: schemas.SessionPeerConfig()}
|
|
),
|
|
workspace.name,
|
|
)
|
|
await db_session.commit()
|
|
|
|
over = LIMIT + 5
|
|
existing = _make_wr_payload(
|
|
explicit=[],
|
|
deductive=[{"conclusion": f"D{i}", "premises": []} for i in range(over)],
|
|
)
|
|
await set_working_representation(
|
|
db_session, existing, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
|
|
# Merge empty new; should trim to last 25 existing
|
|
new_payload = _make_wr_payload(explicit=[], deductive=[])
|
|
await set_working_representation(
|
|
db_session, new_payload, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
|
|
raw = await get_working_representation_data(
|
|
db_session, workspace.name, peer.name, peer.name, session.name
|
|
)
|
|
assert isinstance(raw, dict)
|
|
final = raw["final_observations"]
|
|
assert len(final["deductive"]) == LIMIT
|
|
# Oldest retained index is over - LIMIT
|
|
oldest_kept = over - LIMIT
|
|
assert final["deductive"][0]["conclusion"] == f"D{oldest_kept}"
|
|
assert final["deductive"][-1]["conclusion"] == f"D{over - 1}"
|