mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 08:04:25 -10:00
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:
1 parent
171d061eda
commit
8b5a86eed6
14 files changed
+135
-14
No files matched your search
@@ -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(
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
+7
-1
@@ -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": {
|
||||
|
||||
+7
-1
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user