Repository navigation
test: act on the test-quality audit of the last five commits - #466
Merged
Merged
Conversation
…d tests - The stand-in Recorder's pid line is read behind a 15 s deadline, so a child that hangs before printing fails the test instead of hanging it. - A normal stop asserts the guard ended by the group's SIGTERM (-SIGTERM), where `is not None` held for any exit at all. - The "server exits on its own" case ends the server by closing its stdin instead of a one-second sleep, so the test decides when it goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
… spends The test asserted only refusals, which a store that refused everything would pass. It now spends the link it minted and validates the session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
The refused cases ran in a loop inside the same-origin test. They are now a parametrized test of their own, so each case reports by name and the passing test asserts only the passing case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
Two `if` filters could drop the data under test without a failure: a segment from an unknown speaker was skipped, and a plain_text line without a clock stamp left the ordering check (with none at all, the check compared two empty lists). The test now asserts every segment's speaker is known and that plain_text has one stamped line for each of the eight segments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
The two signed-out dashboard tests replaced the global app's login-link store with a plain assignment and never put it back. The next server start's lifespan happened to rebuild it, so nothing broke, but the global state outlived the test. One helper now makes the swap through `monkeypatch`, and both tests use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
The guard tests polled `ps` against a 15 s deadline and used `Popen.wait(timeout=)`, which also polls. They now open the kernel's exit notification for each process while it is known to be alive (a pidfd on Linux, a kqueue NOTE_EXIT on macOS) and block on it. A zombie that nobody reaps counts as exited, so the `ps` status check is gone too. The stand-in server and Recorder block in `signal.pause()` and no longer sleep. Cleanup kills only a process whose exit has not been seen, so it cannot signal a reused pid. The 15 s budget bounds a single event wait so a regression fails rather than hangs. It is not a poll. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
`start()` polled `server.started` every 20 ms against a deadline. The harness now wraps uvicorn's `startup()` so an Event is set once startup has an outcome. On success `started` is True. A bind or lifespan failure raises SystemExit out of the same call, and a thread that dies earlier sets the Event from `_run_server`. `start()` makes one wait on that Event, and `ready_timeout` only bounds a hang. The ::1 collision regression test (`test_recorder_server_retries_past_a_port_taken_on_ipv6`) now runs its retry on that event too, as does every e2e fixture that starts a server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
…hild node The locale test spawned a child `node` under LANG=da_DK, so it depended on the runner's locale environment. It now sets the viewer locale in process: Intl.DateTimeFormat's fallback (the viewer locale) is set to da-DK, formatters.js is imported afresh under it, and the original is restored with t.after. A first assertion fails if the runtime has no da-DK data, so the test cannot pass vacuously. A mutant that drops the pinned "en-US" fails with `+01.00`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
… to go quiet The dead-cookie test guessed when boot was over: it polled the count of `/web/js/` responses every 0.5 s and waited for three stable readings (at most 20 s). The dashboard had no signal to wait on instead. Production change, a small one: `next/main.js` now sets `<html data-booted>` right after its boot awaits. By then every view module, template and warmed component has arrived, and the dashboard fetches no module after that point. (The one later `import()`, live-channel.js, is already in the module map through capture/taps.) The test waits on that attribute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
…very 50 ms
`harness.wait_until` polled its predicate every 50 ms against a deadline,
and about 80 e2e waits go through it. It now re-reads the predicate only
when watched state changes.
- `tests/change_signal.py` (new): `CHANGES` is a version counter that any
thread may bump and any event loop or thread may wait on. A waiter
reads the version before it checks, so no change can slip between the
check and the wait.
- `build_tap_recorder` wraps the public mutators of each Recorder it
builds: the streams registry, the utterance index, tap settings, jobs,
pipeline results, live transcripts, and the recording and session
toggles. Each mutator bumps the signal after it changes state.
`FakeWlkThread` bumps when a relay connects, sends a frame, or goes
away.
- Three dashboard callers read state the signal does not cover, so they
now wait on their own event:
- the session-meta save waits on the PUT's response;
- the WAV count after a tap waits on `utterance_released`, after which
the WAV is closed and on disk;
- a DOM row check uses `page.wait_for_function`.
- The `interval=` arguments are gone.
`timeout` now bounds a hang only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
…are gone
- `FakeTapServer` bumps `CHANGES` when a connection opens, a frame lands,
or a connection closes, so the six `wait_until` waits in this file are
event-driven too.
- The `asyncio.sleep(0.3)` / `asyncio.sleep(0.4)` pauses before "frames
are flowing" checks are replaced by `frames_flowing()`. It waits until
the WS is open and has carried a PCM frame.
- The drain test slept 0.15 s so that the bridge's onclose would fire and
buffer PCM before the mute. It now holds the redial's handshake
(`hold_handshakes`) and waits for that redial to arrive. That event
means onclose has fired, and the WS stays CONNECTING while PCM buffers.
The mute is then wrapped in `expect_console_message("draining before
close")`, which proves the drain branch ran. After the mute, the
handshake is released.
Run headed under Xvfb: 7 passed in 23 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
… is aged, not slept - `_wait_until` polled every 20 ms against a deadline. It now blocks on `CHANGES.wait_until`, the sync twin of the e2e wait. The Recorder here comes from `build_tap_recorder`, and the relay target is the fake WlK, so every condition it waits on is signalled. - The resume-window test slept 0.25 s to let a 0.05 s window elapse. It now keeps the real window and moves the closed record's `last_close` back past it. That stamp is the clock the index reads. - The rotation test polled the session folder for its WAV in a 10 ms loop. The WAV is opened before the stream registers, and registering signals, so it now waits on that signal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
…in its thread `_wait_until` polled every 5 ms against a deadline. - `_QueueStdout` now counts pushes, and counts how often the reader comes back for another line. The pump only asks again once it has fully handled the previous line, so `wait_processed()` blocks until every pushed line is done. The test then asserts the state once. - The two `start()` tests collect the pump thread `start()` spawns and join it after EOF. That is the event behind "state is stopped". - The TailLog stress test read for "as long as 0.3 s allows". It now waits until the writer has filled the log, so every later append also evicts, and then makes a fixed 2,000 reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
…lay to plateau `_wait_until_relay_settles` polled the fake WlK's byte count every 0.2 s and took three equal readings as "done". It now counts the work where it happens. A fixture wraps the public `TapFanOut.write_frame` to count the frames the /tap loop has fully handled, and wraps `WlKRelay.send` to count the bytes handed to the WlK. The test waits on `CHANGES`, with the tap still open, until every frame is handled and the fake WlK holds every relayed byte. The fake WlK signals each frame it receives. The wait has to happen inside the `websocket_connect` block. Leaving that block makes TestClient cancel the app, which drops whatever frames are still queued. A version that waited after the block forwarded between 0 and 17 KB. Waiting inside it forwards 85,120 bytes on every run (10 of 10, plus 12 of 12 for the gate+relay half alone). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
…rough them
The 12 fixed waits (`wait_for_timeout` / `asyncio.sleep`) in this file
now wait on events.
- `_cross_polls(page, n)` waits for n whole /api/state ticks that started
after the call. The poll loop runs one tick at a time, so a poll
starting means the one before it has finished, render included. That
makes "a poll carrying this change has rendered" an event. It counts
the page's own fetches through `_COUNT_STATE_304S_JS`, and a missing
counter fails loudly. This covers:
- the focus sweep (replaces the 750 ms window and its vacuity check);
- the renderList focus hold;
- the selection hold;
- the virtualized-row poll;
- the player stage walk;
- the idle-churn test's settle and its window (six counted ticks
instead of ~10 s; the old window held ~5-8).
- Job ticks: each tick waits until the job bar shows it before writing
the next.
- Paced retry: each label nudge waits until the label is on screen
(one render), then a final wait on the route's `retried` event.
- Live log dialog: `_COUNT_LIVE_LOG_HANDLED_JS` counts /api/live/log
bodies after renderLogInto has run, and the test waits for two
refreshes that fetched the new line.
- People rename: waits on the saver's PUT response, then reads once.
- Stage walk while playing: waits until the shell player's currentTime
advances while away.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
- `harness.count_tap_frames` / the `tap_frames` fixture counts completed `TapFanOut.write_frame` calls and signals `CHANGES` for each one. - The abort helper paused 5 ms after every frame so the receive loop could catch up before the RST. It now sends everything, then waits until the Recorder has handled every frame. The WlK-crash test does the same before `terminate()`, so the crash lands mid-utterance by event. - The 5 ms pauses in clean-close and refused streams are gone. Frames on one WebSocket arrive in order, and nothing in those tests waited on them. None of these were real-time audio pacing: real pacing is 20 ms per frame. - The TLS fixture polled `server.started` every 20 ms. It now uses `harness.signal_startup`, which is factored out of `RecorderServer`, and waits once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
Playwright takes the Promise an `async` predicate returns as truthy, so
`wait_for_function("async () => ...")` returns at once. A standalone
check confirmed it: `wait_for_function("async () => false")` returns
immediately. Three waits written that way therefore checked nothing:
- dashboard, renderList focus hold: the "server serves the new label"
control is now a single read. The sidecar is written before that line.
- dashboard, tap-mode override: waits on the PUT `/api/tap-mode`
response, then reads /api/state once.
- bridge extension, room disconnect: a sync predicate over
`_MIRROR_STORAGE_JS`, which mirrors chrome.storage.local into the
popup and keeps it fresh from `chrome.storage.onChanged`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
…e's status, not sleeps - Recorder startup: the fixture polled /health every 0.3 s. It now pumps the subprocess's output through a pipe, still writing it to recorder.log, and waits for uvicorn's "Uvicorn running on" line. Then it calls /health once. - "Frames streamed" (`sleep(2.0)` / `sleep(2.5)`): waits until the bridge's published status shows the tapped channels with frames sent. - After mute (`sleep(1.0)` ×2): waits until every channel is muted, drained, and its /tap WS closed. - Pipeline: `_await_pipeline` polled the endpoint every second. It now waits for the popup card to show the summary or a failure, then reads the endpoint once. - The popup reads chrome.storage through `_MIRROR_STORAGE_JS`, a mirror kept fresh by `onChanged`. The Start-meeting wait was an `async` predicate, which Playwright never awaited, so it is now a sync read of the mirror. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
…_logs The pump tests called the private `_pump_logs` on a thread they built, after setting `chan._proc` by hand. They now go through the public `start()`, the way production runs the pump. A `start_with` fixture uses public seams only: - a `whisperlivekit-server` (and an `.exe` twin for Windows) on PATH, which is how `start()` finds the real one, replacing patches of `_find_exe`; - `subprocess.Popen` patched for the call, to hand back the fake child; - every thread `start()` creates is recorded. With a fake child, no live guard spawns, so that is exactly the pump. The tests then assert only on the channel's public `info` and `log`. The device is the one `start()` seeds itself, not a hand-set `info["device"]`. The stale-pump case is driven as it happens: the old child has exited with its tail unread, and a second `start()` spawns the new one. I chose this over making `_pump_logs` a public seam. The behaviour under test is "what `start()` does with the child's output", and `start()` is already public. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
…testable Production change, a small one: `UtteranceIndex(clock=...)`, defaulting to `datetime.now(UTC)` and exposed as a public `clock` attribute. It stamps `last_close` and decides the resume window. Behaviour is unchanged. test_tap_endpoint uses it, and drops the reaches into internals: - the resume-window test steps a `_StepClock` past the window. Before, it mutated the record that `snapshot()` handed back, and before that it slept; - the rotation test drops its `_utc_session_id` patch. The rotation only happens once the tap's WAV is on disk, and `rotate_session` never re-mints an id whose folder exists, so a same-second rotation is distinct anyway; - both turn the live leg off through `PUT /api/tap-settings`, the public per-identity preference, instead of `live._proc = None`; - the `client` / `auth_client` fixtures set the app's dependency override and `app.state.recorder` through monkeypatch, so the global app gets its own state back even when a test fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
… via their route - Live channel start/stop: the fakes stand in for a spawned child only through the channel's public surface. `start`, `stop` and `running()` are monkeypatched, and `info` is set with `monkeypatch.setitem`. `rec.live._proc` is no longer touched. - WYSIWYG language save: the click also starts a transcribe job. The test now waits for it to release (`CHANGES`), so it cannot run on into the next test's teardown. - Real end-of-meeting pipeline: the default summarizer is set through `PUT /api/summarize/config`, the route the Settings card uses, instead of writing `summarizer.json` straight into the config directory. - The idle-churn section comment now says six counted ticks, not ~10 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
… wait it out The closed-tab test waited, in real time and with a 12 s ceiling, for the bridge's last snapshot to age past taps-view `STALE_AFTER_MS`. The popup now opens with Playwright's clock installed (`open_popup(fake_clock=True)`). After the tab closes, the test fast-forwards the popup's clock past the window, which it reads from taps-view.js itself, and the popup's own poll timer fires on the way. The test runs about 6 s faster and no longer depends on wall-clock time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
- `gate_client` set `app.dependency_overrides` and `app.state.recorder` on the global app, and cleared only the overrides, after the yield. A failing test left both behind. Both now go through monkeypatch. - The faster-whisper check moved from an `if ... pytest.skip()` in the body to `@pytest.mark.skipif`, so the test body has no branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ra2PNjYVihekwzjB5wtBR
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An automated test-quality audit flagged 43 tests changed in the last five commits: 23 as "fix" and 20 as "look". I checked each finding against the test and the code it covers. 10 are fixed here. The other 33 are left: 30 are false positives, and 3 are deliberate. None of the 3 involves timing.
The rule for this PR is that a flaky test is fixed when it is found. A bounded poll is not a reason to leave one. Every wait in the tests changed here is on a real event: a process's exit notice, a pipe or an
Event, or a mark the page sets. None polls a condition against a clock.Two small production changes (the second is described in section (e):
UtteranceIndextakes aclock). The first:tapscribe/web/js/next/main.jsnow sets<html data-booted>when boot's awaits finish. By then every view module, template and warmed component has arrived, and the dashboard fetches no module after that point. The dead-cookie e2e test needed a real signal to wait on, and the page had none.(a) Findings fixed
tests/test_live_guard.py::test_the_server_dies_with_a_recorder_that_is_killedpsagainst a 15 s deadline. It now opens the kernel's exit notice for the server and the guard while both are alive: a pidfd (os.pidfd_openplusselect) on Linux, or a kqueueNOTE_EXITon macOS. It then blocks on that notice. A zombie nobody reaps counts as exited, so thepscheck is gone. The pid line is read once the pipe is readable. The stand-ins block insignal.pause()instead of sleeping. Cleanup signals only a process whose exit it has not seen.tests/test_live_guard.py::test_a_normal_stop_ends_the_guard_with_the_serverPopen.wait(timeout=)polls internally, so the test now waits on the exit notice for each process. It also asserts that both ended by-SIGTERM, where beforeis not Nonepassed for any exit.tests/test_live_guard.py::test_the_guard_leaves_when_the_server_exits_on_its_owntime.sleep(1). It now exits when the test closes its stdin. Both exits are waited on through the exit notice.tests/test_login_links.py::test_an_unknown_token_is_refusedtests/test_auth_cookie.py::test_a_same_origin_write_with_a_null_origin_passesforloop inside the passing test. They moved to a new parametrized test,test_an_opaque_null_origin_write_is_refused, with one id per case.tests/e2e/test_pipeline_hallucinations.py::test_large_session_merge_preserves_all_speakers_in_orderiffilters could drop data without failing. A segment from an unknown speaker was skipped. Aplain_textline with no clock stamp left the ordering check, and with no stamped lines at all the check compared two empty lists. The test now asserts that every segment's speaker is known and that there is one stamped line for each of the 8 segments.tests/e2e/test_dashboard_ui.py::test_a_dead_session_cookie_tells_the_operator_instead_of_going_quietfor/elsepolled the count of/web/js/responses every 0.5 s until three readings matched. It now waits forhtml[data-booted], the new boot mark described above, so the loop and itselseare gone. Its store swap also goes throughmonkeypatch, as in the next row.tests/e2e/test_harness.py::test_recorder_server_retries_past_a_port_taken_on_ipv6RecorderServer.start()polledserver.startedevery 20 ms against a deadline. It now wraps uvicorn'sstartup()and sets athreading.Eventonce startup has an outcome. A bind failure raises SystemExit out of that same call, and a thread that dies earlier sets the Event too.start()then makes one wait. Thepytest.skipbranches for hosts without IPv6 stay, since they are the honest form. The test ran and passed here; it was not skipped. Every e2e fixture that starts a server gets this change.tapscribe/web/js/formatters.test.js"fmtClockZ keeps the ASCII spelling under a non-English viewer locale"nodeunderLANG=da_DK. It now runs in process:Intl.DateTimeFormat's fallback locale is set to da-DK,formatters.jsis imported afresh, and the original is restored witht.after. A guard assertion fails if the runtime has no da-DK data. A mutant without the pinned"en-US"fails with+01.00.tests/e2e/test_dashboard_ui.py::test_a_signed_out_tab_is_never_challenged_and_recovers_wholelogin_linksstore with a plain assignment and never restored it. A new helper,_sign_every_session_out, now makes the swap throughmonkeypatch, andtest_a_dead_session_cookie_tells_the_operator_instead_of_going_quietuses it too. The other two flags are false positives: thefor _ in range(2)loop always runs both of its assertions, andapp.state.login_linksis the store's documented seam (ADR-0023).(b) Findings left
tests/e2e/test_dashboard_ui.py::test_login_link_signs_the_browser_in_without_an_auth_dialogtests/e2e/test_dashboard_ui.py::test_a_link_signed_in_dashboard_can_still_writetry/exceptonly re-raises a timeout as anAssertionErrorthat names the refused requests, so no assertion is skipped.tests/test_bringup_defaults.py::test_start_ps1_reads_each_variable_from_its_key_live_codeis a helper in the test module, not a production internal.tests/test_live_supervision.py::test_start_gives_the_server_a_process_group_not_a_sessionskipifis the POSIX gate (process_groupexists only on POSIX). Patching_find_exeis the seam this whole file documents and uses.tests/test_login_routes.py::test_secure_follows_tlsconfig.TLS_ENABLEDflag throughmonkeypatch.tests/test_login_routes.py::test_the_cookie_name_carries_the_portconfig.PORTthroughmonkeypatch.tests/test_login_routes.py::test_a_spent_link_renders_a_page_and_never_a_bare_401clientfixture builds a new app for each test, so the replaced store is thrown away with that app.tests/test_login_routes.py::test_the_access_log_keeps_the_login_line_but_not_the_token_RedactQuerySecretsis the unit under test. Its only public path is the lifespan attaching it touvicorn.access, which would need a live Recorder. The repo tests private units like this directly.tests/test_login_routes.py::test_other_paths_keep_their_query_stringstests/test_login_routes.py::test_a_record_that_is_not_an_access_line_passes_through_untouchedtests/test_preflight.py::test_summarize_probe_agrees_with_resolve_local_backendreset_available_backendsfixture puts the auto-probe back on teardown.tests/test_session_merge.py::test_clock_stamps_an_aware_utc_instant_with_its_offset_one_lineis a test helper over the publicrender_transcript_text.tests/test_session_merge.py::test_clock_treats_a_naive_stamp_as_utctapscribe/web/js/formatters.test.js"fmtClockZ returns ? for missing or unparseable input"tests/test_auth_cookie.py::test_a_browser_background_request_is_never_challenged_even_without_a_cookieWWW-Authenticateis absent, which is the rule under test.tests/test_bundle_recorder_contract.py::test_the_quit_probe_reads_a_count_the_recorder_reportsControlClient.cs. The test asserts that the route is served and that the key is anint. A shape check is the contract here, because the count varies and the C# side reads any int.tests/test_install_matrix.py::test_install_matrix_wheel_index_matches_preflightpreflight.LLAMA_CPP_WHEEL_INDEX, so a drift fails it.tests/test_live_guard.py::test_no_guard_for_a_child_that_is_not_a_real_processis Noneis the exact contract for a stand-in that is not a real process.tests/test_login_links.py::test_the_cookie_is_not_the_tokentest_a_minted_link_spends_once_for_a_session.tests/test_login_links.py::test_a_respend_inside_the_grace_window_reissues_the_same_sessiontests/test_login_links.py::test_a_respend_after_the_grace_window_is_refusedtests/test_login_links.py::test_an_unspent_link_expirestests/test_login_links.py::test_an_unspent_link_expires_even_when_the_sweep_never_runs_sweepis the point: the docstring explains that it holdsspend's own TTL check to the rule.tests/test_login_links.py::test_validate_refuses_a_cookie_this_store_never_issuedtests/test_login_links.py::test_non_ascii_credentials_compare_without_crashingtests/test_login_links.py::test_expired_and_used_up_links_are_not_retained_links, because the public API cannot tell a forgotten link from an expired one.tests/test_login_links.py::test_a_session_nobody_uses_expirestests/test_login_links.py::test_a_session_in_use_outlives_the_idle_limittests/test_login_links.py::test_a_session_in_use_still_ends_at_the_hard_limittests/test_login_links.py::test_an_expired_session_is_not_refreshed_back_to_lifetests/test_login_links.py::test_expired_sessions_are_not_retained_sessions, and the test also asserts that the live session validates.tests/test_login_routes.py::test_minting_a_link_needs_the_password_mintin the neighbouring tests.tests/test_login_routes.py::test_the_redaction_does_not_depend_on_the_parameter_being_called_k_RedactQuerySecretsis the unit under test, as above.(c) Polling waits found along the way
The audit did not flag these. They are the same flaky pattern, so this PR fixes them too.
tests/e2e/harness.py::wait_until, with about 80 e2e callerstests/change_signal.py(new) providesCHANGES, a version counter that any thread may bump and any loop or thread may wait on. A waiter reads the version before it checks, so no change can slip between the check and the wait.build_tap_recorderwraps the public mutators of each Recorder it builds so they bump: streams, utterance index, tap settings, jobs, pipeline results, live transcripts, and the recording and session toggles.FakeWlkThreadbumps when a relay connects, sends a frame, or goes away.timeoutonly bounds a hang.test_dashboard_ui.pycallers that read state the signal does not coverutterance_released, after which the WAV is closed and on disk. The.wavrowcheck usespage.wait_for_function.tests/e2e/test_bridge_extension_e2e.pywait_untilwaits on an unsignalled fake server, 7asyncio.sleep(0.3/0.4)pauses before checking that frames flow, and anasyncio.sleep(0.15)before the drain test's mute.FakeTapServerbumpsCHANGES, andframes_flowing()waits for the first PCM frame. The drain test holds the redial's handshake and waits for it to arrive, which means onclose has fired and PCM is buffering. It then mutes insideexpect_console_message("draining before close"), which proves the drain branch ran.tests/test_tap_endpoint.py::_wait_untiland its 4 helpersCHANGES.wait_until, the blocking twin. The resume-window test no longer sleeps 0.25 s: it moves the closed record'slast_closeback past the real window. The rotation test's 10 ms glob loop now waits on the signal.tests/test_live_supervision.py::_wait_until, 9 callerswait_processed()blocks until every pushed line is handled. The twostart()tests join the pump thread. The TailLog stress test made as many reads as 0.3 s allowed; it now makes a fixed 2,000 reads once the log is full.tests/test_tap_live_gate_real_audio.py::_wait_until_relay_settlesTapFanOut.write_frame) and bytes relayed (WlKRelay.send). It waits, with the tap still open, until every frame is handled and the fake WlK holds every relayed byte. The wait has to be inside the block, because leaving it makes TestClient cancel the app and drop queued frames. Waiting outside forwarded 0 to 17 KB; waiting inside forwards 85,120 bytes every run.(d) The remaining fixed sleeps
test_dashboard_ui.py: 12 waits of the form "let a poll pass"wait_for_timeout(250…10000)/asyncio.sleep(1.0…1.2)_cross_polls(page, n)waits for n whole /api/state ticks that started after the call. The poll loop runs one tick at a time, so the next poll starting proves the previous tick, render included, has finished. It counts the page's own fetches (_COUNT_STATE_304S_JS) and fails loudly if the counter is missing. It covers the focus sweep, the renderList focus hold, the selection hold, the virtualized-row poll, the player stage walk, and idle churn (six counted ticks instead of ~10 s). The rest wait on their own events. Job ticks wait for the job bar to show each tick. Paced-retry nudges wait for the label to render, then on the route'sretriedevent. The log dialog uses_COUNT_LIVE_LOG_HANDLED_JS, which counts bodies once renderLogInto has run. The people rename waits for the PUT response. The stage walk waits for the player'scurrentTimeto advance while away.test_bridge_meeting_e2e.py: 6/healthpoll, 2–2.5 s after tapping, 1 s after each mute, and a 1 s pipeline pollrecorder.log. Streaming waits until the bridge's published status shows frames sent on the tapped channels. Muting waits until every channel is muted, drained and closed. The pipeline waits for the popup card to show the summary or a failure, then reads the endpoint once.test_pipeline_resilience.py(5) andtest_pipeline_tls.py(3)startedpolltap_framesfixture countsTapFanOut.write_frameand signalsCHANGES. The clean-close and refused streams needed no wait, because one WebSocket delivers in order. The TLS server start usesharness.signal_startup.asyncpredicates towait_for_function(dashboard 2, bridge extension 1, bridge meeting 1)wait_for_function("async () => false")returns immediately._MIRROR_STORAGE_JS. That script mirrors chrome.storage into the popup and keeps it fresh fromonChanged.Kept on purpose:
frame_interval_spacing inharness.stream_wav_via_tapsimulates real-time audio.test_next_perf_soak.pyis opt-in and measures over time by design.test_pipeline_e2e.py's Parakeet weight prefetch is the only sleep left that waits for something. It retries after anOSErrorfrom a concurrent writer of the Hugging Face cache, inside a lock that writer does not take. No event or signal reports when that writer finishes. The sleep runs only on that error path, in the opt-in real-audio Parakeet test.(e) Cleanup round: the audit of this PR's own tests
A fresh audit of the 31 tests this PR touches, taken before
8fc85f9, found 23 "fix" and 5 "look" findings. I checked each against the branch head. 14 are fixed in this round, 1 was already fixed by8fc85f9, and 13 are false positives.One production change:
UtteranceIndex(clock=...)intapscribe/recorder.py. It defaults todatetime.now(UTC)and is exposed as a publicclockattribute. It stampslast_closeand decides the resume window, so a test can step past the window. Behaviour is unchanged.Why the pump is tested through
start(): the log pump (_pump_logs) is private, so I drove it through the publicstart()rather than making it a public seam. The behaviour under test is "whatstart()does with the child's output", andstart()is already public. The fake child reachesstart()through public seams only: awhisperlivekit-serveron PATH, which is howstart()finds the real one, andsubprocess.Popenpatched for the call. The assertions read only the channel's publicinfoandlog.Fixed
test_live_supervision.py::test_pump_logs_promotes_starting_to_running_on_uvicorn_bannerstart()via thestart_withfixture. It no longer calls_pump_logs, setschan._proc, or patches_find_exe.…::test_pump_logs_promotes_on_application_startup_complete_line_too…::test_pump_logs_overwrites_seeded_device_with_child_accelerator_reportstart()sets, not a hand-writteninfo["device"].…::test_pump_logs_sets_error_state_with_last_error_tail_on_nonzero_exit…::test_pump_logs_skips_info_update_when_proc_was_replacedstart()spawns the new one before the old pump drains. Assertions are oninfoandlogonly.…::test_start_wires_a_background_pump_that_promotes_state_to_runningstart_withrecords the threadsstart()creates. The test no longer filters threads bychan._pump_logsor patches_find_exe.…::test_start_gives_the_server_a_process_group_not_a_sessionskipifstays:process_groupis POSIX-only.test_tap_endpoint.py::test_tap_resume_past_window_starts_fresh_wavUtteranceIndex.clockpast the window instead of mutating the record thatsnapshot()handed back. Live is turned off throughPUT /api/tap-settings, notlive._proc = None.test_tap_endpoint.py::…::test_open_tap_keeps_its_session_across_rotation_utc_session_idpatch is gone: the rotation happens only once the WAV is on disk, androtate_sessionnever re-mints an id whose folder exists. Live is turned off through the public route. The same file'sclient/auth_clientfixtures now restore the global app's override andapp.state.recorderthrough monkeypatch.test_dashboard_ui.py::test_transcript_transcribe_saves_languages_first_wysiwygtest_dashboard_ui.py::test_dashboard_live_channel_start_stopstart,stopandrunning()(monkeypatched) andinfo(monkeypatch.setitem).rec.live._procis no longer touched.test_dashboard_ui.py::test_dashboard_renders_real_end_of_meeting_pipeline_summaryPUT /api/summarize/configinstead of writingsummarizer.jsoninto the config directory. The other two flags are false positives: the assert inside the wait predicate fails the test with the pipeline's reason, and the config directory is per test.test_bridge_extension_e2e.py::test_closed_spatialchat_tab_flips_popup_to_no_active_tabSTALE_AFTER_MS, read from taps-view.js, instead of waiting it out in real time against a 12 s ceiling. The conditional flag is the DOM predicate's own checks.test_tap_live_gate_real_audio.py::test_tapscribe_gate_forwards_real_speech_and_drops_silence_end_to_end(look)gate_clientrestores the global app's override andapp.state.recorderthrough monkeypatch; before, a failing test left them set. The faster-whisper check is askipifmarker, not anif … pytest.skip()in the body. The internals flag is a false positive: the frame ledger wraps public methods, and monkeypatch restores them.Already fixed by
8fc85f9test_bridge_extension_e2e.py::test_room_disconnect_cleans_up_audio_and_presence_only_tapsasyncpredicate, which never waited. It now reads the storage mirror, a fix in8fc85f9, after this audit's snapshot.window.__tsTestis the mock page's own test driver, not production.False positives
test_bridge_extension_e2e.py::test_pill_transitions_idle_to_ok_to_warn_on_tap_dropframes_flowing).refuse_connectionsis set on a per-test fixture server. The conditions are inside the DOM predicate.…::test_pcm_frames_carry_resolved_display_name_after_sidebar_rerenderCHANGES.window.__tsTestis the mock page's test driver.…::test_popup_token_rotation_triggers_reconnect_with_new_subprotocolCHANGES. No sleeps are left.…::test_mute_drain_reconnect_continues_same_utteranceexpect_console_message. No sleeps are left.…::test_window_room_cleared_without_disconnect_closes_tapsCHANGES.test_dashboard_ui.py::test_next_failed_files_fetch_still_reconciles_after_a_stage_switchbreakis followed by an unconditional wait onretried. Every wait is on an event. The route and its state live on a per-test page.test_dashboard_ui.py::test_a_dead_session_cookie_tells_the_operator_instead_of_going_quiettry/exceptre-raises as anAssertionErrorthat names recent traffic. Boot is waited for ondata-booted.app.state.login_linksis the store's documented seam (thelogin_linksdocstring, ADR-0023), swapped through monkeypatch.test_dashboard_ui.py::test_a_signed_out_tab_is_never_challenged_and_recovers_wholetest_live_guard.py::test_the_server_dies_with_a_recorder_that_is_killedfinally; no assertion sits behind them._ExitWatchand_read_pidsare helpers inside the test module.test_pipeline_hallucinations.py::test_large_session_merge_preserves_all_speakers_in_order(look)test_auth_cookie.py::test_an_opaque_null_origin_write_is_refused(look)test_live_supervision.py::test_log_iteration_is_safe_while_a_thread_appends(look)test_login_links.py::test_an_unknown_token_is_refused(look)Verification
python -m pytest tests -qgives 3341 passed, 28 skipped, 0 failed.test_live_supervision,test_tap_endpoint,test_recorder_subcomponents,test_tap_live_gate_real_audio,test_live_guard): 10 of 10 runs passed.test_dashboard_ui, plustest_bridge_extension_e2eheaded under Xvfb): 10 of 10 runs, 97 passed each. The runs went five at a time in parallel.ruff checkandruff format --checkovertapscribe tools tests benchmarks bridges/local-test-bridge: clean. The frontend typecheck and the web checks were clean whenmain.jslast changed.python -m pytest tests -q: 3341 passed, 28 skipped, 0 failed. This ran with Playwright Chromium and thewhisper-cpuextra installed. The headed bridge tests skip there because the run has no display; they ran under Xvfb in the stability runs below.testson macOS (the kqueueNOTE_EXITpath intest_live_guard.py) andbridge E2E (extension + meeting).test_bridge_meeting_e2e.py::test_multi_person_multi_language_meeting_produces_a_summary, with "English content missing from transcript". It fails the same way, at the same assertion, with the old sleep-based code too, and it passes in CI's bridge E2E job. It comes from the local whisper model, not from timing.wait_until: 10 of 10 runs, 120 tests each.test_tap_endpoint.py: 10 of 10.test_live_supervision.py: 15 of 15. The real-audio gate test: 10 of 10.test_live_guard.py45×;test_harness.py15×; the two signed-out dashboard tests 12×, plus 6× with every CPU core pinned by busy loops.🤖 Generated with Claude Code