Commit Graph
7 Commits
Author SHA1 Message Date
J3vbandClaude d3526968bb release: v1.2.0-alpha.2 (#1333)
* docs: add bug-detection improvements plan

Plan for mechanical bug detection alongside the agentic hunt: activate the 14
unused Go fuzz harnesses, the configured-but-never-run Stryker setup, and
browser-mode vitest; encode recurring bug classes as semgrep rules; add
model-based and fault-injected ordering tests; add a persistent seen-ledger
and sibling-sweep lens to the hunt.

All local-only and on demand - fuzz crashers are working reproducers, and this
repo is public.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* build: add make fuzz target and ignore mutation-test output

`go test ./...` runs each Fuzz* function against its committed seed corpus
only - one pass per seed, zero generated inputs - so the 17 fuzz harnesses in
Server/ have never actually fuzzed. `make fuzz` enumerates every target and
runs each with a time budget (Go fuzzes one target per package per
invocation, hence the loop). Local-only by design: a crasher is a working
reproducer and this repo is public.

Also gitignore Client/tauri-client/.stryker-tmp/ and reports/ - a Stryker run
left 200+ untracked files, and a surviving-mutant report maps exactly which
behaviour nothing tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(client): pin reconnect auth-frame and replay-dedup arming

Stryker found 14 surviving mutants across ws.ts:413/422/428 - the auth frame
built on reconnect. Every condition there could be flipped with all 4777
tests still green: the replay-dedup arming guard, the resume-vs-fresh-connect
ternary, and the conditional active_channel_id spread.

Seven tests through the public send/isReplaying surface, no new exports. Two
isolate each half of the `reconnectAttempt > 0 && lastSeq > 0` AND condition -
the combination no existing test reached, and the one an && -> || mutant
walked straight through.

Verified by flipping the line 413 guard to `if (true)`: 3 of 7 fail, revert
restores green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: record two fuzz corpus traps

Interrupting a fuzz run manufactures a false crasher: Go cannot distinguish a
worker that crashed on an input from one killed externally, so it saves the
in-flight input to testdata/fuzz/ as a suspect. It looks exactly like a real
security finding. Replay before believing it.

And committed seed corpus shares the testdata/fuzz/<Target>/ directory with
any false crasher, so clearing one by removing the directory deletes the
seeds too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(client): enforce three prose invariants as ESLint rules

CLAUDE.md documents the voice-supersession, E2EE staleness and dispatcher
invariants in English. English fails no build, and bug hunts keep rediscovering
the same classes. Five rules encode them as an inline flat-config plugin - no
new dependency, and `npx eslint src/` is already a blocking CI gate.

- no-leave-voice-when-superseded: a global leaveVoice() inside a branch that
  already confirmed supersession tears down the newer live session
- e2ee-epoch-needs-keypair-check: a non-key-holder never bumps the epoch, so
  an epoch-only staleness guard cannot see a restarted session
- e2ee-verified-status-literal: keeps "verified" tied to a hand-written call
  site that earned it, never a computed status
- no-identity-scope-fallback: a `?? 0` placeholder scope mints a keypair under
  the wrong account
- no-store-write-in-ws-on: page-local ws.on handlers may read stores, not
  write them

Each rule proven to fire by reintroducing the historical bug shape and
reverting; RuleTester cases cover both the real shapes that must stay clean
and the bug shapes that must not.

A fourth candidate - await-then-stale-snapshot - was declined as not
AST-expressible: whether an await needs a guard, and whether the guard is
sufficient, is intent rather than shape, and the rule would flag most of the
already-correct guard code in livekitSession.ts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: correct dispatcher invariant, record Tier 2 as shipped

The client CLAUDE.md claimed ws.on(...) appears only in dispatcher.ts. Eight
handlers across main.ts, MainPage.ts and ChannelController.ts say otherwise -
page-local UI (ringing, overlays, slow-mode timers) legitimately subscribes.
The real invariant is narrower: dispatcher is the single path by which server
events WRITE to domain stores. That is what local/no-store-write-in-ws-on
enforces, and the doc now matches the code.

Also record that Tier 2 shipped as ESLint rules rather than semgrep, and why
the fourth candidate was declined.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(client): move the status-picker dot onto the avatar corner

The corner dot on the user bar avatar was a static hardcoded-green div —
never reflected real status and did nothing on click. Removed it and
relocated the actual StatusPicker trigger dot (real color, opens the
status dropdown) to that same corner instead of its own row. The
"Online"/"Idle"/... text label under the username is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(client): return the saved password over IPC again

The remember-password box saved a password the client could never read
back. Hardening had put #[serde(skip)] on CredentialData::password, so
load_credential returned a record whose password was always absent and
the login form could not prefill it — the box appeared to work and
silently did nothing.

Drop the skip and carry the field through the TS wrapper, which now maps
a non-string password to undefined rather than trusting the payload.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(client): add an auto-connect checkbox to the login form

Auto-connect already existed end to end — ServerProfile.autoConnect,
setAutoLogin(), and the boot auto-login block with its cancel overlay —
but was only reachable through the zap button on a server card. This
surfaces the same state as a checkbox under Remember password, where
users look for it.

Ticking it forces Remember password on and disables it: boot auto-login
replays the stored token, which saveCredential only writes when the
password is remembered, so the two cannot be set independently without
producing a setting that silently does nothing.

Unticking is guarded. setAutoLogin(null) clears autoConnect on every
profile, so a bare toggle-off would wipe another server's setting; the
clear now only fires when this profile is the current holder. The guard
lives in ensureProfileExists, which all four auth paths already route
through.

Also consume the password restored in the previous commit, so selecting
a saved server prefills it instead of leaving the field blank.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* chore(release): bump client to 1.2.0-alpha.2

The client version is not derived from the tag — release.yml's
verify-versions job compares the tag against package.json and
tauri.conf.json and fails the release if they drift, so all five
manifests (both lockfiles included) move together.

Also refreshes the literal version in the README and docs build
examples, and closes the Unreleased changelog section as v1.2.0-alpha.2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(changelog): record the three bug-hunt sweeps in v1.2.0-alpha.2

PRs #1328, #1331 and #1332 merged to main after v1.2.0-alpha.1 was tagged
and closed 233 verified defects between them, but none of the three left
an entry in the curated changelog — the generated list covers commits,
this file covers behaviour, and nothing bridged the two.

Verified unreleased by ancestry rather than by date (none of the three
merge commits is an ancestor of v1.2.0-alpha.1), so all of it ships for
the first time in alpha.2.

Nine entries grouped by subsystem, leading with the changes an operator
or user would actually notice: the 24h-retention desync, the avatar-
deleting orphan sweep, the zero-byte restore truncation, the six hot-mic
paths, and the TOFU re-pin that would have warned every install at once.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(client): drop the e2e assertion for the removed user-bar status dot (#1334)

26b46cc removed the hardcoded-green `.status-dot` div from the user bar
avatar and relocated the real StatusPicker trigger dot into that corner,
adding "status picker dot sits on the avatar" to cover the new element.
The old "user bar has status dot" test was left behind and now fails on
an element that no longer exists by design.

The replacement test already asserts the corner dot is present and
visible, so removing the stale one loses no coverage.


Claude-Session: https://claude.ai/code/session_01Rkv9dVo5YEYArqrDRfW41w

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 11:50:12 +02:00
J3vbandClaude Opus 5 82be103794 fix(client): resolve 94 verified defects across voice, identity, transport and UI (#1332)
* fix(client): gate every mic re-enable path on the user's mute state

Six separate paths republished the microphone without consulting whether
the user had muted themselves: the audio-device fallback, selecting the
"Default" input, un-deafening, retryMicPermission, a stale PTT ownership
latch, and auto-reconnect's restoreLocalVoiceState. Each one produced a
hot mic while every remote UI still showed the user as muted.

These were six findings but one missing guard. Adds isMicPolicyGated()
(localMuted || localDeafened || localServerMuted || pttGated) and routes
the device-switch cycle, applyMicMuteState's unmute branch and
retryMicPermission through it, which also covers setDeafened(false) --
a call site no finding named.

Also extracts reconnectSuperseded() so all five supersession checkpoints
in the auto-reconnect loop carry the state-type check that only the
give-up path had, and clears the PTT gate on stopPtt and on ptt-error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(client): stop camera/screenshare publishing after the user turns it off

enableCamera and enableScreenshare set the store flag before awaiting
getUserMedia/getDisplayMedia, so clicking off during the OS picker left
the track publishing to the SFU while the UI showed it off, with no stop
affordance. Adds one shared generation guard: disable bumps, enable
captures before the await and discards the track if it changed.

Also in this area:
- a server refusal of voice_screenshare (or a non-VIDEO_LIMIT refusal of
  voice_camera) never rolled back the published track; the dispatcher now
  correlates the error by envelope id rather than blanket-rolling-back.
- a full-ready resync left every loaded channel with a permanent hole in
  its history, because that tier never replays chat_message frames.
  Loaded windows are now invalidated on a resync (pending and failed rows
  carry through) and the active channel refetched.
- CHANNEL_FULL while joining left voiceStatus stuck; DM mirror rows kept
  phantom entries and stale unread counts across a resync; addMessage and
  setAroundMessages dropped offline/failed optimistic rows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(client): preserve a mid-setup key-holder promotion, and route the
audio graph through the noise suppressor

setupKeyExchange unconditionally wrote the server's key-holder value
captured at join, clobbering a handleParticipantLeft promotion that
landed during its pre-publish awaits. The joiner then waited for an offer
only it could send, timed out, and was ejected from voice. The write now
preserves an existing promotion; it sits after the existing
session-generation check, and clearState bumps that generation and resets
the flag synchronously, so stale state cannot survive a teardown.

Enhanced Noise Suppression silently disabled the input-volume slider and
the VAD gate: livekit-client's setProcessor() does its own internal
replaceTrack(processedTrack) after awaiting addModule and a fetch, so it
landed after ours and wired the sender straight to the raw mic. The
pipeline now sources from the processed track and re-runs after
attaching, so our replaceTrack wins.

Also scopes the voice identity keypair by host AND user id so two
accounts sharing one OS profile stop sharing an identity keypair, guards
peer-key and TOFU writes against a clearState during their IPC awaits,
and seeds VideoGrid tiles from the persisted per-user volume.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(client): drop the previous server's bearer token on a host switch

api.setConfig spread the new config over the old, so switching hosts
carried the previous server's session token forward and the login request
to the next server went out holding a live credential for the first one.
The token is now dropped in the shared setConfig when host changes
without an accompanying token, covering login, register and auto-connect
at once.

Also fixes a packaged-build-only failure: the CSP omitted blob: from
img-src, so avatar upload validation (which measures the image via
URL.createObjectURL) always failed in release and never in dev.

Smaller connection and IPC fixes: ws_disconnect now bumps the connection
generation instead of nulling the sender slot, so an in-flight handshake
cannot install after a disconnect; a dead LiveKit proxy listener
deregisters itself instead of being reused forever; httpProxy no longer
caches an origin the Rust side may have torn down; logPersistence stopped
looping on its own flush-failure logs; ConnectPage subscribes to
transientError instead of reading it once; cert-mismatch accept/reject
only act when the event host matches the live session.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(client): guard the quick-switcher against a double-open

openQuickSwitch assigned its instance only after awaiting the profile
load, so a second click during that window mounted a second overlay and
orphaned the first. Every close affordance destroys only the tracked
instance, leaving a body-mounted position:fixed backdrop that blocks all
input until the app is reloaded. Adds the same `opening` flag the sibling
overlay controllers already use; audited every other opener in these
files and found no second instance of the race.

Also: loadOlderMessages and loadMessages now discard a response whose
window was replaced mid-fetch by a same-channel jump; the ArrowUp
edit-last-message scan skips unsent rows, matching the visual affordance;
unpinning from the pinned panel writes the store row; the pinned panel
forwards the channel it captured at open time rather than reading the
active one at click time; the reaction picker closes on channel teardown;
a non-voice channel switch dismisses the video grid; and destroy() closes
the settings overlay.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(client): repair the status-picker stylesheet and a dozen UI defects

The .status-picker rules targeted a root element the component never
toggles, leaving the popup's own chrome unstyled and the root
display:none. Repointed at .status-picker-dropdown and dropped the dead
rules.

Component and store fixes, all test-first: the upload preview bar never
became visible so upload errors were invisible; replying while editing
left the edit text in the textarea; MessageList's load-older latch keyed
off a raw count so a live tail append refired the fetch; drag-reorder
renumbered channels into a 0..n-1 range instead of reusing the group's
own position slots; DM avatars bypassed the authenticated fetch path;
the member-list moderation gate read a mount-time role snapshot; mention
autocomplete offered usernames the mention grammar cannot express;
notifications titled DMs as "#channel"; the update-notifier catch
dereferenced a null banner; and the channel context menu leaked its node
on teardown.

Also resets authStore in member-list.test.ts's shared reset helper: one
test was leaving role="admin" set for every test after it, unnoticed
because no gate read authStore for role until now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(client): adopt the legacy identity key instead of re-minting one

Scoping the identity keypair by host and user id changed the keyring
account name, so every existing install would have found nothing at the
new account and generated a fresh identity key. Every peer who had
already pinned the old one would then see a TOFU mismatch, which raises
the re-pin modal telling the user to verify the safety number
out-of-band -- a MITM alarm fired at the whole alpha population at once,
which teaches people to click through the one warning meant to matter.

When the scoped account is empty, the legacy host-only account is now
adopted: saved under the scoped name, then the legacy account deleted.
Save happens before delete so a partial failure leaves the legacy key in
place for the next launch rather than stranding the user with neither.
A corrupt legacy blob falls through to fresh generation without throwing.

A second account on the same host still mints its own distinct keypair,
which was the point of the scoping fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(client): surface server errors that had no dedicated handler

The WebSocket error handler bannered only RATE_LIMITED and FORBIDDEN, so
every other code that reached the fallthrough was dropped in silence --
a rejected chat_edit reported nothing at all while the optimistic
"Message edited" toast still fired. Every specific branch above already
returns, so the fallthrough sees only genuinely unhandled codes; it now
banners all of them.

Also:
- reattachToPresent cleared the detached flag eagerly, so a failed tail
  refetch let a live broadcast splice onto the stale around-window with a
  silent gap. The flag now survives until setMessages lands the tail.
- a mixed-case host and its lowercase-normalized URL form resolved to
  different cert-store pin keys; tofu::cert_store_key and ws.ts's
  normalizeHostForCertCompare both lowercase now. attachments.ts already
  did the right thing and is unchanged.
- clearAuth left the channels store populated for the next login.
- capabilities/default.json was missing
  core:window:allow-request-user-attention.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(client): tear down video tiles, focus and the lightbox on leave

Four defects an earlier pass could not finish because each spanned two
files:

- closeVideoGrid only hid the grid, so remote video tiles survived a
  channel leave and reappeared on the next join. VideoGrid grew a
  clearStreams(), called from the real-leave branch of checkVideoMode
  (not the reconnect branch).
- the grid kept its focused-tile state across a close; setFocusedTile now
  accepts null and closeVideoGrid clears it.
- the per-user volume preference key had no host component, so volumes
  set on one server applied to a different user with the same id on
  another. Scoped via setAudioVolumeHost, mirroring channel-mutes.
- the media lightbox stayed mounted after MainPage.destroy().

Also repairs tests/unit/audio-elements.test.ts, which was missing an
afterEach import and failing to compile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(client): close eight defects a review found in this batch

Three of these are incomplete fixes from earlier commits on this branch --
the diagnosis landed, the cure stopped short.

- main.ts kept a hand-copied normalizeHostForCertCompare that never got the
  .toLowerCase() its ws.ts original and tofu::cert_store_key both have. Since
  the Rust side always emits the lowercased host and a profile stores it
  verbatim, any uppercase in the hostname broke all three guards -- worst of
  them the mismatch modal's onReject, which then skipped disconnect/clearAuth
  and left the user connected to the server whose certificate they had just
  refused. ws.ts now exports the one implementation and the copy is gone.
- the status-picker stylesheet repair repointed the root and deleted the old
  .status-option rules without adding replacements under the names the
  component emits, so the trigger dot -- a bare div whose only style is an
  inline background -- stayed 0x0, invisible and unclickable. The picker still
  could not be opened.
- ungateMic's re-open branch was unreachable in the one scenario its comment
  described: a PTT release routes through setMuted(true), so localMuted is
  always true there. It now takes the pttOwnsMute latch read *before* each
  call site resets it; reading the module flag from inside would always see
  false and move the bug rather than fix it.

The rest:

- dispatcher.ts statically imported @lib/screenShare, which has value imports
  from livekit-client -- dragging ~1.3 MB into the entry chunk that the file's
  own comment says is deliberately kept out of it. Now lazy, like every other
  voice call site here.
- replay detection compared payload.timestamp (server clock) against
  Date.now() (client clock). A self-hosted server without NTP made every live
  message after a reconnect look like a replay, silently killing notifications
  for the whole drift window. Both sides are now in server time via an
  observed skew estimate; latency biases it toward treat-as-live, which is the
  side that costs a duplicate rather than a dropped notification.
- identity.ts and livekitE2EE.ts each derived the keyring scope with `?? 0`.
  A missing user id would have adopted-and-deleted the real legacy key into a
  bogus host:0 account, then minted a second keypair under host:<realId> --
  published key and signing key permanently disagreeing, which is a false MITM
  warning for every peer. Unreachable today, irreversible if reached.
- per-user volumes were scoped by host with a legacy fallback that only fired
  when currentHost was null, which MainPage never leaves it as -- so every
  saved volume silently read as the default on upgrade. Reads now fall through
  to the unscoped key once and persist under the scoped one.
- a post-resync invalidate ran unconditionally while its refetch was guarded,
  so a missing getMessages left every window dropped with nothing to reload it.

A ninth finding -- that the DM reconcile could strand activeChannelId -- was
checked and rejected: the block 40 lines above already clears it whenever the
id is absent from both channels and dm_channels.

Two test-suite notes: livekit-session's announce-signing test was joining
voice with no authenticated user, which production does not permit, so it now
sets one (below PEER_ID, leaving key-holder election unchanged) and clears it
after. status-picker-userbar reads app.css from disk rather than `?raw`, which
vitest stubs to an empty string for stylesheets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(client): repair the e2e cert test and four defects found verifying it

The e2e suite caught one behavioural divergence from this branch, and
hand-verifying the hunt's flagged-but-unchecked items turned up four more
defects.

E2E:

- cert-tofu's "disconnect on mismatch returns to the connect page" emitted the
  mismatch for myserver.example:8443 while the session was authenticated
  against localhost:8443, so it asserted the pre-fix behaviour: a certificate
  rotating on ANY unrelated saved profile logs you out of the server you are
  using. That is the bug 8917c28 deliberately fixed. The test now emits for the
  live host, and a new sibling pins the guard itself -- a mismatch for another
  host must leave the session alone. Verified by defeating the guard: only the
  new test goes red, which is why the old one never noticed the change.

Defects found verifying the ledger's open items:

- logging out fired delete_credential fire-and-forget and then navigated to the
  connect page, whose auto-login immediately read the same account back. Since
  B4-3 moved the credential commands to #[tauri::command(async)] they no longer
  serialize on the IPC thread, so a read that wins that race signs the user
  straight back into the server they just left. Two fixes, because the race and
  the intent are separate problems: a CREDENTIAL_LOCK mutex restores the
  one-operation-at-a-time property that also keeps secret_store::set's
  read-modify-write atomic, and the connect page now skips auto-login once
  after a logout that removed the credential -- mirroring the quick-switch
  sessionStorage idiom already in that file. A server_shutdown logout keeps its
  credential and deliberately does not set the flag, so restart auto-login
  still works. e2e-pinned: with the suppression defeated, the user is visibly
  back in the app after clicking Log Out.

- a post-resync history refetch that REJECTED left the active channel's window
  already invalidated but never marked errored, so MessageList fell into its
  "no messages yet" welcome branch -- rendering a failed reload as a genuinely
  empty channel, with no Retry, until the user navigated away and back. Now
  calls setChannelLoadError, reusing MessageController's existing plumbing.

- an invite deep link arriving during the connected overlay's 800ms ready
  countdown hit a gate that assumed isAuthenticated implies the router is on
  "main". It is not: clearAuth() ran without the teardown that only the
  authStore subscriber performs (and only while on "main"), so the overlay's
  timer then mounted MainPage over a nulled-out auth state, and the invite was
  dropped. Gated on the real invariant and the in-flight session is now torn
  down explicitly.

- channel mutes carried the same dead legacy-preference fallback that per-user
  volumes had -- guarded on currentHost === null, which MainPage never leaves
  it as -- so every saved mute was silently discarded on upgrade. Mutes are a
  list, where an empty saved value is real data, so this needed a presence
  probe rather than the volume fix's sentinel.

Also extends the e2e Tauri mock with storedSettings/storedCredential seeds so
auto-login paths are exercisable at all.

Verified clean: 4800 vitest, 293 Playwright, 97 cargo, tsc, tsc -p e2e, eslint,
prettier, clippy -D warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(client): stop Tab escaping every modal, and a duplicate row after a resync

Two bugs left open by the previous round.

The "flaky" a11y focus-trap test was not flaky -- it was a real accessibility
defect surfacing nondeterministically. FOCUSABLE_SELECTOR is structural and
says nothing about visibility, but this codebase hides controls with inline
`style.display = "none"` (MemberPickerModal's group-name field and confirm
button both start hidden). So focusDialog() picked a display:none input as the
dialog's first focusable and called .focus() on it -- which browsers silently
refuse -- and focus never entered the dialog at all. trapFocus() then computed
first/last as those same hidden elements, so neither Tab branch ever matched
document.activeElement, preventDefault() never fired, and Tab fell through to
the browser's native order and walked straight out of the dialog. Whether the
test noticed depended on how much async sidebar content happened to be
focusable at that moment, which is what made it look intermittent.

Fixed in the shared helper rather than in the one modal that exposed it: about
forty call sites hide controls the same way, so every factory modal had the
same hole. trapFocus and focusDialog now filter out inline-hidden elements.
Reproduced first at 3/10 failures under --repeat-each; 10/10 after, and 20/20
at --workers=4. Note the check reads inline styles only -- an element hidden by
a CSS class would still slip through, which no current call site does.

Second: a message the server persisted but whose chat_send_ok ack was lost to
the same disconnect that forced a resync was displayed twice. The optimistic
row keeps id 0 until confirmSend stamps it, so setMessages' id-based carry-over
could never collide it with the real row, while addMessage had solved exactly
this for the live path by matching on content. Extracted that predicate as
isUnreconciledEcho and used it in both, so the two cannot drift apart.

The dangerous direction here is over-merging, not under-merging: collapsing two
genuinely distinct sends of the same text loses a real message. Three things
bound it -- only rows still awaiting reconciliation qualify (pending, or failed
for OFFLINE specifically, since a SLOW_MODE rejection is never broadcast and
eating that row would kill a live retry draft), author and content must both
match, and each snapshot row is consumed at most once, so N identical pending
sends pair off against N identical real rows instead of collapsing onto one.
Both directions are tested.

Verified: 4804 vitest, 293 Playwright with zero flaky, tsc, tsc -p e2e, eslint,
prettier.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 17:56:43 +02:00
jevb 867041b094 test: client test updates — fix failures + align with security hardening
Update 111 test files to match security hardening changes:
- acceptInvalidCerts now conditional on allowSelfSigned
- Credential store no longer returns passwords over IPC
- File type validation uses strict MIME allowlist
- Search rate limiter timing adjustments
- Dispatcher cleanup mock additions
- Audio elements screenshare mute preservation

2962 tests passing across 110 test files.
2026-04-01 11:40:31 +02:00
jevb 5c616d53fe test: fix 10 test quality bugs (BUG-058–067) and resolve 115 TS type errors
BUG-058: Unblock prod-build E2E — created tsconfig.build.json excluding
tests from the production build. Added typecheck/typecheck:build scripts.

BUG-059: Harden native E2E — CDP timeout 30→60s with exponential backoff,
config timeouts doubled (test 120s, action 30s, nav 45s, expect 15s).

BUG-060: Add 25 Rust unit tests across commands.rs, ws_proxy.rs,
livekit_proxy.rs, credentials.rs (was zero behavioral tests).

BUG-061/067: Add behavioral assertions to server coverage_boost_test.go —
GracefulStop verifies client count, channel_focus verifies no error sent.

BUG-062: Upgrade low-signal test assertions in livekit-session,
device-manager, channel-controller (no-op checks → state checks).

BUG-063: Consolidate native E2E skip gates into beforeEach blocks
(voice-controls 7→1 skip, channel-navigation 4→1 skip).

BUG-064: Add 9 integration tests for channel CRUD, member lifecycle,
DM open/close, and presence events.

BUG-065: Replace 3 fixed sleeps with condition-based waits in E2E specs.

BUG-066: Verified toast/audio tests already cleaned in prior session.

TypeScript: Fix 115 type errors across 21 test files — add non-null
assertions for strict indexing, fix mock typing (vi.fn<any>()), add
missing fields (color, version, deleted) to test fixtures.
2026-03-30 16:35:02 +02:00
jevb 1ea21a325a test: Phase 3 coverage — 22 files bumped from 70-92% to 95-100%
Expand 19 existing test files with 434 new tests. All 22 target files
now at 95%+ coverage: dispatcher, ws, store, permissions, ConnectPage,
LoginForm, KeybindsTab, AdvancedTab, AppearanceTab, ChannelController,
messages.store, channels.store, notifications, embeds, context-menu,
renderers, content-parser, formatting, profiles, audioElements,
ui.store, dm.store. Client coverage: 87% → 90.66%.
2026-03-30 13:59:47 +02:00
jevb 6f35973c1b feat: TOFU cert pinning, voice channel sidebar, scroll-to-message, profiles, and review fixes
- Add cert mismatch modal for TOFU certificate pinning
- Separate voice channels from text in sidebar with user lists
- Implement scrollToMessage and jump-to-pinned-message in overlay
- Add server profiles with credential auto-fill on connect page
- Fix credential auto-fill race condition on rapid profile clicks
- Add channel_focus event for channel-scoped message delivery
- Fix member list case-insensitive role filtering
- Server normalizes role names to lowercase for protocol consistency
- Remove redundant permission-denied log in handleChannelFocus
- Voice store: bulk set states from ready payload, leave cleanup
- WebSocket reconnect and structured logging improvements
- Add tests for cert modal, overlay managers, voice sidebar,
  message list scroll, quick switcher, and profile management
2026-03-17 20:25:15 +01:00
jevb 9c1d99683c fix: address PR review findings (issues #9-#14)
- Fix capacity over-allocation and use strings.Builder in getReactionsBatch (#9)
- Replace `any` types and cache Tauri invoke in window-state.ts (#10)
- Remove custom `contains` helper, fix NilHub tests to pass nil (#11)
- Add nil guards before hub method calls in admin handlers (#12)
- Run golangci-lint v2: modernize interface{}/any, range-over-int loops,
  remove dead code, fix errcheck, add .golangci.yml config (#13)
- Add 23 client unit test suites (694 tests), exclude Tauri-coupled
  files from coverage, achieve 80%+ threshold (#14)

Closes #9, closes #10, closes #11, closes #12, closes #13, closes #14
2026-03-17 04:11:04 +01:00