from pathlib import Path from api.streaming import ( _extract_inline_thinking_from_content, _split_thinking_from_content, ) REPO = Path(__file__).resolve().parents[1] MESSAGES_JS = (REPO / "static" / "messages.js").read_text(encoding="utf-8") UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8") WORKSPACE_JS = (REPO / "static" / "workspace.js").read_text(encoding="utf-8") def _function_body(src: str, signature: str) -> str: start = src.index(signature) brace = src.index("{", start) depth = 0 for i in range(brace, len(src)): if src[i] == "{": depth += 1 elif src[i] == "}": depth -= 1 if depth == 0: return src[start : i + 1] raise AssertionError(f"function body not found: {signature}") def test_split_clean_leading_think_block(): content, reasoning = _split_thinking_from_content("plan\nanswer") assert content == "answer" assert reasoning == "plan" def test_split_extracts_non_leading_complete_block(): content, reasoning = _split_thinking_from_content("visible before hidden visible after") assert "" not in content assert "visible before" in content assert "visible after" in content assert reasoning == "hidden" def test_split_extracts_multiple_complete_blocks(): content, reasoning = _split_thinking_from_content("onetwo final") assert content == "final" assert reasoning == "one\n\ntwo" def test_split_keeps_fenced_code_literal_think_visible(): raw = "```html\nliteral\n```\nanswer" content, reasoning = _split_thinking_from_content(raw) assert content == raw assert reasoning == "" def test_split_merges_existing_reasoning_without_duplicate(): content, reasoning = _split_thinking_from_content("sameanswer", "same") assert content == "answer" assert reasoning == "same" def test_split_merges_existing_reasoning_with_new_inline_block(): content, reasoning = _split_thinking_from_content("inlineanswer", "separate") assert content == "answer" assert reasoning == "separate\n\ninline" def test_reasoning_only_content_survives_reload_source_fields(): content, reasoning = _split_thinking_from_content("only reasoning") assert content == "" assert reasoning == "only reasoning" def test_unclosed_inline_thinking_after_content_stays_visible_on_persist(): """#3633 deep-review (Codex catch): on the PERSIST path an unclosed think tag that appears AFTER visible content is almost always a literal typed tag, so the prose after it must NOT be silently truncated into reasoning. A LEADING unclosed block (cut off mid-thought) is still treated as reasoning.""" # Mid-body unclosed → stays fully visible, nothing moved to reasoning. content, reasoning = _split_thinking_from_content("answerstill thinking") assert content == "answerstill thinking" assert reasoning == "" # Leading unclosed → genuine cut-off thinking trace, moves to reasoning. lead_content, lead_reasoning = _split_thinking_from_content("still thinking") assert lead_content == "" assert lead_reasoning == "still thinking" def test_messages_js_live_and_persist_paths_share_extractor(): stream_display = _function_body(MESSAGES_JS, "function _streamDisplay") parse_state = _function_body(MESSAGES_JS, "function _parseStreamState") split_persist = _function_body(MESSAGES_JS, "function _splitThinkFromContent") assert "_extractInlineThinkingFromContent(_stripXmlToolCalls(assistantText), liveReasoningText, {streaming:true}).content" in stream_display assert "return _extractInlineThinkingFromContent(_stripXmlToolCalls(assistantText), liveReasoningText, {streaming:true});" in parse_state assert "return _extractInlineThinkingFromContent(rawContent, existingReasoning, {streaming:false});" in split_persist assert "window._extractInlineThinkingFromContentForRender" in MESSAGES_JS assert "_thinkingFenceMarkerAt" in MESSAGES_JS def test_render_messages_uses_shared_extractor_on_reload(): render = _function_body(UI_JS, "function renderMessages") assert "window._extractInlineThinkingFromContentForRender(content, thinkingText)" in render assert "thinkingText=split.reasoning||thinkingText" in render assert "content=split.content" in render def test_inline_and_separate_reasoning_merge_not_drop(): """#3633: the extractor MERGES inline + an explicitly-passed separate reasoning payload (deduped) rather than dropping either. (The reload render path itself deliberately does NOT seed m.reasoning into this extractor — that separation is pinned by test_issue2565; the merge capability is exercised by the live streaming path which passes liveReasoningText.)""" content, reasoning = _split_thinking_from_content("inlineanswer", "separate") assert content == "answer" assert reasoning == "separate\n\ninline" # Identical inline + separate dedupe to one. content2, reasoning2 = _split_thinking_from_content("sameanswer", "same") assert content2 == "answer" assert reasoning2 == "same" # Separate-only (no inline tag) is preserved and content is untouched # (no promotion of reasoning into visible prose). content3, reasoning3 = _split_thinking_from_content("plain answer", "separate") assert content3 == "plain answer" assert reasoning3 == "separate" def test_extraction_is_linear_on_long_no_newline_content(): """#3633 Codex perf catch: the indented-code / leading checks must not be O(n^2). A 200k-char no-newline message must extract well under a second.""" import time big = "x" * 200_000 + "answer" start = time.time() content, reasoning = _split_thinking_from_content(big) elapsed = time.time() - start assert content == big assert reasoning == "" assert elapsed < 1.0, f"extraction took {elapsed:.2f}s — likely quadratic" def test_per_token_streaming_scan_is_not_quadratic(): """#3633 Codex CORE perf catch: _parseStreamState / syncInflightAssistantMessage call the extractor on the FULL accumulator on every streamed token. Simulate a long stream (both no-tag and leading-thinking-block cases) and assert the cumulative cost stays bounded — a per-token full walk over the growing buffer was O(n^2) (~88s no-tag / ~103s with-tag for 2000x100-char tokens).""" import time def sim(n_tokens, tok_len, lead_tag): acc = "short reasoning" if lead_tag else "" start = time.time() for _ in range(n_tokens): acc += "x" * tok_len # mimic the two per-token extractor calls (_streamDisplay + _parseStreamState) _extract_inline_thinking_from_content(acc, "", streaming=True) _extract_inline_thinking_from_content(acc, "", streaming=True) return time.time() - start no_tag = sim(2000, 100, False) with_tag = sim(2000, 100, True) assert no_tag < 3.0, f"no-tag per-token stream took {no_tag:.1f}s — quadratic" assert with_tag < 3.0, f"with-tag per-token stream took {with_tag:.1f}s — quadratic" def test_streaming_partial_opener_tail_respects_code_context(): """#3633 perf-fix follow-up (Codex): the bulk-skip fast path must not suppress a trailing partial opener that sits inside code — only a partial opener in PLAIN text is a forming block. Mirrors master parity for inline-backtick, fenced, and indented code; a plain partial tail is still suppressed.""" ext = _extract_inline_thinking_from_content # Inside code → the partial opener tail stays visible. assert ext("answer `