turnstone/tests/context/test_diagnose_context.py
pyr0ball 0311d72e53 feat: dual-backend SQLite/Postgres + multi-tenant source namespacing
- Add app/db/ abstraction layer: Backend enum, DbConn wrapper,
  dialect helper (q() for ? vs %s paramstyle), get_conn(), tenant_id()
- Auto-detect backend from DATABASE_URL; SQLite remains default when
  unset — no config change for local deployments
- Add tenant_id column to all three logical DBs (main, context, incidents);
  idempotent ALTER TABLE migration runs before schema scripts on existing DBs
- All INSERTs inject tenant_id; SELECTs use (tenant_id = ? OR tenant_id = '')
  for backward compat with pre-namespacing rows
- Add docker-compose.yml with named volume turnstone_pgdata (survives rebuilds)
  and optional external Postgres support via DATABASE_URL override
- Add scripts/migrate_sqlite_to_postgres.py — one-shot idempotent migration
  for existing SQLite data; ON CONFLICT DO NOTHING for safe re-runs
- Fix SSH glean path in pipeline.py to use ensure_schema + get_conn
  (was still using raw sqlite3.connect + old _SCHEMA without tenant_id)
- Fix FTS5 JOIN ambiguity: qualify repeat_count as f.repeat_count in search
- Update all tests to use ensure_*_schema fixtures; add row_factory where needed
- 394/394 tests passing

Closes: #42
Closes: #50
2026-06-08 08:37:54 -07:00

97 lines
3.1 KiB
Python

"""Verify context SSE event and LLM prompt injection."""
import asyncio
import sqlite3
from pathlib import Path
from unittest.mock import patch
import pytest
from app.db.schema import ensure_schema, ensure_context_schema
from app.services.llm import summarize
from app.services.search import SearchResult
def _entry(text: str) -> SearchResult:
return SearchResult(
entry_id="x", source_id="svc", sequence=0,
timestamp_iso="2026-05-13T00:00:00+00:00",
severity="ERROR", text=text,
matched_patterns=[], repeat_count=1, out_of_order=False, rank=0.0,
)
def test_summarize_includes_context_block():
captured = {}
def fake_post(url, json=None, headers=None, timeout=None):
captured["json"] = json
raise ConnectionError("offline")
with patch("app.services.llm.httpx.post", side_effect=fake_post):
summarize(
"plex stopped",
[_entry("plex error")],
llm_url="http://localhost:11434",
llm_model="llama3",
context_block="Known environment facts:\n [service] plex: port:32400",
)
messages = captured.get("json", {}).get("messages", [])
content = " ".join(m.get("content", "") for m in messages)
assert "Known environment facts" in content
assert "plex: port:32400" in content
def test_summarize_without_context_block_unchanged():
"""When context_block is None the prompt must not contain the context header."""
captured = {}
def fake_post(url, json=None, headers=None, timeout=None):
captured["json"] = json
raise ConnectionError("offline")
with patch("app.services.llm.httpx.post", side_effect=fake_post):
summarize(
"plex stopped",
[_entry("plex error")],
llm_url="http://localhost:11434",
llm_model="llama3",
context_block=None,
)
messages = captured.get("json", {}).get("messages", [])
content = " ".join(m.get("content", "") for m in messages)
assert "Known environment facts" not in content
@pytest.fixture
def db_with_facts(tmp_path):
db_path = tmp_path / "t.db"
ensure_schema(db_path)
ensure_context_schema(db_path)
conn = sqlite3.connect(str(db_path))
conn.execute(
"INSERT INTO context_facts(id, tenant_id, category, key, value, source, created_at) "
"VALUES (?,?,?,?,?,?,?)",
("f1", "", "service", "plex", "port:32400", "wizard", "2026-05-13T00:00:00+00:00"),
)
conn.commit()
conn.close()
return db_path
def test_diagnose_stream_emits_context_event(db_with_facts):
from app.services.diagnose import diagnose_stream
events = []
async def collect():
async for evt in diagnose_stream(
db_path=db_with_facts,
query="plex stopped",
):
events.append(evt)
asyncio.run(collect())
types = [e["type"] for e in events]
assert "context" in types
ctx_event = next(e for e in events if e["type"] == "context")
assert "facts" in ctx_event
assert any(f["key"] == "plex" for f in ctx_event["facts"])