"""#3455 — _splitThinkFromContent persist-path regression tests.
The think-block extraction runs at PERSIST time (inflight state + SSE `done`
finalization), moving inline … reasoning out of m.content into
m.reasoning. Because it rewrites persisted assistant content, the critical
invariant is that it NEVER loses real content: content before/after a think
block survives, partial/unclosed blocks are left intact for the live renderer,
and lookalike tags in code are not falsely extracted.
Drives the live JS via Node (same harness style as the #3368/#1188 suites) so
the test exercises the shipped function, not a Python re-implementation.
"""
from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
import pytest
REPO = Path(__file__).resolve().parents[1]
MESSAGES_JS = (REPO / "static" / "messages.js").read_text(encoding="utf-8")
def _extract_block(src: str, marker: str) -> str:
"""Extract a brace-balanced JS block starting at `marker` (a `const x=[` or
`function name(`)."""
start = src.index(marker)
# find first opening bracket of the block ( '[' for the array, '{' for the fn )
i = start
while src[i] not in "[{":
i += 1
opener = src[i]
closer = "]" if opener == "[" else "}"
depth = 0
j = i
while j < len(src):
if src[j] == opener:
depth += 1
elif src[j] == closer:
depth -= 1
if depth == 0:
return src[start:j + 1]
j += 1
raise AssertionError(f"unbalanced block for {marker!r}")
_DRIVER = """
%s
%s
%s
%s
%s
%s
%s
%s
const args = JSON.parse(process.argv[2]);
process.stdout.write(JSON.stringify(_splitThinkFromContent(args.raw, args.existing || '')));
"""
@pytest.fixture(scope="module")
def driver(tmp_path_factory):
if shutil.which("node") is None:
pytest.skip("node not available")
pairs = _extract_block(MESSAGES_JS, "const _thinkPairs=")
fence = _extract_block(MESSAGES_JS, "function _thinkingFenceMarkerAt(")
nextopener = _extract_block(MESSAGES_JS, "function _nextThinkingOpener(")
tailpartial = _extract_block(MESSAGES_JS, "function _textTailIsPartialOpener(")
indented = _extract_block(MESSAGES_JS, "function _lineIsIndentedCode(")
merge = _extract_block(MESSAGES_JS, "function _mergeInlineThinkingReasoning(")
extract = _extract_block(MESSAGES_JS, "function _extractInlineThinkingFromContent(")
fn = _extract_block(MESSAGES_JS, "function _splitThinkFromContent(")
p = tmp_path_factory.mktemp("think3455") / "driver.js"
p.write_text(_DRIVER % (pairs, fence, nextopener, tailpartial, indented, merge, extract, fn), encoding="utf-8")
return str(p)
def _split(driver, raw, existing=""):
out = subprocess.run(
["node", driver, json.dumps({"raw": raw, "existing": existing})],
capture_output=True, text=True, check=True,
)
return json.loads(out.stdout)
def test_plain_content_untouched(driver):
r = _split(driver, "Hello world, no thinking here.")
assert r["content"] == "Hello world, no thinking here."
assert r["reasoning"] == ""
def test_think_at_start_extracted(driver):
r = _split(driver, "my reasoningThe visible answer")
assert r["content"] == "The visible answer"
assert r["reasoning"] == "my reasoning"
def test_content_before_think_is_extracted(driver):
"""#3599: inline providers can emit visible prose before a complete think block."""
r = _split(driver, "Real prefix mid tail")
assert r["content"] == "Real prefix tail"
assert r["reasoning"] == "mid"
def test_closed_literal_think_in_code_block_preserved(driver):
"""#3455 review (Codex data-loss): a closed literal ... inside
a fenced code block (visible content, not leading) must NOT be extracted into
reasoning — the whole-body scan that did this is removed."""
raw = "```html\nvisible literal\n```"
r = _split(driver, raw)
assert r["content"] == raw, "fenced-code closed think tag must stay in content"
assert r["reasoning"] == ""
assert "visible literal" in r["content"]
def test_unclosed_think_hidden_into_reasoning(driver):
"""Streaming-safe: a partial/unclosed block is hidden from visible content."""
r = _split(driver, "still thinking...")
assert r["content"] == ""
assert r["reasoning"] == "still thinking..."
def test_existing_reasoning_is_merged_not_overwritten(driver):
r = _split(driver, "extraanswer", existing="from on_reasoning stream")
assert r["content"] == "answer"
assert r["reasoning"] == "from on_reasoning stream\n\nextra"
def test_existing_reasoning_substring_does_not_drop_block(driver):
r = _split(driver, "plananswer", existing="planning the approach carefully")
assert r["content"] == "answer"
assert r["reasoning"] == "planning the approach carefully\n\nplan"
def test_multiple_blocks_extracted(driver):
"""#3599: multiple complete inline blocks move to reasoning together."""
r = _split(driver, "abthe answer")
assert r["content"] == "the answer"
assert r["reasoning"] == "a\n\nb"
def test_block_after_content_extracted(driver):
"""#3599: complete inline blocks after visible content are reasoning too."""
r = _split(driver, "leadanswer trailing more")
assert r["content"] == "answer more"
assert r["reasoning"] == "lead\n\ntrailing"
def test_lookalike_tag_without_close_after_content_stays_visible(driver):
"""#3633 deep-review (Codex catch): a literal token used mid-sentence
and never closed is NOT a thinking trace — it must stay visible, not get the
rest of the line swallowed into reasoning. (A LEADING unclosed block is still
treated as reasoning; see test_unclosed_think_hidden_into_reasoning.)"""
r = _split(driver, "use as a literal token, never closed")
assert r["content"] == "use as a literal token, never closed"
assert r["reasoning"] == ""
def test_empty_content(driver):
r = _split(driver, "")
assert r["content"] == ""
assert r["reasoning"] == ""
def test_think_only_message(driver):
r = _split(driver, "only thinking")
assert r["content"] == ""
assert r["reasoning"] == "only thinking"
# ── Backend parity: api/streaming._split_thinking_from_content ──────────────
# #3455 review (Codex): the split must also run server-side before s.save() so
# the PERSISTED session file is compacted (the client-only split left the saved
# file bloated). The backend helper must match the JS semantics exactly.
class TestBackendThinkSplitParity:
def _sp(self, raw, existing=""):
from api.streaming import _split_thinking_from_content
return _split_thinking_from_content(raw, existing)
def test_plain_untouched(self):
assert self._sp("Hello world") == ("Hello world", "")
def test_leading_extracted(self):
assert self._sp("rThe answer") == ("The answer", "r")
def test_mid_body_code_block_preserved(self):
raw = "```html\nvisible literal\n```"
content, reasoning = self._sp(raw)
assert content == raw
assert reasoning == ""
def test_unclosed_hidden_into_reasoning(self):
assert self._sp("still...") == ("", "still...")
def test_existing_reasoning_merged(self):
assert self._sp("newans", "prior") == ("ans", "prior\n\nnew")
def test_multiple_blocks_extracted(self):
assert self._sp("abend") == ("end", "a\n\nb")
def test_substring_reasoning_is_not_dropped(self):
assert self._sp("plananswer", "planning the approach carefully") == (
"answer",
"planning the approach carefully\n\nplan",
)
def test_empty(self):
assert self._sp("") == ("", "")
def test_none_content(self):
# Defensive: non-string content must not crash.
content, reasoning = self._sp(None)
assert content in (None, "")
assert reasoning == ""
# ── #3633 deep-review (Codex catch): code-awareness + unclosed-position ──
def test_inline_backtick_code_span_preserved(self):
"""A literal inside an inline single-backtick code span is code,
not a thinking trace — it must stay visible (the earlier full-scan only
protected triple fences)."""
raw = "Use the `foo` tag in your prompt."
assert self._sp(raw) == (raw, "")
def test_indented_code_block_preserved(self):
"""A literal inside a >=4-space indented code block must stay
visible."""
raw = "Example:\n\n foo\n\ndone"
assert self._sp(raw) == (raw, "")
def test_mid_body_unclosed_stays_visible(self):
"""An unclosed AFTER visible content (a literal typed tag) must
NOT truncate the following prose on the persist path."""
assert self._sp("answerstill thinking") == (
"answerstill thinking",
"",
)
def test_leading_unclosed_still_extracted(self):
"""A LEADING unclosed block (cut off mid-thought) is still reasoning."""
assert self._sp("still thinking") == ("", "still thinking")
def test_indented_fence_1_3_spaces_preserved(self):
"""A fenced code block indented 1-3 spaces is still a fence (valid
Markdown), so a literal think tag inside it stays visible."""
backtick = "text\n ```\n lit\n ```\nend"
assert self._sp(backtick) == (backtick, "")
tilde = "text\n ~~~html\n lit\n ~~~\nend"
assert self._sp(tilde) == (tilde, "")
def test_leading_whitespace_preserved_when_no_thinking_removed(self):
"""#3633 Codex catch: content is only lstripped when a LEADING thinking
block/prefix was actually removed. A reply that legitimately starts with
an indented code block or blank lines (and has no leading thinking
wrapper) keeps its leading whitespace."""
assert self._sp(" indented code\nmore") == (" indented code\nmore", "")
assert self._sp("\n\n hi") == ("\n\n hi", "")
# ...but a leading thinking block still strips the whitespace after it.
assert self._sp("r answer") == ("answer", "r")