phase: 158_thinking_timeline
All verification complete. Every gate is green and the docs check out. Final report:
## Phase 158 final verification pass — COMPLETE
**Verified (all tasks pre-completed; this pass re-ran the full gate):**
- Unit + integration: `uv run pytest --cov=app --cov-report=term-missing` → **3618 passed, 99% coverage on `app/`** (>90% gate)
- Phase E2E in isolation: `uv run pytest tests/e2e/test_thinking_timeline.py -v --no-cov` → **6/6 passed** (order pin, fold-at-rest, live-open, restore+`segments` array, shared page, no-thinking single disclosure)
- Regression E2E, each in isolation: `test_thinking_display` 5✓, `test_tool_call_compact` 4✓, `test_turn_progress_loader` 3✓ (per-run pin updated), `test_big_read_progress` 4✓, `test_agent_document_tools` 4✓, `test_share_chat` 4✓, `test_chat_persistence` 4✓, `test_smoke` 3✓
- Lint/types: `uv run ruff check . && uv run pyright` → **clean (0 errors)**
**Completion criteria:** all 9 met — timeline E2E pins ✓, restore/shared `segments` ✓, no-thinking single-disclosure + the three UNCHANGED regression suites green ✓, pre-phase byte-identical round-trip (integration pin: key absent, never null; >50 / >32 000 / empty text / unknown kind all 422) ✓, full suite + coverage + E2E + lint ✓, `git diff --stat` footprint limited to `app/schemas.py`, `frontend/assets/{app,shared}.js`, `tests/`, README, phase files (a pre-existing owner `TODO.md` edit is out of scope, not staged by the phase commit) ✓, D15→per-run supersession recorded in the updated pin ✓. (Commit + phase move are the harness's job; task dirs already in `complete/`.)
**Documentation:** README.md — Thinking section correctly describes the phase-158 timeline; config reference matches `.env.example` exactly; page table matches registered routes; no stale single-block wording. AGENTS.md — rules and quick-reference commands all accurate. **Verified consistent — no changes needed.**
**Decisions/deviations:** none. `test_tool_call_compact.py` carries a one-line docstring-only update (referenced the removed `closeToolCalls`); its test logic is byte-identical to the phase-117 contract and green.
**Next pending phase:** none — `todo/` contains only 158.
This commit is contained in:
@@ -30,6 +30,18 @@ SSE is exactly ``delta → tool → thinking → delta → thinking → done``
|
||||
the owner's repro, and every window is deterministic (the mock's
|
||||
baked delays, the ``slow_llm.py`` pacing precedent).
|
||||
|
||||
Phase 158 supersession (owner direction 2026-10-04, recorded): phase 109's
|
||||
D15 re-open contract (ONE ``details.thinking`` re-opened by every later
|
||||
thinking frame) is REPLACED by the per-run timeline (D1/D5): each
|
||||
contiguous thinking run streams into its OWN block, and the first frame
|
||||
of a new kind closes the previous run. The SSE stream is unchanged, so
|
||||
this turn renders ONE ``.tool-calls-disclosure`` + TWO ``.thinking``
|
||||
blocks (arrival order, all ABOVE the answer bubble — D2), open in their
|
||||
live windows and folded at rest. The loader pins
|
||||
(``test_loader_visible_from_send_through_the_tool_gap``,
|
||||
``test_loader_a11y_and_reduced_motion``) stay unchanged — the
|
||||
never-stale loader contract (D16) is intact.
|
||||
|
||||
Each test sends the marker question in its OWN fresh conversation
|
||||
(one full turn per test — the ``page`` fixture is a fresh browser
|
||||
context, ``_reset_db`` reseeds the KB deterministically):
|
||||
@@ -40,14 +52,18 @@ context, ``_reset_db`` reseeds the KB deterministically):
|
||||
line lands (the turn is provably in flight), the tool line carries
|
||||
the phase-87 elapsed counter in the tool gap, and ``#send-status``
|
||||
carries a state text (not empty) throughout.
|
||||
* ``test_thinking_block_reopens_after_delta_with_visible_loader`` —
|
||||
THE reported symptom's state: the thinking block re-opens after the
|
||||
answer has started (open + non-empty ``.thinking-text`` while the
|
||||
bubble already carries call 1's content) with the loader STILL
|
||||
visible (the frozen window is gone); the terminal state is clean
|
||||
(loader hidden, the scratchpad carries both thinking rounds, the
|
||||
bubble carries BOTH answers, the send button reads "Send" not
|
||||
"Stop", ``#send-status`` back to the idle shape).
|
||||
* ``test_thinking_runs_open_in_live_window_with_visible_loader`` —
|
||||
THE reported symptom's state, re-pinned for phase 158 (D5 replaces
|
||||
phase-109 D15 — a NEW block instead of a re-open): the post-delta
|
||||
thinking frames open a per-run ``.thinking`` block (exactly one
|
||||
block, open + non-empty ``.thinking-text`` while the bubble already
|
||||
carries call 1's content — the run starts AFTER the first delta) with
|
||||
the loader STILL visible (the frozen window is gone — the user sees
|
||||
the new thinking in its own live block); the terminal state is clean
|
||||
(loader hidden, the scratchpad holds TWO ``.thinking`` blocks —
|
||||
EACH carrying its own round's text — BOTH folded at rest, the bubble
|
||||
carries BOTH answers, the send button reads "Send" not "Stop",
|
||||
``#send-status`` back to the idle shape).
|
||||
* ``test_loader_a11y_and_reduced_motion`` — after a full turn: the
|
||||
loader element is ``aria-hidden="true"`` in the DOM and
|
||||
``#send-status`` (the sole ``aria-live`` announcer) is back to the
|
||||
@@ -192,7 +208,7 @@ def test_loader_visible_from_send_through_the_tool_gap(
|
||||
expect(page.locator("#send-status")).not_to_be_empty()
|
||||
|
||||
|
||||
def test_thinking_block_reopens_after_delta_with_visible_loader(
|
||||
def test_thinking_runs_open_in_live_window_with_visible_loader(
|
||||
page: Page, app_url: str, mock_llm: int, db_ready: None
|
||||
) -> None:
|
||||
summary = _reset_db(mock_llm, seed=True)
|
||||
@@ -202,28 +218,33 @@ def test_thinking_block_reopens_after_delta_with_visible_loader(
|
||||
|
||||
_send(page)
|
||||
|
||||
blocks = page.locator(".msg.brain .thinking")
|
||||
bubble = _answer_bubble(page)
|
||||
block = page.locator(".msg.brain .thinking")
|
||||
text_el = page.locator(".msg.brain .thinking-text")
|
||||
loader = page.locator("#turn-loader")
|
||||
|
||||
# THE reported symptom's state (D15): the thinking block is open
|
||||
# with non-empty text WHILE the answer bubble already carries
|
||||
# call 1's content — the post-delta re-open (call 2's thinking
|
||||
# frames re-opened the block the first delta had closed).
|
||||
# THE reported symptom's state (phase 158 D5 replaces phase-109
|
||||
# D15 — the post-delta thinking frames open a NEW per-run block
|
||||
# instead of re-opening the first delta's block): exactly ONE
|
||||
# ``.thinking`` block exists, it is OPEN with non-empty text WHILE
|
||||
# the answer bubble already carries call 1's content — the first
|
||||
# thinking run starts AFTER the first delta (the owner's "thinking
|
||||
# after responding" repro), and the loader is STILL visible (the
|
||||
# frozen window is gone — the user sees the new thinking in its
|
||||
# own live block).
|
||||
page.wait_for_function(
|
||||
"""() => {
|
||||
const b = document.querySelector('.msg.brain .thinking');
|
||||
const t = b ? b.querySelector('.thinking-text') : null;
|
||||
return !!b && b.open && !!t && t.textContent.trim().length > 0;
|
||||
const b = document.querySelectorAll('.msg.brain .thinking');
|
||||
const t = b.length === 1 ? b[0].querySelector('.thinking-text') : null;
|
||||
return b.length === 1 && !!b[0].open && !!t
|
||||
&& t.textContent.trim().length > 0;
|
||||
}""",
|
||||
timeout=30_000,
|
||||
)
|
||||
expect(block).to_have_count(1)
|
||||
expect(block).to_have_attribute("open", "")
|
||||
expect(text_el).to_contain_text(SENTINEL_THOUGHT)
|
||||
# The re-open is genuinely AFTER a delta: call 1's content is
|
||||
# already in the bubble ...
|
||||
expect(blocks).to_have_count(1)
|
||||
expect(blocks).to_have_attribute("open", "")
|
||||
expect(blocks.nth(0).locator(".thinking-text")).to_contain_text(SENTINEL_THOUGHT)
|
||||
# The run is genuinely AFTER a delta: call 1's content is already
|
||||
# in the bubble ...
|
||||
expect(bubble).to_contain_text(SENTINEL_FIRST)
|
||||
# ... and the loader is STILL visible — the frozen window is gone.
|
||||
expect(loader).to_be_visible()
|
||||
@@ -231,23 +252,24 @@ def test_thinking_block_reopens_after_delta_with_visible_loader(
|
||||
# Terminal approach: call 2's distinctive final sentence landed in
|
||||
# the bubble ...
|
||||
expect(bubble).to_contain_text(SENTINEL_FINAL, timeout=30_000)
|
||||
# ... and the FINAL thinking round (the last frames before done —
|
||||
# thinking AFTER the answer's last delta) re-opened the block
|
||||
# AGAIN: the scratchpad carries the last round's text and the
|
||||
# block is open (the `thinking` handler opens it before it
|
||||
# renders — D15), with the loader still visible (the turn is
|
||||
# still in flight — no terminal frame yet).
|
||||
expect(text_el).to_contain_text(SENTINEL_FINAL_THOUGHT, timeout=30_000)
|
||||
expect(block).to_have_attribute("open", "")
|
||||
expect(loader).to_be_visible()
|
||||
|
||||
# The turn settles (done → idle through setUiState): the loader is
|
||||
# HIDDEN (its sole owner — D16, no per-handler cleanup) ...
|
||||
# ... and the turn settles (done → idle through setUiState): the
|
||||
# loader is HIDDEN (its sole owner — D16, no per-handler cleanup) ...
|
||||
expect(page.locator("#send-label")).to_have_text("Send", timeout=30_000)
|
||||
expect(loader).to_be_hidden()
|
||||
# ... the scratchpad text is intact (BOTH thinking rounds) ...
|
||||
expect(text_el).to_contain_text(SENTINEL_THOUGHT)
|
||||
expect(text_el).to_contain_text(SENTINEL_FINAL_THOUGHT)
|
||||
# ... the scratchpad holds TWO ``.thinking`` blocks (the first
|
||||
# delta closed run 1; the final round — thinking AFTER the
|
||||
# answer's last delta — opened its OWN block rather than
|
||||
# re-opening run 1's) ...
|
||||
expect(blocks).to_have_count(2)
|
||||
# ... EACH carrying its OWN round's text (the flow's two distinct
|
||||
# reasoning streams — separate blocks, not one lump) ...
|
||||
expect(blocks.nth(0).locator(".thinking-text")).to_contain_text(SENTINEL_THOUGHT)
|
||||
expect(blocks.nth(1).locator(".thinking-text")).to_contain_text(SENTINEL_FINAL_THOUGHT)
|
||||
# ... and BOTH runs are FOLDED at rest (D5 — the per-run
|
||||
# fold-at-rest; the phase-109 symptom — the frozen chat — stays
|
||||
# fixed: a run is open in its live window, never re-opened after
|
||||
# done) ...
|
||||
expect(page.locator(".msg.brain .thinking[open]")).to_have_count(0)
|
||||
# ... the bubble carries BOTH answers (call 1 + call 2) ...
|
||||
expect(bubble).to_contain_text(SENTINEL_FIRST)
|
||||
expect(bubble).to_contain_text(SENTINEL_FINAL)
|
||||
|
||||
Reference in New Issue
Block a user