- 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
97 lines
3.1 KiB
Python
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"])
|