"""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()