Files
kadence/backend/tests/test_db_migration.py
timka123andClaude Opus 5 ec7eedbc66 style: say which lint rules this repo keeps, then keep them
`[tool.ruff]` set a line length and a target version and nothing else, so which
rules ran depended on the installed ruff's defaults — fine while nobody ran it,
not fine for a CI job that can turn a push red because a tool changed its mind.

The set is named explicitly (E, F, I, UP, SIM, B, RUF) with three ignores that
are each a real decision rather than a shrug: B008, because FastAPI's whole
dependency idiom is a call in a default argument; E501, because wrapping prose
is a formatter's business; and RUF001, because the reports and the bot speak
Russian and "ambiguous character" is the wrong complaint about a string the
user is meant to read.

The 73 findings that remained are fixed — mostly `datetime.timezone.utc` →
`datetime.UTC`, `typing.Callable` → `collections.abc.Callable`, import order,
and unused `noqa` markers — plus seven by hand: `contextlib.suppress` for two
try/except/pass pairs, unpacking instead of list concatenation, one negated
return.

`ruff format` is deliberately *not* adopted: it would reformat 51 files, and
some of the layout here is hand-set (the comment tables, the DNS wire-format
structs) in ways a formatter flattens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-22 17:28:57 +03:00

85 lines
3.1 KiB
Python

"""Regression test for a schema-drift bug: a database created by an older
build of the app still had a `completed_here NOT NULL` column on `tasks`
that a later refactor dropped from the model. `create_all` never alters an
existing table, so every task insert on such a database failed with
IntegrityError: null value in column "completed_here" ... violates
not-null constraint
which, since task refresh runs from both the scheduler and `/api/tasks`
endpoints, looked like "everything 500s". `app.db.init_db` now reconciles
known-dead columns after `create_all`; this pins that behaviour down.
"""
from __future__ import annotations
from app.db import SessionLocal, engine, init_db
from app.models import TaskRecord
from sqlalchemy import text
def _simulate_pre_refactor_database() -> None:
"""Recreate the exact drift: today's `tasks` table, plus the long-gone
`completed_here` column, `NOT NULL` and with no database-side default —
just like a database that has been running since before the column was
removed from the model.
"""
with engine.begin() as conn:
conn.execute(text("DROP TABLE IF EXISTS tasks"))
conn.execute(
text(
"""
CREATE TABLE tasks (
id INTEGER PRIMARY KEY,
source VARCHAR(32) NOT NULL,
external_id VARCHAR(128) NOT NULL,
title TEXT NOT NULL,
description TEXT,
url TEXT,
project TEXT,
status VARCHAR(64),
priority VARCHAR(32),
due_date DATE,
start_date DATE,
labels JSON,
raw JSON,
is_done BOOLEAN NOT NULL DEFAULT 0,
completed_at DATETIME,
completed_date DATE,
completed_here BOOLEAN NOT NULL,
estimated_pomodoros INTEGER,
pinned BOOLEAN NOT NULL DEFAULT 0,
first_seen_at DATETIME NOT NULL,
last_seen_at DATETIME NOT NULL,
stale BOOLEAN NOT NULL DEFAULT 0
)
"""
)
)
def test_init_db_drops_the_dead_not_null_column(session):
_simulate_pre_refactor_database()
init_db()
db = SessionLocal()
try:
# This is exactly what `tasks_service._upsert` + `refresh_tasks` do on
# a newly-seen task: insert a row without ever mentioning the column
# the model no longer has.
db.add(TaskRecord(source="jira", external_id="VPN-48", title="Renew VPN"))
db.commit() # pre-fix: IntegrityError (null value in "completed_here")
stored = db.query(TaskRecord).filter_by(external_id="VPN-48").one()
assert stored.title == "Renew VPN"
finally:
db.close()
def test_init_db_is_a_no_op_once_the_column_is_already_gone(session):
"""Running it twice (every boot does) must not blow up when there is
nothing left to drop."""
init_db()
init_db()