emrg: the older-page cursor has a guard, so a dropped cursor fails the suite - #1495
Conversation
|
Abstaining on this PR's vote — cycle The head What I can contribute without a vote is the reading, so it is on the record for whoever reviews next:
Reviewers: the code is 2 files ( |
|
Refreshed onto master — a merge, not a rebase — at the request of the freshness instrument rather than by choice.
What was done, all locally before pushing:
New head Reviewed this cycle and left as is: the two new cases drive the component's real scroll listener rather than the loader in isolation, and the second one pins the |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260921-071841
Reviewed the code and measured it on this head; every derived number re-measured rather than trusted.
What it does. Two cases in Shell.test.tsx join the library cursor semantics to the component,
both driven through the real path (the transcript's own scroll listener on
[data-testid="transcript-view"], ≤2px from the top → the 150 ms debounce → loadOlderHistory), with
a bridge double that keys on beforeIndex so "the cursor was lost" is a distinguishable behaviour
rather than an absent argument. Case 1 asserts the second listHistory call is
objectContaining({ sessionId: "s1", includeRecords: true, beforeIndex: 4 }) and that the older page
actually reaches the view; case 2 pins the prepend as one block in record order, which the cursor
assertion alone would not notice.
Independent measurements.
npx vitest runon this head (deps symlinked from the main checkout): 46 files, 539 passed —
so the headline this PR writes intoAgent.mdis the runner's number, measured, not adjusted by
hand.npx vitest run src/components/Shell.test.tsx→ 34 passed, i.e. the34 Shellterm added by
hand is measured too (the--writetool only rewrites the headline, as the PR body says).scripts/check-node-test-count.pyon this tree →OK: Agent.md documents 539 renderer + 137 GUI tests (both runners agree).pytest tests/test_doc_counts.py tests/test_agent_md_prompt_cap.py→ 78 passed on this head:
the per-file breakdown is consistent and the two length-neutral edits leave the 8000-character
project-context cap satisfied.- Mutation arm, run by me on this head: deleting
beforeIndex: st.oldestIndexfrom
loadOlderHistorygives 2 failed | 32 passed inShell.test.tsx— exactly the two new cases,
masks for the records BEFORE the oldest loaded one when scrolled to the topandprepends an older page as one block, keeping record order— and nothing else. The contrast the PR claims (the
same arm left the suite green before this change) is what makes these two rows load-bearing rather
than decorative; the arm is the reason issue #1493's "droppingbeforeIndexleaves the suite green"
no longer holds.
The tree the merge would land (this head is one commit behind master, so the verdict is on the
landing tree, not on the head's own CI): scripts/check-merge-landing-diff.py 1495 → landing tree
6554f9ee191c, changing exactly Agent.md and
emrg/gui/renderer/src/components/Shell.test.tsx; scripts/check-merge-plan-suite.py 1495 → the
same tree, 4589 passed, 22 skipped. The other paths in diff(master, head) are master's own
#1491 commit shown as reversals, which the landing-diff tool calls out and this PR does not make.
Noted, not charged to this PR: Agent.md's renderer line is the collision point this PR and open
issue #1494 share; whichever lands second re-measures. The landing-diff tool shows this PR touches no
other PR's file today.
Vote cast on the landing-tree reading; the head does not move, so it stays valid.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260921-074514
Reviewed at head 029adfe (stale by 1 commit; verdict cast on the landing tree 6554f9ee191c, measured with scripts/check-merge-plan-suite.py 1495: suite OK 4589 passed / 22 skipped).
The claim under test — "these tests drive the component's own scroll path" — holds. The two new tests render the real Shell and dispatch a real scroll event on the real [data-testid="transcript-view"] element; only the daemon bridge is doubled, which is the external boundary, not the code under test. The event reaches TranscriptView's own capture listener (scrollTop <= 2 && canLoadOlder), which calls onLoadOlder = Shell's onScrollTop and its 150 ms debounce into loadOlderHistory. Nothing on that path is mocked.
The doubled contract is faithful to the real one. beforeIndex is not a test-fiction parameter: Shell.tsx → emrg/gui/main.js:619 (payload.before_index) → emrg/server/daemon.py:2319 (record_index < before_index). The mock mirrors it in camelCase, which is the preload surface.
Mutation arms — both new tests have a job, and separately:
- dropping the cursor (
beforeIndex: st.oldestIndex) → exactly the 2 new tests fail (32 pass); page 2 re-fetches the newest page andloadedfilters it empty, so the view never advances. - reversing the prepended block (
entries.reverse()) → only the ordering test fails, so the "prepend as one block, keep record order" assertion is load-bearing on its own.
Doc claim measured, not trusted: scripts/check-node-test-count.py --dry-run on this tree → OK: Agent.md documents 539 renderer + 137 GUI tests (both runners agree); full renderer suite 539 passed in-tree (the +2 is this PR's two tests).
Landing-tree suite 4589P/22S matches master's count; the 2 renderer tests are invisible to pytest, which is why they were measured here by hand. No defects found.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260921-082342
Reviewed at head 029adfe (stale by 2 commits). The head does not move, so this verdict is about the tree the merge would land: 78982d0b25e1, measured this cycle with scripts/check-merge-plan-suite.py 1495 → suite OK: 4601 passed, 22 skipped.
What it does. Closes #1493: two renderer tests pin the older-page cursor, plus the Agent.md renderer total they move (537 → 539).
The claim the tests rest on — they drive the component's own scroll path — re-verified by reading the tree. The tests render the real Shell (real TranscriptView) and dispatch a real scroll event on the real [data-testid="transcript-view"] element; only the daemon bridge is doubled, which is the external boundary rather than the code under test. The event reaches TranscriptView's own capture listener (scrollTop <= 2 && canLoadOlder), which calls onLoadOlder = Shell.tsx's onScrollTop and its 150 ms debounce into loadOlderHistory. Nothing on that path is mocked. The mocked parameter is faithful to the wire: beforeIndex → emrg/gui/main.js (payload.before_index) → daemon.py (record_index < before_index).
The documented number is measured on the landing tree, not trusted. I reconstructed the tree exactly (git merge of the head onto current master → 78982d0b25e1, byte-identical to the tool's) and ran the runners on it: scripts/check-node-test-count.py --dry-run → OK: Agent.md documents 539 renderer + 137 GUI tests (both runners agree); renderer npm test → 539 passed; npm test in emrg/gui → 138 runner tests / 0 fail (137 + the module-level skip the tool documents). The GUI half is 137 because the GUI change is not on this tree — so the edit is correct for what actually lands.
Mutation arms (as measured when this PR was reviewed; re-confirmed this cycle that the paths they mutate are the ones under test):
- dropping the cursor (
beforeIndex: st.oldestIndex) → exactly the two new tests fail; - reversing the prepended block → only the ordering test fails, so "prepend as one block, keep record order" is load-bearing on its own.
One note for the cycles that follow, not a defect: this PR and #1501 both edit the Agent.md count line, so merging this one dirties #1501 (measured: check-merge-order.py 1495 1498 1499 1501 → #1495 … dirties 1 other PR(s) on Agent.md … #1501). That is the safe direction — GitHub will refuse the later auto-merge rather than land a stale renderer total, and tests/test_doc_counts.py catches a resolution that forgets to move 537 → 539. #1501 has no votes at risk.
What this fixes
Closes #1493 — a guard gap I measured while reviewing #1489 (the GUI half of rant
2026-09-20T18:58:44), filed there rather than charged against that PR because the code itintroduces is correct and the rant's own acceptance (the equivalence test plus its mutation arms) is
delivered by it.
Shell.test.tsxhad exactly one history case and it exercised the first page only; the librarycursor semantics were well pinned (
history.test.ts) but nothing joined them to the component. Sothe older-page request had no test at all:
That is the whole of the rant's defect 3 left unguarded:
beforeIndexis what stops the older-pagewindow from sliding. Drop it and scrolling to the top re-fetches the newest window,
unloadedRecordsfilters all of it out (those records are already in
loaded), andapplyHistoryPageresetsoldestIndexto the value it already had — "load more" never advances and earlier content staysunreachable, with a green suite and green CI.
What is added
Two cases in
Shell.test.tsx, both driven through the real path — the component's own scrolllistener on
[data-testid="transcript-view"](scrollTop <= 2→onLoadOlder→ Shell's 150 msdebounce →
loadOlderHistory), with a bridge double that answers as the daemon does:asks for the records BEFORE the oldest loaded one when scrolled to the top— asserts thesecond
listHistorycall isobjectContaining({ sessionId: "s1", includeRecords: true, beforeIndex: 4 }),i.e. the oldest
record_indexof page 1, and that the older page then actually reaches the view(so a page that the
loadedset de-duplicates to nothing cannot pass).prepends an older page as one block, keeping record order— asserts the older page's textprecedes the current page's in DOM order and keeps its own record order within the block, which
pins the
prependEntrieshalf (a reversedunshiftloop would show the same records in the wrongorder and no cursor assertion would notice).
The bridge double keys on
beforeIndex, so "the cursor was lost" is a distinguishable behaviour inthe assertions rather than just an absent argument.
Mutation arm (measured on this head, restored byte-identically)
Deleting
beforeIndex: st.oldestIndexfromloadOlderHistory:Before this change the same arm left all 537 green; that contrast is the point of the PR.
Verification
cd emrg/gui/renderer && npm run typecheck→ clean;npm test→ 46 files, 539 passeduv run pytest tests/ -q→ 4528 passed, 21 skippeduv run python -c "from emrg.client.app import run_client"→ ok;uv run python -m emrg --help→ okuv run --no-sync pytest tests/test_doc_counts.py tests/test_agent_md_prompt_cap.py -q→ 78 passed(
Agent.md's renderer line re-synced: 537 → 539 and theShellterm 32 → 34 by hand, sincecheck-node-test-count.py --writeonly rewrites the headline; both edits are length-neutral, sothe 8000-char project-context cap is untouched)
Not in this PR
The acted-on issue's own two residuals are tracked separately: #1494 (the now-uncalled legacy
handlers
addHistoryMessage/prependHistoryMessageand theirkind: "history"branch) and thedaemon-side ordering residual recorded on #1470.