2 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5.5 86409e02b0 ci: drop the deliberate failure — the gate held on run 8437
Android / Build, or is the channel already serving this? (push) Successful in 3s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 12s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Python tests (push) Successful in 14s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 7s
CI & Build / integration (push) Failing after 49s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Clippy, tests and rustfmt (push) Successful in 2m26s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m54s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m52s
Desktop (Tauri) / Update manifest (push) Successful in 7s
Run 8437 failed `verify` on purpose, and the Linux build, the Windows
installer and the update manifest all reported skipped. This removes the red
step, so this push is the other direction: a green verify still publishes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:26:48 -04:00
bvandeusenandClaude Opus 5.5 1a2f71e381 tests: every note-text write path is pinned, and two of them fail today
Integration tests for the edit sequence at each door: create, PATCH, ticking an
item, restoring a revision and sync push. Two fail on today's code, on purpose:

- restoring a revision never queues link previews, so a restored link stays a
  bare URL (#5164, audit B5);
- a pushed note whose standalone #tag gets lifted stores the server's clock as
  its edit time instead of the client's, because the lift's second flush lets
  the column's onupdate overwrite it.

The fix follows in the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:26:15 -04:00
2 changed files with 107 additions and 5 deletions
-5
View File
@@ -136,11 +136,6 @@ jobs:
- name: Rust format check
run: cargo fmt --all --check
# TEMPORARY — the red half of rule 177's proof for #5184. Removed in the very
# next commit. Both publishers must report `skipped` on this run.
- name: Deliberately red, to prove the publishers wait (#5184)
run: exit 1
build:
name: Tauri desktop (Linux)
needs: [decide, verify]
+107
View File
@@ -630,3 +630,110 @@ async def test_creating_a_tag_that_differs_only_in_case_returns_the_existing_one
assert (await second.get_json())["id"] == (await first.get_json())["id"]
listing = (await (await app_client.get("/api/labels")).get_json())["labels"]
assert len(listing) == 1
# --- One save path for a note's text (#5164) ---------------------------------
#
# A body edit is a sequence: keep a revision, rename the note, lift #tags, commit,
# queue link previews. It was written out once per route, and the copies drifted —
# restoring a revision skipped the previews. These pin the sequence at each door.
async def _signed_in(app_client, who: str):
reg = await app_client.post(
"/api/auth/register",
json={"email": f"{who}@example.test", "password": "a-long-enough-password"},
)
assert reg.status_code == 201
def _record_previews(monkeypatch, module: str) -> list[tuple[str, str]]:
"""Capture what a write path queues for unfurling, instead of fetching."""
queued: list[tuple[str, str]] = []
monkeypatch.setattr(f"{module}.schedule_unfurls", lambda nid, body: queued.append((str(nid), body)))
return queued
async def test_restoring_a_revision_queues_previews_for_its_links(app_client, db, monkeypatch):
"""The bug that motivated the shared path: restore was the one copy of the edit
sequence without the unfurl step, so a link brought back by a restore stayed a
bare URL on every surface."""
await _signed_in(app_client, "restore")
queued = _record_previews(monkeypatch, "inkwell.notes")
note = await (await app_client.post("/api/notes", json={"body": "read https://example.com/a"})).get_json()
await app_client.patch(f"/api/notes/{note['id']}", json={"body": "no link any more"})
revisions = (await (await app_client.get(f"/api/notes/{note['id']}/revisions")).get_json())["revisions"]
assert [r["body"] for r in revisions] == ["read https://example.com/a"]
queued.clear()
resp = await app_client.post(f"/api/notes/{note['id']}/revisions/{revisions[0]['id']}/restore")
assert resp.status_code == 200
assert (await resp.get_json())["body"] == "read https://example.com/a"
assert queued == [(note["id"], "read https://example.com/a")], "the restored link gets its preview"
async def test_restoring_keeps_the_text_it_replaces(app_client, db):
"""Restore snapshots unconditionally — inside an editing session too — so a
restore can itself be undone."""
await _signed_in(app_client, "undo")
note = await (await app_client.post("/api/notes", json={"body": "first"})).get_json()
await app_client.patch(f"/api/notes/{note['id']}", json={"body": "second"})
rev = (await (await app_client.get(f"/api/notes/{note['id']}/revisions")).get_json())["revisions"][0]
await app_client.post(f"/api/notes/{note['id']}/revisions/{rev['id']}/restore")
bodies = [r["body"] for r in (await (await app_client.get(f"/api/notes/{note['id']}/revisions")).get_json())["revisions"]]
assert sorted(bodies) == ["first", "second"]
async def test_each_route_that_writes_text_queues_previews_only_when_it_changed(app_client, db, monkeypatch):
await _signed_in(app_client, "routes")
queued = _record_previews(monkeypatch, "inkwell.notes")
note = await (await app_client.post("/api/notes", json={"body": "https://example.com/new"})).get_json()
assert queued == [(note["id"], "https://example.com/new")], "create"
queued.clear()
await app_client.patch(f"/api/notes/{note['id']}", json={"pinned": True})
assert queued == [], "a pin is not a text change"
await app_client.patch(f"/api/notes/{note['id']}", json={"body": "https://example.com/new\n- [ ] milk"})
assert queued == [(note["id"], "https://example.com/new\n- [ ] milk")], "PATCH"
queued.clear()
await app_client.patch(f"/api/notes/{note['id']}/items/0", json={"checked": True})
assert queued == [(note["id"], "https://example.com/new\n- [x] milk")], "ticking an item"
async def test_a_pushed_note_is_named_tagged_and_queued_like_a_typed_one(app_client, db, monkeypatch):
await _signed_in(app_client, "push")
queued = _record_previews(monkeypatch, "inkwell.sync")
nid = str(uuid.uuid4())
resp = await app_client.post(
"/api/sync/push",
json={"changes": [{"entity": "note", "id": nid, "op": "upsert", "body": "Trip https://example.com/t\n#travel",
"edited_at": "2026-01-01T00:00:00Z", "created_at": "2026-01-01T00:00:00Z"}]},
)
assert (await resp.get_json())["results"][0]["status"] == "created"
note = await (await app_client.get(f"/api/notes/{nid}")).get_json()
assert note["body"] == "Trip https://example.com/t", "the standalone tag was lifted"
assert [lb["name"] for lb in note["labels"]] == ["travel"]
assert note["display_title"] == "Trip https://example.com/t"
assert queued == [(nid, "Trip https://example.com/t")], "queued with the text as stored"
async def test_a_pushed_edit_keeps_the_clients_edit_time_when_a_tag_is_lifted(app_client, db):
"""Last-write-wins compares edit times, so the stored one has to be the client's.
Lifting a tag rewrites the body in a second flush, and the column's onupdate
used to stamp the server's clock over the time the client sent."""
await _signed_in(app_client, "edited")
nid = str(uuid.uuid4())
await app_client.post(
"/api/sync/push",
json={"changes": [{"entity": "note", "id": nid, "op": "upsert", "body": "milk\n#groceries",
"edited_at": "2026-01-01T00:00:00Z", "created_at": "2026-01-01T00:00:00Z"}]},
)
note = await (await app_client.get(f"/api/notes/{nid}")).get_json()
assert datetime.fromisoformat(note["updated_at"]) == datetime(2026, 1, 1, tzinfo=timezone.utc)