# SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. """Contracts for the update popup's release-notes preview. The popup shows the newest GitHub release's announcement. Two risks are guarded: showing a release the maintainers have not published, and showing a body's generated sections as though they were the announcement.""" from __future__ import annotations import http.server import json import re import shutil import subprocess import sys import threading import time from dataclasses import dataclass from pathlib import Path import pytest REPO = Path(__file__).resolve().parents[2] BACKEND = REPO / "studio/backend" FRONTEND = REPO / "studio/frontend/src" MODULE = BACKEND / "utils/release_notes.py" BODIES = Path(__file__).parent / "fixtures/release_bodies" PANEL = FRONTEND / "components/update/release-notes-panel.tsx" NOTES_HOOK = FRONTEND / "hooks/use-release-notes.ts" PREVIEW = FRONTEND / "lib/release-notes-preview.ts" CODE_SPANS = FRONTEND / "lib/markdown-code-spans.ts" LINKS = FRONTEND / "lib/release-body-links.ts" LIST_COLUMNS = FRONTEND / "lib/markdown-list-columns.ts" INLINE_COMMENTS = FRONTEND / "lib/markdown-inline-comments.ts" WEB_BANNER = FRONTEND / "components/web/update-banner.tsx" TAURI_BANNER = FRONTEND / "components/tauri/update-banner.tsx" # The scanners are the frontend half of the contract the parser implements, so they are # run rather than read. Node strips the types and nothing imports a package: no install. _TS_ALIAS = re.compile(r'"@/lib/([a-z-]+)"') _TS_RUNNER = """ import { resolveReleaseBodyLinks } from "./release-body-links.ts"; import { releaseNotesPreview } from "./release-notes-preview.ts"; const chunks: Buffer[] = []; process.stdin.on("data", (chunk: Buffer) => chunks.push(chunk)); process.stdin.on("end", () => { const markdown = Buffer.concat(chunks).toString("utf8"); const result = process.argv[2] === "links" ? resolveReleaseBodyLinks(markdown) : releaseNotesPreview(markdown); process.stdout.write(JSON.stringify(result)); }); """ SAMPLE = """Intro prose, the announcement itself. ## Kimi K3 - a real section ## Updating / installing Unsloth ```bash curl -fsSL https://unsloth.ai/install.sh | sh ``` ## What's Changed * Something by @someone in https://github.com/unslothai/unsloth/pull/1 **Full Changelog**: https://github.com/unslothai/unsloth/compare/v1...v2 """ @dataclass(frozen = True) class Section: """A heading and the lines under it, to the next heading of any level.""" version: str heading: str body: str def sections(module, text: str) -> list[Section]: """Every document-level heading in `text`, with the lines beneath it. The shipped scanner decides what a heading is; this only groups its events. """ found: list[Section] = [] bodies: list[list[str]] = [] for event in module.scan_blocks(text): if isinstance(event, module.Heading): # A setext heading is the paragraph above it, already collected. if bodies and event.retract: del bodies[-1][len(bodies[-1]) - event.retract :] title = event.title.strip() found.append(Section(version = title.split()[0] if title else "", heading = title, body = "")) bodies.append([]) continue if bodies: bodies[-1].append(event.line) return [ Section(version = entry.version, heading = entry.heading, body = "\n".join(body).strip()) for entry, body in zip(found, bodies) ] def parse_sections(module, text: str) -> list[Section]: """`sections`, keeping only those whose heading starts with a version.""" return [entry for entry in sections(module, text) if module._parse_version(entry.version)] def find_section(module, text: str, version: str) -> Section | None: for entry in parse_sections(module, text): if entry.version == version: return entry wanted = module._parse_version(version) for entry in parse_sections(module, text): if wanted is not None and module._parse_version(entry.version) == wanted: return entry return None def releases_payload(*entries: dict) -> str: """A GitHub releases response, with the fields the selector reads.""" defaults = { "draft": False, "prerelease": False, "name": "", "body": "", "html_url": "", } return json.dumps([{**defaults, **entry} for entry in entries]) @pytest.fixture(scope = "module") def notes_module(): sys.path.insert(0, str(BACKEND)) try: from utils import release_notes finally: sys.path.pop(0) release_notes.reset_release_notes_cache() yield release_notes release_notes.reset_release_notes_cache() @pytest.fixture def serve_releases(notes_module, monkeypatch): """Serve a releases payload locally, and point the module at it.""" monkeypatch.delenv(notes_module.DISABLE_ENV_VAR, raising = False) servers: list[http.server.HTTPServer] = [] def serve( body: str, status: int = 200, headers: dict[str, str] | None = None, reset: bool = True, ): hits = {"count": 0} class Handler(http.server.BaseHTTPRequestHandler): def do_GET(self): # noqa: N802 - stdlib naming hits["count"] += 1 payload = body.encode("utf-8") self.send_response(status) for name, value in (headers or {}).items(): self.send_header(name, value) self.send_header("Content-Length", str(len(payload))) self.end_headers() self.wfile.write(payload) def log_message(self, *_args): pass server = http.server.HTTPServer(("127.0.0.1", 0), Handler) servers.append(server) threading.Thread(target = server.serve_forever, daemon = True).start() monkeypatch.setenv( notes_module.RELEASES_URL_ENV_VAR, f"http://127.0.0.1:{server.server_port}/releases", ) if reset: notes_module.reset_release_notes_cache() return hits yield serve for server in servers: server.shutdown() server.server_close() notes_module.reset_release_notes_cache() @pytest.fixture def isolated_releases(notes_module, monkeypatch): """Keep the module away from the network entirely.""" monkeypatch.setenv(notes_module.DISABLE_ENV_VAR, "1") notes_module.reset_release_notes_cache() yield notes_module notes_module.reset_release_notes_cache() def test_only_real_headings_become_sections(notes_module): headings = [entry.heading for entry in sections(notes_module, SAMPLE)] # The heading inside the fenced install sample is not one of them. assert headings == ["Kimi K3", "Updating / installing Unsloth", "What's Changed"] def test_section_body_stops_at_the_next_heading(notes_module): entry = sections(notes_module, SAMPLE)[0] assert "a real section" in entry.body assert "install.sh" not in entry.body def test_the_announcement_survives_and_the_generated_sections_do_not(notes_module): stripped = notes_module.strip_release_body(SAMPLE) assert "Intro prose" in stripped assert "## Kimi K3" in stripped and "a real section" in stripped for gone in ( "Updating / installing Unsloth", "install.sh", "What's Changed", "pull/1", "Full Changelog", ): assert gone not in stripped def test_response_reports_no_notes_without_markdown(isolated_releases): """Update checks are off, so there is no release and nothing to show.""" payload = isolated_releases.get_release_notes("2026.7.7") assert payload["matched"] is False assert payload["markdown"] is None assert payload["version"] == "2026.7.7" # The UI still needs somewhere to send the user. assert payload["release_notes_url"] def test_the_offered_version_is_echoed_not_looked_up(notes_module, serve_releases): """No tag could match the PyPI version the pip popup offers, so it comes back untouched for the UI to match a stale answer against.""" serve_releases( releases_payload( { "tag_name": "v0.1.60-beta", "name": "Meta Muse Glimmer", "body": "The announcement.\n", "html_url": "https://github.com/unslothai/unsloth/releases/tag/v0.1.60-beta", "published_at": "2026-08-10T11:59:46Z", } ) ) payload = notes_module.get_release_notes("2026.8.11") assert payload["version"] == "2026.8.11" assert payload["tag"] == "v0.1.60-beta" assert payload["heading"] == "Meta Muse Glimmer" assert payload["html_url"].endswith("/releases/tag/v0.1.60-beta") assert payload["matched"] is True and payload["source"] == "github" assert "The announcement." in payload["markdown"] def test_unsupported_version_query_is_rejected(isolated_releases): assert isolated_releases.is_supported_version_query("2026.7.6") is True for bad in ("../etc/passwd", "2026.7.6 OR 1", "", "a" * 80): assert isolated_releases.is_supported_version_query(bad) is False assert isolated_releases.get_release_notes("../etc/passwd")["matched"] is False def test_the_newest_published_release_wins(notes_module, serve_releases): """Ordered by publication, never by tag: v0.1.60-beta was published after v0.1.527-beta, so any numeric or SemVer sort picks the wrong one.""" serve_releases( releases_payload( { "tag_name": "v0.1.527-beta", "body": "older", "published_at": "2026-08-09T17:14:42Z", }, { "tag_name": "v0.1.60-beta", "body": "newer", "published_at": "2026-08-10T11:59:46Z", }, ) ) payload = notes_module.get_release_notes("2026.8.11") assert payload["tag"] == "v0.1.60-beta" assert "newer" in payload["markdown"] @pytest.mark.parametrize( "entry", [ # A draft whose tag the filter accepts, so the flag must reject it. {"tag_name": "v9.9.9", "draft": True}, # A desktop build, which is where the drafts come from. {"tag_name": "desktop-v0.1.60-beta"}, # llama.cpp prebuilts and the legacy month tags are ordinary releases. {"tag_name": "b8475"}, {"tag_name": "February-2026"}, ], ) def test_only_a_published_studio_release_is_shown(notes_module, serve_releases, entry): serve_releases( releases_payload( {"body": "not an announcement", "published_at": "2026-08-11T00:00:00Z", **entry}, { "tag_name": "v0.1.60-beta", "body": "the announcement", "published_at": "2026-08-10T11:59:46Z", }, ) ) payload = notes_module.get_release_notes("2026.8.11") assert payload["tag"] == "v0.1.60-beta" assert "not an announcement" not in (payload["markdown"] or "") @pytest.mark.parametrize("body", ['{"message": "Not Found"}', "[]", "not json at all"]) def test_a_payload_without_releases_is_an_error_not_a_crash(notes_module, serve_releases, body): serve_releases(body) payload = notes_module.get_release_notes("2026.8.11") assert payload["matched"] is False assert payload["error"], "the UI needs to know it can retry" def test_a_release_with_no_announcement_shows_none(notes_module, serve_releases): """v0.1.527-beta's body is only the generated list, so nothing survives stripping: the popup links out rather than showing an older release.""" serve_releases( releases_payload( { "tag_name": "v0.1.527-beta", "name": "Unsloth v0.1.527-beta", "body": (BODIES / "v0.1.527-beta.md").read_text(encoding = "utf-8"), "html_url": "https://github.com/unslothai/unsloth/releases/tag/v0.1.527-beta", "published_at": "2026-08-09T17:14:42Z", }, { "tag_name": "v0.1.526-beta", "body": "An earlier announcement.\n", "published_at": "2026-08-04T16:06:43Z", }, ) ) payload = notes_module.get_release_notes("2026.8.10") assert payload["matched"] is False and payload["markdown"] is None # Still the release it found, so the popup can name it and link to it. assert payload["tag"] == "v0.1.527-beta" assert payload["html_url"].endswith("/releases/tag/v0.1.527-beta") assert payload["error"] is None, "no notes is not a failure" def test_longer_outer_fence_does_not_leak_a_fake_section(notes_module): """A ``` sample inside a ```` block must not close the block and let the sample's heading be indexed as a real release.""" text = "## 1.0\n\n````md\n```\n## 9.9.9\n```\n````\n\n- real note\n" assert [e.version for e in parse_sections(notes_module, text)] == ["1.0"] assert find_section(notes_module, text, "9.9.9") is None def test_tilde_fence_is_not_closed_by_backticks(notes_module): text = "## 1.0\n\n~~~\n```\n## 9.9.9\n~~~\n\n- real\n" assert [e.version for e in parse_sections(notes_module, text)] == ["1.0"] def test_utf8_bom_does_not_hide_the_first_section(notes_module): """Editors on Windows can leave a BOM on the first line.""" assert [e.version for e in parse_sections(notes_module, "\ufeff## 1.0\n\n- x\n")] == ["1.0"] @pytest.mark.parametrize("newline", ["\r\n", "\r"]) def test_non_unix_line_endings(notes_module, newline): text = f"## 1.0{newline}{newline}- windows note{newline}" entry = find_section(notes_module, text, "1.0") assert entry is not None and "windows note" in entry.body assert "\r" not in entry.body def test_closing_fence_must_carry_nothing_after_it(notes_module): """CommonMark: a closer is the delimiter plus whitespace only. A ```` line with trailing text inside a ```` block is content, not the end.""" text = "## 1.0\n\n````md\n```` not a closer\n## 9.9.9\n````\n\n- real\n" assert [e.version for e in parse_sections(notes_module, text)] == ["1.0"] # An opening fence may still carry an info string. info = "## 1.0\n\n```python\n## 9.9.9\n```\n\n- real\n" assert [e.version for e in parse_sections(notes_module, info)] == ["1.0"] @pytest.mark.parametrize( "text", [ "## 1.0\n\n- real\n\n\n", "## 1.0\n\n- real\n\n\n", ], ) def test_commented_out_sections_are_not_releases(notes_module, text): """Markdown does not render them, so they are not published notes.""" assert [e.version for e in parse_sections(notes_module, text)] == ["1.0"] assert find_section(notes_module, text, "9.9.9") is None def test_no_changelog_file_is_packaged_or_read(): """The releases are the only source now. A file left in the packaging would be a second one, editable in a checkout and stale in a wheel.""" assert not (REPO / "CHANGELOG.md").exists() assert not (REPO / "_changelog_build.py").exists() for name in ("pyproject.toml", "build.sh", ".gitignore"): assert "CHANGELOG.md" not in (REPO / name).read_text(encoding = "utf-8") source = MODULE.read_text(encoding = "utf-8") # "Full Changelog" is the footer line it strips; a file is what must be gone. assert "CHANGELOG.md" not in source and "changelog.py" not in source def test_preview_keeps_identifier_underscores(): """UNSLOTH_DISABLE_UPDATE_CHECK must not render as UNSLOTHDISABLEUPDATECHECK.""" src = PREVIEW.read_text(encoding = "utf-8") assert "BOLD_UNDERSCORE" in src and "ITALIC_UNDERSCORE" in src assert "parkCodeSpans" in src, "code spans are parked so their underscores survive" assert "const EMPHASIS" not in src, "the blanket emphasis strip is gone" def test_panel_prefers_the_page_the_notes_came_from(): """The page the notes came from wins over the caller's URL and the changelog.""" src = PANEL.read_text(encoding = "utf-8") assert "notes?.htmlUrl ?? releaseNotesUrl ?? notes?.releaseNotesUrl" in src def test_remote_failure_is_reported_so_the_ui_can_retry(notes_module, monkeypatch): """An unreachable GitHub is retryable; "no notes published" is not.""" monkeypatch.delenv(notes_module.DISABLE_ENV_VAR, raising = False) # Port 9 (discard) refuses fast, standing in for an unreachable host. monkeypatch.setenv(notes_module.RELEASES_URL_ENV_VAR, "http://127.0.0.1:9/releases") notes_module.reset_release_notes_cache() try: payload = notes_module.get_release_notes("2.0") assert payload["matched"] is False assert payload["error"], "remote failure must reach the UI" finally: notes_module.reset_release_notes_cache() def test_preview_keeps_comparison_operators(): """The tag strip must keep the operators in "Support Python <3.15 and >3.9".""" src = PREVIEW.read_text(encoding = "utf-8") assert "/<\\/?[a-zA-Z][^>]*>/g" in src, "tag strip must require a name character" def test_preview_hides_commented_out_notes(): """Unpublished notes inside are not rendered, so not previewed.""" src = PREVIEW.read_text(encoding = "utf-8") assert "stripCommentSpans" in src and "COMMENT_OPEN" in src def test_hook_treats_a_reported_failure_as_retryable(): src = NOTES_HOOK.read_text(encoding = "utf-8") assert "next.error !== null" in src def test_comment_delimiter_in_inline_code_is_literal(notes_module): """A note documenting ` renders as nothing, so it is unpublished.""" serve_releases( releases_payload( { "tag_name": "v2.0", "body": "\n", "published_at": "2026-08-10T00:00:00Z", } ) ) staged = notes_module.get_release_notes("2.0") assert staged["matched"] is False and staged["markdown"] is None assert staged["source"] is None, "nothing was shown, so nothing sourced it" @pytest.mark.parametrize( "body,visible", [ ("- note", True), ("", False), ("```\n```", True), ("
\n
", True), (" ", False), ], ) def test_visibility_check_only_hides_comments(notes_module, body, visible): assert notes_module._renders_visibly(body) is visible @pytest.mark.parametrize( "block", [ "", "", "", ], ) def test_processing_instructions_and_declarations_are_literal(notes_module, block): """Raw block types 3 to 5 render literally, so a heading in one is a sample.""" text = f"## 1.0\n\n{block}\n\n- real note\n" assert [e.version for e in parse_sections(notes_module, text)] == ["1.0"] assert "real note" in find_section(notes_module, text, "1.0").body def test_headings_need_a_space_or_tab_after_the_hashes(notes_module): """A non-breaking space after the hashes renders as text, not a heading.""" text = "## 1.0\n\n- real note\n\n## 9.9.9\n\n- not a release\n" assert [e.version for e in parse_sections(notes_module, text)] == ["1.0"] assert find_section(notes_module, text, "9.9.9") is None # A tab is valid and still opens a heading. tabbed = "## 1.0\n\n- one\n\n##\t2.0\n\n- two\n" assert [e.version for e in parse_sections(notes_module, tabbed)] == ["1.0", "2.0"] def test_preview_skips_every_raw_block_form(): """The extractor tracks the same block forms as the parser.""" src = PREVIEW.read_text(encoding = "utf-8") assert "RAW_BLOCKS" in src assert "CDATA" in src and "[A-Za-z]" in src @pytest.mark.parametrize("banner", [WEB_BANNER, TAURI_BANNER]) def test_expanded_popup_fits_a_short_viewport(banner): """A window under roughly 430px used to push the card's title off screen.""" panel = PANEL.read_text(encoding = "utf-8") # The notes region shrinks inside the capped card, so header and actions stay on screen. assert "min-h-0 flex-1" in panel, "notes height must follow the viewport" src = banner.read_text(encoding = "utf-8") assert "max-h-[calc(100dvh_-_2rem)]" in src, "card is the backstop on tiny viewports" def test_relative_release_body_links_point_at_the_repository(): """Repository-relative links would resolve against Studio's own origin.""" src = LINKS.read_text(encoding = "utf-8") assert "https://github.com/unslothai/unsloth/blob/main/" in src assert "https://raw.githubusercontent.com/unslothai/unsloth/main/" in src # Absolute targets, fragments, fenced code and code spans stay untouched. assert "ABSOLUTE" in src and "codeSpans" in src and "FENCE" in src panel = PANEL.read_text(encoding = "utf-8") assert "resolveReleaseBodyLinks" in panel @pytest.mark.parametrize("query", ["latest", "main", "not-a-version", "abc"]) def test_unparseable_versions_are_rejected(notes_module, query): """A query that cannot parse is a bad request, not an empty result.""" assert notes_module.is_supported_version_query(query) is False @pytest.mark.parametrize("query", ["2026.7.5", "v2026.7.5", "2026.07.5", "1.0.0rc1"]) def test_real_versions_are_still_accepted(notes_module, query): assert notes_module.is_supported_version_query(query) is True def test_reference_style_images_resolve_to_the_raw_host(): """An image needs the raw file: the blob URL is an HTML page.""" src = LINKS.read_text(encoding = "utf-8") assert "IMAGE_REFERENCE" in src assert "imageLabels" in src def test_collapsed_notes_surface_is_hidden_when_nothing_previews(): """Notes that preview as nothing leave an empty strip, worse than none.""" src = PANEL.read_text(encoding = "utf-8") assert "preview?.items.length === 0" in src def test_a_fence_closer_accepts_only_spaces_and_tabs(notes_module): """A delimiter followed by a non-breaking space is content, not a closer.""" text = "## 1.0\n\n```\n```\u00a0\n## 9.9.9\n```\n\n- real note\n" assert [e.version for e in parse_sections(notes_module, text)] == ["1.0"] plain = "## 1.0\n\n```\nx\n```\t\n\n## 2.0\n\n- two\n" assert [e.version for e in parse_sections(notes_module, plain)] == ["1.0", "2.0"] # The same rule in both frontend scanners. for source in (PREVIEW, LINKS): assert "/[^ \\t]/" in source.read_text(encoding = "utf-8") def test_code_spans_close_on_a_run_of_equal_length(): """`a``b [x](y.md)` is one code span, so the link inside it is literal.""" src = CODE_SPANS.read_text(encoding = "utf-8") assert "candidate === ticks" in src, "closer length must match the opener" # Shared, so the preview and the link resolver cannot drift apart. assert "markdown-code-spans" in PREVIEW.read_text(encoding = "utf-8") assert "markdown-code-spans" in LINKS.read_text(encoding = "utf-8") def test_preview_decodes_entities_like_the_renderer(): """Streamdown renders `AT&T` as AT&T, so the raw entity must not show.""" src = PREVIEW.read_text(encoding = "utf-8") assert "NAMED_ENTITIES" in src and "decodeEntity" in src # Decoded before code spans are restored, so code keeps the literal text. assert src.index(".replace(ENTITY, decodeEntity)") < src.index(".replace(PARKED") def test_release_notes_request_refreshes_an_expired_token(): """A direct fetch cannot recover from a 401; authFetch refreshes first.""" src = NOTES_HOOK.read_text(encoding = "utf-8") assert "authFetch(" in src assert "getAuthToken" not in src def test_preview_handles_the_desktop_updater_line_endings(): """CRLF used to hide fences and promote a code sample to a headline.""" src = PREVIEW.read_text(encoding = "utf-8") assert "LINE_ENDINGS" in src assert "LINE_ENDINGS" in LINKS.read_text(encoding = "utf-8") def test_preview_renders_reference_links_as_text(): """`[text][label]` renders as a link, so its raw markup must not show.""" src = PREVIEW.read_text(encoding = "utf-8") assert "LINK_REFERENCE" in src and "IMAGE_REFERENCE" in src # A definition line renders as nothing, so it is not a preview item. assert "DEFINITION" in src def test_preview_treats_escaped_punctuation_as_literal(): """`\\*not italic\\*` keeps its stars, and an escaped backtick opens no span.""" assert "ESCAPE" in PREVIEW.read_text(encoding = "utf-8") assert "escaped(" in CODE_SPANS.read_text(encoding = "utf-8") def test_link_resolver_skips_every_code_form(): """Indented code and cross-line code spans render as code, so leave them.""" src = LINKS.read_text(encoding = "utf-8") assert "INDENTED_CODE" in src # Spans are scanned over the whole document, not line by line. assert "codeSpans(masked)" in src # A definition cannot interrupt a paragraph. assert "definition.has(index)" in src def test_badge_links_resolve_both_targets(): """`[![alt](img)](link)`: the outer link needs a nested label to resolve.""" assert "NESTED_LABEL" in LINKS.read_text(encoding = "utf-8") def test_in_flight_requests_are_identified_not_just_versioned(): """Two requests for one version could resolve out of order.""" assert "requestIdRef" in NOTES_HOOK.read_text(encoding = "utf-8") def test_notes_repair_the_shared_previews_width_reset(): """MarkdownPreview clears max-width on descendants, so wide content escapes.""" src = PANEL.read_text(encoding = "utf-8") assert "[&_img]:max-w-full" in src assert "[&_[data-streamdown=link-safety-modal]>*]:max-w-md" in src @pytest.mark.parametrize("banner", [WEB_BANNER, TAURI_BANNER]) def test_only_the_notes_region_scrolls(banner): """The dismiss control sits inside the card, so the card must not scroll.""" src = banner.read_text(encoding = "utf-8") assert "flex max-h-[calc(100dvh_-_2rem)] min-h-0 flex-col overflow-hidden" in src assert 'className="min-h-0 flex-1"' in src panel = PANEL.read_text(encoding = "utf-8") assert "max-h-64 min-h-0 flex-1 overflow-y-auto" in panel # The collapsed summary scrolls too: without it the bullets were painted # over the row of buttons once the card's slot for them got small. assert "min-h-0 flex-1 space-y-1 overflow-y-auto" in panel def test_a_comment_marker_in_prose_cannot_swallow_later_releases(notes_module): """A note that mentions `\n\n- note\n" assert [e.version for e in parse_sections(notes_module, hidden)] == ["2.0"] def test_unmatched_backtick_runs_stay_linear(notes_module): """Rescanning the suffix per opener was quadratic: 800 never-closing runs took 7.7s at 321 KB, and notes are reparsed on every popup request.""" line = "".join("`" * (i + 1) + "x" for i in range(800)) assert len(line) > 300_000 started = time.monotonic() assert notes_module._code_span_ranges(line) == [] assert time.monotonic() - started < 2.0 def test_a_base_exception_releases_the_single_flight_flag(notes_module, monkeypatch): """The flag was cleared only after `except Exception`, so a BaseException stranded it and every later caller waited out the full deadline.""" notes_module.reset_release_notes_cache() def explode(): raise KeyboardInterrupt monkeypatch.setattr(notes_module, "_fetch_latest_release", explode) with pytest.raises(KeyboardInterrupt): notes_module.get_latest_release() assert notes_module._remote_fetching is False notes_module.reset_release_notes_cache() @pytest.mark.parametrize("marker", ["", ""]) def test_an_empty_comment_does_not_swallow_later_releases(notes_module, marker): """`` and `` are complete comments: the closer overlaps the opener, so searching past it hid every release below.""" text = f"## 2.0\n\n- new stuff\n\n{marker}\n\n## 1.0\n\n- old stuff\n" assert [e.version for e in parse_sections(notes_module, text)] == ["2.0", "1.0"] assert find_section(notes_module, text, "1.0") is not None assert "old stuff" not in find_section(notes_module, text, "2.0").body # The frontend scanner has to agree, or the preview and the body disagree. assert "!line.includes(COMMENT_CLOSE)" in PREVIEW.read_text(encoding = "utf-8") def test_an_unterminated_comment_still_hides_the_rest(notes_module): """The fix must not turn every `## 9.9.9\n\n- note\n", "## 1.0\n\n
\nx\n
## 9.9.9\n\n- note\n", ): assert [e.version for e in parse_sections(notes_module, text)] == ["1.0"] def test_an_exact_heading_is_never_shadowed(notes_module): """PEP 440 says 1.0 == 1.0.0, so the normalised match used to beat exact.""" text = "## 1.0.0\n\n- padded\n\n## 1.0\n\n- exact\n" assert find_section(notes_module, text, "1.0").body == "- exact" assert find_section(notes_module, text, "1.0.0").body == "- padded" # Normalised matching still applies when there is no exact heading. assert find_section(notes_module, "## 2026.7.6\n\n- x\n", "2026.07.6") is not None def test_setext_headings_are_release_boundaries(notes_module): """A version over a line of dashes is the same heading in setext form.""" text = "2.0\n---\n\n- new\n\n1.0\n---\n\n- old\n" assert [e.version for e in parse_sections(notes_module, text)] == ["2.0", "1.0"] assert find_section(notes_module, text, "2.0").body == "- new" # A rule between sections is still a rule, and a setext h1 is not a release. assert [ e.version for e in parse_sections(notes_module, "## 2.0\n\n- a\n\n---\n\n## 1.0\n\n- b\n") ] == ["2.0", "1.0"] def test_a_long_backtick_run_does_not_stall_the_parser(notes_module): """The code-span guard used to backtrack: 20k backticks took over a minute.""" import time text = "## 1.0\n\n- " + "`" * 20_000 + " " * 16_000 assert len(line) < notes_module.RELEASES_MAX_BYTES started = time.monotonic() visible, in_comment = notes_module._strip_comments(line, False, False) elapsed = time.monotonic() - started # Roughly 40ms scanning forward against roughly 11s restarting each time. assert elapsed < 2.0, f"comment stripping took {elapsed:.1f}s" # Same result as before: the spans survive and the comments are gone. assert in_comment is False assert "`\n- See [docs](docs/a.md)\n") assert repo in spanned # A comment starting a line is a block: it hides down to the closer's line, that line included. block = run_scanner("links", "\n") assert repo not in block closer = run_scanner("links", " See [docs](docs/a.md)\n") assert repo not in closer def test_a_bare_level_two_marker_ends_the_release(notes_module, run_scanner): """An ATX heading's opening sequence may be followed by the end of the line (spec 0.31.2 section 4.2), so a bare `##` is an empty level-two heading. Requiring whitespace kept everything below it inside the release above.""" text = "## 2.0\n\n- new thing\n\n##\n\n- SECRET: not part of 2.0\n" entry = find_section(notes_module, text, "2.0") assert "new thing" in entry.body assert "SECRET" not in entry.body # An empty heading has no version, so it ends a release without indexing one. assert [e.version for e in parse_sections(notes_module, text)] == ["2.0"] # Prose still needs a space or a tab: `##x` is a paragraph, not a heading. prose = "## 2.0\n\n- new thing\n\n##x\n\n- still 2.0\n" assert "still 2.0" in find_section(notes_module, prose, "2.0").body # The preview agrees: an empty heading renders as nothing, so it ends the bullet. preview = run_scanner("preview", "- new thing\n##\nUnrelated scratch notes\n") assert preview_leads(preview) == ["new thing"] def test_a_comment_between_bullets_closes_the_list(notes_module, run_scanner): """A comment is an HTML block (spec 0.31.2 section 4.6, type 2), so one at the margin under a bullet closes the list. Blanking the line before list tracking saw it reads as a blank line, which leaves the item open and made the release heading below look like nested content.""" text = "## 1.0\n\n- old item\n\n ## 2.0\n\n- new item\n" assert [e.version for e in parse_sections(notes_module, text)] == ["1.0", "2.0"] assert "new item" not in find_section(notes_module, text, "1.0").body assert "new item" in find_section(notes_module, text, "2.0").body # At the item's content column the comment stays inside it, so the heading under it is nested. nested = "## 1.0\n\n- old item\n \n ## 2.0\n\n- new item\n" assert [e.version for e in parse_sections(notes_module, nested)] == ["1.0"] # The link resolver reads the same column: list closed, four spaces is code, left untouched. code = run_scanner("links", "- old item\n\n [guide](docs/a.md)\n") assert "[guide](docs/a.md)" in code and "github.com" not in code # Inside the item those four spaces are two columns in, so it is prose and the link resolves. prose = run_scanner("links", "- old item\n \n [guide](docs/a.md)\n") assert "https://github.com/unslothai/unsloth/blob/main/docs/a.md" in prose # The preview agrees: the fence is indented code, not a fence swallowing the bullet below. preview = run_scanner( "preview", "- Details:\n\n ```\n - hidden sample\n- Real second item\n", ) assert preview_leads(preview) == ["Details:", "Real second item"] def test_a_parenthesised_link_destination_still_resolves(run_scanner): """A destination may hold parentheses while they balance (spec 0.31.2 section 6.3), so `[x]((draft).md)` points at `(draft).md`. Stopping at the first paren matched an empty destination and left the link relative.""" leading = run_scanner("links", "[details]((draft).md)\n") assert "https://github.com/unslothai/unsloth/blob/main/(draft).md" in leading # An image resolves against the raw host the same way. image = run_scanner("links", "![shield]((badge).png)\n") assert "https://raw.githubusercontent.com/unslothai/unsloth/main/(badge).png" in image # A pair in the middle of a path balances too. middle = run_scanner("links", "[api](docs/(v2)/api.md)\n") assert "https://github.com/unslothai/unsloth/blob/main/docs/(v2)/api.md" in middle # An unbalanced paren makes the destination invalid, so `[x](a(b.md)` is plain text, not a link. unbalanced = run_scanner("links", "[x](a(b.md)\n") assert unbalanced == "[x](a(b.md)\n" # One more closer balances the pair, and then it is a link again. closed = run_scanner("links", "[x](a(b.md))\n") assert "https://github.com/unslothai/unsloth/blob/main/a(b.md)" in closed # Pairs nest, and one level was all the expression allowed, so a path with two stayed relative. nested = run_scanner("links", "[x](((draft)).md)\n") assert "https://github.com/unslothai/unsloth/blob/main/((draft)).md" in nested deep = run_scanner("links", "![shot](((((v2))))).png)\n") assert "https://raw.githubusercontent.com/unslothai/unsloth/main/((((v2))))" in deep # The closer must still be there: an unbalanced run below a nested pair is not a link. across = run_scanner("links", "[x](((a).md\n[y](docs/y.md)\n") assert "https://github.com/unslothai/unsloth/blob/main/docs/y.md" in across assert "[x](((a).md" in across def test_a_fence_inside_a_container_still_hides_its_sample(run_scanner): """A fence is measured from its container (spec 0.31.2 section 4.5), so `> ~~~` and one under a nested bullet both open a fence. Reading the margin never saw them, so a link in a code block was rewritten into verbatim text.""" quoted = run_scanner("links", "> ~~~\n> [guide](docs/a.md)\n> ~~~\n") assert "[guide](docs/a.md)" in quoted and "github.com" not in quoted nested = run_scanner("links", "- a\n - b\n ~~~\n [x](docs/x.md)\n ~~~\n") assert "[x](docs/x.md)" in nested and "github.com" not in nested # A longer closer is still a closer, so the pair is not something a code span hid. uneven = run_scanner("links", "> ```\n> [guide](docs/a.md)\n> ````\n") assert "[guide](docs/a.md)" in uneven and "github.com" not in uneven # The fence ends with its container: a line outside the quote, or left of the item, is Markdown. left = run_scanner("links", "> ~~~\n[guide](docs/a.md)\n") assert "https://github.com/unslothai/unsloth/blob/main/docs/a.md" in left dedented = run_scanner("links", "- a\n ~~~\n[guide](docs/a.md)\n") assert "https://github.com/unslothai/unsloth/blob/main/docs/a.md" in dedented # A document-level fence owns the quoted lines below, so the marker does not undo it. document = run_scanner("links", "~~~\n> [guide](docs/a.md)\n~~~\n") assert "[guide](docs/a.md)" in document and "github.com" not in document # Four columns past the item's content column it is indented code, not a fence: still literal. code = run_scanner("links", "- Details:\n\n ~~~\n [guide](docs/a.md)\n") assert "[guide](docs/a.md)" in code and "github.com" not in code def test_an_html_block_inside_a_container_is_literal_too(run_scanner): """Type 1 and type 6 blocks are measured from their container too, so a `
` under a nested bullet and a `
` in a quote are verbatim.
    Missing the opener rewrote the literal examples inside them."""
    nested = run_scanner("links", "- a\n  - b\n    
\n [x](docs/x.md)\n
\n") assert "[x](docs/x.md)" in nested and "github.com" not in nested quoted = run_scanner("links", ">
\n> [x](docs/x.md)\n> 
\n") assert "[x](docs/x.md)" in quoted and "github.com" not in quoted # The block ends with its container, so a line dedented out of the item is Markdown again. dedented = run_scanner("links", "- a\n - b\n
\n[x](docs/x.md)\n") assert "https://github.com/unslothai/unsloth/blob/main/docs/x.md" in dedented # Inside a quote a bare marker holds nothing, the blank line that ends a type 6 block. blank = run_scanner("links", ">
\n>\n> [x](docs/x.md)\n") assert "https://github.com/unslothai/unsloth/blob/main/docs/x.md" in blank def test_an_underline_left_of_an_item_is_lazy_text_of_it(notes_module, run_scanner): """A setext underline may never be a lazy continuation (spec 0.31.2 section 4.3), so `===` left of an open item is more of the item's paragraph. Rejecting it ended the list and promoted the nested "## 2.0" to a release.""" nested = "## 1.0\n- old note\n===\n ## 2.0\n- new\n" assert [e.version for e in parse_sections(notes_module, nested)] == ["1.0"] # A row of dashes is a thematic break, closing the item, so the heading is the next release. broken = "## 1.0\n- old note\n---\n ## 2.0\n" assert [e.version for e in parse_sections(notes_module, broken)] == ["1.0", "2.0"] # With no paragraph above it the underline opens one, so the blank line closes the item. apart = "## 1.0\n- old note\n\n===\n ## 2.0\n" assert [e.version for e in parse_sections(notes_module, apart)] == ["1.0", "2.0"] # The link scanner keeps the item open, so the four-space line is a paragraph and resolves. resolved = run_scanner("links", "- Details:\n===\n\n [guide](docs/a.md)\n") assert "https://github.com/unslothai/unsloth/blob/main/docs/a.md" in resolved def test_a_quote_keeps_its_paragraph_to_itself(notes_module, run_scanner): """A marker written outside a blockquote is not text of the quote's paragraph, so `2. item` under `> quote` opens a list even though an ordered marker past 1 may not interrupt one (spec 0.31.2 section 5.2). Lending the paragraph to the document left the list closed and the heading exposed.""" quoted = "## 1.0\n> quote\n2. item\n ## 2.0\n- new\n" assert [e.version for e in parse_sections(notes_module, quoted)] == ["1.0"] # A quote holding a heading leaves no paragraph, nor does an empty one, so the list opens. heading = "## 1.0\n> # inner\n2. item\n ## 2.0\n" assert [e.version for e in parse_sections(notes_module, heading)] == ["1.0"] # An unquoted line the quote's paragraph swallows keeps it open, the marker still outside. lazy = "## 1.0\n> quote\ntext\n2. item\n ## 2.0\n" assert [e.version for e in parse_sections(notes_module, lazy)] == ["1.0"] # Under an ordinary paragraph the marker is its text, so no list opens and the heading is real. prose = "## 1.0\nprose\n2. item\n ## 2.0\n" assert [e.version for e in parse_sections(notes_module, prose)] == ["1.0", "2.0"] # The preview reads the marker as a bullet for the same reason. assert preview_leads(run_scanner("preview", "> quote\n2. item\n")) == ["item"] def test_indented_code_before_an_ordered_marker_still_opens_a_list(notes_module): """An indented code block ends at the first line not indented enough, and no paragraph is open, so `2. item` opens a list whatever its start number. Reading it as code text left the list closed and the nested heading exposed.""" joined = "## 1.0\n\n code\n2. item\n ## 2.0\n- new\n" assert [e.version for e in parse_sections(notes_module, joined)] == ["1.0"] # A blank line between the two changes nothing: the list opens either way. apart = "## 1.0\n\n code\n\n2. item\n ## 2.0\n- new\n" assert [e.version for e in parse_sections(notes_module, apart)] == ["1.0"] # Four columns past its container the marker is code, so no list opens and the heading stands. inside = "## 1.0\n\n code\n - item\n ## 2.0\n" assert [e.version for e in parse_sections(notes_module, inside)] == ["1.0", "2.0"] def test_a_fence_written_as_an_item_first_content_opens_in_that_item(run_scanner): """A block straight after a marker is the item's own first content, measured from where that content starts (spec 0.31.2 section 5.2), so "- ```md" opens a fence. Reading the whole line never saw one, so the sample below was treated as prose and rewritten, and its info string became a headline.""" sample = run_scanner("links", "- ```md\n [example](docs/a.md)\n ```\n") assert "[example](docs/a.md)" in sample and "github.com" not in sample ordered = run_scanner("links", "1. ~~~\n [example](docs/a.md)\n ~~~\n") assert "[example](docs/a.md)" in ordered and "github.com" not in ordered # The preview agrees: an item of only a code block previews as nothing; the next is a bullet. preview = run_scanner("preview", "- ```md\n sample text\n ```\n- Added tests\n") assert preview_leads(preview) == ["Added tests"] # One column further in it is indented code inside the item, so the link is prose and resolves. padded = run_scanner("links", "- ```\n [example](docs/a.md)\n") assert "https://github.com/unslothai/unsloth/blob/main/docs/a.md" in padded # A marker the paragraph above swallows opens no item, so no fence: ordered items open at 1. lazy = run_scanner("links", "Intro.\n2. ```\n[guide](docs/a.md)\n") assert "https://github.com/unslothai/unsloth/blob/main/docs/a.md" in lazy def test_an_html_block_ends_with_the_item_it_was_written_in(notes_module, run_scanner): """An HTML block takes no lazy continuation line, so one opened on an item's continuation line ends with the item, as a fence there does. Ending it only on a blank line let it swallow the next release heading.""" text = "## 1.0\n\n- item\n\n
\n## 2.0\n\n- new thing\n" assert [e.version for e in parse_sections(notes_module, text)] == ["1.0", "2.0"] assert "new thing" in find_section(notes_module, text, "2.0").body # A raw block such as
 is scoped the same way.
    raw = "## 1.0\n\n- item\n\n  
\n## 2.0\n\n- new thing\n"
    assert [e.version for e in parse_sections(notes_module, raw)] == ["1.0", "2.0"]
    # At the item's content column the block holds the heading, which is nested and indexes nothing.
    nested = "## 1.0\n\n- item\n\n  
\n ## 2.0\n" assert [e.version for e in parse_sections(notes_module, nested)] == ["1.0"] # The preview reads it the same way: the bullet below the block is a bullet. preview = run_scanner("preview", "- item\n\n
\n- Added tests\n") assert preview_leads(preview) == ["item", "Added tests"] # An opener straight after a marker opens in that item, so the dedented heading is a release. marked = "## 1.0\n\n-
\n## 2.0\n\n- new thing\n" assert [e.version for e in parse_sections(notes_module, marked)] == ["1.0", "2.0"] def test_a_comment_may_close_on_a_later_line_of_its_paragraph(run_scanner): """A comment written mid-sentence belongs to its paragraph, so its `-->` may arrive on a later line of it and everything between renders as nothing. Ending it at its own line left a backtick inside pairing with a real one below, hiding a link, and left the preview quoting hidden text.""" carried = run_scanner("links", "Note see [d](docs/a.md) and `x`\n") assert "https://github.com/unslothai/unsloth/blob/main/docs/a.md" in carried # Text inside the comment renders as nothing, so it is left alone. inside = run_scanner("links", "Note end\n") assert "[c](docs/c.md)" in inside and "github.com" not in inside # The preview hides it too, rather than quoting the comment at the reader. preview = run_scanner( "preview", "- Added X \n- Second\n" ) assert preview_leads(preview) == ["Added X", "Second"] # An opener cannot outlive its paragraph: with it closed the ` end [d](docs/a.md)\n") assert "https://github.com/unslothai/unsloth/blob/main/docs/a.md" in broken # A heading breaks into the paragraph, so it ends the comment's reach too. headed = run_scanner("links", "Note end [d](docs/a.md)\n") assert "https://github.com/unslothai/unsloth/blob/main/docs/a.md" in headed assert preview_leads(run_scanner("preview", "Note ` on a line of its own, and a wrapped line may open with emphasis. Reading any leading punctuation as a new block meant neither continued the paragraph, so the comment never closed and the popup showed the author's internal note.""" closer = run_scanner( "preview", "- DoRA training is available in Studio. \n", ) assert preview_leads(closer) == ["DoRA training is available in Studio."] # A continuation may open with emphasis, which is text and not a block. starred = run_scanner( "preview", "- DoRA training is available. \n", ) assert preview_leads(starred) == ["DoRA training is available."] underscored = run_scanner( "preview", "- DoRA training is available. \n", ) assert preview_leads(underscored) == ["DoRA training is available."] # A real block still ends the paragraph, so the opener below one is text and hides nothing. broken = run_scanner("links", "Note [d](docs/a.md)\n") assert "https://github.com/unslothai/unsloth/blob/main/docs/a.md" in broken # So does a list item with content, which may interrupt a paragraph. item = run_scanner("links", "Note [d](docs/a.md)\n") assert "https://github.com/unslothai/unsloth/blob/main/docs/a.md" in item def test_a_comment_written_as_an_item_first_content_is_a_block(notes_module, run_scanner): """A comment is an HTML block (spec 0.31.2 section 4.6, type 2), so one written as an item's first content opens inside that item, as a fence does. Looking for the opener at the margin let a marker in front of it hide the block, so the resolver rewrote hidden text and the preview quoted it.""" item = run_scanner("links", "- AMD support, see [the guide](docs/amd.md)\n") assert item == "- AMD support, see [the guide](docs/amd.md)\n" # Every marker opens an item, and a nested one is still an item. for text in ( "* see [the guide](docs/amd.md)\n", "1. see [the guide](docs/amd.md)\n", "- outer\n - see [the guide](docs/amd.md)\n", ): assert "github.com" not in run_scanner("links", text) # The multiline form hides lines to the closer, as a comment at the item's content column did. multiline = run_scanner("links", "- \n") assert "[a](docs/x.md)" in multiline and "github.com" not in multiline # Still scoped to the item it was written in, so a line dedented out of it ends the block. dedented = run_scanner("links", "- hidden note\n- Real bullet\n") assert preview_leads(preview) == ["Real bullet"] # The parser agrees too: the item keeps its column, so a heading inside is nested, not indexed. text = "## 1.0\n\n-