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 datetime import date, datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from tests.helpers import fake_note, make_mock_session
# ── Timestamp side-effect tests ────────────────────────────────────────────── # ── Timestamp side-effect tests ──────────────────────────────────────────────
async def test_update_note_sets_started_at_on_in_progress(): async def test_update_note_sets_started_at_on_in_progress():
"""started_at is set when status transitions to 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.status = "in_progress"
mock_note.started_at = None mock_note.started_at = None
mock_note.recurrence_rule = 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(): async def test_update_note_sets_completed_at_on_done():
"""completed_at is set when status transitions to done.""" """completed_at is set when status transitions to done."""
mock_note = MagicMock() mock_note = fake_note()
mock_note.status = "done" mock_note.status = "done"
mock_note.started_at = datetime(2026, 3, 1, tzinfo=timezone.utc) mock_note.started_at = datetime(2026, 3, 1, tzinfo=timezone.utc)
mock_note.completed_at = None 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(): async def test_update_note_clears_timestamps_on_todo():
"""started_at and completed_at are cleared when status resets to 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.status = "todo"
mock_note.started_at = datetime(2026, 3, 1, tzinfo=timezone.utc) mock_note.started_at = datetime(2026, 3, 1, tzinfo=timezone.utc)
mock_note.completed_at = datetime(2026, 3, 15, 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(): async def test_update_note_preserves_started_at_if_already_set():
"""started_at is not overwritten on a second transition to in_progress.""" """started_at is not overwritten on a second transition to in_progress."""
original_start = datetime(2026, 3, 1, tzinfo=timezone.utc) original_start = datetime(2026, 3, 1, tzinfo=timezone.utc)
mock_note = MagicMock() mock_note = fake_note()
mock_note.status = "in_progress" mock_note.status = "in_progress"
mock_note.started_at = original_start mock_note.started_at = original_start
mock_note.recurrence_rule = None mock_note.recurrence_rule = None
@@ -96,7 +106,6 @@ async def test_update_note_preserves_started_at_if_already_set():
# ── Recurrence rule validation ──────────────────────────────────────────────── # ── Recurrence rule validation ────────────────────────────────────────────────
import pytest import pytest
from tests.helpers import make_mock_session
def test_validate_interval_rule_valid(): def test_validate_interval_rule_valid():