Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/interview-contract-versions.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ can select it.

## The active bundle

Bundle 40: live prompt 31, report prompt 23, rubric 1, report schema 3.
Bundle 41: live prompt 32, report prompt 23, rubric 1, report schema 3.

| Bundle | Introduced |
|---|---|
| 41 | A candidate may opt into leading the Algorithm and Coding steps. Enabled, the live prompt stops the interviewer from proposing the next step on a stall during those two steps only: it may still answer a direct clarifying question about the problem statement, but turns an approach check back to the candidate, gates hints behind an explicit request, and never gives code, the same rules it always follows. Repeat, Example, Test and Optimizations are unchanged. The setting is carried into `RuntimeState`, so the editor and board silence nudges, which are what actually volunteer the next step on a stall, send the same open-question instruction fresh with each stall rather than relying on the system instruction alone; accumulated evidence that Repeat, Example and Algorithm each have evidence cannot prove the live conversation has left Example for good, since a candidate may return to it before touching the editor, so the nudge never substitutes the open question outright; once all three are recorded, it instead leads with the question scoped to "if this stall is in the Algorithm or Coding step", folding the usual next-step text into the same conditional as its "otherwise", which both avoids stating two contradictory instructions in the same event and leaves a stall still in Example with exactly the wording it would get unchanged. The signed participant setting is carried in token metadata, like the existing Example drawing opt-in. Live prompt 32; the report prompt, rubric and report schema are unchanged. |
| 40 | The interview no longer has a Pause control, so the live interviewer is never sent the resume line ("The interview has resumed.") in either round, nor that line joined to the request for a reply a lost connection owed, and the evidence session line no longer reports `paused`. A cold replacement is never deferred to an unpause: during a thinking hold it is still briefed at once as context that asks for no answer, and a reply a replaced socket owed is still asked for when the hold ends. Recordings made before this bundle keep their `lifecycle` `paused` rows, which the replay still reads; see `docs/recording-contract.md`. Live prompt 31; the report prompt, rubric and report schema are unchanged. |
| 39 | All local interviewer-audio cancellations record delivery uncertainty beside unchanged raw transcript text. Recovery, interim-review, phase-judge and report prompt views omit affected generated words; conversation lookup preserves text and byte cursors with an explicit delivery field. Late provider fragments remain uncertain until the same socket's next completion; replacement ends that window. A cut of completed playout marks its open line and the fragments that follow until the next generation's audio, a cut that stops nothing marks nothing, and a marked line is sealed so later words open a new line. Candidate speech, raw independence checks, protected output and presentation are unchanged. A negative follow-up judgment becomes unknown when cancelled output could have contained it. Live prompt 30 covers recovery, lookup and phase judging; report prompt 23 covers interim and final assessment. Rubric and schema are unchanged. |
| 38 | An outage reported by a run in the language on screen, once Test is recorded or while an earlier run still covers the code on screen, no longer reaches the setup-error reaction that asks the candidate to diagnose it. A reaction of its own says the platform failed rather than their code, that they may click Run once more later, and to continue from the conversation; it invites no hand trace, so the bundle 17 rule that an outage stops inviting one once Test is recorded still holds. The first outage while Test is open keeps the trace reaction, a coding-only session past the gate keeps its continuation, and an outage from a language the candidate has since left stays a setup error. Live prompt 29; the report prompt, rubric and report schema are unchanged. |
Expand Down
12 changes: 10 additions & 2 deletions src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ pub const THINKING_CHECK_IN_S: u64 = 120;
pub(crate) const THINKING_RELEASE_COOLDOWN: std::time::Duration =
std::time::Duration::from_secs(10);

pub const INTERVIEW_CONTRACT_BUNDLE_VERSION: u32 = 40;
pub const LIVE_PROMPT_VERSION: u32 = 31;
pub const INTERVIEW_CONTRACT_BUNDLE_VERSION: u32 = 41;
pub const LIVE_PROMPT_VERSION: u32 = 32;
Comment on lines +183 to +184

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description says this bumps LIVE_PROMPT_VERSION and INTERVIEW_CONTRACT_BUNDLE_VERSION to 31 and 40, but the diff moves them from 31 and 40 to 32 and 41 (and the docs row and golden digest agree with 32 and 41). Update the description so it matches what merges.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — updated the description's two mentions (the main paragraph and cubic's auto-generated summary) from 31/40 to 32/41, matching the diff, the docs row, and the golden digest.

pub const REPORT_PROMPT_VERSION: u32 = 23;
pub const RUBRIC_VERSION: u32 = 1;
pub const REPORT_SCHEMA_VERSION: u32 = 3;
Expand Down Expand Up @@ -967,6 +967,10 @@ pub struct RuntimeState {
pub interview_mode: InterviewMode,
pub code_execution_disabled: bool,
pub example_drawing_enabled: bool,
/// The candidate opted to lead the Algorithm and Coding steps; see
/// `prompts::candidate_led_policy` and the nudge it tells `silence_nudge`
/// to send instead of naming the next step.
pub candidate_led_enabled: bool,
pub coding_minutes: u32,
pub behavioral_minutes: u32,
pub round_transition_seen: bool,
Expand Down Expand Up @@ -1250,6 +1254,7 @@ impl Default for RuntimeState {
interview_mode: InterviewMode::Coding,
code_execution_disabled: false,
example_drawing_enabled: false,
candidate_led_enabled: false,
coding_minutes: 37,
behavioral_minutes: 8,
round_transition_seen: false,
Expand Down Expand Up @@ -3023,6 +3028,7 @@ pub struct MetadataConfig {
pub examples_hidden: bool,
pub code_execution_disabled: bool,
pub example_drawing_enabled: bool,
pub candidate_led_enabled: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Default)]
Expand Down Expand Up @@ -3393,6 +3399,7 @@ pub fn parse_participant_metadata(metadata: Option<&str>) -> MetadataConfig {

let example_drawing_enabled = !interview_mode.is_whiteboard()
&& value.get("exampleDrawing") == Some(&serde_json::Value::Bool(true));
let candidate_led_enabled = value.get("candidateLed") == Some(&serde_json::Value::Bool(true));
MetadataConfig {
voice: sanitize_interviewer_choice(value.get("interviewerVoice"), INTERVIEWER_VOICES),
accent: sanitize_interviewer_choice(value.get("interviewerAccent"), INTERVIEWER_ACCENTS),
Expand All @@ -3405,6 +3412,7 @@ pub fn parse_participant_metadata(metadata: Option<&str>) -> MetadataConfig {
examples_hidden,
code_execution_disabled,
example_drawing_enabled,
candidate_led_enabled,
}
}

Expand Down
109 changes: 104 additions & 5 deletions src/agent/prompts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,29 @@ call `log_hint` with `requested` false."#
)
}

/// Said only when the candidate opted into leading the Algorithm and Coding
/// steps themselves; empty otherwise, like every policy here with nothing to
/// say. Scoped to those two steps because `silence_nudge` and
/// `board_silence_nudge` are the ones that volunteer the next step on a
/// stall there; the other four steps already wait on the candidate. The
/// stall itself is answered under `candidate_led_nudge`, said fresh in that
/// system event rather than relying on this policy alone to still hold by
/// then.
fn candidate_led_policy(enabled: bool) -> String {
if !enabled {
return String::new();
}
"CANDIDATE-LED CODING — the candidate opted to lead the Algorithm and Coding \
steps. During those two steps only, do not propose the next step yourself, even \
when they stall: ask an open question such as \"What are you thinking of doing \
next?\" instead, and wait for them to state one. You may still answer a direct \
clarifying question about the problem statement as flow 4 says; flow 4's \
turn-back rule for approach checks and flow 5's hint and no-code rules still \
apply unchanged. This does not change Repeat, Example, Test, or Optimizations, \
where you lead as usual."
.to_string()
}

/// Every prompt that has to honour a declined behavioral probe names it in
/// these words, so the live interviewer, its recovery and timer briefings,
/// and the report reviewer cannot drift into different ideas of what counts
Expand Down Expand Up @@ -368,6 +391,7 @@ pub fn build_instructions_for_plan(
interview_mode,
code_execution_disabled,
example_drawing_enabled,
candidate_led_enabled,
} = options;
let metadata = problem.question_metadata();
let [_, optimal_point, pitfalls_point] = metadata.expected_discussion_points;
Expand Down Expand Up @@ -565,6 +589,7 @@ policies, which come out of the conversation as they would with a person."
};
let policies = [
reacto_policy(interview_mode, code_execution_disabled),
candidate_led_policy(candidate_led_enabled),
star_round_policy,
disclosure_policy.to_string(),
profile_policy(profile),
Expand Down Expand Up @@ -1863,6 +1888,28 @@ pub fn silence_nudge(state: &RuntimeState, evidence: &str, excerpt: Option<&str>
"ask for whichever of their understanding, example, or planned algorithm they have not explained."
};

// Repeat, Example and Algorithm recorded with no code yet means the stall
// is in Coding, not in an earlier step, as far as accumulated evidence can
// say — `phases_evidenced` only sees that each phase has evidence somewhere
// in the session, not that the live conversation has left Example for good.
// A candidate can cover all three and then return to Example, with a
// concrete concern, before touching the editor, so this stays a scoped
// conditional (`candidate_led_scoped_question`) rather than an outright
// substitution for `unwritten`: the model, which can see whether the stall
// is actually in Algorithm or Coding, decides, and a stall still in Example
// gets exactly what it would without candidate-led enabled. The trailing
// append below is skipped whenever a branch already carries that scoped
// clause itself, which is every `!written` branch, to avoid doubling it up.
let candidate_led_certain = state.candidate_led_enabled

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

phases_evidenced only says some non-skipped Repeat and Example evidence exists. It does not say the candidate has left Example. Take a candidate who walks through the ordinary case, gets an Example entry, and then stalls before the boundary case with the editor untouched. candidate_led_certain is true, so the nudge swaps "ask for whichever of their understanding, example, or planned algorithm they have not explained" for the open "what next" question and also drops the hedged clause. That changes Example, which the description promises stays the same. The same happens when a candidate goes back to Example after starting Algorithm. Add Algorithm to the check (Repeat, Example and Algorithm all evidenced, editor untouched, means the stall is in Coding) or keep the hedged form here, and add a test with only Repeat and Example recorded while the conversation is still in Example.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added Algorithm to the check: candidate_led_certain now requires Repeat, Example and Algorithm all evidenced, matching what unwritten itself requires, so the open question only replaces the hedged wording once the stall is certainly in Coding. With only Repeat and Example recorded the hedged form still runs, covering a candidate back in Example or about to start Algorithm. Added silenceCandidateLedInExample, a golden case with only Repeat and Example recorded, and renamed silenceCandidateLedBeforeCoding's evidence to add Algorithm so it still hits the certain case.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

candidate_led_certain treats "Repeat, Example and Algorithm have evidence and the editor is untouched" as proof the stall is in Coding, but phases_evidenced checks accumulated evidence, not the active step. A candidate who goes back to Example with a concrete concern before writing code still satisfies it, so the nudge swaps in the candidate-led open question and drops the hedged unwritten wording. That applies the new policy during Example, which the bundle 41 entry says is unchanged. Keep the "if this stall is in the Algorithm or Coding step" scope on this branch unless the active step is known independently, and add a golden case for a return to Example after all three phases are recorded.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and fixed: this branch (and the touched-but-not-written one below it) no longer substitutes the open question outright. Both now lead with "if this stall is in the Algorithm or Coding step, {open question} Otherwise, {the usual unwritten/with_code text}", so a return to Example gets exactly the wording it would without candidate-led enabled. Added silenceCandidateLedReturnedToExample: same evidence as silenceCandidateLedBeforeCoding plus a second Example entry recorded after Algorithm, asserting the identical (correct, scoped) output text.

&& super::phases_evidenced(
state,
&[
super::FrameworkPhase::Repeat,
super::FrameworkPhase::Example,
super::FrameworkPhase::Algorithm,
],
);

// The server knows whether the editor holds edits, so the nudge says which
// rather than offering both branches: asked to choose, the model read the
// editor first, a tool round in front of the question. Only an editor
Expand All @@ -1871,33 +1918,85 @@ pub fn silence_nudge(state: &RuntimeState, evidence: &str, excerpt: Option<&str>
let branch = if written {
format!(" {with_code}")
} else if super::code_untouched(state) {
format!(" They have not written code yet: {unwritten}")
if candidate_led_certain {
format!(
" They have not written code yet: {}{unwritten}",
candidate_led_scoped_question()
)
} else {
format!(" They have not written code yet: {unwritten}")
}
} else if candidate_led_certain {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The arm for an editor that was touched but not written (If the editor holds none of their code, ... Otherwise: {with_code}) has no case in tests/golden/prompts.json: silenceCandidateLedWriting is the written arm and silenceCandidateLedBeforeCoding the untouched one. Replacing this arm with the plain unwritten text leaves the suite green. Add a case with Repeat, Example and Algorithm recorded and code that differs from the starter but is not counted as written.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added silenceCandidateLedTouchedNotWritten: Repeat, Example and Algorithm recorded, the editor one character over the starter (too little to count as written). Also fixed the same evidence-vs-active-step gap you flagged below in this branch, not just the untouched one, so its text leads with the scoped "if this stall is in the Algorithm or Coding step" clause too instead of the plain open question.

format!(
" {}if the editor holds none of their code, {unwritten} Otherwise: {with_code}",
candidate_led_scoped_question()
)
} else {
format!(" If the editor holds none of their code, {unwritten} Otherwise: {with_code}")
};
format!(
"[SYSTEM EVENT] Silent and not typing for over {SILENCE_THRESHOLD_S:.0} seconds.{}{}Flow 2: ONE short question about their current decision.{branch} Do not restart them, restate the problem, supply an example, suggest an approach or reveal a bug.{}",
"[SYSTEM EVENT] Silent and not typing for over {SILENCE_THRESHOLD_S:.0} seconds.{}{}Flow 2: ONE short question about their current decision.{branch} Do not restart them, restate the problem, supply an example, suggest an approach or reveal a bug.{}{}",
evidence_section(evidence),
code_access(excerpt),
behavioral_ban(state),
if candidate_led_certain && !written {
""
} else {
candidate_led_nudge(state.candidate_led_enabled)
},
)
}

/// The scoped clause `candidate_led_certain` branches lead with instead of
/// asking `CANDIDATE_LED_OPEN_QUESTION` outright: see the comment on
/// `candidate_led_certain` above for why evidence alone cannot earn the
/// unconditional form. Ends with a lowercase-ready "Otherwise, " so a caller
/// appends its own fallback text directly.
fn candidate_led_scoped_question() -> String {
format!(
"if this stall is in the Algorithm or Coding step, {CANDIDATE_LED_OPEN_QUESTION} Otherwise, "
)
}

/// The open question asked, scoped by `candidate_led_scoped_question` or by
/// `candidate_led_nudge`, in place of naming the next step.
const CANDIDATE_LED_OPEN_QUESTION: &str = "ask what they are thinking of doing next \
without naming or steering toward one yourself, and wait for them to state it.";

/// Said by every silence nudge, editor and board alike, when the candidate
/// opted to lead the Algorithm and Coding steps: the fresh instruction this
/// stall is answered under, where it has to hold, not only the system prompt
/// set once at the start of the session. Appended, rather than scoped inline
/// like `candidate_led_scoped_question`, for the branches in `silence_nudge`
/// that never name a next step to replace (the `written` branch, where code
/// can exist in Test or Optimizations too) — there the model needs the scope
/// stated once, not woven into text that already stands on its own.
fn candidate_led_nudge(enabled: bool) -> &'static str {
if enabled {
" If this stall is in the Algorithm or Coding step, ask what they are \
thinking of doing next without naming or steering toward one yourself, and \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When Repeat, Example and Algorithm are all recorded and the editor is untouched, silence_nudge already says "ask them to begin writing their code in the editor.", and this sentence is added after it in the same event. So during Coding the model gets two instructions at once: name the next step, and don't name or steer toward one. When candidate_led_enabled is set, pick the open question instead of the unwritten branch rather than appending to it. Add a golden case for that state as well: silenceCandidateLed uses a default RuntimeState with no steps recorded, so it never reaches this conflict.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When Repeat and Example are both recorded and the editor is untouched, silence_nudge now substitutes the open question for naming the next step instead of appending it after; the hedged candidate_led_nudge wording is kept only for the states where Rust can't tell the current step from RuntimeState alone (code already written, so the stall could be in Coding, Test, or Optimizations). Added silenceCandidateLedBeforeCoding, a golden case that hits exactly the state you named.

wait for them to state it; outside those two steps, ask as above."
} else {
""
}
}

/// The silence nudge for a board, which carries no snapshot of its own.
///
/// The editor's version quotes the code into the prompt; the board cannot be
/// quoted, and the image the interviewer already has is the one it would have
/// sent. What is left to say is how much is on it, which is what decides
/// whether the question should be about the drawing or about the problem.
pub fn board_silence_nudge(evidence: &str, strokes: u32) -> String {
pub fn board_silence_nudge(evidence: &str, strokes: u32, candidate_led_enabled: bool) -> String {
let board = if strokes == 0 {
"The board is still empty.".to_string()
} else {
format!("The board holds {strokes} strokes, and you have the latest image of it.")
};
format!(
"[SYSTEM EVENT] Silent and not drawing for over {SILENCE_THRESHOLD_S:.0} seconds.{}{board}\nFlow 2: ONE short question about their current decision. If the board is empty, ask for whichever of their understanding, example, or planned algorithm they have not explained; if there is a drawing, ask them to narrate or trace it, and refer to a part of it only after looking at the image. Do not restart them, restate the problem, supply an example, suggest an approach or reveal a bug.{BEHAVIORAL_BAN}",
evidence_section(evidence)
"[SYSTEM EVENT] Silent and not drawing for over {SILENCE_THRESHOLD_S:.0} seconds.{}{board}\nFlow 2: ONE short question about their current decision. If the board is empty, ask for whichever of their understanding, example, or planned algorithm they have not explained; if there is a drawing, ask them to narrate or trace it, and refer to a part of it only after looking at the image. Do not restart them, restate the problem, supply an example, suggest an approach or reveal a bug.{BEHAVIORAL_BAN}{}",
evidence_section(evidence),
candidate_led_nudge(candidate_led_enabled),
)
}

Expand Down
2 changes: 2 additions & 0 deletions src/livekit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3192,6 +3192,7 @@ fn candidate_bootstrap<'a>(
interview_loop: candidate.interview_loop,
examples_hidden: candidate.examples_hidden,
example_drawing_enabled: candidate.example_drawing_enabled,
candidate_led_enabled: candidate.candidate_led_enabled,
interview_mode: candidate.interview_mode,
code_execution_disabled: candidate.code_execution_disabled
&& !candidate.interview_mode.is_whiteboard(),
Expand Down Expand Up @@ -3245,6 +3246,7 @@ fn initial_runtime_state(boot: &RuntimeBootstrap<'_>, started_at: Instant) -> Ru
interview_mode: boot.interview_mode,
code_execution_disabled: boot.code_execution_disabled,
example_drawing_enabled: boot.example_drawing_enabled,
candidate_led_enabled: boot.candidate_led_enabled,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing tests that this assignment and the one in candidate_bootstrap carry the setting into RuntimeState. The new test in tests/runtime.rs calls bootstrap_with_rounds directly, and the token tests stop at parse_participant_metadata. If either line is dropped, the suite still passes, yet opted-in candidates get the ordinary silence nudges. tests/unit/livekit.rs already checks example drawing this way (candidate_bootstrap then initial_runtime_state, asserting state.example_drawing_enabled). Add candidateLed true, false and absent to that test, or add a test beside it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added candidate_led_initial_state_carries_signed_choice beside the example drawing test, calling candidate_bootstrap then initial_runtime_state with candidateLed true, false and absent across both modes.

coding_minutes: boot.coding_minutes,
behavioral_minutes: boot.behavioral_minutes,
context_compression: boot.context_compression,
Expand Down
2 changes: 1 addition & 1 deletion src/livekit/turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1901,7 +1901,7 @@ impl RuntimeActivity {
// they can carry rather than only in wording; see
// `board_silence_nudge`.
let nudge = if state.interview_mode.is_whiteboard() {
board_silence_nudge(&evidence, state.board_strokes)
board_silence_nudge(&evidence, state.board_strokes, state.candidate_led_enabled)
} else {
silence_nudge(state, &evidence, excerpt.as_deref())
};
Expand Down
5 changes: 5 additions & 0 deletions src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub struct RuntimeBootstrap<'a> {
pub interview_mode: InterviewMode,
pub code_execution_disabled: bool,
pub example_drawing_enabled: bool,
pub candidate_led_enabled: bool,
pub coding_minutes: u32,
pub behavioral_minutes: u32,
pub profile: InterviewProfile,
Expand Down Expand Up @@ -75,6 +76,7 @@ pub struct RuntimeOptions {
pub interview_mode: InterviewMode,
pub code_execution_disabled: bool,
pub example_drawing_enabled: bool,
pub candidate_led_enabled: bool,
}

pub fn bootstrap<'a>(
Expand Down Expand Up @@ -109,6 +111,7 @@ pub fn bootstrap_with_rounds<'a>(
interview_mode,
code_execution_disabled,
example_drawing_enabled,
candidate_led_enabled,
} = options;
let example_drawing_enabled = example_drawing_enabled && !interview_mode.is_whiteboard();
let problem = get_problem(problem_id);
Expand All @@ -131,6 +134,7 @@ pub fn bootstrap_with_rounds<'a>(
interview_mode,
code_execution_disabled,
example_drawing_enabled,
candidate_led_enabled,
},
);

Expand All @@ -142,6 +146,7 @@ pub fn bootstrap_with_rounds<'a>(
interview_mode,
code_execution_disabled,
example_drawing_enabled,
candidate_led_enabled,
coding_minutes,
behavioral_minutes,
instructions,
Expand Down
6 changes: 6 additions & 0 deletions src/web/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,12 @@ pub fn token_response(
.expect("metadata is an object")
.insert("codeExecution".to_string(), Value::Bool(false));
}
if request.get("candidateLed") == Some(&Value::Bool(true)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing tests this path. candidateLedInstructions calls the prompt builder directly, so if the flag is dropped anywhere between the lobby, the token request, the signed metadata and parse_participant_metadata, everything stays green and the checkbox does nothing. Copy the exampleDrawing cases already in tests/web/token.rs (literal true kept; false, "true" and 1 dropped), tests/runtime.rs and tests/browser/lobby.test.js.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added tests/web/token.rs::token_carries_candidate_led_only_when_true, tests/runtime.rs::candidate_led_is_explicit_in_both_modes, and three lobby.test.js cases, mirroring the exampleDrawing coverage.

metadata
.as_object_mut()
.expect("metadata is an object")
.insert("candidateLed".to_string(), Value::Bool(true));
}
for (field, choices) in interviewer_choices() {
if let Some(choice) = resolve_interviewer_choice(request.get(field), choices, || {
let mut byte = [0u8];
Expand Down
Loading