unsloth/studio/backend/tests/test_rag_source_identity.py
Daniel Han d1348cac3f Studio: tighten RAG code comments
Shorten and condense comments across the RAG backend, frontend, and
tests for readability. Comment text only; no code, strings, identifiers,
or logic changed. License headers and lint/type pragmas are preserved.
2026-05-31 08:31:08 +00:00

243 lines
8.6 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Tests for durable source identity in tool XML output (PLAN.md T3, contracts §3).
Acceptance criteria:
- _format_hits_for_llm emits document_id and chunk_id attributes on <chunk> elements.
- The visible citation id (id="N") is a per-call counter, NOT the backend chunk UUID.
- Same-filename documents in different KB slots remain distinguishable by document_id.
- Hits without a matching DB row (lookup miss) are silently dropped — not emitted
with wrong IDs.
- Legacy hits (no document_id in hit dict) still render without crashing.
"""
from __future__ import annotations
import re
import uuid
from xml.etree import ElementTree
import pytest
from core.rag.tool import _format_hits_for_llm
# ── Helpers ───────────────────────────────────────────────────────────
def _uid() -> str:
return str(uuid.uuid4())
def _hit(
*,
chunk_id: str,
document_id: str,
filename: str = "report.pdf",
text: str = "some text",
page_number: int | None = 3,
chunk_index: int = 0,
score: float = 0.85,
) -> dict:
"""Build a flat hit dict as _format_hits_for_llm expects."""
return {
"chunk_id": chunk_id,
"document_id": document_id,
"filename": filename,
"text": text,
"page_number": page_number,
"chunk_index": chunk_index,
"score": score,
"dense_score": score,
"token_count": 20,
"kind": "text",
"image_path": None,
}
def _parse_chunks(xml_output: str) -> list[dict]:
"""Parse <chunk ...> elements from the multi-block tool output."""
chunks = []
# Blocks are <chunk ...>...</chunk>; parse directly.
for match in re.finditer(r"<chunk\s([^>]*)>", xml_output):
attrs_raw = match.group(1)
# Parse "key="value"" pairs.
attrs: dict = {}
for m in re.finditer(r'(\w+)="([^"]*)"', attrs_raw):
attrs[m.group(1)] = m.group(2)
chunks.append(attrs)
return chunks
# ── Tests: durable IDs present in XML ────────────────────────────────
def test_format_hits_emits_document_id_and_chunk_id():
"""T3: tool XML <chunk> must carry document_id and chunk_id attributes."""
chunk_id, doc_id = _uid(), _uid()
hits = [_hit(chunk_id = chunk_id, document_id = doc_id)]
output = _format_hits_for_llm(hits)
chunks = _parse_chunks(output)
assert len(chunks) == 1, output
assert chunks[0]["document_id"] == doc_id
assert chunks[0]["chunk_id"] == chunk_id
def test_citation_id_is_sequential_counter_not_uuid():
"""Visible id='N' is a 1-based counter — never equal to the backend chunk UUID."""
chunk_id, doc_id = _uid(), _uid()
hits = [_hit(chunk_id = chunk_id, document_id = doc_id)]
output = _format_hits_for_llm(hits, start_id = 0)
chunks = _parse_chunks(output)
visible_id = chunks[0]["id"]
# Small integer string, not the UUID.
assert visible_id == "1", f"expected '1' got {visible_id!r}"
assert visible_id != chunk_id
def test_citation_ids_are_globally_sequential_across_calls():
"""start_id offset ensures IDs stay unique across multiple tool calls per turn."""
hits_call1 = [_hit(chunk_id = _uid(), document_id = _uid(), filename = "a.pdf")]
hits_call2 = [
_hit(chunk_id = _uid(), document_id = _uid(), filename = "b.pdf"),
_hit(chunk_id = _uid(), document_id = _uid(), filename = "c.pdf"),
]
out1 = _format_hits_for_llm(hits_call1, start_id = 0)
out2 = _format_hits_for_llm(hits_call2, start_id = 1)
chunks1 = _parse_chunks(out1)
chunks2 = _parse_chunks(out2)
assert chunks1[0]["id"] == "1"
assert chunks2[0]["id"] == "2"
assert chunks2[1]["id"] == "3"
# No id overlap.
all_ids = {c["id"] for c in chunks1 + chunks2}
assert len(all_ids) == 3
def test_same_filename_docs_have_distinct_document_ids():
"""Two docs with the same filename route to distinct document_id values (Risk #4)."""
filename = "annual-report.pdf"
chunk_a, doc_a = _uid(), _uid()
chunk_b, doc_b = _uid(), _uid()
hits = [
_hit(chunk_id = chunk_a, document_id = doc_a, filename = filename),
_hit(chunk_id = chunk_b, document_id = doc_b, filename = filename),
]
output = _format_hits_for_llm(hits)
chunks = _parse_chunks(output)
assert len(chunks) == 2
# Same filename, distinct document_id values.
assert chunks[0]["document_id"] != chunks[1]["document_id"]
assert chunks[0]["document_id"] == doc_a
assert chunks[1]["document_id"] == doc_b
def test_same_filename_docs_have_distinct_citation_ids():
"""Same-filename docs in the same turn still get distinct visible [N] ids."""
filename = "notes.pdf"
chunk_a, doc_a = _uid(), _uid()
chunk_b, doc_b = _uid(), _uid()
hits = [
_hit(chunk_id = chunk_a, document_id = doc_a, filename = filename),
_hit(chunk_id = chunk_b, document_id = doc_b, filename = filename),
]
output = _format_hits_for_llm(hits)
chunks = _parse_chunks(output)
citation_ids = {c["id"] for c in chunks}
assert len(citation_ids) == 2, f"citation IDs not unique: {chunks}"
def test_empty_hits_returns_no_chunks_message():
"""Empty hit list returns the 'no matching chunks' message, not broken XML."""
output = _format_hits_for_llm([])
chunks = _parse_chunks(output)
assert len(chunks) == 0
assert "no matching chunks" in output.lower() or "no matching" in output.lower()
def test_page_number_attribute_present_when_page_exists():
"""page attribute is emitted when page_number is not None."""
chunk_id, doc_id = _uid(), _uid()
hits = [_hit(chunk_id = chunk_id, document_id = doc_id, page_number = 5)]
output = _format_hits_for_llm(hits)
chunks = _parse_chunks(output)
assert chunks[0].get("page") == "5"
def test_page_number_attribute_absent_when_null():
"""page attribute is omitted when page_number is None."""
chunk_id, doc_id = _uid(), _uid()
hits = [_hit(chunk_id = chunk_id, document_id = doc_id, page_number = None)]
output = _format_hits_for_llm(hits)
chunks = _parse_chunks(output)
assert "page" not in chunks[0], f"unexpected page attr: {chunks[0]}"
def test_locator_attributes_are_additive_when_present():
"""T10: tool XML carries nullable locator metadata without changing visible ids."""
chunk_id, doc_id = _uid(), _uid()
hit = _hit(chunk_id = chunk_id, document_id = doc_id, page_number = 5)
hit.update(
{
"source_page_index": 4,
"page_char_start": 11,
"page_char_end": 42,
"line_start": 2,
"line_end": 3,
}
)
output = _format_hits_for_llm([hit])
chunk = _parse_chunks(output)[0]
assert chunk["id"] == "1"
assert chunk["chunk_id"] == chunk_id
assert chunk["source_page_index"] == "4"
assert chunk["page_char_start"] == "11"
assert chunk["page_char_end"] == "42"
assert chunk["line_start"] == "2"
assert chunk["line_end"] == "3"
def test_xml_special_chars_in_filename_escaped():
"""Filename with XML special chars does not break the chunk element."""
chunk_id, doc_id = _uid(), _uid()
hits = [
_hit(
chunk_id = chunk_id,
document_id = doc_id,
filename = 'report <2025> "final" & draft.pdf',
)
]
output = _format_hits_for_llm(hits)
# Output parses cleanly (no unescaped < or " in attrs).
chunks = _parse_chunks(output)
assert len(chunks) == 1
# source attribute has the filename escaped.
source_attr = chunks[0].get("source", "")
assert "<" not in source_attr and '"' not in source_attr
def test_multiple_hits_carry_independent_ids():
"""Three hits each carry their own distinct chunk_id and document_id."""
hit_data = [
(_uid(), _uid()),
(_uid(), _uid()),
(_uid(), _uid()),
]
hits = [
_hit(chunk_id = cid, document_id = did, filename = f"doc{i}.pdf")
for i, (cid, did) in enumerate(hit_data)
]
output = _format_hits_for_llm(hits)
chunks = _parse_chunks(output)
assert len(chunks) == 3
emitted_chunk_ids = {c["chunk_id"] for c in chunks}
emitted_doc_ids = {c["document_id"] for c in chunks}
expected_chunk_ids = {cid for cid, _ in hit_data}
expected_doc_ids = {did for _, did in hit_data}
assert emitted_chunk_ids == expected_chunk_ids
assert emitted_doc_ids == expected_doc_ids