Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions packages/studyloop/tests/_sessions_db_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""One migrated ``sessions.db`` template per process, copied per test.

``studyloop.history._connection._connect`` creates the sessions database from
``SCHEMA_FILE`` and runs every migration the first time any caller opens a
path that does not exist yet. A test module that points ``STUDYLOOP_DB`` at a
fresh ``tmp_path / "sessions.db"`` therefore pays that bootstrap on its first
write — ~60 ms and a 1 MB, 75-table file per test on a laptop, and on a slow
runner disk the first step to stall (CI run 35350636500 timed out two seam
tests inside ``plan new`` creating the database). Seeding the path from a
template turns the bootstrap into one buffered file copy.

This is deliberately *not* a shared live database. ``test_plan_application.py``
(council review 1, F6) needs "no history" to be a fact about the test rather
than about what a shared file holds, so every test still gets its own file at
its own path; only the schema bootstrap is shared.

The template is built through the production ``_connect`` — not a copy of its
steps — so a seeded database is what a genuine first connect would have
produced (``test_sessions_db_template.py`` pins the schema and
``user_version`` against a fresh bootstrap). ``connect_db`` sets
``journal_mode=WAL`` on it; the connection is closed before the first copy so
SQLite has checkpointed and removed the ``-wal``/``-shm`` sidecars, and the
main file is the whole database.

Kept out of ``conftest.py`` for the same reason ``_readiness.py`` and
``_vault_isolation.py`` are: the helper is the subject of a guard test, and a
test cannot import symbols from a conftest in a way a type checker resolves.
"""

from __future__ import annotations

import atexit
import shutil
import tempfile
from pathlib import Path

import pytest

_TEMPLATE: Path | None = None


def _build(destination: Path) -> Path:
"""Bootstrap ``destination`` exactly as the first production connect would."""
from studyloop.history import _connection

with pytest.MonkeyPatch.context() as env:
env.setenv("STUDYLOOP_DB", str(destination))
# An absent config file falls through to defaults, so the config's own
# ``session_db`` key — which outranks STUDYLOOP_DB by design — cannot
# route the bootstrap at a test's or the learner's database.
env.setenv("STUDYLOOP_CONFIG", str(destination.parent / "no-config.yaml"))
conn = _connection._connect()
if conn is None: # pragma: no cover - agent-session-tools is a hard dependency here
raise RuntimeError("could not bootstrap the sessions.db template")
conn.close()
return destination


def template_path() -> Path:
"""The process-wide migrated template; built on first use."""
global _TEMPLATE
if _TEMPLATE is None:
root = Path(tempfile.mkdtemp(prefix="studyloop-test-sessions-template-"))
atexit.register(shutil.rmtree, root, ignore_errors=True)
_TEMPLATE = _build(root / "sessions.db")
return _TEMPLATE


def seed_sessions_db(destination: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Copy the template to ``destination`` and point ``STUDYLOOP_DB`` at it.

The one-line replacement for ``monkeypatch.setenv("STUDYLOOP_DB",
str(tmp_path / "sessions.db"))``: same path, same isolation, no bootstrap
on the test's first write.
"""
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(template_path(), destination)
monkeypatch.setenv("STUDYLOOP_DB", str(destination))
return destination
3 changes: 2 additions & 1 deletion packages/studyloop/tests/test_agent_prompt_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import click
import pytest
import typer
from _sessions_db_template import seed_sessions_db
from typer.testing import CliRunner

runner = CliRunner()
Expand Down Expand Up @@ -455,7 +456,7 @@ def test_bridge_add_persists_to_the_database_not_config_yaml(tmp_path, monkeypat
)
before = config_path.read_bytes()
monkeypatch.setenv("STUDYLOOP_CONFIG", str(config_path))
monkeypatch.setenv("STUDYLOOP_DB", str(tmp_path / "sessions.db"))
seed_sessions_db(tmp_path / "sessions.db", monkeypatch)

result = click_runner.invoke(
cli,
Expand Down
3 changes: 2 additions & 1 deletion packages/studyloop/tests/test_cli_doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import click
import pytest
from _sessions_db_template import seed_sessions_db
from click.testing import CliRunner

from studyloop.doctor.models import CheckResult
Expand Down Expand Up @@ -201,7 +202,7 @@ def _isolated_plans(self, tmp_path, monkeypatch):
from studyloop.planning import store

monkeypatch.setenv(store.PLANS_DIR_ENV, str(tmp_path / "study-plans"))
monkeypatch.setenv("STUDYLOOP_DB", str(tmp_path / "sessions.db"))
seed_sessions_db(tmp_path / "sessions.db", monkeypatch)
self.plans_dir = store.plans_dir()

def test_doctor_names_each_active_but_unready_plan_with_its_blockers(self) -> None:
Expand Down
3 changes: 2 additions & 1 deletion packages/studyloop/tests/test_cli_plan_seam.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import re

import pytest
from _sessions_db_template import seed_sessions_db
from click.testing import CliRunner

from studyloop.cli import cli
Expand All @@ -51,7 +52,7 @@ def isolated_plans_dir(tmp_path, monkeypatch):

@pytest.fixture(autouse=True)
def isolated_checkpoint_db(tmp_path, monkeypatch):
monkeypatch.setenv("STUDYLOOP_DB", str(tmp_path / "sessions.db"))
seed_sessions_db(tmp_path / "sessions.db", monkeypatch)


@pytest.fixture
Expand Down
3 changes: 2 additions & 1 deletion packages/studyloop/tests/test_mcp_plan_record_seam.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

pytest.importorskip("mcp")

from _sessions_db_template import seed_sessions_db
from mcp.server.fastmcp.exceptions import ToolError

from studyloop.planning import (
Expand All @@ -38,7 +39,7 @@ def isolated_plans_dir(tmp_path, monkeypatch):
def isolated_checkpoint_db(tmp_path, monkeypatch):
"""``store.create_plan`` refreshes the derived index in the sessions
database; keep that off any developer database (council review 2, F10)."""
monkeypatch.setenv("STUDYLOOP_DB", str(tmp_path / "sessions.db"))
seed_sessions_db(tmp_path / "sessions.db", monkeypatch)


def _tool():
Expand Down
3 changes: 2 additions & 1 deletion packages/studyloop/tests/test_mcp_plan_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@

pytest.importorskip("mcp")

from _sessions_db_template import seed_sessions_db
from mcp.server.fastmcp.exceptions import ToolError

from studyloop.planning import (
Expand Down Expand Up @@ -121,7 +122,7 @@ def isolated_plans_dir(tmp_path, monkeypatch):
def isolated_checkpoint_db(tmp_path, monkeypatch):
"""``store.create_plan`` refreshes the derived index in the sessions
database; keep that off any developer database (council review 2, F10)."""
monkeypatch.setenv("STUDYLOOP_DB", str(tmp_path / "sessions.db"))
seed_sessions_db(tmp_path / "sessions.db", monkeypatch)


@pytest.fixture
Expand Down
3 changes: 2 additions & 1 deletion packages/studyloop/tests/test_now_plan_guidance.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from typing import TYPE_CHECKING

import pytest
from _sessions_db_template import seed_sessions_db

from studyloop.learning import decision
from studyloop.learning.decision import PlanRef, _Candidate, build_now_plan
Expand Down Expand Up @@ -76,7 +77,7 @@ def isolate_now_world(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
encoding="utf-8",
)
monkeypatch.setenv("STUDYLOOP_CONFIG", str(config))
monkeypatch.setenv("STUDYLOOP_DB", str(tmp_path / "sessions.db"))
seed_sessions_db(tmp_path / "sessions.db", monkeypatch)
monkeypatch.setenv("STUDYLOOP_STATE_DIR", str(tmp_path / "state"))
monkeypatch.setenv(store.PLANS_DIR_ENV, str(tmp_path / "study-plans"))
monkeypatch.setattr(decision, "datetime", _FrozenDatetime)
Expand Down
3 changes: 2 additions & 1 deletion packages/studyloop/tests/test_plan_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import json

import pytest
from _sessions_db_template import seed_sessions_db

from studyloop.planning import store
from studyloop.planning.application import PlanApplication
Expand Down Expand Up @@ -49,7 +50,7 @@ def isolated_plans_dir(tmp_path, monkeypatch):
def isolated_checkpoint_db(tmp_path, monkeypatch):
"""A fresh checkpoint database per test, so "no history" is a fact about
this test rather than about what the suite's shared database holds (F6)."""
monkeypatch.setenv("STUDYLOOP_DB", str(tmp_path / "sessions.db"))
seed_sessions_db(tmp_path / "sessions.db", monkeypatch)
return tmp_path / "sessions.db"


Expand Down
3 changes: 2 additions & 1 deletion packages/studyloop/tests/test_plan_application_mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import json

import pytest
from _sessions_db_template import seed_sessions_db

from studyloop.planning import index as index_module
from studyloop.planning import store
Expand Down Expand Up @@ -63,7 +64,7 @@ def isolated_plans_dir(tmp_path, monkeypatch):
@pytest.fixture(autouse=True)
def isolated_checkpoint_db(tmp_path, monkeypatch):
"""A fresh checkpoint database per test (council review 1, F6)."""
monkeypatch.setenv("STUDYLOOP_DB", str(tmp_path / "sessions.db"))
seed_sessions_db(tmp_path / "sessions.db", monkeypatch)
return tmp_path / "sessions.db"


Expand Down
3 changes: 2 additions & 1 deletion packages/studyloop/tests/test_plan_guidance.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from datetime import UTC, date, datetime, timedelta

import pytest
from _sessions_db_template import seed_sessions_db

from studyloop.planning import store
from studyloop.planning.application import PlanApplication
Expand All @@ -44,7 +45,7 @@ def isolated_plans_dir(tmp_path, monkeypatch):

@pytest.fixture(autouse=True)
def isolated_checkpoint_db(tmp_path, monkeypatch):
monkeypatch.setenv("STUDYLOOP_DB", str(tmp_path / "sessions.db"))
seed_sessions_db(tmp_path / "sessions.db", monkeypatch)


@pytest.fixture
Expand Down
3 changes: 2 additions & 1 deletion packages/studyloop/tests/test_plan_intent_snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@
from collections.abc import Mapping

import pytest
from _sessions_db_template import seed_sessions_db

from studyloop.planning import CreatePlan, PlanApplication, store


@pytest.fixture(autouse=True)
def isolated_world(tmp_path, monkeypatch):
monkeypatch.setenv(store.PLANS_DIR_ENV, str(tmp_path / "study-plans"))
monkeypatch.setenv("STUDYLOOP_DB", str(tmp_path / "sessions.db"))
seed_sessions_db(tmp_path / "sessions.db", monkeypatch)


def _answers() -> dict[str, object]:
Expand Down
3 changes: 2 additions & 1 deletion packages/studyloop/tests/test_plan_journey_combined.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
pytest.importorskip("fastapi")
pytest.importorskip("mcp")

from _sessions_db_template import seed_sessions_db
from fastapi.testclient import TestClient # pyright: ignore[reportMissingImports]

from studyloop.planning import store
Expand Down Expand Up @@ -111,7 +112,7 @@ def plans_dir(tmp_path, monkeypatch) -> Path:
"""One plans directory for BOTH the Web routes and the MCP tools — the
point of the journey is that they see the same documents."""
monkeypatch.setenv(store.PLANS_DIR_ENV, str(tmp_path / "study-plans"))
monkeypatch.setenv("STUDYLOOP_DB", str(tmp_path / "sessions.db"))
seed_sessions_db(tmp_path / "sessions.db", monkeypatch)
return tmp_path / "study-plans"


Expand Down
5 changes: 3 additions & 2 deletions packages/studyloop/tests/test_plan_recording_failures.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from __future__ import annotations

import pytest
from _sessions_db_template import seed_sessions_db

from studyloop.planning import evaluation as evaluation_module
from studyloop.planning import index as index_module
Expand All @@ -33,8 +34,8 @@ def isolated_plans_dir(tmp_path, monkeypatch):

@pytest.fixture(autouse=True)
def isolated_checkpoint_db(tmp_path, monkeypatch):
"""A fresh sessions database per test; the schema is created on first connect."""
monkeypatch.setenv("STUDYLOOP_DB", str(tmp_path / "sessions.db"))
"""A fresh sessions database per test, seeded from the migrated template."""
seed_sessions_db(tmp_path / "sessions.db", monkeypatch)
return tmp_path / "sessions.db"


Expand Down
3 changes: 2 additions & 1 deletion packages/studyloop/tests/test_session_start_purpose.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

pytest.importorskip("fastapi")

from _sessions_db_template import seed_sessions_db
from fastapi.testclient import TestClient # pyright: ignore[reportMissingImports]

from studyloop.planning import store
Expand Down Expand Up @@ -112,7 +113,7 @@ def isolated_plans_dir(tmp_path, monkeypatch):

@pytest.fixture(autouse=True)
def _isolated_db(tmp_path, monkeypatch):
monkeypatch.setenv("STUDYLOOP_DB", str(tmp_path / "sessions.db"))
seed_sessions_db(tmp_path / "sessions.db", monkeypatch)


@pytest.fixture()
Expand Down
Loading
Loading