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
26 changes: 25 additions & 1 deletion .cargo/mutants.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Functions the mutation gate cannot judge, because `cargo test` cannot reach
# them. Matched against the mutant names that `cargo mutants --list` prints.
#
# EXCLUSIONS: 36
# EXCLUSIONS: 39
#
# That number is checked by `scripts/test.sh`, so adding an entry means editing
# this line too. The point is not the count, it is that the list only ever grows
Expand Down Expand Up @@ -164,6 +164,27 @@
# reaches. The decision it makes with an error in hand is `is_locked`, which is
# tested.
#
# `generate_content_once` is the one call both report generation and the
# idle-window review now go through, and it is the transport the `generate_
# report` entry above already describes: it posts to Google and reads the text
# back, so every line of it needs an API key. It became its own function when
# the two callers stopped each carrying a copy of the same request. What it is
# handed is `content_request` and the two generation configs, and what it reads
# out is `gemini_text`; all three are tested in tests/unit/gemini.rs.
#
# `generate_interim_review` is that transport with the interim config and its
# own deadline around it, so `Ok(String::new())` -- an empty note -- looks the
# same from outside as a call nobody made. The same case as `generate_report`,
# one layer down. What comes back is bounded and stored by
# `record_interim_notes`, which is tested.
#
# `end_through_control` is the two lines the server deadline and the
# interviewer's own ending share: it builds an `end_interview` packet and hands
# it to `handle_data_packet`, which is already in this list for taking a
# `&Room`. Replacing it with `Ok(())` publishes no report, and a test with no
# room cannot tell that from publishing one. What it decides before it writes
# is `ready_to_close`, tested in tests/unit/livekit.rs.
#
# Keep this list short and each entry justified. An entry that is really "we
# never got around to testing this" belongs in a test, not here.
exclude_re = [
Expand Down Expand Up @@ -197,6 +218,9 @@ exclude_re = [
"open_live_session_at",
"leave_room",
"publish_interviewer_state",
"generate_content_once",
"generate_interim_review",
"end_through_control",
"handle_media_event",
"attach_audio",
"next_audio_frame",
Expand Down
264 changes: 246 additions & 18 deletions src/agent.rs

Large diffs are not rendered by default.

45 changes: 31 additions & 14 deletions src/agent/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
//! so each applier decides what it is willing to believe before it stores it.

use super::{
DataEventResult, EvidenceKind, FrameworkPhase, InterviewLoop, LanguageChoiceContext,
MAX_INTEGRITY_EVENTS, ROUND_TRANSITION_SKEW, RuntimeState, cold_restart, format_test_run,
DataEventResult, InterviewLoop, LanguageChoiceContext, MAX_INTEGRITY_EVENTS,
ROUND_TRANSITION_SKEW, RuntimeState, TIME_WARNING_S, cold_restart, format_test_run,
integrity_hash, json_int, language_choice, python_truthy, sanitize_integrity_event,
sanitize_test_run, spoken_language, spoken_minutes_from_remaining_seconds,
test_reaction_decision, test_results_reaction, time_warning,
Expand Down Expand Up @@ -185,7 +185,12 @@ fn apply_control(state: &mut RuntimeState, payload: &serde_json::Value) -> DataE
{
control_round_transition(state)
}
Some("time_warning") if !state.ended && !state.paused => {
Some("time_warning")
if !state.ended
&& !state.paused
&& !state.time_warning_seen
&& time_warning_is_due(state) =>
{
control_time_warning(state, payload)
}
Some("end_interview") if !state.ended => control_end_interview(state, payload),
Expand Down Expand Up @@ -236,13 +241,7 @@ fn control_pause(state: &mut RuntimeState, payload: &serde_json::Value) -> DataE
/// The reserved behavioral round, opened or refused, once.
fn control_round_transition(state: &mut RuntimeState) -> DataEventResult {
state.round_transition_seen = true;
let completed = |phase| {
state
.framework_evidence
.iter()
.any(|item| item.phase == phase && item.kind != EvidenceKind::Skipped)
};
if completed(FrameworkPhase::Test) && completed(FrameworkPhase::Optimizations) {
if super::coding_round_complete(state) {
state.behavioral_round_started = true;
DataEventResult {
round_changed: Some("started"),
Expand All @@ -258,11 +257,29 @@ fn control_round_transition(state: &mut RuntimeState) -> DataEventResult {
}
}

/// The clock crossing the warning threshold.
/// Whether the interview has actually run far enough to be nearly over.
///
/// The browser owns the countdown and the candidate owns the browser, so this
/// packet is a claim like any other from that side. Unchecked it was worse than
/// noise: accepting one at minute one both interrupts the candidate with a
/// warning that is not true and consumes `time_warning_seen`, so the real
/// five-minute warning is then refused for the rest of the interview. The
/// adjacent round transition has been validated against this clock all along;
/// this is the same check for the same reason.
fn time_warning_is_due(state: &RuntimeState) -> bool {
let planned = u64::from(state.coding_minutes + state.behavioral_minutes) * 60;
state.started_at.elapsed() + ROUND_TRANSITION_SKEW
>= std::time::Duration::from_secs(planned.saturating_sub(TIME_WARNING_S))
}

/// The clock crossing the warning threshold, once.
///
/// The one control message that reads the state without writing any, which is
/// what the shared reference says.
fn control_time_warning(state: &RuntimeState, payload: &serde_json::Value) -> DataEventResult {
/// The browser releases its own latch after a pause because its first packet
/// may have arrived while this side was paused. Remembering an accepted warning
/// here lets that retry through when needed while refusing it after it already
/// interrupted the candidate.
fn control_time_warning(state: &mut RuntimeState, payload: &serde_json::Value) -> DataEventResult {
state.time_warning_seen = true;
let remaining_seconds = payload
.get("remainingSeconds")
.and_then(json_int)
Expand Down
Loading