feat(notes): a note's check is writable through both doors, and empty means empty (#3164, milestone 317 step 2)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 38s
CI & Build / integration (push) Successful in 32s
CI & Build / Python tests (push) Failing after 49s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 38s
CI & Build / integration (push) Successful in 32s
CI & Build / Python tests (push) Failing after 49s
CI & Build / Build & push image (push) Skipped
The rules path's three lessons (#3096), inherited: EMPTY MEANS NULL. The sweep's whole signal is `verify_with IS NULL` = "this is a decision, there is nothing to go and check". A "" that is not NULL makes a norm look like a constraint nobody has verified — and never-checked sorts FIRST, so it would sit at the top of the sweep forever. CLEARING IS EXPLICIT. At the MCP door "" means "leave this alone", so an agent updating a body does not wipe a check it was never asked about — which leaves no value meaning "remove it". `clear` names the field, and naming it cannot happen by accident. The REST door, where a cleared form input arrives as "", reaches the same place through normalisation: two idioms, one outcome. THE STAMP CERTIFIES A CHECK, NOT A RECORD. Rewrite or clear `verify_with` and `verified_at` is dropped, so the note re-enters the sweep. A note wrongly listed as due costs one look; a note wrongly vouched for costs exactly what the sweep exists to catch. `verified_at` is also no longer settable through an ordinary edit — a stamp says somebody performed THIS check, and minting one from a write that ran no check is the one thing that would make the sweep lie. And one this path adds: not every record may carry a check. A task's decay is its status — a done issue records what happened rather than asserting something that can go false — and a snippet already has verify_snippet, which compares its recorded location and code against the repo and expires its own verdict. Both are refused with a message naming the alternative, never dropped silently (minted_kind's reasoning, #3129), and the gate lives at the service so the two doors cannot come to disagree. Written as an INVARIANT over the resulting record, not a filter on which fields were passed. That is what catches the sideways route — a checked note being turned into a task, a write that names no check at all and would sail past any per-field gate. The MCP docstrings carry the norm-vs-constraint test, because at that door the docstring IS the contract and a field described only as "how to verify this" gets filled in on every note. Step 5 does this properly across the instruction surfaces; this is the minimum that stops the field being misused on arrival. tests/helpers gains `drive_update_note` — the patch stack for driving update_note, written twice before this and now once. The note-shaped fakes gain the trio explicitly, for fake_note's own stated reason: an unset attribute is a truthy MagicMock, and a truthy verify_with reads as a check that is there.
This commit is contained in:
+33
-1
@@ -9,7 +9,33 @@ from __future__ import annotations
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timezone
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
|
||||
async def drive_update_note(note, **kwargs):
|
||||
"""Run `services/notes.update_note` against a stand-in row.
|
||||
|
||||
The patch stack is the point: update_note reaches for a version snapshot,
|
||||
an embedding refresh and a project reactivation on its way out, none of
|
||||
which a unit test has. Written twice — once for the snippet mirror
|
||||
(#3128) and once for the verification fields (#3182/317) — before being
|
||||
consolidated here.
|
||||
|
||||
Returns whatever update_note returned; assert on the `note` you passed in.
|
||||
"""
|
||||
from unittest.mock import AsyncMock as _AsyncMock
|
||||
|
||||
session = make_mock_session()
|
||||
result = MagicMock()
|
||||
result.scalars.return_value.first.return_value = note
|
||||
session.execute = _AsyncMock(return_value=result)
|
||||
with patch("scribe.services.notes.async_session") as cls, \
|
||||
patch("scribe.services.notes.embed_note", MagicMock()), \
|
||||
patch("scribe.services.notes._maybe_reactivate_project", _AsyncMock()), \
|
||||
patch("scribe.services.note_versions.create_version", _AsyncMock()):
|
||||
cls.return_value = session
|
||||
from scribe.services.notes import update_note
|
||||
return await update_note(user_id=7, note_id=note.id, **kwargs)
|
||||
|
||||
|
||||
def compiled_sql(element) -> str:
|
||||
@@ -102,6 +128,9 @@ def fake_note(**attrs) -> MagicMock:
|
||||
"id": 1, "title": "t", "body": "", "tags": [], "user_id": 7,
|
||||
"note_type": "note", "is_task": False, "task_kind": "work",
|
||||
"data": None, "deleted_at": None,
|
||||
# Milestone 317: a truthy mock here reads as "this note carries a
|
||||
# check", which trips the guard on records that may not have one.
|
||||
"verify_with": None, "expires_when": None, "verified_at": None,
|
||||
}, attrs)
|
||||
|
||||
|
||||
@@ -111,6 +140,7 @@ def fake_task(**attrs) -> MagicMock:
|
||||
"id": 1, "title": "t", "body": "", "status": "todo", "priority": "none",
|
||||
"tags": [], "parent_id": None, "project_id": None, "is_task": True,
|
||||
"task_kind": "work", "user_id": 7, "deleted_at": None,
|
||||
"verify_with": None, "expires_when": None, "verified_at": None,
|
||||
}, attrs)
|
||||
|
||||
|
||||
@@ -122,6 +152,8 @@ def fake_snippet(**attrs) -> MagicMock:
|
||||
"body": "```js\nreturn 1\n```\n", "tags": ["js", "snippet"],
|
||||
"note_type": "snippet", "is_task": False, "task_kind": "work",
|
||||
"user_id": 7, "data": None, "deleted_at": None,
|
||||
"status": None,
|
||||
"verify_with": None, "expires_when": None, "verified_at": None,
|
||||
}, attrs)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user