From 79e4fd0d4b2b4ffb00641e2f3eedf5a09a2f3b9c Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 7 Sep 2026 15:07:33 +0200 Subject: [PATCH] Load the test schema once and truncate data between files. Co-authored-by: Cursor --- .gitea/workflows/test.yml | 2 +- backend/db.py | 15 ++++++--- backend/db_init.py | 11 +++++++ backend/tests/conftest.py | 32 ++++++++++++------- backend/tests/harness.py | 31 ++++++++++++++++++ backend/tests/test_journal_style_context.py | 5 ++- .../test_journal_style_legacy_immutable.py | 5 ++- .../technical/mvp_implementation.md | 2 +- 8 files changed, 78 insertions(+), 25 deletions(-) diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 78ddf62..6b9d431 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -59,7 +59,7 @@ jobs: export KANSHO_PROVIDER_KEY= export KANSHO_DETECT_PROVIDER_KEY= pip install -q -r requirements-dev.txt - python -m pytest tests -m "not slow" -ra -vv --tb=short + python -m pytest tests -m "not slow" -ra --tb=short ' smoke-dev: diff --git a/backend/db.py b/backend/db.py index a369bfc..23ce2a6 100644 --- a/backend/db.py +++ b/backend/db.py @@ -555,16 +555,21 @@ def _seed_runtime(conn: Any) -> None: seed_generation_instructions(conn) +def refresh_runtime_seed() -> None: + """Re-seed catalog rows after a data truncate. Schema must already exist.""" + with get_db() as conn: + _seed_runtime(conn) + from writing_profile_store import bootstrap_from_existing + + bootstrap_from_existing() + + def init_db() -> None: if use_postgres(): from db_init import ensure_postgres_ready ensure_postgres_ready() - with get_db() as conn: - _seed_runtime(conn) - from writing_profile_store import bootstrap_from_existing - - bootstrap_from_existing() + refresh_runtime_seed() return schema = SCHEMA_PATH.read_text(encoding="utf-8") diff --git a/backend/db_init.py b/backend/db_init.py index 9b4eeb6..40546a6 100644 --- a/backend/db_init.py +++ b/backend/db_init.py @@ -36,6 +36,13 @@ SQLITE_HISTORY_MIGRATIONS = ( "021_voice_legacy_immutable", ) _LEADING_DIGITS = re.compile(r"^(\d{3})_.*\.sql$") +_schema_ready = False + + +def mark_schema_dirty() -> None: + """Call after DROP SCHEMA so the next init_db reloads schema.sql.""" + global _schema_ready + _schema_ready = False def _connect_raw(): @@ -137,13 +144,17 @@ def apply_schema_and_migrations() -> None: def ensure_postgres_ready() -> None: + global _schema_ready if not use_postgres(): return + if _schema_ready: + return wait_for_postgres() try: apply_schema_and_migrations() except Exception: sys.exit(1) + _schema_ready = True def main() -> None: diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index f2a062a..75a5e09 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,9 +1,9 @@ """pytest runner for the existing test_*.py scripts. Each file stays one case (`main()`) until it is split into native pytest -functions. Integration files reset `kansho_test` only; `kansho_dev` is never -the test target. Provider keys and fake flags are restored per file so the -shared pytest process matches the old one-script-per-process isolation. +functions. Schema is loaded once per session; each integration file only +truncates data so kansho_dev is never the target and CI does not reload +schema.sql twenty times. """ from __future__ import annotations @@ -21,6 +21,7 @@ from tests.harness import ( # noqa: E402 apply_module_env, is_postgres_test_database, prepare_test_env, + reset_postgres_data, reset_postgres_schema, ) @@ -64,6 +65,20 @@ class KanshoScriptModule(pytest.Module): yield pytest.Function.from_parent(self, name="test_main", callobj=main) +@pytest.fixture(scope="session") +def _session_postgres(): + if not is_postgres_test_database(): + pytest.skip( + "integration requires KANSHO_DB_BACKEND=postgres and DB_NAME ending _test " + "(Gitea: kansho_test on the Dev Postgres, never kansho_dev)" + ) + reset_postgres_schema() + from db import init_db + + init_db() + yield + + @pytest.fixture(scope="module", autouse=True) def _module_backend(request: pytest.FixtureRequest): source = Path(str(request.path)) @@ -74,15 +89,8 @@ def _module_backend(request: pytest.FixtureRequest): if stem in UNIT_MODULES: yield return - if not is_postgres_test_database(): - pytest.skip( - "integration requires KANSHO_DB_BACKEND=postgres and DB_NAME ending _test " - "(Gitea: kansho_test on the Dev Postgres, never kansho_dev)" - ) - reset_postgres_schema() - from db import init_db - - init_db() + request.getfixturevalue("_session_postgres") + reset_postgres_data() yield finally: os.environ.clear() diff --git a/backend/tests/harness.py b/backend/tests/harness.py index 1d7c478..9de832a 100644 --- a/backend/tests/harness.py +++ b/backend/tests/harness.py @@ -89,6 +89,7 @@ def _require_test_database() -> None: def reset_postgres_schema() -> None: import psycopg + from db_init import mark_schema_dirty from sql_compat import postgres_connect_kwargs kwargs = postgres_connect_kwargs() @@ -103,6 +104,36 @@ def reset_postgres_schema() -> None: conn.execute("GRANT ALL ON SCHEMA public TO CURRENT_USER") finally: conn.close() + mark_schema_dirty() + + +def reset_postgres_data() -> None: + """Clear rows on kansho_test without reloading schema.sql.""" + import psycopg + + from sql_compat import postgres_connect_kwargs + + kwargs = postgres_connect_kwargs() + if "conninfo" in kwargs: + conn = psycopg.connect(kwargs["conninfo"], autocommit=True) + else: + conn = psycopg.connect(autocommit=True, **kwargs) + try: + rows = conn.execute( + """ + SELECT tablename FROM pg_tables + WHERE schemaname = 'public' AND tablename <> 'schema_migrations' + """ + ).fetchall() + names = [row[0] for row in rows] + if names: + joined = ", ".join('"' + name.replace('"', '""') + '"' for name in names) + conn.execute(f"TRUNCATE TABLE {joined} RESTART IDENTITY CASCADE") + finally: + conn.close() + from db import refresh_runtime_seed + + refresh_runtime_seed() def configure_test_engine() -> None: diff --git a/backend/tests/test_journal_style_context.py b/backend/tests/test_journal_style_context.py index 778f828..ce10518 100644 --- a/backend/tests/test_journal_style_context.py +++ b/backend/tests/test_journal_style_context.py @@ -209,10 +209,9 @@ def main() -> None: test_compile_flags() test_budget_omission_in_effective_trace() - from tests.harness import reset_postgres_schema + from tests.harness import reset_postgres_data - reset_postgres_schema() - init_db() + reset_postgres_data() reset_debug() with TestClient(app) as client: setup = client.post( diff --git a/backend/tests/test_journal_style_legacy_immutable.py b/backend/tests/test_journal_style_legacy_immutable.py index dc73739..59fd44a 100644 --- a/backend/tests/test_journal_style_legacy_immutable.py +++ b/backend/tests/test_journal_style_legacy_immutable.py @@ -268,10 +268,9 @@ def test_later_semantic_seed_creates_successor() -> None: def test_legacy_generate_trace_and_prompt() -> None: - from tests.harness import reset_postgres_schema + from tests.harness import reset_postgres_data - reset_postgres_schema() - init_db() + reset_postgres_data() client = TestClient(app) setup = client.post( "/api/auth/setup", diff --git a/docs/architecture/technical/mvp_implementation.md b/docs/architecture/technical/mvp_implementation.md index 89e3f04..ec44905 100644 --- a/docs/architecture/technical/mvp_implementation.md +++ b/docs/architecture/technical/mvp_implementation.md @@ -227,7 +227,7 @@ Gateway-Verfahren und offener Security Layer: `privacy_gateway.md` §9.1–9.2 u Tests beweisen API-Verträge und Invarianten, nicht Dialogqualität. -**Additiv 2026-09-07:** pytest ist der Backend-Runner (`backend/pytest.ini`). Gitea führt `python -m pytest tests -m "not slow"` nach erfolgreichem Dev-Deploy auf `kansho_test` aus. `scripts/test-mvp.ps1` wrappt denselben Aufruf lokal. Unit-Marker laufen ohne Postgres; Integration nie gegen `kansho_dev`. +**Additiv 2026-09-07:** pytest ist der Backend-Runner (`backend/pytest.ini`). Gitea führt `python -m pytest tests -m "not slow"` nach erfolgreichem Dev-Deploy auf `kansho_test` aus. `scripts/test-mvp.ps1` wrappt denselben Aufruf lokal. Unit-Marker laufen ohne Postgres; Integration nie gegen `kansho_dev`. Schema einmal pro Lauf, dazwischen nur Daten-Truncate — nicht 22× `schema.sql`. ---