test(conformance): replay the recording host's optional tools

Hosted Linux on wave ce1ecc8dc (PR CI 36750896459, and both shared-process
passes of 36750913302) drifted in 6 events goldens: the tool_request_snapshot
counted 41 registry-only tools against the recorded 43. Four tools register
only when this host has their backend (code_execution: Python, js_execution:
Node, pandoc_convert: pandoc, image_ocr: Vision/tesseract); the Mac that
recorded the goldens has all four, the runner lacks pandoc and OCR.

dependencies::host_tool_available is now the one probe seam for those four.
Under a conformance replay it answers with recorded_platform.host_tools
(thread-scoped, test builds, unix only), beside os/shell/sandbox posture;
production probes exactly as before. No golden is re-recorded; each case
gains the recorded list, and a case without it fails loud.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: CodeWhale Bot <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-09-30 11:32:42 -07:00
1 parent 171d061eda
commit 8b5a86eed6
14 files changed
+135 -14

No files matched your search

+18
View File
@@ -288,6 +288,7 @@ fn run_scripted_turn_with_deadline(
// Held across the whole turn: the engine builds and refreshes its system
// prompt on this thread's current-thread runtime.
let _environment = crate::prompts::pin_recorded_environment(&os, &shell);
let _host_tools = crate::dependencies::pin_recorded_host_tools(recorded_host_tools(case));
let record = runtime.block_on(async {
let config = Config::default();
let engine_config = EngineConfig {
@@ -355,6 +356,23 @@ fn recorded_environment(case: &Value) -> (String, String) {
(field("os"), field("shell"))
}
/// The optional host-backed tools the recording machine had (see
/// `dependencies::host_tool_available`). They change the registry the golden
/// snapshot counts; a case without the list fails loud.
fn recorded_host_tools(case: &Value) -> Vec<String> {
case.get("recorded_platform")
.and_then(|platform| platform.get("host_tools"))
.and_then(Value::as_array)
.expect("recorded_platform.host_tools must list the recording host's optional tools")
.iter()
.map(|tool| {
tool.as_str()
.expect("recorded_platform.host_tools entries are tool names")
.to_string()
})
.collect()
}
/// Run one turn and return every engine event up to and including
/// `TurnComplete`, then everything the engine emits while it shuts down.
async fn drive_turn(
+6 -2
View File
@@ -310,7 +310,9 @@ pub(crate) fn ensure_advanced_tooling(
// `crate::dependencies::resolve_python_interpreter` for the probe.
if mode != AppMode::Plan
&& !catalog.iter().any(|t| t.name == CODE_EXECUTION_TOOL_NAME)
&& crate::dependencies::resolve_python_interpreter().is_some()
&& crate::dependencies::host_tool_available(CODE_EXECUTION_TOOL_NAME, || {
crate::dependencies::resolve_python_interpreter().is_some()
})
{
catalog.push(Tool {
tool_type: Some(CODE_EXECUTION_TOOL_TYPE.to_string()),
@@ -341,7 +343,9 @@ pub(crate) fn ensure_advanced_tooling(
// the tool only when `resolve_node()` succeeds.
if mode != AppMode::Plan
&& !catalog.iter().any(|t| t.name == JS_EXECUTION_TOOL_NAME)
&& crate::dependencies::resolve_node().is_some()
&& crate::dependencies::host_tool_available(JS_EXECUTION_TOOL_NAME, || {
crate::dependencies::resolve_node().is_some()
})
{
let mut tool = crate::tools::js_execution::js_execution_tool_definition();
tool.defer_loading = Some(should_default_defer_tool(&tool.name, always_load));
+40
View File
@@ -269,6 +269,46 @@ pub fn resolve_pandoc() -> Option<String> {
.clone()
}
/// Whether an optional tool whose backend lives on this host (an interpreter,
/// a converter, an OCR engine) is available: `probe` decides, except that a
/// conformance replay answers with the recorded host's set so goldens do not
/// depend on what the machine running them has installed (test builds only).
pub(crate) fn host_tool_available(tool: &str, probe: impl FnOnce() -> bool) -> bool {
#[cfg(all(test, unix))]
if let Some(available) = RECORDED_HOST_TOOLS.with(|cell| {
cell.borrow()
.as_ref()
.map(|tools| tools.iter().any(|name| name == tool))
}) {
return available;
}
probe()
}
#[cfg(all(test, unix))]
thread_local! {
static RECORDED_HOST_TOOLS: std::cell::RefCell<Option<Vec<String>>> =
const { std::cell::RefCell::new(None) };
}
/// Pin [`host_tool_available`] on this thread to a recorded host's tools until
/// the guard drops.
#[cfg(all(test, unix))]
pub(crate) fn pin_recorded_host_tools(tools: Vec<String>) -> RecordedHostToolsGuard {
RECORDED_HOST_TOOLS.with(|cell| *cell.borrow_mut() = Some(tools));
RecordedHostToolsGuard
}
#[cfg(all(test, unix))]
pub(crate) struct RecordedHostToolsGuard;
#[cfg(all(test, unix))]
impl Drop for RecordedHostToolsGuard {
fn drop(&mut self) {
RECORDED_HOST_TOOLS.with(|cell| *cell.borrow_mut() = None);
}
}
/// Resolve the Node.js runtime once per process. Used by the
/// `js_execution` tool to decide whether to advertise itself in
/// the catalog. Unlike Python, the executable name `node` is the
+4 -2
View File
@@ -932,7 +932,9 @@ impl ToolRegistryBuilder {
/// sees a binary it can't actually use.
#[must_use]
pub fn with_pandoc_tools(self) -> Self {
if crate::dependencies::resolve_pandoc().is_some() {
if crate::dependencies::host_tool_available("pandoc_convert", || {
crate::dependencies::resolve_pandoc().is_some()
}) {
use super::pandoc::PandocConvertTool;
self.with_tool(Arc::new(PandocConvertTool))
} else {
@@ -945,7 +947,7 @@ impl ToolRegistryBuilder {
/// Tesseract when installed.
#[must_use]
pub fn with_image_ocr_tools(self) -> Self {
if super::image_ocr::ocr_available() {
if crate::dependencies::host_tool_available("image_ocr", super::image_ocr::ocr_available) {
use super::image_ocr::ImageOcrTool;
self.with_tool(Arc::new(ImageOcrTool))
} else {
+4 -1
View File
@@ -41,7 +41,10 @@ runner for a fact about the machine, not the Engine. A case that omits the
field fails loud. The same record carries `os` and `shell` (`macos`, `/bin/bash`: the shell
dispatcher names the recording process's `$SHELL` path verbatim),
which the `## Environment` block renders into the frozen prompt prefix and so
into `prefix_cache_change`'s prefix hash; the harness replays those too.
into `prefix_cache_change`'s prefix hash; the harness replays those too, and
`host_tools`: the optional tools whose backend the recording machine had
(`code_execution`, `js_execution`, `pandoc_convert`, `image_ocr`), which
change the registry the `tool_request_snapshot` counts.
Production always probes the host. The per-platform label
text is owned and tested in `sandbox::policy`. All current cases were
recorded on macOS. A Linux- or Windows-recorded golden is separate evidence.
@@ -9,7 +9,13 @@
"os": "macos",
"shell": "/bin/bash",
"sandbox_enforcement": "local_os",
"no_new_privs_active": null
"no_new_privs_active": null,
"host_tools": [
"code_execution",
"image_ocr",
"js_execution",
"pandoc_convert"
]
},
"workspace_files": {
"README.md": "conformance fixture\n"
@@ -9,7 +9,13 @@
"os": "macos",
"shell": "/bin/bash",
"sandbox_enforcement": "local_os",
"no_new_privs_active": null
"no_new_privs_active": null,
"host_tools": [
"code_execution",
"image_ocr",
"js_execution",
"pandoc_convert"
]
},
"workspace_files": {
"README.md": "conformance fixture\n",
@@ -9,7 +9,13 @@
"os": "macos",
"shell": "/bin/bash",
"sandbox_enforcement": "local_os",
"no_new_privs_active": null
"no_new_privs_active": null,
"host_tools": [
"code_execution",
"image_ocr",
"js_execution",
"pandoc_convert"
]
},
"workspace_files": {
"README.md": "conformance fixture\n",
@@ -9,7 +9,13 @@
"os": "macos",
"shell": "/bin/bash",
"sandbox_enforcement": "local_os",
"no_new_privs_active": null
"no_new_privs_active": null,
"host_tools": [
"code_execution",
"image_ocr",
"js_execution",
"pandoc_convert"
]
},
"workspace_files": {
"README.md": "conformance fixture\n",
@@ -9,7 +9,13 @@
"os": "macos",
"shell": "/bin/bash",
"sandbox_enforcement": "local_os",
"no_new_privs_active": null
"no_new_privs_active": null,
"host_tools": [
"code_execution",
"image_ocr",
"js_execution",
"pandoc_convert"
]
},
"workspace_files": {
"README.md": "conformance fixture\n",
@@ -9,7 +9,13 @@
"os": "macos",
"shell": "/bin/bash",
"sandbox_enforcement": "local_os",
"no_new_privs_active": null
"no_new_privs_active": null,
"host_tools": [
"code_execution",
"image_ocr",
"js_execution",
"pandoc_convert"
]
},
"workspace_files": {
"README.md": "conformance fixture\n",
@@ -9,7 +9,13 @@
"os": "macos",
"shell": "/bin/bash",
"sandbox_enforcement": "local_os",
"no_new_privs_active": null
"no_new_privs_active": null,
"host_tools": [
"code_execution",
"image_ocr",
"js_execution",
"pandoc_convert"
]
},
"user_message": "hello",
"workspace_files": {
@@ -9,7 +9,13 @@
"os": "macos",
"shell": "/bin/bash",
"sandbox_enforcement": "local_os",
"no_new_privs_active": null
"no_new_privs_active": null,
"host_tools": [
"code_execution",
"image_ocr",
"js_execution",
"pandoc_convert"
]
},
"user_message": "make a plan",
"workspace_files": {
@@ -9,7 +9,13 @@
"os": "macos",
"shell": "/bin/bash",
"sandbox_enforcement": "local_os",
"no_new_privs_active": null
"no_new_privs_active": null,
"host_tools": [
"code_execution",
"image_ocr",
"js_execution",
"pandoc_convert"
]
},
"trusted_workspace": true,
"user_message": "hello",