add blocker presets

This commit is contained in:
timka123 committed 2026-10-19 11:54:15 +03:00
1 parent 2be074f191
commit 772fdbfded
12 files changed
+426 -83

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
"""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 sqlalchemy import text
from app.db import SessionLocal, engine, init_db
from app.models import TaskRecord
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()
+25 -1
View File
@@ -2,10 +2,34 @@
from __future__ import annotations
from app.services import settings_service
from app.services import pomodoro, settings_service
from app.settings_schema import SECRET_MASK
def test_a_preset_change_resizes_the_running_interval(session, settings):
row = pomodoro.start(session, settings, kind="work")
assert row.planned_seconds == settings.pomodoro.work_minutes * 60
updated = settings_service.apply_patch(
session, {"pomodoro": {"work_minutes": 85, "short_break_minutes": 10}}
)
current = pomodoro.current_session(session)
assert current.id == row.id
assert current.planned_seconds == 85 * 60
# Elapsed time is wall-clock, not stored — resizing never has to touch it.
assert pomodoro.elapsed_seconds(current) >= 0
assert updated.pomodoro.work_minutes == 85
def test_a_patch_unrelated_to_durations_leaves_the_running_interval_alone(session, settings):
row = pomodoro.start(session, settings, kind="work")
settings_service.apply_patch(session, {"pomodoro": {"daily_goal_pomodoros": 12}})
current = pomodoro.current_session(session)
assert current.id == row.id
assert current.planned_seconds == row.planned_seconds
def test_patch_merges_without_clobbering_siblings(session):
settings_service.apply_patch(session, {"pomodoro": {"work_minutes": 50}})
updated = settings_service.apply_patch(