notes: color leaves the model, the wire and all three surfaces
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Successful in 19s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 2m28s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m52s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Failing after 4m1s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Successful in 19s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 2m28s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m52s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Failing after 4m1s
Step 3 of M315, and the destructive half. Steps 1 and 2 stopped every read of this field: a card is one neutral surface per theme, and the only coloured thing on a board is a tag. What was left was a column written by a picker and read by nothing. Rule 22 — the old path comes out completely. No flag, no fallback, no "override if set". Server: the column, the `?color=` facet, the create/update/serialise paths, the sync assignment, the front-matter line, and Keep's colour map. Alembic 0029 drops it and sweeps `"color"` out of stored saved-filter params — a view that silently filtered on a field the app no longer has would return nothing and never say why. That sweep is Python, not `params::jsonb - 'color'`, because Postgres has no try-cast and one malformed blob would abort a migration that is running over somebody's saved views. `NOTE_COLORS` moves from `models/note.py` to `colors.py`. A palette defined on the model that lost one is an invitation to put the column back; labels still name a colour, so the vocabulary belongs where the normalizer already is. Core: the field, the facet, the `NoteCreateInput`, and every read and write in store/push/pull. Local schema v9 drops the column and does the same saved-filter sweep, guarded on `json_valid` so a corrupt blob loses a key rather than becoming NULL. The uniffi layer drops `NoteEdit::Color` and `NoteDraft.color` with it. Web: `ColorPicker.vue`, the per-card swatch popover and its stylesheet rule, the FilterBar colour row, the facet in the query round-trip, and the colour half of the editor's baseline-and-save. Android: the `ColorSheet`, the `Picker.COLOR` case, the toolbar's swatch dot, `EditorAction.SetColor`. ## The protocol: v4, and the floor deliberately stays at 3 Checked against `compat.rs` and the push handler rather than trusting the `#[serde(default)]` annotation, because the v2 precedent points the other way: v2 dropped `kind` and `title` and DID raise both floors, on the rule that dropping a field a client sends and expects back is breaking. `color` fails the second half of that test. A v3 client reading a v4 note gets `"default"` from its own serde default and draws the colour it derives locally — the board it drew yesterday. A v3 client pushing `color` has the key ignored, since `_assign_note_fields` reads its payload key by key and never validates the shape. Neither direction errors and neither shows anything wrong. `title` was the note's NAME; this is a field that no longer renders. So `SYNC_PROTOCOL_VERSION` and `CLIENT_PROTOCOL_VERSION` go to 4, and both floors stay at 3. `docs/sync.md` carries the reasoning and the per-version history, and its push example is brought back in line — it still listed `title`, `kind` and `items`, all gone before this. Import stays tolerant: a pre-M315 export or a Keep takeout carrying `color:` imports fine, the key simply read past. Old exports must still import. #3041 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+15
-9
@@ -4,7 +4,8 @@ import pytest
|
||||
|
||||
from thoughtsync.app import create_app
|
||||
from thoughtsync.common import coerce_bool, parse_dt
|
||||
from thoughtsync.models.note import NOTE_COLORS, Note
|
||||
from thoughtsync.colors import NOTE_COLORS
|
||||
from thoughtsync.models.note import Note
|
||||
from thoughtsync.notes.checklist import (
|
||||
append_item,
|
||||
parse_items,
|
||||
@@ -76,16 +77,16 @@ def test_normalize_color():
|
||||
|
||||
|
||||
def test_palette_has_core_colors():
|
||||
# A LABEL's vocabulary since M315 — a note has no colour to be one of these.
|
||||
for c in ("default", "red", "orange", "yellow", "green", "teal", "blue", "purple", "pink", "gray"):
|
||||
assert c in NOTE_COLORS
|
||||
|
||||
|
||||
def test_serialize_shape():
|
||||
n = Note(body="b", color="blue", pinned=True, archived=False)
|
||||
n = Note(body="b", pinned=True, archived=False)
|
||||
s = n.serialize()
|
||||
assert "title" not in s # there is no title field any more (M13 step 3)
|
||||
assert s["body"] == "b"
|
||||
assert s["color"] == "blue"
|
||||
assert s["pinned"] is True
|
||||
assert s["archived"] is False
|
||||
assert s["trashed"] is False
|
||||
@@ -442,6 +443,8 @@ def test_keep_spec_list_note_keeps_its_text_too():
|
||||
"textContent": "for the weekend",
|
||||
"listContent": [{"text": "Milk", "isChecked": False}, {"text": "Eggs", "isChecked": True}],
|
||||
"labels": [{"name": "shopping"}],
|
||||
# Keep's own colour, which the importer now reads past: there is nothing on a
|
||||
# note for it to land on, and a spec carrying a key nobody applies is a lie.
|
||||
"color": "TEAL",
|
||||
"isPinned": True,
|
||||
"isArchived": False,
|
||||
@@ -451,7 +454,7 @@ def test_keep_spec_list_note_keeps_its_text_too():
|
||||
}
|
||||
spec = _keep_spec(kn, "Takeout/Keep")
|
||||
assert spec["body"] == "for the weekend"
|
||||
assert spec["color"] == "teal"
|
||||
assert "color" not in spec
|
||||
assert spec["pinned"] is True
|
||||
assert spec["archived"] is False
|
||||
assert spec["trashed"] is False
|
||||
@@ -460,16 +463,18 @@ def test_keep_spec_list_note_keeps_its_text_too():
|
||||
assert spec["created_at"].year == 2020
|
||||
|
||||
|
||||
def test_keep_spec_text_note_folds_annotation_urls_and_maps_color():
|
||||
def test_keep_spec_text_note_folds_annotation_urls_and_drops_color():
|
||||
kn = {
|
||||
"textContent": "Read this later",
|
||||
"annotations": [{"url": "https://example.com"}],
|
||||
"color": "BROWN", # no brown in our palette → nearest (orange)
|
||||
"color": "BROWN",
|
||||
"attachments": [{"filePath": "img.jpg", "mimetype": "image/jpeg"}],
|
||||
}
|
||||
spec = _keep_spec(kn, "Takeout/Keep")
|
||||
assert "https://example.com" in spec["body"]
|
||||
assert spec["color"] == "orange"
|
||||
# BROWN used to map to the nearest hue we had. There is no hue to map TO now, so a
|
||||
# Keep import brings across everything except the one thing this app stopped having.
|
||||
assert "color" not in spec
|
||||
# attachment path is resolved relative to the note JSON's folder
|
||||
assert spec["attachments"] == [{"file": "Takeout/Keep/img.jpg", "mime": "image/jpeg"}]
|
||||
|
||||
@@ -478,6 +483,8 @@ def test_native_spec_roundtrip_fields():
|
||||
n = {
|
||||
"title": "T",
|
||||
"body": "b",
|
||||
# An export taken before M315 still carries this. Reading past it rather than
|
||||
# rejecting the file is the whole point — old exports must still import.
|
||||
"color": "blue",
|
||||
"pinned": True,
|
||||
"archived": False,
|
||||
@@ -491,7 +498,7 @@ def test_native_spec_roundtrip_fields():
|
||||
# _create_imported_note folds it into the body rather than dropping it.
|
||||
assert spec["title"] == "T"
|
||||
assert spec["body"] == "b"
|
||||
assert spec["color"] == "blue"
|
||||
assert "color" not in spec
|
||||
assert spec["pinned"] is True
|
||||
assert spec["trashed"] is False # exports only carry live notes
|
||||
assert spec["created_at"].year == 2026
|
||||
@@ -642,7 +649,6 @@ def test_note_markdown_writes_a_checklist_once():
|
||||
note = Note(
|
||||
display_title="shopping",
|
||||
body="shopping\n\n- [ ] milk\n- [x] eggs",
|
||||
color="default",
|
||||
pinned=False,
|
||||
archived=False,
|
||||
)
|
||||
|
||||
@@ -12,13 +12,16 @@ def app():
|
||||
def test_clean_params_whitelists_facet_keys():
|
||||
raw = {
|
||||
"q": "hi",
|
||||
# A facet until M315. It is junk now, and has to be dropped like any other —
|
||||
# a stored view that still filtered on a field the app lost would return
|
||||
# nothing and never say why.
|
||||
"color": "yellow",
|
||||
"label": ["a"],
|
||||
"has_reminder": True,
|
||||
"junk": 1,
|
||||
"__proto__": 2,
|
||||
}
|
||||
assert clean_params(raw) == {"q": "hi", "color": "yellow", "label": ["a"], "has_reminder": True}
|
||||
assert clean_params(raw) == {"q": "hi", "label": ["a"], "has_reminder": True}
|
||||
assert clean_params("nope") == {}
|
||||
assert clean_params(None) == {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user