Skip to content

test(notes-tree): mount a viewport of rows in the folder-rename suite - #1958

Merged
h4yfans merged 3 commits into
mainfrom
notes-tree-rename-suite-timeout
Sep 2, 2026
Merged

test(notes-tree): mount a viewport of rows in the folder-rename suite#1958
h4yfans merged 3 commits into
mainfrom
notes-tree-rename-suite-timeout

Conversation

@h4yfans

@h4yfans h4yfans commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #1921

Root cause. The suite's useVirtualizer mock mounted every row (Array.from({ length: count }), 151 rows with the 150 fillers that push the tree past VIRTUALIZATION_THRESHOLD), and every keystroke into the folder-rename input re-renders the whole tree: the draft value lives in useNoteTreeActions at the tree root, and FolderRow/NoteRow in virtualized-notes-tree.tsx are not memoized. An instrumented run counts 32 VirtualizedNotesTree renders for a second rename targets the new path (one per keystroke, clear, Enter and refetch), which is 4,832 row renders in jsdom for one test. On the last green main CI run (33613758936) that test took 15,136 ms, rename, rename again, delete 15,677 ms and a renamed folder is still renameable 15,111 ms, against a 30 s testTimeout. Their non-virtualized twins in the same file took 0.5 to 1.2 s. A 2x slowdown from parallel load turns the file red, which is what three unrelated branches saw.

It is not a race in the rename path: no assertion ever fails, the tests run out of clock. It is not a leak from a sibling suite either, and this repo carries a stored note asserting that focusManager leaks across renderer test files, so the refutation is spelled out. The renderer project runs with the root test block in apps/desktop/config/vitest.config.ts:

pool: 'threads',
isolate: true,

and vitest 4.1.10 only hands a finished worker to the next file when isolation is off (node_modules/vitest/dist/chunks/cli-api.*.js):

if (!task.isolate && !runner.isTerminated && !isMemoryLimitReached && this.queue[0]?.task.isolate === false && isEqualRunner(runner, this.queue[0].task)) {
    this.sharedRunners.push(runner);
    return this.schedule();
}
...
function isEqualRunner(runner, task) {
    if (task.isolate) throw new Error("Isolated tasks should not share runners");

Every renderer file therefore gets a fresh worker thread. A two-file probe run in one --no-file-parallelism invocation confirmed it: file B saw a different threadId, an unset global, and nothing that file A had put in focusManager or localStorage. Whatever that stored note observed, it was not a cross-file global under this configuration.

Fix. Both suites that combine 150 fillers with the mount-every-row mock now mount Math.min(count, 20) rows, which is what the real virtualizer does (visible rows plus overscan: 10). The tree still has 150+ items, so shouldVirtualize still picks VirtualizedNotesTree and both files still cover the renderer #1529 taught to show a rename input.

  • notes-tree-folder-rename-duplicate.test.tsx (the file in the issue). Folders sort before notes, so the folder under test and any phantom duplicate the original bug produced are rows 0 and 1, and its notes Alpha/Beta are rows 1 and 2 when expanded. Mutation check: a 1-row window fails an open folder stays open through a rename on Unable to find an element with the text: Alpha.

  • notes-tree-virtualized-rename.test.tsx (same defect, less margin). On the same green CI run its typing case above the threshold: the inline input commits the rename took 6,650 ms, so it sat 4.5x under the same 30 s ceiling before this change, less headroom than the issue's file had on that run. Its largeVault is 150 root notes plus Projects/Alpha.md; Projects is row 0 (folders sort first) and every other case addresses Note 0. Mutation check: a 1-row window fails both Note 0 cases on Unable to find an element with the text: Note 0; the Projects cases still pass at row 0, as they should.

    The first push of this window failed those two Note 0 cases in CI (run 33630793068) while passing locally, and that disagreement was a fixture defect, not a window size. The sidebar's default sort for collections is manual, whose comparator falls back to newest-first (notes-tree-sort.ts, compareNotes), and createNote stamped every note with new Date(). On a fast machine all 151 stamps land in one millisecond, tie, and the stable sort keeps insertion order, so Note 0 is row 1. On the CI runner the same loop crosses millisecond boundaries, the last note created is newest, and Note 0 sorts to row 150, outside any viewport. Reproduced locally by giving each note a distinct increasing stamp: the same two cases fail with the same message. The mount-every-row mock had hidden this since the file was written. The fixture now stamps each note one millisecond older than the previous one, so newest-first equals insertion order on every machine and no Date.now() is left in the fixture. Both files keep the same 20-row window; the issue's file never addresses a filler row (its cases name the folder and its two children, rows 0 to 2), so its new Date() stamps are left as they are.

Checked and left alone: virtualized-notes-tree.test.tsx carries the same mock over small hand-built trees (slowest case 1.4 s on that CI run) and folder-view/folder-table-view.test.tsx mounts every row of a handful of table rows (slowest 187 ms). Neither has 150 fillers or a typing loop, so a window there is churn with no timeout to remove.

Minimal reproducer. Run either file alone and read vitest's per-test durations; no sibling file is needed:

./node_modules/.bin/vitest run --config apps/desktop/config/vitest.config.ts --project renderer \
  apps/desktop/src/renderer/src/components/notes-tree-folder-rename-duplicate.test.tsx

Before/after on the same machine, back to back, load average ~10 to 12:

notes-tree-folder-rename-duplicate, above the threshold origin/main this branch
renaming a folder that holds notes leaves exactly one folder row 3473 ms 418 ms
a second rename targets the new path, not the one already gone 4169 ms 647 ms
rename, rename again, delete — nothing is left behind 3915 ms 726 ms
a renamed folder is still renameable and does not revert 3528 ms 632 ms
notes-tree-virtualized-rename, above the threshold (load ~29, both runs) origin/main this branch
the virtualized note row offers them too 597 ms 563 ms
renaming a note opens an inline input too 1221 ms 529 ms
entering rename mode scrolls the row into view 1448 ms 723 ms
the inline input commits the rename 3866 ms 1287 ms

An earlier pair at load ~12 read 999 ms to 241 ms for the typing case; the ratio is the same, the machine was busier for this pair.

Not changed. No timeout raised, no retry, no separate vitest project. The product-side "one keystroke re-renders the tree" is real but bounded in the app by the real virtualizer (roughly 40 mounted rows), so it is noted here rather than memoized for a test's sake.

Release note

none

Test plan

Full test:renderer under contention, three consecutive runs (first commit only; the second commit changes one sibling file, timed alone below), because contention is the failure condition the issue describes. Load averages are from uptime immediately before and after each run; file durations are from vitest's JSON reporter.

run load avg before → after (15 cores) files tests notes-tree-folder-rename-duplicate file / slowest test composer.test.tsx file / slowest test
1 108.7 → 141.8 707/707 8703 pass, 0 fail 46.8 s / 10,957 ms 29.7 s / 2,674 ms
2 141.8 → 71.1 707/707 8703 pass, 0 fail 41.1 s / 7,156 ms 38.6 s / 3,647 ms
3 71.1 → 38.2 707/707 8703 pass, 0 fail 27.3 s / 5,591 ms 26.7 s / 3,155 ms

The load was five sibling agents plus at least one other full suite on the same box; that is the condition, not a caveat. The measured headroom is 2.7x, not the 30x the isolated timings suggest: the worst single test under that load, a second rename targets the new path at 10,957 ms, is 2.7x inside the 30 s testTimeout. Its row work is 7.5x smaller than before (640 vs 4,832 mounted-row renders), so the old mock at the same load lands around 80 s, which is the failure the issue describes. Run from apps/desktop (process.cwd() is what tests/utils/contrast.ts and the CSS contract tests read from); a run from the repo root fails seven unrelated files on ENOENT .../src/renderer/src/assets/base.css.

notes-tree-virtualized-rename.test.tsx alone at head, load ~29: 8/8, slowest case 1,287 ms (3,866 ms on origin/main one run earlier). notes-tree-folder-rename-duplicate.test.tsx re-run at head, load ~34: 14/14, slowest 2,514 ms. CI on this PR's head 00dcae9db is green: Unit & integration tests, run 33648007574. On that runner the issue's three two-rename cases took 3,771 / 4,306 / 3,526 ms (15,136 / 15,677 / 15,111 ms on the last green main run before this change), and the sibling's typing case took 2,014 ms (6,650 ms before), with both Note 0 cases green at 784 ms and 1,017 ms.

  • pnpm lint exit 0
  • pnpm typecheck exit 0
  • pnpm --filter @memry/desktop typecheck:test exit 0 (both changed test files compile)
  • pnpm check:architecture passed
  • pnpm check:contracts passed
  • git diff --check exit 0
  • pnpm docs:impact --base origin/main --strict: no docs-relevant changes
  • npx -y react-doctor@latest .: no finding in the changed files

The virtualizer mock mounted all 150 filler rows, and every keystroke into
the rename input re-renders the whole tree, so each two-rename case cost
~4,800 row renders: 15s on a quiet CI worker against a 30s test timeout.
Mount 20 rows, which is what the real virtualizer does.
@github-actions github-actions Bot added the test label Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit 00dcae9.

…suite too

Same mount-every-row virtualizer mock as the folder-rename suite, same
150 fillers, same whole-tree re-render per keystroke: its typing case took
6.6s on a quiet CI worker against the same 30s timeout.
The sidebar's default sort is manual, which falls back to newest-first, and
createNote stamped every note with new Date(). On a fast machine all 151
stamps tie and insertion order survives; on the CI runner they spread across
milliseconds and 'Note 0' sorted to row 150, outside the 20-row window. Each
note is now one millisecond older than the one before it.
@codecov

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@h4yfans
h4yfans marked this pull request as ready for review September 2, 2026 16:31
@h4yfans
h4yfans merged commit 3c39b27 into main Sep 2, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: notes-tree-folder-rename-duplicate renderer suite times out under parallel load

1 participant