AGPL-3.0-only, a source link in the app, and everything on dev since #5 #6

Merged
bvandeusen merged 214 commits from dev into main 2026-10-21 21:40:00 -04:00
Showing only changes of commit 1a2f71e381 - Show all commits
+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)