"""A snippet's notes — the explanation that had nowhere to go (#4378). Before this field, a snippet had name, code, signature, locations and `when_to_use`, and nothing for prose. What a session learned about a snippet went into `when_to_use` — the trigger, joined onto the title of every chunk the snippet is embedded as — so a multi-paragraph write-up there blurred the one situation it is ranked on. The rules pinned here, each with a way to rot silently: - Notes live in the BODY, after the code, under `## Notes`, and read back from it. A `## Notes` line inside the code is code, not the section. - A snippet with no notes composes the body it always did, byte for byte — no re-embed for the corpus that has none. - Every path that rebuilds the body CARRIES the notes. `update_snippet`, merge and un-merge all compose the body from scratch, so a field one of them forgets is erased by any edit to something else. - `trigger_advice` speaks for a write-up and stays quiet for a situation. """ import pytest import pytest_asyncio from tests.helpers import ensure_user from scribe.services import snippets as s CODE = "def helper():\n return 1" NOTES = "Shaped this way because the caller owns the session.\n\nSuperseded #12." # --- unit: the body convention ----------------------------------------------- def test_notes_follow_the_code_under_their_heading(): body = s.compose_body(code=CODE, language="python", when_to_use="a helper", notes=NOTES) code_at = body.index("```python") notes_at = body.index(s.NOTES_HEADING) assert code_at < notes_at assert body.rstrip().endswith("Superseded #12.") def test_notes_round_trip_through_the_body(): body = s.compose_body(code=CODE, language="python", notes=NOTES) got = s.parse_snippet_fields("helper", body, ["python", "snippet"]) assert got["notes"] == NOTES assert got["code"] == CODE def test_a_snippet_without_notes_composes_the_body_it_always_did(): """No trailing section, no extra blank line: the corpus that has no notes must embed exactly as before.""" body = s.compose_body(code=CODE, language="python", when_to_use="a helper") assert body == "**When to use:** a helper\n\n```python\n" + CODE + "\n```\n" assert s.parse_snippet_fields("helper", body)["notes"] == "" def test_a_notes_heading_inside_the_code_is_code(): md = "# Title\n\n## Notes\n\nthis is markdown being recorded" body = s.compose_body(code=md, language="markdown") got = s.parse_snippet_fields("md_template", body) assert got["notes"] == "" assert got["code"] == md def test_notes_after_code_that_itself_contains_the_heading(): md = "## Notes\ninside" body = s.compose_body(code=md, language="markdown", notes="the real notes") got = s.parse_snippet_fields("md_template", body) assert got["notes"] == "the real notes" assert got["code"] == md def test_long_notes_are_chunked_apart_from_the_code(): """The heading is the chunker's split point, so a long explanation gets vectors of its own instead of averaging into the code's.""" from scribe.services.embeddings import chunk_document long_notes = "\n\n".join(["An explanatory paragraph about the history. " * 12] * 4) body = s.compose_body(code=CODE, language="python", when_to_use="a helper", notes=long_notes) chunks = chunk_document("helper — a helper", body) assert len(chunks) > 1 assert "```python" in chunks[0] assert "```python" not in chunks[-1] # --- unit: the advice --------------------------------------------------------- def test_a_situation_draws_no_advice(): assert s.trigger_advice("Debouncing a reactive input before it fetches.") is None assert s.trigger_advice("") is None assert s.trigger_advice(None) is None def test_a_long_trigger_is_advised_toward_notes(): advice = s.trigger_advice("x" * (s.TRIGGER_ADVISE_CHARS + 1)) assert advice and "notes" in advice assert f"{s.TRIGGER_ADVISE_CHARS + 1} characters" in advice def test_a_headed_or_multi_paragraph_trigger_is_advised_even_when_short(): assert "headings" in s.trigger_advice("When adding a record.\n\n## Why\nbecause") assert "paragraphs" in s.trigger_advice("When adding a record.\n\nAlso, history.") # --- integration: every body rebuild carries the notes ------------------------ @pytest_asyncio.fixture async def user_id(_dispose_engine): from scribe.models import async_session async with async_session() as session: uid = (await ensure_user(session, "snippet_notes_itest")).id await session.commit() return uid async def _fields(uid, note_id): return s.snippet_fields(await s.get_snippet(uid, note_id)) @pytest.mark.integration async def test_an_edit_to_another_field_keeps_the_notes(user_id): note = await s.create_snippet( user_id, name="notes_keep", code=CODE, language="python", when_to_use="a helper", notes=NOTES, ) assert (await _fields(user_id, note.id))["notes"] == NOTES await s.update_snippet(user_id, note.id, when_to_use="a sharper situation") got = await _fields(user_id, note.id) assert got["notes"] == NOTES assert got["when_to_use"] == "a sharper situation" # And an empty string clears them, like every other field. await s.update_snippet(user_id, note.id, notes="") assert (await _fields(user_id, note.id))["notes"] == "" @pytest.mark.integration async def test_a_merge_keeps_the_survivors_notes(user_id): survivor = await s.create_snippet( user_id, name="notes_merge_a", code=CODE, language="python", repo="R", path="a.py", symbol="helper", notes=NOTES, ) source = await s.create_snippet( user_id, name="notes_merge_b", code=CODE + " # variant", language="python", repo="R", path="b.py", symbol="helper", ) await s.merge_snippets(user_id, survivor.id, [source.id]) assert (await _fields(user_id, survivor.id))["notes"] == NOTES await s.unmerge_snippet(user_id, survivor.id, source.id) assert (await _fields(user_id, survivor.id))["notes"] == NOTES