Run backend tests with pytest after Dev deploy.
Some checks failed
Deploy Development / deploy (push) Successful in 51s
Test Suite / pytest-backend (push) Failing after 2m32s
Test Suite / smoke-dev (push) Successful in 0s
Test Suite / frontend-build (push) Successful in 15s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Lars 2026-09-07 14:47:04 +02:00
parent 6ea36f7d1f
commit 9d79e3e92f
33 changed files with 188 additions and 150 deletions

View File

@ -2,29 +2,33 @@ name: Test Suite
on: on:
push: push:
branches: [develop, main]
pull_request:
branches: [develop] branches: [develop]
workflow_run:
workflows: ["Deploy Development"]
types: [completed]
workflow_dispatch:
jobs: jobs:
backend-postgres: pytest-backend:
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Backend tests against Dev Postgres (kansho_test) - name: pytest against Dev Postgres (kansho_test)
run: | run: |
set -e set -e
APP_DIR="/home/lars/docker/kansho-dev" APP_DIR="/home/lars/docker/kansho-dev"
COMPOSE_FILE="docker-compose.dev-env.yml" COMPOSE_FILE="docker-compose.dev-env.yml"
echo "backend tests always use Dev Postgres (${APP_DIR})" echo "pytest uses Dev Postgres (${APP_DIR}); event=${{ github.event_name }}"
cd "$APP_DIR" cd "$APP_DIR"
for i in $(seq 1 60); do for i in $(seq 1 60); do
if docker compose -f "$COMPOSE_FILE" exec -T backend true 2>/dev/null; then if [ -f "$APP_DIR/backend/pytest.ini" ] && docker compose -f "$COMPOSE_FILE" exec -T backend true 2>/dev/null; then
echo "backend ready (attempt $i)" echo "backend and pytest.ini ready (attempt $i)"
break break
fi fi
if [ "$i" -eq 60 ]; then if [ "$i" -eq 60 ]; then
echo "timeout waiting for backend" echo "timeout waiting for Dev checkout/backend"
docker compose -f "$COMPOSE_FILE" ps || true docker compose -f "$COMPOSE_FILE" ps || true
ls -la "$APP_DIR/backend" || true
exit 1 exit 1
fi fi
sleep 5 sleep 5
@ -51,24 +55,32 @@ jobs:
backend sh -lc ' backend sh -lc '
set -e set -e
unset KANSHO_DB_PATH unset KANSHO_DB_PATH
for test in tests/test_*.py; do pip install -q -r requirements-dev.txt
echo "=== $test ===" python -m pytest tests -m "not slow" -ra -vv --tb=short
python "$test"
done
' '
frontend-build: smoke-dev:
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Frontend production build on deploy checkout - name: Non-destructive smoke against the running Dev app
run: |
set -e
echo "live Dev API (kansho_dev), read-only health"
curl -sf http://localhost:8096/api/health
echo
curl -sf http://localhost:3096/api/health
echo
echo "LAN smoke OK. Public URL after TLS: https://dev.kansho.jinkendo.de"
frontend-build:
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Frontend production build on Dev checkout
run: | run: |
set -e set -e
EVENT_REF="${{ github.ref_name }}"
BASE_REF="${{ github.base_ref }}"
APP_DIR="/home/lars/docker/kansho"
if [ "$EVENT_REF" = "develop" ] || [ "$BASE_REF" = "develop" ]; then
APP_DIR="/home/lars/docker/kansho-dev" APP_DIR="/home/lars/docker/kansho-dev"
fi
cd "$APP_DIR/frontend" cd "$APP_DIR/frontend"
npm ci npm ci
npm run build npm run build

12
backend/pytest.ini Normal file
View File

@ -0,0 +1,12 @@
[pytest]
testpaths = tests
# Default python collector is disabled; tests/conftest.py collects each
# test_*.py as one case via main(). That keeps the existing scripts until
# they are split into native pytest functions.
python_files = __pytest_default_collector_disabled__.py
python_functions = test_*
addopts = -ra --tb=short
markers =
unit: no Postgres; safe on Windows SQLite checkouts
integration: API/store tests on kansho_test (never kansho_dev)
slow: long-running; omitted in the default Gitea command

View File

@ -0,0 +1,3 @@
# CI / local test runner. Install on top of requirements.txt:
# pip install -r requirements.txt -r requirements-dev.txt
pytest>=8.0

89
backend/tests/conftest.py Normal file
View File

@ -0,0 +1,89 @@
"""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.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from tests.harness import ( # noqa: E402
is_postgres_test_database,
prepare_test_env,
reset_postgres_schema,
)
UNIT_MODULES = frozenset(
{
"test_sql_compat",
"test_local_backup",
"test_provenance",
"test_model_catalog",
"test_privacy_detect_eval",
"test_journal_shape",
"test_journal_body",
"test_writing_profile",
}
)
def pytest_configure(config: pytest.Config) -> None:
prepare_test_env()
def pytest_collect_file(file_path: Path, parent: pytest.Collector):
if file_path.suffix == ".py" and file_path.name.startswith("test_"):
return KanshoScriptModule.from_parent(parent, path=file_path)
return None
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
for item in items:
stem = Path(str(item.path)).stem
marker = pytest.mark.unit if stem in UNIT_MODULES else pytest.mark.integration
item.add_marker(marker)
class KanshoScriptModule(pytest.Module):
def collect(self):
module = self.obj
def test_main() -> None:
main = getattr(module, "main", None)
if main is None or not callable(main):
pytest.fail(f"{self.path.name} has no callable main()")
main()
yield pytest.Function.from_parent(self, name="test_main", callobj=test_main)
@pytest.fixture(scope="module", autouse=True)
def _module_backend(request: pytest.FixtureRequest):
stem = Path(str(request.path)).stem
if stem in UNIT_MODULES:
saved = dict(os.environ)
try:
yield
finally:
os.environ.clear()
os.environ.update(saved)
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()
yield

View File

@ -29,7 +29,24 @@ def _db_name() -> str:
return (os.environ.get("DB_NAME") or "").strip() return (os.environ.get("DB_NAME") or "").strip()
def is_postgres_test_database() -> bool:
if _backend() not in {"postgres", "postgresql", "pg"}:
return False
name = _db_name().lower()
if not name or name in LIVE_DB_NAMES:
return False
return name.endswith("_test")
def prepare_test_env() -> None:
os.environ.setdefault("KANSHO_PROVIDER_KEY", "")
os.environ.setdefault("KANSHO_DETECT_PROVIDER_KEY", "")
os.environ.pop("KANSHO_DB_PATH", None)
def _require_test_database() -> None: def _require_test_database() -> None:
if is_postgres_test_database():
return
backend = _backend() backend = _backend()
if backend not in {"postgres", "postgresql", "pg"}: if backend not in {"postgres", "postgresql", "pg"}:
raise SystemExit( raise SystemExit(
@ -44,7 +61,6 @@ def _require_test_database() -> None:
"DB_NAME fehlt. Tests nutzen auf der Dev-Postgres die Datenbank kansho_test, " "DB_NAME fehlt. Tests nutzen auf der Dev-Postgres die Datenbank kansho_test, "
"nicht kansho_dev." "nicht kansho_dev."
) )
if name in LIVE_DB_NAMES or not name.endswith("_test"):
raise SystemExit( raise SystemExit(
f"DB_NAME={_db_name()!r} ist für Tests gesperrt. " f"DB_NAME={_db_name()!r} ist für Tests gesperrt. "
"Die Suite setzt das Schema zurück und darf daher nicht kansho oder kansho_dev treffen." "Die Suite setzt das Schema zurück und darf daher nicht kansho oder kansho_dev treffen."
@ -71,10 +87,8 @@ def reset_postgres_schema() -> None:
def configure_test_engine() -> None: def configure_test_engine() -> None:
"""Call after sys.path includes backend/, before importing main/db.""" """Reset kansho_test. pytest uses the fixture in tests/conftest.py instead."""
os.environ.setdefault("KANSHO_PROVIDER_KEY", "") prepare_test_env()
os.environ.setdefault("KANSHO_DETECT_PROVIDER_KEY", "")
os.environ.pop("KANSHO_DB_PATH", None)
_require_test_database() _require_test_database()
try: try:
reset_postgres_schema() reset_postgres_schema()

View File

@ -11,10 +11,6 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -11,10 +11,6 @@ ROOT = Path(__file__).resolve().parents[1]
REPO = ROOT.parent REPO = ROOT.parent
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_MEDIA_ROOT"] = str(Path(tempfile.gettempdir()) / "kansho-debug-persist-media") os.environ["KANSHO_MEDIA_ROOT"] = str(Path(tempfile.gettempdir()) / "kansho-debug-persist-media")
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -16,10 +16,6 @@ from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -9,10 +9,6 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from main import app from main import app

View File

@ -10,10 +10,6 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_PROVIDER_KEY"] = "" os.environ["KANSHO_PROVIDER_KEY"] = ""
os.environ["KANSHO_DETECT_PROVIDER_KEY"] = "" os.environ["KANSHO_DETECT_PROVIDER_KEY"] = ""

View File

@ -10,10 +10,6 @@ from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -11,10 +11,6 @@ from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -10,10 +10,6 @@ from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -9,10 +9,6 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -12,10 +12,6 @@ ROOT = Path(__file__).resolve().parents[1]
FRONTEND = ROOT.parent / "frontend" FRONTEND = ROOT.parent / "frontend"
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -11,10 +11,6 @@ from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -11,10 +11,6 @@ from datetime import date
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_MEDIA_ROOT"] = str(Path(tempfile.gettempdir()) / "kansho-opening-media") os.environ["KANSHO_MEDIA_ROOT"] = str(Path(tempfile.gettempdir()) / "kansho-opening-media")
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -10,10 +10,6 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -10,10 +10,6 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -9,10 +9,6 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_MEDIA_ROOT"] = str(Path(tempfile.gettempdir()) / "kansho-trash-media") os.environ["KANSHO_MEDIA_ROOT"] = str(Path(tempfile.gettempdir()) / "kansho-trash-media")
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -9,10 +9,6 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_MEDIA_ROOT"] = str(Path(tempfile.gettempdir()) / "kansho-mvp-journal-media") os.environ["KANSHO_MEDIA_ROOT"] = str(Path(tempfile.gettempdir()) / "kansho-mvp-journal-media")
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -15,10 +15,6 @@ from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -13,10 +13,6 @@ from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -11,10 +11,6 @@ ROOT = Path(__file__).resolve().parents[1]
REPO = ROOT.parent REPO = ROOT.parent
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -9,10 +9,6 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -9,10 +9,6 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -16,10 +16,6 @@ import httpx
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT))
from tests.harness import configure_test_engine
configure_test_engine()
os.environ["KANSHO_FAKE_PROVIDER"] = "1" os.environ["KANSHO_FAKE_PROVIDER"] = "1"
os.environ["KANSHO_FAKE_DETECT"] = "1" os.environ["KANSHO_FAKE_DETECT"] = "1"

View File

@ -20,6 +20,7 @@ Datenübernahme SQLite → Postgres: `backend/sqlite_to_postgres.py` (Probe auf
| **Backend-Port** | 8005 | 8096 | | **Backend-Port** | 8005 | 8096 |
| **PostgreSQL** | nur Compose-Netz, DB `kansho` | nur Compose-Netz, DB `kansho_dev` | | **PostgreSQL** | nur Compose-Netz, DB `kansho` | nur Compose-Netz, DB `kansho_dev` |
| **Domain** | kansho.jinkendo.de | dev.kansho.jinkendo.de | | **Domain** | kansho.jinkendo.de | dev.kansho.jinkendo.de |
| **Öffentliche URL** | https://kansho.jinkendo.de | https://dev.kansho.jinkendo.de |
| **Compose** | `docker-compose.yml` | `docker-compose.dev-env.yml` | | **Compose** | `docker-compose.yml` | `docker-compose.dev-env.yml` |
Prod-Frontend ist **3006**, nicht 3005: auf dem Pi lauscht Bookstack bereits auf `0.0.0.0:3005`. Dev 3096/8096 und Prod-API 8005 waren frei. Prod-Frontend ist **3006**, nicht 3005: auf dem Pi lauscht Bookstack bereits auf `0.0.0.0:3005`. Dev 3096/8096 und Prod-API 8005 waren frei.
@ -46,7 +47,7 @@ cp .env.example .env
# Prod-DB_PASSWORD und Provider-Keys setzen # Prod-DB_PASSWORD und Provider-Keys setzen
``` ```
Host-nginx: `nginx/kansho.conf` und `nginx/kansho-dev.conf` nach `/etc/nginx/sites-available/`, dann `nginx/certbot-setup.sh`. DNS für beide Hostnamen auf den Reverse-Proxy. Host-nginx: `nginx/kansho.conf` und `nginx/kansho-dev.conf` nach `/etc/nginx/sites-available/`, dann `nginx/certbot-setup.sh`. DNS A/AAAA für `dev.kansho.jinkendo.de` und `kansho.jinkendo.de` auf den Reverse-Proxy. Bis TLS steht, bleibt der LAN-Zugriff über die Publish-Ports.
Gitea: Actions aktivieren. Derselbe Pi-Runner wie die Schwesterprodukte (`ubuntu-latest`). Gitea: Actions aktivieren. Derselbe Pi-Runner wie die Schwesterprodukte (`ubuntu-latest`).
@ -60,7 +61,7 @@ Watchtower bleibt aus.
|----------|---------|--------| |----------|---------|--------|
| `deploy-dev.yml` | Push `develop` | Deploy nach `/home/lars/docker/kansho-dev`, Health `localhost:8096` | | `deploy-dev.yml` | Push `develop` | Deploy nach `/home/lars/docker/kansho-dev`, Health `localhost:8096` |
| `deploy-prod.yml` | Push `main` | Deploy nach `/home/lars/docker/kansho`, Health `localhost:8005` | | `deploy-prod.yml` | Push `main` | Deploy nach `/home/lars/docker/kansho`, Health `localhost:8005` |
| `test.yml` | Push `develop`/`main`, PR `develop` | Backend-Tests auf der bestehenden Compose-Postgres in DB `kansho_test` (nicht Live-`kansho_dev`) plus Frontend-Build. Keine Live-Provider-Keys. | | `test.yml` | nach erfolgreichem `Deploy Development` | pytest auf `kansho_test`, nicht-schreibender Smoke gegen die laufende Dev-API, Frontend-Build. Keine Live-Provider-Keys. |
Prod nur über Merge `develop``main`, kein direkter Prod-Schreibzugriff. Prod nur über Merge `develop``main`, kein direkter Prod-Schreibzugriff.

View File

@ -84,7 +84,7 @@ Entsprechen den Interview-Leitfragen Phase F1/F4 und werden hier nicht vorab bea
Additiv, ohne das Fachschema vorzuziehen: Additiv, ohne das Fachschema vorzuziehen:
- Windows-Checkout: SQLite unter `backend/data/` (gitignoriert) für die lokale App ohne Docker. - Windows-Checkout: SQLite unter `backend/data/` (gitignoriert) für die lokale App ohne Docker.
- Test-Suite und Gitea: dieselbe Dev-Postgres-Instanz, Datenbank `kansho_test` neben `kansho_dev` (keine dritte Instanz). Nicht `kansho` / `kansho_dev`, weil die Suite das Schema zurücksetzt. - Test-Suite und Gitea: pytest auf derselben Dev-Postgres-Instanz, Datenbank `kansho_test` neben `kansho_dev` (keine dritte Instanz). Nicht `kansho` / `kansho_dev`, weil die Suite das Schema zurücksetzt. Live-Smoke trifft nur `/api/health` der laufenden Dev-App.
- Docker auf dem Pi: PostgreSQL 16, eigene Instanz je Umgebung, Medien im Compose-Volume `kansho-media` / `dev-kansho-media`. - Docker auf dem Pi: PostgreSQL 16, eigene Instanz je Umgebung, Medien im Compose-Volume `kansho-media` / `dev-kansho-media`.
- Identity-Mappings bleiben Klasse A in der Local Trusted Zone (jetzt: Pi-Postgres, nicht Provider). - Identity-Mappings bleiben Klasse A in der Local Trusted Zone (jetzt: Pi-Postgres, nicht Provider).
- Kein Shared-Schema mit Mitai. - Kein Shared-Schema mit Mitai.

View File

@ -164,6 +164,8 @@ Offen, vor Compose festlegen — **beantwortet 2026-09-07:**
**Additiv 2026-09-07 (Test-Engine):** Zwei Postgres-Instanzen bleiben Dev und Prod. Die Gitea-Suite läuft auf der Dev-Postgres in der Datenbank `kansho_test` (neben `kansho_dev`), nicht gegen SQLite und nicht gegen das persönliche `kansho_dev`-Journal. Die Suite setzt dort das Schema zurück. SQLite bleibt die Windows-App ohne Docker und das lokale Backup-Format. **Additiv 2026-09-07 (Test-Engine):** Zwei Postgres-Instanzen bleiben Dev und Prod. Die Gitea-Suite läuft auf der Dev-Postgres in der Datenbank `kansho_test` (neben `kansho_dev`), nicht gegen SQLite und nicht gegen das persönliche `kansho_dev`-Journal. Die Suite setzt dort das Schema zurück. SQLite bleibt die Windows-App ohne Docker und das lokale Backup-Format.
**Additiv 2026-09-07 (pytest + öffentliche URLs):** Kanonischer Runner ist `python -m pytest tests` nach Dev-Deploy. `scripts/test-mvp.ps1` wrappt pytest lokal. Produkt-URLs nach Nginx/TLS: `https://dev.kansho.jinkendo.de` und `https://kansho.jinkendo.de`.
Empfohlene Reihenfolge in Welle 2: Empfohlene Reihenfolge in Welle 2:
1. Offene Host-Fragen beantworten und in `runtime_and_deploy.md` §1/§6 eintragen. 1. Offene Host-Fragen beantworten und in `runtime_and_deploy.md` §1/§6 eintragen.

View File

@ -37,7 +37,7 @@ Lokal, eine Instanz, ein Profil-Nutzer nach Setup:
Proxy: Vite leitet `/api` an 8018. CORS erlaubt `localhost:5188`. Proxy: Vite leitet `/api` an 8018. CORS erlaubt `localhost:5188`.
**Abweichung vom technischen Zielrahmen (lokal):** `product_frame_and_stack.md` nennt PostgreSQL. Der Slice auf dem Windows-Checkout bleibt SQLite (`5188`/`8018`). **Additiv 2026-09-07:** Docker/Server nutzt PostgreSQL 16 (`KANSHO_DB_BACKEND=postgres`). Stores behalten SQLite-förmiges SQL; `sql_compat.py` übersetzt zur Laufzeit. Nummerierte Dateien ab `backend/migrations/022_*.sql`. Siehe `runtime_and_deploy.md` und `docs/DEPLOYMENT.md`. **Additiv (Test-Suite):** API- und Store-Tests setzen `tests/harness.py` auf die Dev-Postgres, Datenbank `kansho_test` neben `kansho_dev` (keine dritte Instanz). Gitea `test.yml` trifft nicht das persönliche `kansho_dev`-Journal. SQLite-Testdateien sind abgelöst; `test_local_backup.py` prüft weiter das Windows-Backup-Format. **Abweichung vom technischen Zielrahmen (lokal):** `product_frame_and_stack.md` nennt PostgreSQL. Der Slice auf dem Windows-Checkout bleibt SQLite (`5188`/`8018`). **Additiv 2026-09-07:** Docker/Server nutzt PostgreSQL 16 (`KANSHO_DB_BACKEND=postgres`). Stores behalten SQLite-förmiges SQL; `sql_compat.py` übersetzt zur Laufzeit. Nummerierte Dateien ab `backend/migrations/022_*.sql`. Siehe `runtime_and_deploy.md` und `docs/DEPLOYMENT.md`. **Additiv (Qualitätssystem 2026-09-07):** Kanonischer Backend-Runner ist pytest im Dev-Checkout nach erfolgreichem Deploy. `python -m pytest tests -m "not slow"` gegen `kansho_test`. `scripts/test-mvp.ps1` ist nur noch der lokale Wrapper (Unit ohne Postgres, Integration nur mit `DB_NAME=kansho_test`). SQLite-Testdateien sind abgelöst; `test_local_backup.py` prüft weiter das Windows-Backup-Format.
Health: `GET /api/health`. Health: `GET /api/health`.
@ -223,11 +223,11 @@ Gateway-Verfahren und offener Security Layer: `privacy_gateway.md` §9.19.2 u
| Erster Journal-Impuls | `backend/tests/test_journal_opening.py` | | Erster Journal-Impuls | `backend/tests/test_journal_opening.py` |
| Writing-Profile-Hülle / Initial Build | `backend/tests/test_profile_review.py`, `backend/tests/test_profile_governance.py` | | Writing-Profile-Hülle / Initial Build | `backend/tests/test_profile_review.py`, `backend/tests/test_profile_governance.py` |
| Markdown-Runde | `frontend/src/journal/document.test.js` | | Markdown-Runde | `frontend/src/journal/document.test.js` |
| Lokaler Abnahmelauf | `scripts/test-mvp.ps1` | | Lokaler Wrapper | `scripts/test-mvp.ps1` (pytest, kein kanonischer Runner) |
Tests beweisen API-Verträge und Invarianten, nicht Dialogqualität. Tests beweisen API-Verträge und Invarianten, nicht Dialogqualität.
**Additiv 2026-09-07:** Die Backend-Suite in Gitea läuft auf der Dev-Postgres in `kansho_test` (Harness `backend/tests/harness.py`). `scripts/test-mvp.ps1` führt Store-/API-Tests lokal nur mit `KANSHO_DB_BACKEND=postgres` und `DB_NAME=kansho_test` aus; Unit-Tests ohne Datenbank laufen weiter. Kanonisch bleibt der Pi-Lauf. **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`.
--- ---
@ -279,7 +279,7 @@ Additiv zum Slice, 2026-08-25. Kein Target-Model-Vorbau.
Additiv zum Slice. Fachliche Abnahme: `../functional/mvp_freeze_candidate.md`. Additiv zum Slice. Fachliche Abnahme: `../functional/mvp_freeze_candidate.md`.
- Lokaler Testeinstieg `scripts/test-mvp.ps1` (UTF-8, isolierte Temp-Daten, keine Live-Keys; Fake nur in den Suites, die ihn setzen). - Lokaler Wrapper `scripts/test-mvp.ps1` (pytest; UTF-8, isolierte Temp-Daten, keine Live-Keys; Fake nur in den Suites, die ihn setzen). Kanonisch: Gitea pytest auf `kansho_test`.
- Backup: `backend/local_backup.py`, Einstieg `scripts/backup-local.ps1`. SQLite-Backup-API, Medien, Manifest, keine Secrets. Restore bestätigt, prüft Checksummen, legt Sicherheitsbackup an, bricht bei geöffneter DB ab. - Backup: `backend/local_backup.py`, Einstieg `scripts/backup-local.ps1`. SQLite-Backup-API, Medien, Manifest, keine Secrets. Restore bestätigt, prüft Checksummen, legt Sicherheitsbackup an, bricht bei geöffneter DB ab.
- Papierkorb: `GET /spaces/{id}/entries`, `GET /spaces/{id}/trash`, `POST /entries/{id}/undelete`, `POST /entries/{id}/purge`. - Papierkorb: `GET /spaces/{id}/entries`, `GET /spaces/{id}/trash`, `POST /entries/{id}/undelete`, `POST /entries/{id}/purge`.
- Erster Impuls: `journal_opening.py`, derselbe Gateway-Pfad, `{{opening_hint}}`. **Additiv 2026-09-03:** Vorhaben nur aus dem user-Dialog des vorigen Kalendertags (`calendar_date` an `space_recent_sources`); Journal-Entries und ältere Recency sind kein Opening-Mandat. Der Opening-Call setzt `include_space_recency=False`. Tests: abgeschlossener Entry mit „nächsten Tag“ und Plan von vorgestern → `local_neutral`; „Morgen wollen wir …“ nur am Folgetag → Modellpfad. **Additiv 2026-09-07:** Prefix-Recency (5×400 Entries, 3×400 Source-Conversations) bleibt im *laufenden* Dialogzug; das ist kein Opening-Mandat. Ein Nachsatz mit Morgenuhrzeit darf den Folgetag nicht über den Entry-Anfang steuern. - Erster Impuls: `journal_opening.py`, derselbe Gateway-Pfad, `{{opening_hint}}`. **Additiv 2026-09-03:** Vorhaben nur aus dem user-Dialog des vorigen Kalendertags (`calendar_date` an `space_recent_sources`); Journal-Entries und ältere Recency sind kein Opening-Mandat. Der Opening-Call setzt `include_space_recency=False`. Tests: abgeschlossener Entry mit „nächsten Tag“ und Plan von vorgestern → `local_neutral`; „Morgen wollen wir …“ nur am Folgetag → Modellpfad. **Additiv 2026-09-07:** Prefix-Recency (5×400 Entries, 3×400 Source-Conversations) bleibt im *laufenden* Dialogzug; das ist kein Opening-Mandat. Ein Nachsatz mit Morgenuhrzeit darf den Folgetag nicht über den Entry-Anfang steuern.

View File

@ -77,12 +77,14 @@ Mitai-Pipeline als Vorlage:
```text ```text
push develop → deploy-dev.yml → compose build/up → Healthcheck push develop → deploy-dev.yml → compose build/up → Healthcheck
push/PR nach Tests → test.yml (pytest, Frontend-Build) successful Dev deploy → test.yml (pytest on kansho_test, Live-Smoke /api/health, Frontend-Build)
merge in main → deploy-prod.yml merge in main → deploy-prod.yml
``` ```
**Status: entschieden.** Workflows liegen unter `.gitea/workflows/` (`deploy-dev.yml`, `deploy-prod.yml`, `test.yml`). Muster analog Kairo: `git reset --hard`, `build --no-cache`, Health `GET /api/health`. **Status: entschieden.** Workflows liegen unter `.gitea/workflows/` (`deploy-dev.yml`, `deploy-prod.yml`, `test.yml`). Muster analog Kairo: `git reset --hard`, `build --no-cache`, Health `GET /api/health`.
**Additiv 2026-09-07 (Qualitätssystem):** Backend-Runner ist pytest (`backend/pytest.ini`, `backend/tests/conftest.py`). Gitea startet die Suite erst nach erfolgreichem Dev-Deploy (`workflow_run` auf `Deploy Development`), nicht parallel zum Image-Build. Destruktive Tests bleiben auf `kansho_test`. Zusätzlich ein nicht-schreibender Smoke gegen die laufende Dev-API (`kansho_dev`, nur `/api/health`). Öffentliche URLs nach Host-Nginx/TLS: `https://dev.kansho.jinkendo.de` und `https://kansho.jinkendo.de`. LAN-Ports 3096/8096 und 3006/8005 bleiben die Compose-Publish-Ziele.
Kanshō-Repo liegt auf Gitea (`Lars/Kansho`). HTTPS-Push ist eingerichtet. Der Pi-Runner (`ubuntu-latest`) ist derselbe wie bei Mitai/Shinkan/Kairo. Kanshō-Repo liegt auf Gitea (`Lars/Kansho`). HTTPS-Push ist eingerichtet. Der Pi-Runner (`ubuntu-latest`) ist derselbe wie bei Mitai/Shinkan/Kairo.
## 5. Was Deploy nicht übernimmt ## 5. Was Deploy nicht übernimmt
@ -103,7 +105,8 @@ Kanshō-Repo liegt auf Gitea (`Lars/Kansho`). HTTPS-Push ist eingerichtet. Der P
| Postgres | eigene Instanz je Umgebung (`kansho` / `kansho_dev`), kein Shared-Schema | entschieden 2026-09-07 | | Postgres | eigene Instanz je Umgebung (`kansho` / `kansho_dev`), kein Shared-Schema | entschieden 2026-09-07 |
| Gitea Workflows | `.gitea/workflows/` analog Kairo | entschieden 2026-09-07 | | Gitea Workflows | `.gitea/workflows/` analog Kairo | entschieden 2026-09-07 |
| Auto-Rollback | nein | verworfen | | Auto-Rollback | nein | verworfen |
| Dual-Backend | SQLite nur Windows-App ohne Docker; Server Dev+Prod und Test-Suite PostgreSQL. Tests auf der Dev-Instanz in DB `kansho_test` neben `kansho_dev` (keine dritte Instanz) | entschieden 2026-09-07, Tests nachgezogen | | Dual-Backend | SQLite nur Windows-App ohne Docker; Server Dev+Prod PostgreSQL. pytest auf der Dev-Instanz in DB `kansho_test` neben `kansho_dev` (keine dritte Instanz) | entschieden 2026-09-07, pytest 2026-09-07 |
| Öffentliche URLs | `https://dev.kansho.jinkendo.de` / `https://kansho.jinkendo.de` (Host-Nginx + Let's Encrypt; Einrichtung parallel) | entschieden 2026-09-07 |
| Urlaubs-Laptop ohne Docker/SQLite | Phase A auf dem Heimrechner restored | dokumentiert 2026-09-07 | | Urlaubs-Laptop ohne Docker/SQLite | Phase A auf dem Heimrechner restored | dokumentiert 2026-09-07 |
| Transfer vor Postgres | SQLite-Restore auf dem Heimrechner zuerst | entschieden als Reihenfolge | | Transfer vor Postgres | SQLite-Restore auf dem Heimrechner zuerst | entschieden als Reihenfolge |

View File

@ -80,39 +80,29 @@ function Test-PostgresSuite {
return ($backend -in @("postgres", "postgresql", "pg")) -and $name.EndsWith("_test") return ($backend -in @("postgres", "postgresql", "pg")) -and $name.EndsWith("_test")
} }
Write-Host "Kansho local MVP tests" Write-Host "Kansho local tests (pytest wrapper)"
Write-Host "Python: $py" Write-Host "Python: $py"
Write-Host "Isolation: $($iso.Base)" Write-Host "Isolation: $($iso.Base)"
Write-Host "No live provider/detect calls. Production DB/media untouched." Write-Host "No live provider/detect calls. Production DB/media untouched."
Write-Host "Fail-closed stays testable; suites that need a fake provider set it themselves." Write-Host "Canonical backend suite: Gitea pytest on kansho_test after Dev deploy."
$runBackend = Test-PostgresSuite $runBackend = Test-PostgresSuite
if ($runBackend) { if ($runBackend) {
Write-Host "Backend tests: Postgres $($env:DB_NAME)" Write-Host "Backend pytest: Postgres $($env:DB_NAME)"
} else { } else {
Write-Host "Backend Postgres-Suite: SKIP (KANSHO_DB_BACKEND=postgres und DB_NAME=kansho_test auf der Dev-Postgres; kanonisch Gitea auf dem Pi). Reine Unit-Tests ohne DB laufen trotzdem." Write-Host "Backend pytest: unit only (set KANSHO_DB_BACKEND=postgres and DB_NAME=kansho_test for integration)."
} }
try { try {
$backendTests = Get-ChildItem -Path (Join-Path $root "backend\tests") -Filter "test_*.py" | Sort-Object Name Invoke-Step -Name "backend pytest" -Action {
if (-not $backendTests) { Push-Location (Join-Path $root "backend")
throw "no backend/tests/test_*.py files found"
}
foreach ($test in $backendTests) {
$needsPg = $false
try { try {
$needsPg = [bool](Select-String -Path $test.FullName -Pattern "configure_test_engine" -Quiet) if ($runBackend) {
} catch { & $py -m pytest tests -m "not slow" -ra --tb=short
$needsPg = $true } else {
& $py -m pytest tests -m "unit and not slow" -ra --tb=short
} }
if ($needsPg -and -not $runBackend) { } finally {
$script:ran++ Pop-Location
$script:passed++
$script:results += [pscustomobject]@{ Name = "backend $($test.Name)"; Status = "SKIP" }
continue
}
Invoke-Step -Name "backend $($test.Name)" -Action {
& $py $test.FullName
} }
} }
@ -161,5 +151,5 @@ Write-Host ("{0} von {1} Schritten erfolgreich." -f $passed, $ran)
if ($failed) { if ($failed) {
exit 1 exit 1
} }
Write-Host "MVP local acceptance: OK" Write-Host "local tests: OK"
exit 0 exit 0