Files
meshcore-analyzer/tests/unit
efitenandClaude Opus 5.5 8933e2c703 fix(channels): keep client-only state across a channel-list refresh (#2095) (#2096)
Fixes #2095.

`loadChannels()` replaced the `channels` array with the server snapshot
and carried nothing across, and `mergeUserChannels()` only ever ran from
`init()`. So any refresh destroyed every field that exists only in the
tab.

**No race needed for the worst of it.** Change the region filter or
toggle show-encrypted with a PSK channel open, and:

- the **My Channels** section disappears — `renderChannelList()` derives
it as `channels.filter(c => c.userAdded === true)`, and the server
returns neither `userAdded` nor a `user:`-prefixed hash
- every unread badge resets to 0
- the user's own labels vanish
- `reconcileSelectionAfterChannelRefresh()` cannot find the `user:*`
hash in the snapshot, so it nulls `selectedHash`, sets `messages = []`,
rewrites the URL to `#/channels` and replaces the open conversation with
"Choose a channel from the sidebar"

## What this does

`mergeClientChannelState(fresh, prev)` mirrors
`mergeWsAppendedIntoRest()`, which already does exactly this job for
`messages` (#1498). Same shape: pure, takes both arrays as parameters,
returns a fresh array, never aliases or mutates an input.

It carries `unread`, `userAdded` and `userLabel` by hash, and keeps
`lastActivityMs` / `lastSender` / `lastMessage` / `messageCount` when a
WebSocket batch landed while the request was in flight and is therefore
newer than the snapshot. Those four move together: a sender without its
message reads as a different message.

`mergeUserChannels()` now runs inside `loadChannels()`, before the
render and before the reconcile, so all three call sites get it instead
of `init()` alone.

## What it deliberately does not do

**It does not resurrect a channel the snapshot left out.** A
region-filter change legitimately narrows the list, so carrying
survivors over would defeat the filter, which is a worse bug than the
one being fixed. The helper only enriches rows already present in the
fresh snapshot.

That leaves half of finding 2 in the issue unfixed: a channel pushed by
the WebSocket handler during the initial in-flight window is still
dropped. That one self-heals on the channel's next packet, and the
reverted preview line is overwritten by the next WS batch. Fixing it
properly needs a way to tell "dropped because the snapshot is stale"
from "dropped because the filter excludes it", which is a larger change
than this.

## Tests

`tests/unit/test-issue-2095-channels-client-state.js`, 11 cases,
registered in `test-all.sh`.

Part 1 exercises the helper directly. Part 2 drives the real
`loadChannels()` through the existing `_channelsLoadChannelsForTest`
hook with a stubbed `api()`, which is what proves the helper is wired in
rather than merely defined.

**Verified by mutation.** With the wiring removed from `loadChannels()`
but the helper left in place, the three reproduction cases fail with
exactly the reported symptoms:

```
FAIL  a refresh keeps the My Channels rows
      My Channels lost on refresh (got ["public1"])
FAIL  a refresh keeps unread badges
      the unread badge reset to 0 on refresh   undefined !== 7
FAIL  a refresh does not close an open PSK conversation
      the open PSK channel was deselected      + null  - 'user:MyPSK'
```

The fourth case, "a refresh still drops a channel the server filtered
out", stays green throughout, so the fix cannot be defeating the region
filter.

Full frontend suite green: `sh test-all.sh` exits 0.

## Not done

**No browser validation.** This is frontend JS covered by unit tests
that call the production function through its own hook, but I did not
run Playwright against a server, so I am not claiming a browser check I
did not do.

`init()` still calls `mergeUserChannels()` and `renderChannelList()`
after `loadChannels()` resolves, which is now redundant and costs one
extra full sidebar render per page load. Left alone deliberately: it is
idempotent, and the comment there records the regression it was added to
fix (`test-channel-issue-1111-e2e.js`, case 2). Worth removing
separately with that e2e test watched.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:55:39 +02:00
..