Skip to content
Open
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
6 changes: 4 additions & 2 deletions agent-kit/references/backend-endpoint-walkthrough.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,7 @@ source of truth.
## Tests

`backend/tests/test_videos.py` runs against a real SQLite database: the session fixture in
`conftest.py` creates every table and sets `TEST_MODE=true` before any test runs, then
tears down afterwards. There is no mocking layer to satisfy.
`conftest.py` creates every table before any test runs, then tears down afterwards. There
is no mocking layer to satisfy. The database is a throwaway `backend/test_db.sqlite3`:
`tests/db_isolation.py` sets `TEST_MODE=true` ahead of every app import so `DATABASE_PATH`
never resolves to the user's real library.
5 changes: 3 additions & 2 deletions agent-kit/skills/add-backend-endpoint/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ Template: `agent-kit/templates/route.py.md`.
## 6. Tests — `backend/tests/test_<resource>.py`

Follow `backend/tests/test_videos.py`. Cover the success path, the empty case, and at least
one failure path. The session fixture in `conftest.py` creates every table and sets
`TEST_MODE=true`, so tests get a real database.
one failure path. The session fixture in `conftest.py` creates every table, so tests get a
real SQLite database. It is a throwaway `backend/test_db.sqlite3`, not the user's library:
`tests/db_isolation.py` sets `TEST_MODE=true` before any app import to redirect it.

## 7. Verify

Expand Down
7 changes: 6 additions & 1 deletion backend/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,12 @@ Miss one of these and it silently half-works:
## Tests

- `tests/test_<resource>.py`. Config in `pytest.ini`: `pythonpath = .`, `testpaths = tests`.
- The session fixture in `conftest.py` creates every table and sets `TEST_MODE=true`.
- `tests/db_isolation.py` sets `TEST_MODE=true` before any app import, which points
`DATABASE_PATH` at a throwaway `backend/test_db.sqlite3`. Never move that import
below the others in `conftest.py`, and never unset `TEST_MODE` mid-run: `settings.py`
resolves the path at import time, so either one sends the suite at the user's real
library, which it then drops and truncates.
- The session fixture in `conftest.py` creates every table.
- Run with `cd backend && pytest`.

## Environment
Expand Down
4 changes: 3 additions & 1 deletion backend/app/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@

TEST_INPUT_PATH = "tests/inputs"
TEST_OUTPUT_PATH = "tests/outputs"
if os.getenv("GITHUB_ACTIONS") == "true":
# TEST_MODE has to be honoured here, not just GITHUB_ACTIONS: CI sets the latter
# for free, so a local pytest run would otherwise write to the real library.
if os.getenv("GITHUB_ACTIONS") == "true" or os.getenv("TEST_MODE") == "true":
DATABASE_PATH = os.path.join(os.getcwd(), "test_db.sqlite3")
else:
DATABASE_PATH = os.path.join(user_data_dir("PictoPy"), "database", "PictoPy.db")
Expand Down
13 changes: 6 additions & 7 deletions backend/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Must come first: it sets TEST_MODE, which settings.py reads at import time to
# keep the suite off the user's real library database.
import tests.db_isolation # noqa: F401

import pytest
import os

# Import database table creation functions
from app.database.faces import db_create_faces_table
Expand All @@ -20,9 +23,6 @@
def setup_before_all_tests():
print("\n=== Running manual setup fixture ===")

# Set test environment
os.environ["TEST_MODE"] = "true"

# Create all database tables in the same order as main.py
print("Creating database tables...")
try:
Expand All @@ -49,6 +49,5 @@ def setup_before_all_tests():
# Teardown code runs after all tests
print("\n=== Running cleanup after all tests ===")

# Cleanup code here
if "TEST_MODE" in os.environ:
del os.environ["TEST_MODE"]
# TEST_MODE stays set: unsetting it would let any late import of settings.py
# resolve DATABASE_PATH back to the real library.
24 changes: 24 additions & 0 deletions backend/tests/db_isolation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Points the database at a throwaway file before anything imports app.config."""

import os

# settings.py resolves DATABASE_PATH at import time, so this has to happen before
# any app import. CI gets the same redirect for free via GITHUB_ACTIONS.
os.environ["TEST_MODE"] = "true"

from app.config.settings import DATABASE_PATH # noqa: E402
from platformdirs import user_data_dir # noqa: E402


def _assert_not_user_library() -> None:
# The suite drops and truncates tables, so a silent redirect failure would
# destroy a real library. Fail the session instead.
library_dir = os.path.abspath(user_data_dir("PictoPy"))
resolved = os.path.abspath(DATABASE_PATH)
if os.path.commonpath([resolved, library_dir]) == library_dir:
raise RuntimeError(
f"Refusing to run tests against the PictoPy library database: {resolved}"
)


_assert_not_user_library()
35 changes: 35 additions & 0 deletions backend/tests/test_db_isolation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import os

import pytest
from platformdirs import user_data_dir

from app.config.settings import DATABASE_PATH
from app.database.connection import DATABASE_PATH as CONNECTION_DATABASE_PATH
from tests import db_isolation


def test_test_mode_redirects_database_path() -> None:
assert os.environ["TEST_MODE"] == "true"
assert os.path.basename(DATABASE_PATH) == "test_db.sqlite3"


def test_database_path_is_outside_the_user_library() -> None:
library_dir = os.path.abspath(user_data_dir("PictoPy"))
resolved = os.path.abspath(DATABASE_PATH)
assert os.path.commonpath([resolved, library_dir]) != library_dir


def test_connection_module_uses_the_redirected_path() -> None:
# The connection module binds DATABASE_PATH at import time, so a redirect that
# lands after it would leave every query pointed at the real library.
assert CONNECTION_DATABASE_PATH == DATABASE_PATH


def test_guard_rejects_a_path_inside_the_user_library(
monkeypatch: pytest.MonkeyPatch,
) -> None:
library_db = os.path.join(user_data_dir("PictoPy"), "database", "PictoPy.db")
monkeypatch.setattr(db_isolation, "DATABASE_PATH", library_db)

with pytest.raises(RuntimeError, match="Refusing to run tests"):
db_isolation._assert_not_user_library()
Loading