Refine preflight language and clean up ended starts

Merged
MA
18h ago
+832-72

Follows up jserv's review comments on #502 after it merged, and his review of this PR. Rebased onto b326b6f. There are two commits.

Address post-merge review of preflight language

  • Before a Strict assignment there is no judge to load, but the Before you join menu labeled C C (unavailable: waiting for judge), and its title told the candidate to reload, which does nothing in Strict (comment). While strict && !problem, applyLanguages now gives C a reason that names the assigned problem: C appears in the editor's language menu after joining if that problem can run it. After assignment the usual labels return, and docs/interview-selection.md describes this.
  • connect commits the language before the Strict assignment, so the late-judge branch in initializeLanguages never ran in the five Strict cases of strict-selection, and all five passed for the same reason wait did (comment). They are now one Strict case, which checks that the token's language survives a late judge. The edit, choose, run and end guards are tested during the bounded C wait of a Practice start instead. There the workspace is open and a judge can still land before anything is committed, and a wait case confirms the switch to C happens when nothing is. preflight takes a pause option that stops the page's clock once Start is ready and before it is pressed, so the 5 s wait cannot run out on a slow runner.

Leave nothing behind when a start is ended early

The workspace stays open from Start until the room is joined, so a candidate can end the interview while the start is still waiting for consent, a C judge, the token response, a Strict problem or the join, and an interviewer's report can arrive during the join. connect carried on anyway: the ended interview got a room and an interviewer, and the preflight's tracks were never released, because the report and leave paths stop state.localUserStream, which is only assigned when a join finishes. This window was already on main before #502, which did not introduce it.

  • Before the room exists, connect checks state.phase before the token request and after the token response. The second check comes after the Strict receipt is saved, so an assignment ended there is still excluded next time. An ended start releases the preflight's tracks through abandonStart, which the failure path now uses too.
  • A join the interview moved on from, whether it finished or failed, is settled by settleEndedJoin before state.room = room, while the room is still the start's own. Any phase past live gives the room up, including the report the offline summary reaches 300 ms after an end with no interviewer. If an agent report is still using the room, the tracks stop and nothing starts behind it. report_recovery is handed the room for its retry, and the offline summary's timer no longer draws over it. A report still being received keeps the room through its own handler, which leaves it once the receipt is out, so the join does not claim it.
  • A report that arrives once the page already shows one no longer claims the page, since receiveReport ignores it and nothing would release the claim.
  • Once the room is claimed, the join stops before restarting the integrity heartbeat and worker if the interview ended or a report landed during its integrity publishes, and connect only stops. The room and tracks belong to the report and leave paths from there.
  • A start that fails after the interview ended releases its tracks and stops, without offering the offline fallback or the preflight again under the ending.
  • The interview fixture records the tracks it hands out and can hold a room join, a track publish or a data publish. New cases cover an end while the token response, the join or an integrity publish is held; the offline summary appearing before a held join finishes, with and without a late report; a recoverable or complete report during the join's track or integrity publishes, with its receipt leaving before or after the join finishes; a failed start after the interview ended; and an end before the join with a recoverable report arriving. They check that an unclaimed room is left, a room in use is kept and never taken back once left, recovery can still send its retry, nothing starts behind a report, no fallback appears, and every released track has ended. Cases that depend on a timer firing let it fire; the rest pause the clock before Start so the ending's own timers cannot act first. The self-view source check now follows each path that stops the camera separately.

Not in this PR, as follow-ups: the server keeps a dispatch slot for up to 300 s when a start is abandoned after the token response; a start that fails after the integrity monitors started leaves their timers running; and connect does not check the phase after startRecording returns. The last two also exist on main.

Testing, on Windows, with each commit checked out on its own:

  • tests/browser/strict-selection.test.js: 57 passed after the first commit and 69 after the second, in repeated runs.
  • First commit: removing the commit in the editor input handler, in chooseLanguage or in runTests, or the state.phase check in initializeLanguages, fails its Practice case. Removing the Strict label replacement fails the preflight label cases.
  • Second commit: each check was removed or weakened in turn, and each change fails at least one case. That covers the phase checks before the room, in settleEndedJoin and after the join; each exemption and the room handover in settleEndedJoin; the failed-join path; the offline summary guard; the report claim while a report is shown; the integrity monitor check; the catch check; and the track release in abandonStart. Removing each abandonStart call in connect fails the self-view check.
  • The full browser suite at f8de280: 2030 passed, 3 failed, 14 skipped. The three failures, one in face-detector.test.js and two floor-capture.test.js interruption tests, fail the same way on main on this machine, and pass in CI. ESLint and Prettier are clean.