Read a mock sandbox's recordings from the driver double

MockSandbox forwarded a dozen read-backs to the driver's scripted doubles
one line each: the commands run, the term stops, the stdin fed, the files
written and deleted, the lifecycle counts, whether a walk ran. Tests now
ask the double through MockSandbox::driver. The accessors that convert a
recorded spec into the shape a test asserts on stay: the last command, the
timeouts in milliseconds, the caller's environment without the exec
policy's BASH_ENV blank, and written files as text.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-09-10 16:55:05 -06:00
parent 054a37f830
commit 20a5500510
No known key found for this signature in database
9 changed files with 42 additions and 104 deletions

View file

@ -1781,7 +1781,7 @@ diff --git a/src/live.rs b/src/live.rs
assert_eq!(body.meta.source, RunFilesMetaSource::Sandbox);
assert_eq!(body.meta.scope, RunFilesMetaScope::Uncommitted);
assert_eq!(body.data.len(), 1);
let commands = sandbox.captured_commands();
let commands = sandbox.driver().scripted_exec().commands();
assert_eq!(commands.len(), 2);
assert!(commands[0].contains(" show -s --format="));
assert!(commands[1].contains(" diff --patch --find-renames=50% HEAD"));

View file

@ -706,7 +706,15 @@ mod tests {
assert!(output.starts_with("Command timed out.\n"), "{output}");
assert_eq!(env.captured_timeout(), Some(7_000));
assert_eq!(env.captured_working_dirs(), vec![Some("/repo".to_string())]);
assert_eq!(
env.driver()
.scripted_exec()
.recorded()
.iter()
.map(|spec| spec.working_dir.clone())
.collect::<Vec<_>>(),
vec![Some("/repo".to_string())]
);
assert_eq!(env.captured_env_vars(), Some(tool_env));
assert_eq!(env.captured_command().as_deref(), Some("echo $TOKEN"));
}

View file

@ -1,9 +1,11 @@
//! Test doubles for fabro's sandbox layer.
//!
//! [`MockSandbox`] is a configuration and a recorder over the sandbox
//! driver's scripted double: a test writes down the files, the command
//! answer, and the failures it wants, takes a [`RunSandbox`] from it, and
//! reads back what the code under test ran or wrote. Nothing here fakes
//! [`MockSandbox`] is a configuration over the sandbox driver's scripted
//! double: a test writes down the files, the command answer, and the
//! failures it wants, and takes a [`RunSandbox`] from it. What the code
//! under test ran or wrote is read back from the driver double itself,
//! through [`MockSandbox::driver`]; the few accessors here convert what a
//! spec records into the shape fabro's tests assert on. Nothing here fakes
//! fabro's own logic; every call goes through the real `RunSandbox` and
//! fabro's exec policy, down to the scripted driver.
@ -171,13 +173,6 @@ impl MockSandbox {
self
}
/// Queues the result for the next command, ahead of `exec_result`.
/// Results answer in the order they were pushed.
pub fn push_exec_result(&self, result: ExecResult) -> &Self {
self.driver().scripted_exec().push_result(result);
self
}
fn built(&self) -> &Built {
self.built.get_or_init(|| {
let driver = Arc::new(self.build_driver());
@ -259,17 +254,12 @@ impl MockSandbox {
.unwrap_or_default()
}
/// The Bash source of every command run so far, in order.
pub fn captured_commands(&self) -> Vec<String> {
self.recorded()
.iter()
.map(|spec| spec.args.last().cloned().unwrap_or_default())
.collect()
}
/// The last command's Bash source.
/// The last command's Bash source. Every command, in order, is
/// `driver().scripted_exec().commands()`.
pub fn captured_command(&self) -> Option<String> {
self.captured_commands().pop()
self.recorded()
.last()
.and_then(|spec| spec.args.last().cloned())
}
/// The last command's timeout in milliseconds.
@ -289,23 +279,6 @@ impl MockSandbox {
.collect()
}
/// Whether each command was given the run's cancellation to stop on,
/// in order.
pub fn captured_term_stops(&self) -> Vec<bool> {
self.built
.get()
.map(|built| built.driver.scripted_exec().term_stops())
.unwrap_or_default()
}
/// The working directory of every command, in order.
pub fn captured_working_dirs(&self) -> Vec<Option<String>> {
self.recorded()
.iter()
.map(|spec| spec.working_dir.clone())
.collect()
}
/// The explicit variables of the last command as the caller passed them.
/// The exec policy's own `BASH_ENV` blank is not the caller's.
pub fn captured_env_vars(&self) -> Option<HashMap<String, String>> {
@ -318,13 +291,6 @@ impl MockSandbox {
})
}
/// The bytes the last streaming command was fed on standard input.
pub fn captured_stdin(&self) -> Option<Vec<u8>> {
self.built
.get()
.and_then(|built| built.driver.scripted_exec().captured_stdin().pop())
}
/// Every file written so far as `(path, content)`, in order.
pub fn written_files(&self) -> Vec<(String, String)> {
self.built
@ -340,46 +306,6 @@ impl MockSandbox {
})
.unwrap_or_default()
}
/// Every file deleted so far by absolute path, in order.
pub fn deleted_files(&self) -> Vec<String> {
self.built
.get()
.map(|built| built.driver.memory_fs().deletes())
.unwrap_or_default()
}
/// How many times the code under test asked whether a path exists.
pub fn exists_calls(&self) -> usize {
self.built
.get()
.map_or(0, |built| built.driver.memory_fs().exists_calls())
}
pub fn start_count(&self) -> u32 {
self.built
.get()
.map_or(0, |built| built.driver.start_count())
}
pub fn stop_count(&self) -> u32 {
self.built
.get()
.map_or(0, |built| built.driver.stop_count())
}
pub fn delete_count(&self) -> u32 {
self.built
.get()
.map_or(0, |built| built.driver.delete_count())
}
/// How many walks the code under test ran.
pub fn walk_files_was_called(&self) -> bool {
self.built
.get()
.is_some_and(|built| built.driver.scripted_search().walk_calls() > 0)
}
}
// --- MockStdioProcess ---

View file

@ -1239,7 +1239,7 @@ mod tests {
.unwrap();
assert_eq!(
env.exists_calls(),
env.driver().memory_fs().exists_calls(),
1,
"sandbox locality should be probed once per resolution pass"
);

View file

@ -1466,7 +1466,7 @@ mod tests {
assert_eq!(outcome.status, StageOutcome::Succeeded);
assert_eq!(
mock.captured_stdin(),
mock.driver().scripted_exec().captured_stdin().pop(),
Some(serde_json::to_vec(&parallel_results).unwrap())
);
assert!(
@ -1502,7 +1502,11 @@ mod tests {
assert_eq!(outcome.status, StageOutcome::Succeeded);
assert_eq!(
mock.captured_stdin().as_deref(),
mock.driver()
.scripted_exec()
.captured_stdin()
.pop()
.as_deref(),
Some(b"first\nlast".as_slice())
);
}
@ -1776,7 +1780,7 @@ mod tests {
.await
.unwrap();
assert_eq!(spy.captured_term_stops(), vec![true]);
assert_eq!(spy.driver().scripted_exec().term_stops(), vec![true]);
}
#[tokio::test]

View file

@ -942,10 +942,10 @@ async fn execute_reactivates_sandbox_after_a_stage_can_leave_it_stopped() {
.unwrap();
assert_eq!(outcome.status, StageOutcome::Succeeded);
assert_eq!(sandbox.stop_count(), 1);
assert!(sandbox.walk_files_was_called());
assert_eq!(sandbox.driver().stop_count(), 1);
assert!(sandbox.driver().scripted_search().walk_calls() > 0);
assert_eq!(
sandbox.start_count(),
sandbox.driver().start_count(),
1,
"the stopped sandbox is started again before the walk"
);

View file

@ -1644,8 +1644,8 @@ mod tests {
.await
.unwrap();
assert_eq!(sandbox.stop_count(), 1);
assert_eq!(sandbox.delete_count(), 0);
assert_eq!(sandbox.driver().stop_count(), 1);
assert_eq!(sandbox.driver().delete_count(), 0);
}
#[tokio::test]
@ -1678,8 +1678,8 @@ mod tests {
.await
.unwrap();
assert_eq!(sandbox.stop_count(), 0);
assert_eq!(sandbox.delete_count(), 0);
assert_eq!(sandbox.driver().stop_count(), 0);
assert_eq!(sandbox.driver().delete_count(), 0);
}
#[tokio::test]

View file

@ -1072,7 +1072,7 @@ mod tests {
.await
.expect("git identity should configure");
let commands = sandbox.captured_commands();
let commands = sandbox.driver().scripted_exec().commands();
assert_eq!(commands, vec![
"git config --local user.name 'Fabro Bot' && git config --local user.email \
fabro-bot@example.com"

View file

@ -820,7 +820,7 @@ mod tests {
fn scripted(exec_results: &[ExecResult]) -> MockSandbox {
let sandbox = MockSandbox::default();
for result in exec_results {
sandbox.push_exec_result(result.clone());
sandbox.driver().scripted_exec().push_result(result.clone());
}
sandbox
}
@ -993,10 +993,10 @@ mod tests {
);
assert_ne!(write_paths[0], write_paths[1]);
let delete_paths = sandbox.deleted_files();
let delete_paths = sandbox.driver().memory_fs().deletes();
assert_eq!(delete_paths, write_paths);
let commands = sandbox.captured_commands();
let commands = sandbox.driver().scripted_exec().commands();
let commit_commands = commands
.iter()
.filter(|command| command.contains(" commit "))
@ -1078,7 +1078,7 @@ mod tests {
.await
.expect("checkpoint should succeed");
let commands = sandbox.captured_commands();
let commands = sandbox.driver().scripted_exec().commands();
let commit_cmd = commands
.iter()
.find(|c| c.contains(" commit "))
@ -1105,7 +1105,7 @@ mod tests {
.await
.expect("checkpoint should succeed");
let commands = sandbox.captured_commands();
let commands = sandbox.driver().scripted_exec().commands();
let commit_cmd = commands
.iter()
.find(|c| c.contains(" commit "))