Skip to content

Commit b24e24d

Browse files
committed
feat: add write execution tests
1 parent f8ae39a commit b24e24d

18 files changed

Lines changed: 965 additions & 33 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on:
55
pull_request:
66

77
jobs:
8-
write-validation:
8+
tests:
99
runs-on: ubuntu-latest
1010
services:
1111
db:
@@ -41,13 +41,13 @@ jobs:
4141
python -m pip install --upgrade pip
4242
pip install -r requirements.txt
4343
44-
- name: Run write validation tests
45-
run: ./scripts/run_write_validation_tests
44+
- name: Run tests
45+
run: ./scripts/run_tests
4646

4747
- name: Add visualization report to summary
4848
if: always()
4949
run: |
50-
REPORT="tests/visualization/artifacts/write_validation_status.md"
50+
REPORT="tests/visualization/artifacts/tests_status.md"
5151
if [ -f "$REPORT" ]; then
5252
cat "$REPORT" >> "$GITHUB_STEP_SUMMARY"
5353
else
@@ -58,6 +58,6 @@ jobs:
5858
if: always()
5959
uses: actions/upload-artifact@v4
6060
with:
61-
name: write-validation-status-report
62-
path: tests/visualization/artifacts/write_validation_status.md
61+
name: tests-status-report
62+
path: tests/visualization/artifacts/tests_status.md
6363
if-no-files-found: warn

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ temp/
6464
# Local runtime state for this repo
6565
.memory/
6666
artifacts/
67+
!tests/visualization/artifacts/
68+
!tests/visualization/artifacts/*.md
6769
*.db
6870
*.db-*
6971
*.sqlite

‎app/core/policies/write_policy/pipeline.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ def _build_create_plan(payload: dict[str, Any], *, embedding_model: str) -> list
177177

178178
for association in (memory.get("links") or {}).get("associations", []):
179179
confidence = association.get("confidence")
180+
salience = association.get("salience")
180181
plan.append(
181182
make_side_effect(
182183
"association.upsert_and_observe",
@@ -192,7 +193,7 @@ def _build_create_plan(payload: dict[str, Any], *, embedding_model: str) -> list
192193
"observation_id": str(uuid4()),
193194
"observation_source": "agent_explicit",
194195
"valence": confidence if confidence is not None else 0.5,
195-
"salience": association.get("salience", 0.5),
196+
"salience": salience if salience is not None else 0.5,
196197
"evidence_refs": list(memory["evidence_refs"]),
197198
},
198199
)
@@ -240,6 +241,7 @@ def _build_update_plan(payload: dict[str, Any]) -> list[dict[str, Any]]:
240241

241242
if update_type == "association_link":
242243
confidence = update.get("confidence")
244+
salience = update.get("salience")
243245
return [
244246
make_side_effect(
245247
"association.upsert_and_observe",
@@ -255,7 +257,7 @@ def _build_update_plan(payload: dict[str, Any]) -> list[dict[str, Any]]:
255257
"observation_id": str(uuid4()),
256258
"observation_source": "agent_explicit",
257259
"valence": confidence if confidence is not None else 0.5,
258-
"salience": update.get("salience", 0.5),
260+
"salience": salience if salience is not None else 0.5,
259261
"evidence_refs": list(update.get("evidence_refs", [])),
260262
},
261263
)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,4 @@ with psycopg.connect(dsn, autocommit=True) as conn:
4242
PY
4343

4444
alembic upgrade head
45-
python3 -m pytest -q tests/operations/write/validation
45+
python3 -m pytest -q tests -m "not real_embedding"
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
"""Shared fixtures for write execution integration tests."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
import subprocess
7+
from collections.abc import Callable, Iterator
8+
from pathlib import Path
9+
10+
import pytest
11+
from sqlalchemy import select, text
12+
from sqlalchemy.engine import Engine
13+
from sqlalchemy.orm import sessionmaker
14+
from sqlalchemy.sql import Selectable
15+
16+
from app.core.entities.memory import Memory, MemoryKind, MemoryScope
17+
from app.core.interfaces.embeddings import IEmbeddingProvider
18+
from app.periphery.db.engine import get_engine
19+
from app.periphery.db.models.registry import target_metadata
20+
from app.periphery.db.session import get_session_factory
21+
from app.periphery.db.uow import PostgresUnitOfWork
22+
23+
24+
class _StubEmbeddingProvider(IEmbeddingProvider):
25+
"""Deterministic embedding provider for integration tests."""
26+
27+
def embed(self, text: str) -> list[float]:
28+
length = float(len(text) % 7) / 10.0
29+
return [length, length + 0.1, length + 0.2, length + 0.3]
30+
31+
32+
@pytest.fixture(scope="session")
33+
def db_dsn() -> str:
34+
"""Resolve integration database DSN from environment."""
35+
36+
dsn = os.getenv("MEMORY_DB_DSN_TEST")
37+
if not dsn:
38+
pytest.skip("Set MEMORY_DB_DSN_TEST to run PostgreSQL integration tests.")
39+
return dsn
40+
41+
42+
@pytest.fixture(scope="session")
43+
def integration_engine(db_dsn: str) -> Iterator[Engine]:
44+
"""Create and migrate integration engine once per test session."""
45+
46+
engine = get_engine(db_dsn)
47+
_run_alembic_upgrade(db_dsn)
48+
yield engine
49+
engine.dispose()
50+
51+
52+
@pytest.fixture(scope="session")
53+
def integration_session_factory(integration_engine: Engine) -> sessionmaker:
54+
"""Build reusable session factory for integration tests."""
55+
56+
return get_session_factory(integration_engine)
57+
58+
59+
@pytest.fixture(autouse=True)
60+
def clear_database(integration_engine: Engine) -> Iterator[None]:
61+
"""Truncate all tables between integration tests."""
62+
63+
table_names = [table.name for table in reversed(target_metadata.sorted_tables)]
64+
if table_names:
65+
joined = ", ".join(table_names)
66+
with integration_engine.begin() as conn:
67+
conn.execute(text(f"TRUNCATE TABLE {joined} RESTART IDENTITY CASCADE;"))
68+
yield
69+
70+
71+
@pytest.fixture
72+
def uow_factory(integration_session_factory: sessionmaker) -> Callable[[], PostgresUnitOfWork]:
73+
"""Provide unit-of-work factory bound to integration database."""
74+
75+
def _factory() -> PostgresUnitOfWork:
76+
return PostgresUnitOfWork(integration_session_factory)
77+
78+
return _factory
79+
80+
81+
@pytest.fixture
82+
def stub_embedding_provider() -> IEmbeddingProvider:
83+
"""Provide deterministic embedding provider for execution tests."""
84+
85+
return _StubEmbeddingProvider()
86+
87+
88+
@pytest.fixture
89+
def seed_memory(uow_factory: Callable[[], PostgresUnitOfWork]) -> Callable[..., Memory]:
90+
"""Provide helper for seeding memory rows into integration database."""
91+
92+
def _seed(
93+
*,
94+
memory_id: str,
95+
repo_id: str,
96+
scope: MemoryScope,
97+
kind: MemoryKind,
98+
text_value: str,
99+
confidence: float | None = 0.7,
100+
) -> Memory:
101+
memory = Memory(
102+
id=memory_id,
103+
repo_id=repo_id,
104+
scope=scope,
105+
kind=kind,
106+
text=text_value,
107+
create_confidence=confidence,
108+
)
109+
with uow_factory() as uow:
110+
uow.memories.create(memory)
111+
return memory
112+
113+
return _seed
114+
115+
116+
@pytest.fixture
117+
def count_rows(integration_engine: Engine) -> Callable[[str], int]:
118+
"""Provide helper for counting rows in a table."""
119+
120+
def _count(table_name: str) -> int:
121+
with integration_engine.connect() as conn:
122+
return int(conn.execute(text(f"SELECT COUNT(*) FROM {table_name};")).scalar_one())
123+
124+
return _count
125+
126+
127+
@pytest.fixture
128+
def fetch_rows(integration_engine: Engine) -> Callable[[Selectable, object], list[dict[str, object]]]:
129+
"""Provide helper for selecting rows from a SQLAlchemy table with optional where clauses."""
130+
131+
def _fetch(table: Selectable, *conditions: object) -> list[dict[str, object]]:
132+
stmt = select(table)
133+
if conditions:
134+
stmt = stmt.where(*conditions)
135+
with integration_engine.connect() as conn:
136+
return [dict(row) for row in conn.execute(stmt).mappings().all()]
137+
138+
return _fetch
139+
140+
141+
def _run_alembic_upgrade(dsn: str) -> None:
142+
"""Run alembic upgrade head against integration database."""
143+
144+
repo_root = _find_repo_root()
145+
env = dict(os.environ)
146+
env["MEMORY_DB_DSN"] = dsn
147+
subprocess.run(
148+
["alembic", "upgrade", "head"],
149+
check=True,
150+
cwd=repo_root,
151+
env=env,
152+
)
153+
154+
155+
def _find_repo_root() -> Path:
156+
"""Resolve repository root by walking upward until alembic.ini is found."""
157+
158+
current = Path(__file__).resolve().parent
159+
while current != current.parent:
160+
if (current / "alembic.ini").exists():
161+
return current
162+
current = current.parent
163+
raise RuntimeError("Could not resolve repository root from execution test fixtures.")

0 commit comments

Comments
 (0)