Files
thoughtsync/tests/test_notes.py
T
bvandeusenandClaude Opus 4.8 0ae02858f7
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 7s
CI & Build / Build & push image (push) Successful in 33s
m4.5: content-aware [[ linking — autocomplete searches note body
The [[ autocomplete only matched note names, so you could only link a
note you could name. Now it searches note NAME *and* body, so you can
link by recalling any phrase.

- new GET /api/notes/link-search?q= — owner-scoped, non-trashed;
  substring ILIKE on display_title OR body; ranked name-first, then
  name-prefix, then recency; empty q returns recent notes as
  suggestions. Deterministic (no semantic/AI search); the FTS index
  still powers the heavier /search. LIKE wildcards in q are escaped.
- editor [[ autocomplete now calls link-search (debounced 120ms)
  instead of filtering the cached titles index; excludes the note
  itself; inserts the matched note's display name as [[Name]].
- unit tests for the LIKE-escaping + a link-search auth guard.

Second item of M4.5; builds on the display_title work (every note has
a name to link to). Command-palette content search is a natural
follow-on, left out to keep this focused.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
2026-07-22 08:19:05 -04:00

163 lines
4.9 KiB
Python

import pytest
from thoughtsync.app import create_app
from thoughtsync.models.note import NOTE_COLORS, Note
from thoughtsync.notes import (
_escape_like,
derive_display_title,
is_empty_note,
normalize_color,
parse_link_titles,
rewrite_link_title,
)
@pytest.fixture
def app():
return create_app()
def test_is_empty_note():
assert is_empty_note(None, None)
assert is_empty_note("", " ")
assert not is_empty_note("title", "")
assert not is_empty_note("", "body")
def test_normalize_color():
assert normalize_color("blue") == "blue"
assert normalize_color("chartreuse") == "default"
assert normalize_color(None) == "default"
assert normalize_color(123) == "default"
def test_palette_has_core_colors():
for c in ("default", "red", "orange", "yellow", "green", "teal", "blue", "purple", "pink", "gray"):
assert c in NOTE_COLORS
def test_serialize_shape():
n = Note(title="t", body="b", color="blue", pinned=True, archived=False)
s = n.serialize()
assert s["title"] == "t"
assert s["body"] == "b"
assert s["color"] == "blue"
assert s["pinned"] is True
assert s["archived"] is False
assert s["trashed"] is False
async def test_notes_list_requires_auth(app):
client = app.test_client()
resp = await client.get("/api/notes")
assert resp.status_code == 401
async def test_notes_create_requires_auth(app):
client = app.test_client()
resp = await client.post("/api/notes", json={"body": "hi"})
assert resp.status_code == 401
async def test_search_requires_auth(app):
client = app.test_client()
resp = await client.get("/api/notes/search?q=hello")
assert resp.status_code == 401
async def test_add_item_requires_auth(app):
client = app.test_client()
resp = await client.post("/api/notes/00000000-0000-0000-0000-000000000000/items", json={"text": "x"})
assert resp.status_code == 401
async def test_upload_attachment_requires_auth(app):
client = app.test_client()
resp = await client.post("/api/notes/00000000-0000-0000-0000-000000000000/attachments")
assert resp.status_code == 401
async def test_reorder_requires_auth(app):
client = app.test_client()
resp = await client.post("/api/notes/reorder", json={"ids": []})
assert resp.status_code == 401
def test_parse_link_titles():
titles = parse_link_titles("see [[Alpha]] and [[ beta ]] and [[Alpha]] again")
assert titles == ["alpha", "beta"]
def test_parse_link_titles_empty():
assert parse_link_titles(None) == []
assert parse_link_titles("no links here") == []
def test_rewrite_link_title():
body = "see [[Alpha]] and [[ alpha ]] and [[Beta]]"
assert rewrite_link_title(body, "alpha", "Gamma") == "see [[Gamma]] and [[Gamma]] and [[Beta]]"
def test_rewrite_link_title_noop():
assert rewrite_link_title("", "alpha", "Gamma") == ""
assert rewrite_link_title(None, "alpha", "Gamma") == ""
assert rewrite_link_title("no links here", "alpha", "Gamma") == "no links here"
def test_derive_display_title_explicit_wins():
assert derive_display_title("My Title", "some body line") == "My Title"
assert derive_display_title(" Padded ", "body") == "Padded"
def test_derive_display_title_from_first_body_line():
assert derive_display_title(None, "first line\nsecond line") == "first line"
assert derive_display_title("", " spaced first \nnext") == "spaced first"
# leading blank/whitespace lines are skipped to the first line with content
assert derive_display_title(None, "\n \nreal line\nmore") == "real line"
# a whitespace-only title falls through to the body
assert derive_display_title(" ", "body wins") == "body wins"
def test_derive_display_title_empty():
assert derive_display_title(None, None) == ""
assert derive_display_title("", "") == ""
assert derive_display_title(" ", " \n ") == ""
def test_derive_display_title_caps_length():
long = "x" * 300
assert derive_display_title(None, long) == "x" * 200
assert derive_display_title(long, "body") == "x" * 200
def test_escape_like():
# LIKE wildcards in user input must be neutralized so they match literally.
assert _escape_like("100%") == "100\\%"
assert _escape_like("a_b") == "a\\_b"
assert _escape_like("c:\\path") == "c:\\\\path"
assert _escape_like("plain") == "plain"
async def test_titles_requires_auth(app):
client = app.test_client()
resp = await client.get("/api/notes/titles")
assert resp.status_code == 401
async def test_link_search_requires_auth(app):
client = app.test_client()
resp = await client.get("/api/notes/link-search?q=hi")
assert resp.status_code == 401
async def test_graph_requires_auth(app):
client = app.test_client()
resp = await client.get("/api/graph")
assert resp.status_code == 401
async def test_reminders_requires_auth(app):
client = app.test_client()
resp = await client.get("/api/notes/reminders")
assert resp.status_code == 401