fix(tests): the recurrence stand-ins come from fake_note, not a bare MagicMock (#3164)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 26s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 1m7s
CI & Build / Build & push image (push) Successful in 23s

Four tests predating the helper built their note with MagicMock(), which is
truthy on every attribute nobody set. update_note now reads verify_with, so
the stand-in claimed to carry a check and the milestone-317 guard refused the
write.

That is note 2109 exactly, and the reason fake_note exists: a stand-in has to
be able to say NO. The product behaviour is right — a real column is None or a
string, so this cannot happen outside a test.

Observation, not changed here: fake_note sets is_task=False but leaves
`status` unset, so it too is a truthy mock on the column is_task is derived
FROM. Nothing depends on it today; worth making self-consistent when something
does.
This commit is contained in:
2026-08-28 16:01:28 -04:00
parent 700ef20eb0
commit 4736a0a0ba
+15 -6
View File
@@ -1,13 +1,23 @@
"""Tests for task lifecycle timestamps and recurrence logic."""
"""Tests for task lifecycle timestamps and recurrence logic.
The note stand-ins come from `fake_note`, not a bare MagicMock. These four
predated that helper and broke the moment `update_note` started reading a
column they did not set (milestone 317): a fresh MagicMock is truthy on every
attribute, so `verify_with` read as "this record carries a check" and the
guard refused the write. That is note 2109's lesson, and the reason fake_note
exists — a stand-in has to be able to say NO.
"""
from datetime import date, datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch
from tests.helpers import fake_note, make_mock_session
# ── Timestamp side-effect tests ──────────────────────────────────────────────
async def test_update_note_sets_started_at_on_in_progress():
"""started_at is set when status transitions to in_progress."""
mock_note = MagicMock()
mock_note = fake_note()
mock_note.status = "in_progress"
mock_note.started_at = None
mock_note.recurrence_rule = None
@@ -28,7 +38,7 @@ async def test_update_note_sets_started_at_on_in_progress():
async def test_update_note_sets_completed_at_on_done():
"""completed_at is set when status transitions to done."""
mock_note = MagicMock()
mock_note = fake_note()
mock_note.status = "done"
mock_note.started_at = datetime(2026, 3, 1, tzinfo=timezone.utc)
mock_note.completed_at = None
@@ -50,7 +60,7 @@ async def test_update_note_sets_completed_at_on_done():
async def test_update_note_clears_timestamps_on_todo():
"""started_at and completed_at are cleared when status resets to todo."""
mock_note = MagicMock()
mock_note = fake_note()
mock_note.status = "todo"
mock_note.started_at = datetime(2026, 3, 1, tzinfo=timezone.utc)
mock_note.completed_at = datetime(2026, 3, 15, tzinfo=timezone.utc)
@@ -74,7 +84,7 @@ async def test_update_note_clears_timestamps_on_todo():
async def test_update_note_preserves_started_at_if_already_set():
"""started_at is not overwritten on a second transition to in_progress."""
original_start = datetime(2026, 3, 1, tzinfo=timezone.utc)
mock_note = MagicMock()
mock_note = fake_note()
mock_note.status = "in_progress"
mock_note.started_at = original_start
mock_note.recurrence_rule = None
@@ -96,7 +106,6 @@ async def test_update_note_preserves_started_at_if_already_set():
# ── Recurrence rule validation ────────────────────────────────────────────────
import pytest
from tests.helpers import make_mock_session
def test_validate_interval_rule_valid():