e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
244 lines
9.1 KiB
Python
244 lines
9.1 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 the OpenAI Responses-API citation marker rewriter.
|
|
|
|
The stream interleaves text deltas with ``\\ue200cite\\ue202SOURCE_ID\\ue201``
|
|
markers. The rewriter resolves each to `[N](URL)` once the annotation arrives
|
|
and drops it otherwise; the URL list still flows to Sources via
|
|
`_record_url_citation`.
|
|
|
|
Reference: https://developers.openai.com/api/docs/guides/citation-formatting
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from core.inference.external_provider import (
|
|
_replace_openai_citation_markers,
|
|
_rewrite_citation_markers_partial,
|
|
)
|
|
|
|
|
|
# Citation marker control codepoints (private-use area):
|
|
CITE_START = ""
|
|
CITE_STOP = ""
|
|
CITE_DELIM = ""
|
|
|
|
|
|
def _marker(source_id: str, locator: str | None = None) -> str:
|
|
payload = f"{CITE_START}cite{CITE_DELIM}{source_id}"
|
|
if locator:
|
|
payload = f"{payload}{CITE_DELIM}{locator}"
|
|
return f"{payload}{CITE_STOP}"
|
|
|
|
|
|
def _has_marker_codepoints(text: str) -> bool:
|
|
return any(c in text for c in (CITE_START, CITE_STOP, CITE_DELIM))
|
|
|
|
|
|
def test_passthrough_when_no_marker_present():
|
|
text = "Plain text with no citation markers."
|
|
assert _replace_openai_citation_markers(text, []) == text
|
|
|
|
|
|
def test_marker_rewritten_to_link_when_annotation_known():
|
|
text = f"The capital is Paris {_marker('turn0view0')}."
|
|
citations = [
|
|
{
|
|
"source_id": "turn0view0",
|
|
"url": "https://example.com/paris",
|
|
"title": "Paris",
|
|
},
|
|
]
|
|
out = _replace_openai_citation_markers(text, citations)
|
|
assert not _has_marker_codepoints(out)
|
|
assert "[[1]](https://example.com/paris)" in out
|
|
|
|
|
|
def test_unknown_source_marker_dropped_silently():
|
|
text = f"Foo {_marker('turn9view9')} bar."
|
|
out = _replace_openai_citation_markers(text, [])
|
|
# Marker stripped, no garbled "E202" glyph leaks, surrounding text intact.
|
|
assert not _has_marker_codepoints(out)
|
|
assert "E202" not in out
|
|
assert "turn9view9" not in out
|
|
assert "Foo" in out and "bar" in out
|
|
|
|
|
|
def test_multiple_concatenated_markers_resolved_in_order():
|
|
"""Real-world wire shape: markers butted together after a sentence (user-reported bug)."""
|
|
markers = "".join(_marker(f"turn{i}view{j}") for i, j in [(1, 0), (1, 1), (3, 0)])
|
|
text = f"All animals ranked. {markers}"
|
|
citations = [
|
|
{"source_id": "turn1view0", "url": "https://a.example/dog", "title": "Dog"},
|
|
{"source_id": "turn1view1", "url": "https://a.example/cat", "title": "Cat"},
|
|
{"source_id": "turn3view0", "url": "https://a.example/tiger", "title": "Tiger"},
|
|
]
|
|
out = _replace_openai_citation_markers(text, citations)
|
|
assert "[[1]](https://a.example/dog)" in out
|
|
assert "[[2]](https://a.example/cat)" in out
|
|
assert "[[3]](https://a.example/tiger)" in out
|
|
assert not _has_marker_codepoints(out)
|
|
|
|
|
|
def test_marker_with_locator_resolves():
|
|
text = f"See {_marker('turn2file0', 'L8-L13')}."
|
|
citations = [
|
|
{"source_id": "turn2file0", "url": "https://example.com/doc.txt"},
|
|
]
|
|
out = _replace_openai_citation_markers(text, citations)
|
|
assert "[[1]](https://example.com/doc.txt)" in out
|
|
assert "L8-L13" not in out # locator detail dropped; we just link.
|
|
assert not _has_marker_codepoints(out)
|
|
|
|
|
|
def test_mixed_known_and_unknown_markers():
|
|
known = _marker("turn0view0")
|
|
unknown = _marker("turn0view99")
|
|
text = f"Known {known} and unknown {unknown}."
|
|
citations = [
|
|
{"source_id": "turn0view0", "url": "https://example.com/known"},
|
|
]
|
|
out = _replace_openai_citation_markers(text, citations)
|
|
assert "[[1]](https://example.com/known)" in out
|
|
# Unknown markers leave no trace, but surrounding prose stays.
|
|
assert "Known" in out and "unknown" in out
|
|
assert not _has_marker_codepoints(out)
|
|
assert "E202" not in out
|
|
|
|
|
|
def test_empty_text_returns_verbatim():
|
|
assert _replace_openai_citation_markers("", []) == ""
|
|
|
|
|
|
def test_idempotent_on_pre_stripped_text():
|
|
"""Pre-stripped text (no private-use codepoints) returns verbatim."""
|
|
text = "citeturn1view0 plain"
|
|
assert _replace_openai_citation_markers(text, []) == text
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"citation",
|
|
[
|
|
{"url": "https://example.com/a"}, # no source_id at all
|
|
{"source_id": None, "url": "https://example.com/b"},
|
|
{"source_id": "", "url": "https://example.com/c"},
|
|
],
|
|
)
|
|
def test_citation_without_source_id_does_not_crash(citation):
|
|
text = f"X {_marker('turnXviewY')} Y"
|
|
out = _replace_openai_citation_markers(text, [citation])
|
|
# No mapping, marker stripped. Crash-free is the contract.
|
|
assert not _has_marker_codepoints(out)
|
|
assert "turnXviewY" not in out
|
|
|
|
|
|
def test_multiple_source_id_aliases_resolve_to_same_url():
|
|
"""Every alias for the same URL must resolve, not just the first (Codex P1 regression)."""
|
|
a = _marker("turn0view0")
|
|
b = _marker("turn0view0_span_1")
|
|
c = _marker("turn0view0_span_2")
|
|
text = f"Triple {a}{b}{c} cite."
|
|
citations = [
|
|
{
|
|
"source_ids": ["turn0view0", "turn0view0_span_1", "turn0view0_span_2"],
|
|
"url": "https://example.com/paris",
|
|
"title": "Paris",
|
|
},
|
|
]
|
|
out = _replace_openai_citation_markers(text, citations)
|
|
# All three aliases collapse onto citation [1] -- same URL, so showing
|
|
# three different numbers would mislead.
|
|
assert out.count("[[1]](https://example.com/paris)") == 3
|
|
assert not _has_marker_codepoints(out)
|
|
|
|
|
|
def test_source_ids_list_and_legacy_source_id_both_resolve():
|
|
"""Mixed-shape citation: legacy ``source_id`` plus newer ``source_ids`` aliases both resolve."""
|
|
legacy = _marker("legacy_id")
|
|
alias = _marker("alias_id")
|
|
text = f"Both {legacy} and {alias} work."
|
|
citations = [
|
|
{
|
|
"source_id": "legacy_id",
|
|
"source_ids": ["alias_id"],
|
|
"url": "https://example.com/doc",
|
|
},
|
|
]
|
|
out = _replace_openai_citation_markers(text, citations)
|
|
assert out.count("[[1]](https://example.com/doc)") == 2
|
|
assert not _has_marker_codepoints(out)
|
|
|
|
|
|
# _rewrite_citation_markers_partial: deferred-annotation tests. OpenAI emits
|
|
# url_citation annotations on a later SSE event; this helper reports
|
|
# `has_unresolved` so the stream loop defers emission. See PR #5713 audit.
|
|
|
|
|
|
def test_partial_known_marker_resolves_and_clears_unresolved():
|
|
text = f"Foo {_marker('s1')} bar."
|
|
out, unresolved = _rewrite_citation_markers_partial(
|
|
text,
|
|
[{"source_id": "s1", "url": "https://example.com/a"}],
|
|
)
|
|
assert "[[1]](https://example.com/a)" in out
|
|
assert unresolved is False
|
|
assert not _has_marker_codepoints(out)
|
|
|
|
|
|
def test_partial_unknown_marker_preserves_verbatim_and_flags():
|
|
text = f"Foo {_marker('s1')} bar."
|
|
out, unresolved = _rewrite_citation_markers_partial(text, [])
|
|
assert unresolved is True
|
|
# Codepoints must remain so a follow-up pass can re-parse.
|
|
assert _has_marker_codepoints(out)
|
|
assert "Foo" in out and "bar." in out
|
|
|
|
|
|
def test_partial_resolves_after_late_annotation():
|
|
"""Two-pass: first call sees no citations; second resolves after annotation."""
|
|
text = f"See {_marker('s1')} for details."
|
|
out1, unresolved1 = _rewrite_citation_markers_partial(text, [])
|
|
assert unresolved1 is True
|
|
citations = [{"source_id": "s1", "url": "https://example.com/x"}]
|
|
out2, unresolved2 = _rewrite_citation_markers_partial(out1, citations)
|
|
assert unresolved2 is False
|
|
assert "[[1]](https://example.com/x)" in out2
|
|
assert not _has_marker_codepoints(out2)
|
|
|
|
|
|
def test_partial_multi_source_partial_resolution_keeps_marker_pending():
|
|
"""Any unresolved token in a multi-source marker leaves the whole marker verbatim with ``unresolved`` True until every id resolves or end-of-stream flushes."""
|
|
cite = f"{CITE_START}cite{CITE_DELIM}known{CITE_DELIM}locator{CITE_STOP}"
|
|
text = f"Pre {cite} post."
|
|
citations = [{"source_id": "known", "url": "https://example.com/y"}]
|
|
out, unresolved = _rewrite_citation_markers_partial(text, citations)
|
|
assert unresolved is True
|
|
assert cite in out
|
|
# End-of-stream force flush: drop the unresolved token, keep the resolved
|
|
# link. The streamer routes pending segments through
|
|
# `_replace_openai_citation_markers` at force=True for this.
|
|
forced = _replace_openai_citation_markers(out, citations)
|
|
assert "[[1]](https://example.com/y)" in forced
|
|
assert "locator" not in forced
|
|
assert not _has_marker_codepoints(forced)
|
|
|
|
|
|
def test_partial_idempotent_on_marker_free_text():
|
|
text = "Plain text."
|
|
out, unresolved = _rewrite_citation_markers_partial(text, [])
|
|
assert out == text
|
|
assert unresolved is False
|
|
|
|
|
|
def test_partial_mixed_known_and_pending_markers_flags_unresolved():
|
|
known = _marker("known")
|
|
pending = _marker("pending")
|
|
text = f"{known} {pending}"
|
|
citations = [{"source_id": "known", "url": "https://example.com/k"}]
|
|
out, unresolved = _rewrite_citation_markers_partial(text, citations)
|
|
assert unresolved is True # the pending marker drives the flag
|
|
assert "[[1]](https://example.com/k)" in out
|
|
# The pending marker stays verbatim for the next pass.
|
|
assert CITE_START in out and "pending" in out
|