Problem class: eduos-admin-route-tests-unbounded-n-plus-one-over-fixture-accumulating-shared-test-db
The referenced repo is not mounted in this environment (only /workspace/problem.json exists), so I could not execute against the real tree. The solution below is written to be directly usable with the real file/dependency names substituted where noted. It's saved at /workspace/SOLUTION.md.
Problem class: eduos-admin-route-tests-unbounded-n-plus-one-over-fixture-accumulating-shared-test-db
Scope: admin dashboard route tests (getToken admin leg), full-suite runs only
Fix pattern: make the affected route tests file-local with in-memory repositories (bounded fixtures), preserve route/RBAC coverage, add identity-wiring regression guards. No timeout bumps.
22–25s baseline; under host load it crosses 30s.getToken request into the dashboard endpoints.| Signal | Observation | Meaning |
|---|---|---|
| pg_stat_activity sampling | admin endpoints scan every visible class row |
workload size is a function of table cardinality |
| Timing probe | latency grows with number of class rows |
unbounded O(rows) / N+1, not a constant-factor stall |
| Peak pool connections | 11 / 100 |
pool exhaustion ruled out |
| Lock waits | 0 |
lock contention ruled out |
| Individual query time | none >= 1s |
no single slow query; total is the sum of many fast ones |
| Cross-worker pool survivors | none | leaked pools / connection leak ruled out |
| Isolation run | fast | the DB is small/clean after a targeted run |
| Full suite | accumulates fixtures in shared eduos_test |
row count is monotonic across files and across prior runs |
for loop of queries). This is an N+1: 1 + k·N queries where N = visible class rows.eduos_test Postgres and do not guarantee cleanup. Rows committed by file i remain visible to file j.N is not the 5–20 classes a single test intends; by the time the admin route test runs in a full suite, N includes everything every earlier file (and every prior suite run) left behind.≈ (1 + k·N_total) grows without bound relative to the test's intent. At 22–25s idle, any host-load perturbation tips it over the 30s matcher.N; the shared DB is only "big" after accumulation.>= 1s; it is many small queries.The admin route tests exercise the real FastAPI route against the real app dependency graph, so the DB-backed repository binding makes them read the shared, ever-growing table. Route/RBAC coverage does not actually require the shared DB — it requires a bounded, controllable repository. The fix restores that separation.
N is constant, e.g. 5 classes).app.dependency_overrides fixture with guaranteed restore so overrides cannot leak into other files.# Tests that hit admin routes and request an admin token
rg -l "getToken" tests | xargs rg -l -i "admin"
# Dashboard/stat route definitions and their DB calls
rg -n -i "dashboard|overview|/admin" app/api | rg -n -i "class|select|query"
# Where the DB / repositories are injected
rg -n "def get_db|dependency_overrides|Depends\(" app/api app/db
Identify the exact repository dependency name (examples below use get_class_repository, get_enrollment_repository; substitute the real ones).
tests/support/factories.py — deterministic, bounded fixtures (never touch the shared DB):
"""File-local, in-memory test doubles. No Postgres, no shared state."""
from dataclasses import dataclass, field
@dataclass
class InMemoryClassRepository:
rows: list = field(default_factory=list)
def list_visible(self, user):
return [c for c in self.rows if c.visible]
def count_visible(self, user):
return len(self.list_visible(user))
def get(self, class_id):
return next((c for c in self.rows if c.id == class_id), None)
@dataclass
class InMemoryEnrollmentRepository:
rows: list = field(default_factory=list)
def list_for_classes(self, class_ids):
return [e for e in self.rows if e.class_id in set(class_ids)]
def make_class(i, *, owner_id=1, visible=True):
return type("C", (), {
"id": i,
"owner_id": owner_id,
"visible": visible,
"name": f"class-{i}",
})()
def bounded_fixtures(n_classes=5):
"""Constant-size fixture set: admin latency must not depend on DB size."""
return {
"classes": InMemoryClassRepository([make_class(i) for i in range(n_classes)]),
"enrollments": InMemoryEnrollmentRepository([]),
}
tests/support/file_local_app.py:
from contextlib import contextmanager
from app.main import app
@contextmanager
def file_local_app(**overrides):
"""Install dependency overrides for exactly this test/file, then restore.
Restoring the *whole* dict (not just the keys we set) prevents override
leakage into other test files — a common source of cross-test coupling.
"""
saved = dict(app.dependency_overrides)
app.dependency_overrides.update(overrides)
try:
yield app
finally:
app.dependency_overrides.clear()
app.dependency_overrides.update(saved)
tests/admin/test_dashboard_routes.py (the affected file):
import pytest
from fastapi.testclient import TestClient
from app.api.deps import get_class_repository, get_enrollment_repository
from tests.support.factories import bounded_fixtures
from tests.support.file_local_app import file_local_app
from tests.support.auth import admin_headers # existing helper; keep RBAC coverage
@pytest.fixture
def repos():
return bounded_fixtures(n_classes=5) # bounded, deterministic
@pytest.fixture
def client(repos):
with file_local_app(
**{
get_class_repository: lambda: repos["classes"],
get_enrollment_repository: lambda: repos["enrollments"],
}
):
with TestClient(__import__("app.main", fromlist=["app"]).app) as c:
yield c
# overrides restored by file_local_app on exit
def test_admin_dashboard_ok(client):
# NOTE: a valid ADMIN token is still required — this is the getToken admin leg.
r = client.get("/api/v1/admin/dashboard", headers=admin_headers(role="admin"))
assert r.status_code == 200
body = r.json()
assert body["visible_class_count"] == 5
def test_non_admin_dashboard_forbidden(client):
r = client.get("/api/v1/admin/dashboard", headers=admin_headers(role="teacher"))
assert r.status_code == 403
def test_dashboard_output_is_bounded_by_repo(client, repos):
# Route must not reach beyond the injected repository.
r = client.get("/api/v1/admin/dashboard", headers=admin_headers(role="admin"))
assert r.status_code == 200
assert r.json()["visible_class_count"] == len(repos["classes"].list_visible(None))
Key points:
- The admin token/RBAC path is unchanged (admin_headers), so route and RBAC coverage are preserved.
- The fixture set is 5 rows regardless of eduos_test size.
These assert the file-local wiring cannot silently regress to the shared DB.
# tests/admin/test_dashboard_routes.py
from app.main import app
from app.api.deps import get_class_repository
def test_admin_dashboard_wiring_is_file_local(client, repos):
override = app.dependency_overrides.get(get_class_repository)
assert override is not None, (
"admin dashboard route is no longer wired to a file-local repository; "
"it will read the shared, fixture-accumulating eduos_test DB again"
)
assert override() is repos["classes"], (
"admin dashboard repository override does not match the bounded fixture repo"
)
def test_admin_dashboard_never_touches_shared_engine(client, monkeypatch):
"""Fail loudly if the route reaches the shared DB despite the override."""
from app.db import engine
def boom(*a, **k):
raise AssertionError(
"admin route reached the shared eduos_test engine — file-local "
"repository override regressed; admin test is unbounded again"
)
monkeypatch.setattr(engine, "connect", boom, raising=False)
monkeypatch.setattr(engine, "begin", boom, raising=False)
r = client.get("/api/v1/admin/dashboard", headers=admin_headers(role="admin"))
assert r.status_code == 200
test_admin_dashboard_wiring_is_file_local is the structural guard; test_admin_dashboard_never_touches_shared_engine is the behavioral guard. Together they catch both re-pointing the dependency and bypassing the repository.
The test fix bounds the test; the endpoint is still O(classes) in production. Replace the per-class loop with a set-based query/aggregate:
# Before (conceptual): for each visible class, issue per-class queries.
# After: one set-based aggregate.
def dashboard_summary(db, user):
visible = db.execute(
select(Class.id)
.where(Class.visible.is_(True))
.where(tenant_filter(user))
).scalars().all()
if not visible:
return {"visible_class_count": 0, "enrollment_count": 0}
enrollment_count = db.execute(
select(func.count(Enrollment.id))
.where(Enrollment.class_id.in_(visible))
).scalar_one()
return {"visible_class_count": len(visible), "enrollment_count": enrollment_count}
Treat this as a bonus; the deterministic full-suite fix is §2.3–§2.6.
psql "$EDUOS_TEST_DATABASE_URL" -c "select count(*) as classes from classes;"
# Run the suite once, then:
psql "$EDUOS_TEST_DATABASE_URL" -c "select count(*) as classes from classes;"
# Row count must be >= the starting count (fixtures accumulated).
pytest tests/admin/test_dashboard_routes.py -q --durations=10
# Expect: each admin test well under 2s (previously 22-25s).
Pad eduos_test with extra classes, then run the affected file. Because the tests are file-local, timing must not change.
# Insert e.g. 10,000 visible classes into the shared test DB (outside a transaction).
psql "$EDUOS_TEST_DATABASE_URL" -c \
"insert into classes (name, visible) select 'pad-'||g, true from generate_series(1,10000) g;"
pytest tests/admin/test_dashboard_routes.py -q --durations=10
# Must remain flat (<2s per test). Then clean up:
psql "$EDUOS_TEST_DATABASE_URL" -c "delete from classes where name like 'pad-%';"
This is the regression signature: pre-fix, the same command blows past 30s; post-fix, it is unchanged.
Run the whole suite twice without resetting the DB between runs — the second run is the harsher, accumulated case.
set -o pipefail
for i in 1 2; do
echo "=== full-suite run $i ==="
pytest -q --timeout=30 2>&1 | tee "/tmp/eduos-run-$i.log"
done
Acceptance criteria:
- Both runs exit 0, no test hits the 30s timeout.
- No occurrence of Timeout > 30.0s in either log.
- Admin route tests show constant, DB-size-independent duration across both runs.
Temporarily remove the get_class_repository override and confirm the guard fails for the right reason:
# With override removed, this must fail with the "shared eduos_test engine"
# or "no longer wired to a file-local repository" message, NOT time out.
pytest tests/admin/test_dashboard_routes.py::test_admin_dashboard_never_touches_shared_engine -q
Restore the override afterward.
eduos_test padded to 10k+ classes: admin file still <2s/test.Caveat: I could not run the verification because the EduOS repository is not present in this sandbox. The dependency names (get_class_repository, get_enrollment_repository, /api/v1/admin/dashboard, admin_headers) are placeholders — map them to the real symbols found with the rg commands in §2.2. The structural fix and the decisive DB-padding + two-run verification are otherwise complete and executable as written.
# Evidence - Problem class: eduos-admin-route-tests-unbounded-n-plus-one-over-fixture-accumulating-shared-test-db - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-13T12:03:45.384Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Full-suite-only 30s timeouts in dashboard admin route tests (getToken admin leg). Root cause (evidenced by pg sampling + timing probe): admin dashboard endpoints scan EVERY visible class row; the shared eduos_test Postgres accumulates fixtures across the 195-file suite and prior runs, so the admin workload becomes an unbounded N+1 scaling with DB size (22-25s baseline, host load tips past 30s). Pool exhaustion/lock waits/leaked pools ruled out (peak 11/100 conns, 0 lock waits, 0 queries >=1s, no cross-worker pool survivors). Fix pattern: make affected route tests file-local with in-memory repositories (bounded fixtures), preserving route/RBAC coverage; regression guards assert file-local identity wiring. Avoid timeout-bump masking; validate by 2 consecutive full-suite runs.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "eduos-admin-route-tests-unbounded-n-plus-one-over-fixture-accumulating-shared-test-db", "provider": "openrouter", "solved_at": "2026-09-13T12:03:45.384Z", "version": ""}