"""Roadmap #28 Part 1 — auto-link RFC references in PR text + comments. Two layers: * Unit tests over the pure scanner (`rfc_links.segment_text` / `_keys_for` / `LinkIndex`) — the matching rules and their false-positive guards, no DB. * End-to-end tests that the PR description, PR review comments, and PR-less discussion comments all surface `*_segments` enriched against the live accepted-RFC corpus, with self-references suppressed. Reuses the FakeGitea + session helpers from test_propose_vertical.py and the active-RFC seed from test_rfc_view_vertical.py. """ from __future__ import annotations from app import rfc_links from test_propose_vertical import ( # noqa: F401 FakeGitea, app_with_fake_gitea, grant_rfc_collaborator, provision_user_row, sign_in_as, tmp_env, ) from test_rfc_view_vertical import SEED_BODY, seed_active_rfc from test_pr_flow_vertical import _cut_branch_and_accept_change # --------------------------------------------------------------------------- # Unit — the pure scanner # --------------------------------------------------------------------------- def _idx(*terms): """Build a LinkIndex from raw (key, slug, title) tuples (keys lower).""" return rfc_links.LinkIndex(list(terms)) def test_empty_text_is_single_empty_segment(): assert rfc_links.segment_text("", []) == [{"type": "text", "text": ""}] assert rfc_links.segment_text(None, []) == [{"type": "text", "text": ""}] def test_no_terms_returns_plain_text(): out = rfc_links.segment_text("hello world", []) assert out == [{"type": "text", "text": "hello world"}] def test_multiword_title_links_and_preserves_casing(): idx = _idx(("open human model", "open-human-model", "Open Human Model")) out = idx.segment("See the Open Human Model for details.") assert out == [ {"type": "text", "text": "See the "}, {"type": "rfc", "slug": "open-human-model", "label": "Open Human Model", "title": "Open Human Model"}, {"type": "text", "text": " for details."}, ] def test_match_is_case_insensitive(): idx = _idx(("open human model", "open-human-model", "Open Human Model")) out = idx.segment("see the OPEN HUMAN MODEL") assert out[-1] == {"type": "rfc", "slug": "open-human-model", "label": "OPEN HUMAN MODEL", "title": "Open Human Model"} def test_word_boundary_prevents_substring_match(): # "harm" must not match inside "charming" / "harmless". idx = _idx(("rfc-0001", "open-human-model", "Open Human Model")) out = idx.segment("a charming rfc-00012 not real") # rfc-0001 is a prefix of rfc-00012 but the trailing '2' is a word char, # so no match — the whole string stays plain text. assert out == [{"type": "text", "text": "a charming rfc-00012 not real"}] def test_rfc_id_token_links(): idx = _idx(("rfc-0001", "open-human-model", "Open Human Model")) out = idx.segment("as established in RFC-0001.") assert out[1] == {"type": "rfc", "slug": "open-human-model", "label": "RFC-0001", "title": "Open Human Model"} def test_longest_match_wins(): # A bare "Open" term and the full title both present; the full title # (longer) must win at the position. idx = _idx( ("open", "open", "Open"), ("open human model", "open-human-model", "Open Human Model"), ) out = idx.segment("the Open Human Model") assert out[-1]["slug"] == "open-human-model" assert out[-1]["label"] == "Open Human Model" def test_keys_for_gating(): keys = lambda **kw: set(rfc_links._keys_for(**kw)) # rfc_id always contributes. assert "rfc-0001" in keys(slug="x", title="X", rfc_id="RFC-0001") # multi-word title contributes; single common word does NOT. assert "open human model" in keys(slug="ohm", title="Open Human Model", rfc_id=None) assert keys(slug="human", title="Human", rfc_id=None) == set() # hyphenated slug contributes; single-token slug does NOT. assert "open-human-model" in keys(slug="open-human-model", title="X", rfc_id=None) assert "ohm" not in keys(slug="ohm", title="OHM", rfc_id=None) # --------------------------------------------------------------------------- # End-to-end — enrichment surfaces on the read paths # --------------------------------------------------------------------------- def _open_pr_on(client, fake, *, host_slug: str, description: str): """Seed branch + accepted change on host_slug and open a PR. Returns the pr_number.""" # `original` must exist verbatim in SEED_BODY or the accept is "stale". branch, _ = _cut_branch_and_accept_change( client, fake, slug=host_slug, original="It defines consent, trait, and agency in compatible terms.", proposed="It defines consent, trait, harm, and agency in compatible terms.", ) r = client.post( f"/api/rfcs/{host_slug}/branches/{branch}/open-pr", json={"title": "A change", "description": description}, ) assert r.status_code == 200, r.text return r.json()["pr_number"] def _rfc_segments(segments): return [s for s in segments if s["type"] == "rfc"] def test_pr_description_autolinks_other_rfc(app_with_fake_gitea): from fastapi.testclient import TestClient app, fake = app_with_fake_gitea with TestClient(app) as client: provision_user_row(user_id=2, login="alice", role="contributor") # Two accepted RFCs: a host for the PR + a referenceable target. seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY) seed_active_rfc(fake, slug="open-human-model", title="Open Human Model", body=SEED_BODY) sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor") pr_number = _open_pr_on( client, fake, host_slug="ohm", description="This builds on the Open Human Model definition.", ) r = client.get(f"/api/rfcs/ohm/prs/{pr_number}") assert r.status_code == 200, r.text pr = r.json() links = _rfc_segments(pr["description_segments"]) assert len(links) == 1 assert links[0]["slug"] == "open-human-model" assert links[0]["label"] == "Open Human Model" # The plain text is still present for non-segment callers (the bot # appends a §6.5 On-behalf-of trailer, so this is a containment check). assert "This builds on the Open Human Model definition." in pr["description"] def test_pr_review_comment_autolinked(app_with_fake_gitea): from fastapi.testclient import TestClient app, fake = app_with_fake_gitea with TestClient(app) as client: provision_user_row(user_id=2, login="alice", role="contributor") seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY) seed_active_rfc(fake, slug="open-human-model", title="Open Human Model", body=SEED_BODY) sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor") pr_number = _open_pr_on(client, fake, host_slug="ohm", description="plain.") r = client.post( f"/api/rfcs/ohm/prs/{pr_number}/review", json={"text": "See Open Human Model and RFC-0001.", "anchor_payload": {}}, ) assert r.status_code == 200, r.text pr = client.get(f"/api/rfcs/ohm/prs/{pr_number}").json() all_msgs = [m for msgs in pr["messages_by_thread"].values() for m in msgs] review_msgs = [m for m in all_msgs if "Open Human Model" in (m["text"] or "")] assert review_msgs, "review comment not found in payload" links = _rfc_segments(review_msgs[0]["text_segments"]) # Both "Open Human Model" (title) and "RFC-0001" (id) point to the # one referenceable RFC. assert {s["slug"] for s in links} == {"open-human-model"} assert {s["label"] for s in links} == {"Open Human Model", "RFC-0001"} def test_discussion_comment_autolinked(app_with_fake_gitea): from fastapi.testclient import TestClient app, fake = app_with_fake_gitea with TestClient(app) as client: provision_user_row(user_id=2, login="alice", role="contributor") seed_active_rfc(fake, slug="ohm", title="OHM", body=SEED_BODY) seed_active_rfc(fake, slug="open-human-model", title="Open Human Model", body=SEED_BODY) # alice is the seeded owner of ohm (owners=["alice"]); grant the # per-RFC collaborator row explicitly so the #12 discuss gate passes. grant_rfc_collaborator(user_id=2, rfc_slug="ohm", role_in_rfc="contributor") sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor") r = client.post( "/api/rfcs/ohm/discussion/threads", json={"message": "Compare with the Open Human Model."}, ) assert r.status_code == 200, r.text thread_id = r.json()["thread_id"] r = client.get(f"/api/rfcs/ohm/discussion/threads/{thread_id}/messages") assert r.status_code == 200, r.text msgs = r.json()["messages"] assert msgs and "text_segments" in msgs[0] links = _rfc_segments(msgs[0]["text_segments"]) assert len(links) == 1 assert links[0]["slug"] == "open-human-model" def test_self_reference_not_linked(app_with_fake_gitea): from fastapi.testclient import TestClient app, fake = app_with_fake_gitea with TestClient(app) as client: provision_user_row(user_id=2, login="alice", role="contributor") # The host RFC has a multi-word title, so absent exclude_slug it # WOULD self-link. exclude_slug must suppress it. seed_active_rfc(fake, slug="open-human-model", title="Open Human Model", body=SEED_BODY) sign_in_as(client, user_id=2, gitea_login="alice", display_name="Alice", role="contributor") pr_number = _open_pr_on( client, fake, host_slug="open-human-model", description="Refines the Open Human Model definition.", ) pr = client.get(f"/api/rfcs/open-human-model/prs/{pr_number}").json() assert _rfc_segments(pr["description_segments"]) == []