diff --git a/crates/stella-pipeline/src/pipeline/tests/verification_hardening.rs b/crates/stella-pipeline/src/pipeline/tests/verification_hardening.rs index ea63ff374..276742341 100644 --- a/crates/stella-pipeline/src/pipeline/tests/verification_hardening.rs +++ b/crates/stella-pipeline/src/pipeline/tests/verification_hardening.rs @@ -7,14 +7,14 @@ use super::*; use crate::LineMutation; -/// Both arming witnesses for the mid-turn flip halt (#1793), and the shell -/// doubles they share. +/// Both arming paths of the mid-turn flip halt (#1793), and the two doubles +/// they share. A child rather than a sibling module so it still reaches this +/// file's scripted ports through `use super::*`, and so the already-oversized +/// `tests.rs` does not grow another module declaration. /// -/// A child rather than a sibling module so the already-oversized `tests.rs` -/// does not grow another module declaration. The doubles deliberately live -/// *inside* it rather than here: see that file's own module doc for why -/// colocating them with the two witnesses is what turns a wholesale rewrite -/// of this parent into a merge conflict instead of a silent deletion. +/// The doubles live in the child with their only users, not here: when they +/// sat in this file, a wholesale rewrite of it took them and the +/// configured-command witness with them, and no gate objected (#1997). mod flip_halt_arming; /// #860 acceptance: a baseline that TIMES OUT observed no failing assertion, diff --git a/crates/stella-pipeline/src/pipeline/tests/verification_hardening/flip_halt_arming.rs b/crates/stella-pipeline/src/pipeline/tests/verification_hardening/flip_halt_arming.rs index 5d5c21cb1..887230fb7 100644 --- a/crates/stella-pipeline/src/pipeline/tests/verification_hardening/flip_halt_arming.rs +++ b/crates/stella-pipeline/src/pipeline/tests/verification_hardening/flip_halt_arming.rs @@ -20,6 +20,55 @@ use super::*; +/// The shell tool both witnesses script their revision against. +/// +/// A distinct name from [`WRITING_TOOL`] because the two fakes answer +/// differently and a flip must be attributable: only this one emits the exit +/// marker [`crate::flip_halt::exit_status`] reads. +const SHELL_TOOL: &str = "bash"; + +/// One model turn that runs `command` through the shell tool. +/// +/// The `command` key is what [`crate::flip_halt::command_of`] looks for, so a +/// call built any other way would be invisible to the halt and the test would +/// pass for the wrong reason. +fn shell_call_result(command: &str) -> CompletionResult { + CompletionResult { + tool_calls: vec![ToolCall { + call_id: "call-shell".into(), + name: SHELL_TOOL.into(), + input: serde_json::json!({ "command": command }), + }], + ..text_result("") + } +} + +/// A shell whose every command succeeds, reported the way the real bash tool +/// reports it — the trailing `[exit code: 0]` marker. +/// +/// That marker is the whole point: [`crate::flip_halt::FlipHalt::observe`] +/// latches only on a tracked command that exited zero, and output without a +/// marker can never stop a turn. A fake returning bare prose would leave the +/// halt unlatched and the arming test green for no reason. +struct PassingShell; +#[async_trait] +impl ToolExecutor for PassingShell { + fn schemas(&self) -> Vec { + vec![ToolSchema { + name: SHELL_TOOL.into(), + description: "run a shell command".into(), + input_schema: serde_json::json!({ "type": "object" }), + read_only: false, + speculation_safe: false, + }] + } + async fn execute(&self, _name: &str, _input: &Value) -> ToolOutput { + ToolOutput::Ok { + content: "ok\n[exit code: 0]".into(), + } + } +} + /// #1793 witness (authored side): after `witness_on_demand` seeds a failing /// witness, a revision that observes the witness command pass halts at that /// step boundary. As in the configured-command twin, the provider is @@ -114,42 +163,6 @@ async fn an_authored_witness_arms_the_revision_flip_halt() { flipped, not spend the scripted steps beyond it" ); } -/// A shell double whose every command "passes": the output carries the -/// trailing exit-0 marker [`crate::flip_halt::exit_status`] parses. What -/// [`EmptyTools`] can never express — a worker *observing* the tracked test -/// succeed through a tool result. -struct PassingShell; -#[async_trait] -impl ToolExecutor for PassingShell { - fn schemas(&self) -> Vec { - vec![ToolSchema { - name: "bash".into(), - description: "run a shell command".into(), - input_schema: serde_json::json!({ "type": "object" }), - read_only: false, - speculation_safe: false, - }] - } - async fn execute(&self, _name: &str, _input: &Value) -> ToolOutput { - ToolOutput::Ok { - content: "1 passed\n[exit code: 0]".into(), - } - } -} - -/// A completion that runs `command` through the shell — the observation the -/// flip halt correlates by `call_id` and scores against the tracked test. -fn shell_call_result(command: &str) -> CompletionResult { - CompletionResult { - tool_calls: vec![ToolCall { - call_id: format!("call-shell-{command}"), - name: "bash".into(), - input: serde_json::json!({ "command": command }), - }], - ..text_result("") - } -} - /// #1793 witness (configured-command side): a revision that observes the /// tracked test go fail→pass halts at that step boundary instead of running /// on. The provider is scripted with steps BEYOND the flip; consuming them diff --git a/crates/stella-protocol/src/event.rs b/crates/stella-protocol/src/event.rs index 6c77d3150..d91b0bda3 100644 --- a/crates/stella-protocol/src/event.rs +++ b/crates/stella-protocol/src/event.rs @@ -542,8 +542,7 @@ pub enum AgentEvent { /// The replacement bytes each in-place rewrite left behind, one entry /// per digest — what lets reconstruction resolve a compacted block to /// the bytes the model received rather than the pre-compaction output - /// under the same `call_id` (#1667); see - /// [`CompactionRewrite`](crate::CompactionRewrite). + /// under the same `call_id` (#1667); see [`crate::CompactionRewrite`]. /// `serde(default)` — absent on journals written before rewrites were /// journaled, whose compacted blocks surface as digest mismatches. #[serde(default, skip_serializing_if = "Vec::is_empty")]