Spaces:
Running
Running
Sync ShopStack 2026-06-15 round 2: primary-nav More, undo bar, freshness stamps, safe_render_html, home-flow TabContext
af69759 verified | """End-to-end integration: planner.process() leaves an o/p eval record. | |
| Asserts the wiring in shopstack/planner/engine.py actually persists a | |
| ModelCallRecord to the SQLite sink with the expected domain_route, | |
| capability, and outcome. | |
| """ | |
| from __future__ import annotations | |
| import tempfile | |
| from pathlib import Path | |
| from types import SimpleNamespace | |
| import pytest | |
| from shopstack.eval import ( | |
| CAP_PLANNER_TOOL_CALLING, | |
| ModelCallRecorder, | |
| OUTCOME_SUCCESS, | |
| SHAPE_TOOL_CALLS, | |
| SqliteSink, | |
| ) | |
| from shopstack.eval.checks import EvalCheckRegistry | |
| class _RecordingCheck: | |
| def __init__(self, name: str, passed: bool = True, score: float = 1.0): | |
| from shopstack.eval import CheckResult | |
| self._result = CheckResult(name, passed, score, "ok") | |
| self.calls: list = [] | |
| def __call__(self, record, history, **kwargs): | |
| from shopstack.eval import CheckResult | |
| self.calls.append(record.record_id) | |
| return self._result | |
| class _NoOpRegistry: | |
| def __init__(self): | |
| from shopstack.eval import CheckResult | |
| self.always = CheckResult("noop", True, 1.0, "") | |
| self.history: list = [] | |
| def run(self, record): | |
| self.history.append(record) | |
| return [self.always] | |
| def recorder_with_temp_db(monkeypatch, tmp_path): | |
| """Replace the singleton recorder with one that writes to a temp DB.""" | |
| sqlite = SqliteSink(tmp_path / "test.db") | |
| reg = _NoOpRegistry() | |
| ModelCallRecorder.reset_instance() | |
| ModelCallRecorder._instance = ModelCallRecorder( | |
| jsonl_sink=_NoOpJsonl(), | |
| sqlite_sink=sqlite, | |
| check_registry=reg, | |
| ) | |
| yield sqlite | |
| ModelCallRecorder.reset_instance() | |
| class _NoOpJsonl: | |
| """JsonlSink stub for tests — no real file write.""" | |
| def write(self, record): # pragma: no cover - trivial | |
| pass | |
| def read_all(self): # pragma: no cover - trivial | |
| return [] | |
| def test_planner_process_records_eval(tmp_path, monkeypatch): | |
| """Run planner.process() with a mock provider and assert a record | |
| lands in the SQLite sink with the expected fields.""" | |
| # Use a fresh recorder for this test (don't use the shared fixture | |
| # because we want the planner's call to use the same instance) | |
| sqlite = SqliteSink(tmp_path / "test.db") | |
| reg = _NoOpRegistry() | |
| ModelCallRecorder.reset_instance() | |
| ModelCallRecorder._instance = ModelCallRecorder( | |
| jsonl_sink=_NoOpJsonl(), | |
| sqlite_sink=sqlite, | |
| check_registry=reg, | |
| ) | |
| # Build a minimal in-process planner with a mock provider. | |
| from shopstack.planner.engine import PlannerEngine | |
| # Tiny mock provider that returns a known tool_calls payload. | |
| mock_provider = SimpleNamespace( | |
| plan=lambda payload: { | |
| "tool_calls": [ | |
| {"tool": "respond", "args": {"message": "hello"}}, | |
| ], | |
| "text": '[{"tool": "respond", "args": {"message": "hello"}}]', | |
| "model": "mock-eval-test", | |
| "usage": {"prompt_tokens": 7, "completion_tokens": 3}, | |
| "cost": {"usd": 0.0, "latency_ms": 50.0}, | |
| }, | |
| available=True, | |
| name="mock_eval_provider", | |
| backend="mock", | |
| model_id="mock-eval-test", | |
| ) | |
| mock_registry = SimpleNamespace(planner=mock_provider) | |
| # Real DB + tool registry for a meaningful run. | |
| from shopstack.persistence.database import Database | |
| from shopstack.tools.registry import ToolRegistry | |
| db = Database(db_path=str(tmp_path / "shop.db")) | |
| tools = ToolRegistry(db) | |
| engine = PlannerEngine(db=db, tool_registry=tools, provider_registry=mock_registry) | |
| # Bypass cost guard (we have no real session) | |
| engine._cost_tracker = SimpleNamespace(over_budget=False) # type: ignore[attr-defined] | |
| out = engine.process("test question") | |
| assert "hello" in out or "Planner" in out | |
| # The recorder should have written one row to the SQLite sink | |
| rows = sqlite.query(domain_route="planner") | |
| assert len(rows) == 1 | |
| row = rows[0] | |
| assert row["capability"] == CAP_PLANNER_TOOL_CALLING | |
| assert row["capability_shape"] == SHAPE_TOOL_CALLS | |
| assert row["domain_route"] == "planner" | |
| assert row["model"] == "mock-eval-test" | |
| assert row["backend"] == "mock" | |
| assert row["outcome"] == OUTCOME_SUCCESS | |
| assert row["input_tokens"] == 7 | |
| assert row["output_tokens"] == 3 | |
| assert row["eval_passed"] == 1 | |
| assert row["code_route"].endswith("planner.engine.process") # auto-resolved | |
| assert "test question" in row["prompt"] | |
| def test_planner_process_records_exception_outcome(tmp_path): | |
| """When the provider raises, the recorder captures outcome=exception.""" | |
| from shopstack.eval import OUTCOME_EXCEPTION | |
| sqlite = SqliteSink(tmp_path / "test.db") | |
| reg = _NoOpRegistry() | |
| ModelCallRecorder.reset_instance() | |
| ModelCallRecorder._instance = ModelCallRecorder( | |
| jsonl_sink=_NoOpJsonl(), | |
| sqlite_sink=sqlite, | |
| check_registry=reg, | |
| ) | |
| def _explode(_payload): | |
| raise RuntimeError("provider blew up") | |
| mock_provider = SimpleNamespace( | |
| plan=_explode, | |
| available=True, | |
| name="mock_exploder", | |
| backend="mock", | |
| model_id="mock-ex", | |
| ) | |
| mock_registry = SimpleNamespace(planner=mock_provider) | |
| from shopstack.persistence.database import Database | |
| from shopstack.tools.registry import ToolRegistry | |
| from shopstack.planner.engine import PlannerEngine | |
| db = Database(db_path=str(tmp_path / "shop.db")) | |
| tools = ToolRegistry(db) | |
| engine = PlannerEngine(db=db, tool_registry=tools, provider_registry=mock_registry) | |
| engine._cost_tracker = SimpleNamespace(over_budget=False) # type: ignore[attr-defined] | |
| out = engine.process("this will fail") | |
| assert "error" in out.lower() or "Planner error" in out | |
| rows = sqlite.query(domain_route="planner") | |
| assert len(rows) == 1 | |
| assert rows[0]["outcome"] == OUTCOME_EXCEPTION | |
| assert "provider blew up" in rows[0]["error"] | |