test+fix: make the suite run cleanly in the test container
Five fixes uncovered by actually running the suite in docker-compose.test.yml:
1. (real prod bug) PATCH /api/settings/digest mutated principal.user which
require_token had loaded in a now-closed session — the commit on the
handler's session persisted nothing. Re-fetch the user via the active
session before writing.
2. Portable PK type. SQLite only auto-fills `INTEGER PRIMARY KEY`; plain
BIGINT requires explicit values. Define a `_PK` alias of
`BigInteger().with_variant(Integer(), "sqlite")` and use it for all 10
autoincrement primary keys in app/models.py. No prod-schema change
(MariaDB still gets BIGINT).
3. job_lifecycle's MariaDB GET_LOCK / RELEASE_LOCK is now gated behind
`dialect.name == "mysql"`, so the test SQLite engine doesn't trip on
the missing function. Single-process test runs can't race themselves.
4. tests/test_news_window.py seeded Headline rows without `fingerprint`,
which is NOT NULL — added an `fp-{title}` value per row.
5. tests/test_email_digest_job.py now also patches `llm_configured` to
True so the job doesn't short-circuit on the missing API key.
6. (test container hygiene) Drop `COPY tests ./tests` from the test stage
in the Dockerfile — .dockerignore excludes `tests/` (correct: prod
image must not bake tests), and docker-compose.test.yml bind-mounts
./tests at run time anyway.
Suite now: 198 passed, 5 skipped, 1 pre-existing failure
(test_default_groups_present — Phase G dropped the "pie" group from
config/default.toml but the assertion wasn't updated; unrelated to this
branch).
This commit is contained in:
parent
80e2ec53ac
commit
a113a7f3ce
6 changed files with 51 additions and 25 deletions
|
|
@ -23,17 +23,21 @@ async def job_lifecycle(name: str) -> AsyncIterator[tuple[AsyncSession, JobRun]]
|
|||
handles the bookkeeping.
|
||||
|
||||
A MariaDB GET_LOCK(name, 0) is acquired to prevent concurrent runs of the
|
||||
same job across processes. If the lock is busy, we skip the run."""
|
||||
same job across processes. If the lock is busy, we skip the run.
|
||||
The lock dance is MariaDB-specific; on SQLite (used in tests) it's a
|
||||
no-op, since the single-process test runner can't race itself."""
|
||||
factory = get_session_factory()
|
||||
async with factory() as session:
|
||||
# Try lock; skip if held.
|
||||
got = (await session.execute(
|
||||
text("SELECT GET_LOCK(:n, 0)"), {"n": f"cassandra_{name}"}
|
||||
)).scalar()
|
||||
if not got:
|
||||
log.warning("job.skipped_locked", name=name)
|
||||
yield session, JobRun(name=name, started_at=utcnow(), status="skipped")
|
||||
return
|
||||
bind = session.get_bind()
|
||||
use_lock = bind is not None and bind.dialect.name == "mysql"
|
||||
if use_lock:
|
||||
got = (await session.execute(
|
||||
text("SELECT GET_LOCK(:n, 0)"), {"n": f"cassandra_{name}"}
|
||||
)).scalar()
|
||||
if not got:
|
||||
log.warning("job.skipped_locked", name=name)
|
||||
yield session, JobRun(name=name, started_at=utcnow(), status="skipped")
|
||||
return
|
||||
run = JobRun(name=name, started_at=utcnow(), status="running")
|
||||
session.add(run)
|
||||
await session.commit()
|
||||
|
|
@ -53,6 +57,7 @@ async def job_lifecycle(name: str) -> AsyncIterator[tuple[AsyncSession, JobRun]]
|
|||
log.error("job.failed", name=name, error=str(e))
|
||||
raise
|
||||
finally:
|
||||
await session.execute(text("SELECT RELEASE_LOCK(:n)"),
|
||||
{"n": f"cassandra_{name}"})
|
||||
await session.commit()
|
||||
if use_lock:
|
||||
await session.execute(text("SELECT RELEASE_LOCK(:n)"),
|
||||
{"n": f"cassandra_{name}"})
|
||||
await session.commit()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue