1use std::fs;
2use std::io::Write;
3use std::path::{Path, PathBuf};
4use std::time::{Instant, SystemTime, UNIX_EPOCH};
5
6use anyhow::{bail, Context, Result};
7use sha2::{Digest, Sha256};
8
9use super::condition::apply_condition;
10use super::dry_run::{effective_matrix, write_dry_run_json};
11use super::failure::{classify_failure_reason, output_indicates_compile_failure, FailureEvidence};
12use super::fixture::{load_fixture, selected_conditions, selected_tasks, validate_relative_path};
13use super::isolation::{prepare_codex_isolation, runner_isolation_violation};
14use super::process::{command_output, ensure_success, CommandOutcome};
15use super::run_plan::randomized_run_plan;
16use super::score::{
17 build_memory_attribution, parse_changed_paths, parse_codex_jsonl_usage, patch_pattern_failures,
18 summarize_runs, unauthorized_paths, update_memory_attribution_outcome,
19};
20use super::types::{
21 BenchCondition, BenchTokenUsage, CodingBenchFixture, CodingBenchOptions, CodingBenchReport,
22 CodingBenchTask, CommandReport, ConditionReport, RunArtifacts, RunReport, RunnerReport,
23};
24
25pub fn dry_run_plan(options: &CodingBenchOptions) -> Result<String> {
26 super::live_approval::validate_local_planning(options)?;
27 let fixture = load_fixture(&options.fixture_path)?;
28 let conditions = selected_conditions(options)?;
29 let tasks = selected_tasks(&fixture, options)?;
30 let total = conditions.len() * tasks.len() * options.runs_per_condition;
31 if !options.json_out.trim().is_empty() {
32 write_dry_run_json(options, &conditions, &tasks, total)?;
33 }
34 let mut output = String::new();
35 output.push_str("coding benchmark dry run\n");
36 output.push_str(&format!("fixture: {}\n", options.fixture_path));
37 output.push_str(&format!(
38 "runs_per_condition: {}\n",
39 options.runs_per_condition
40 ));
41 output.push_str(&format!("task_set: {}\n", options.task_set));
42 output.push_str(&format!("matrix: {}\n", effective_matrix(options)));
43 output.push_str(&format!(
44 "runner: {} model: {}\n",
45 options.runner, options.model
46 ));
47 output.push_str(&format!("planned_runs: {total}\n"));
48 for condition in &conditions {
49 for task in &tasks {
50 output.push_str(&format!(
51 "- {} {} x{}\n",
52 condition.as_str(),
53 task.id,
54 options.runs_per_condition
55 ));
56 }
57 }
58 Ok(output)
59}
60
61pub async fn run_coding_bench(options: &CodingBenchOptions) -> Result<CodingBenchReport> {
62 super::live_approval::enforce_execution_gate(options)?;
63 if options.runs_per_condition == 0 {
64 bail!("--runs-per-condition must be greater than zero");
65 }
66 let fixture = load_fixture(&options.fixture_path)?;
67 let conditions = selected_conditions(options)?;
68 let tasks = selected_tasks(&fixture, options)?;
69 ensure_selected_conditions_are_executable(&conditions)?;
70 super::preflight::validate_condition_inputs(options, &conditions, &tasks)?;
71 let run_plan = randomized_run_plan(&conditions, tasks.len(), options.runs_per_condition)?;
72 let fixture_sha256 = file_sha256(&options.fixture_path)?;
73 let generated_at_epoch = current_epoch();
74 let artifact_root = report_artifact_root(&options.json_out, generated_at_epoch)?;
75 let runner_version = runner_version(options);
76 let mut grouped_runs = conditions
77 .iter()
78 .map(|condition| (*condition, Vec::new()))
79 .collect::<Vec<_>>();
80
81 for entry in run_plan {
82 let task = tasks
83 .get(entry.task_index)
84 .context("coding benchmark run plan referenced missing task")?;
85 let run = run_one(
86 options,
87 &fixture,
88 entry.condition,
89 task,
90 entry.run_index,
91 &artifact_root,
92 )
93 .await?;
94 super::audit_contract::validate_run_context_audit(&run)?;
95 eprintln!(
96 "[coding-bench] {} {} run {}: resolved={} tokens={}",
97 entry.condition.as_str(),
98 task.id,
99 entry.run_index,
100 run.resolved,
101 run.usage.total_tokens
102 );
103 if let Some((_, runs)) = grouped_runs
104 .iter_mut()
105 .find(|(condition, _)| *condition == entry.condition)
106 {
107 runs.push(run);
108 } else {
109 bail!(
110 "coding benchmark run plan referenced unselected condition {}",
111 entry.condition.as_str()
112 );
113 }
114 }
115
116 let mut condition_reports = Vec::new();
117 for (condition, runs) in grouped_runs {
118 let summary = summarize_runs(&runs);
119 condition_reports.push(ConditionReport {
120 name: condition,
121 summary,
122 runs,
123 });
124 }
125
126 Ok(CodingBenchReport {
127 schema_version: 1,
128 generated_at_epoch,
129 fixture_path: options.fixture_path.clone(),
130 fixture_sha256,
131 remem_rev: current_git_rev(Path::new(".")).unwrap_or_else(|| "unknown".to_string()),
132 source_dirty: current_git_dirty(Path::new(".")),
133 command: report_command(options),
134 artifact_policy: "raw_artifacts_local_ignored".to_string(),
135 runner: RunnerReport {
136 provider: options
137 .provider
138 .clone()
139 .unwrap_or_else(|| options.runner.clone()),
140 model: options.model.clone(),
141 runner: options.runner.clone(),
142 version: runner_version,
143 },
144 runs_per_condition: options.runs_per_condition,
145 ignore_budget: options.ignore_budget,
146 conditions: condition_reports,
147 })
148}
149
150async fn run_one(
151 options: &CodingBenchOptions,
152 fixture: &CodingBenchFixture,
153 condition: BenchCondition,
154 task: &CodingBenchTask,
155 run_index: usize,
156 artifact_root: &Path,
157) -> Result<RunReport> {
158 let start = Instant::now();
159 let run_root = unique_temp_dir(condition, &task.id, run_index);
160 let repo_dir = run_root.join("repo");
161 let data_dir = run_root.join("remem-data");
162 let artifact_dir =
163 artifact_root.join(format!("{}-{}-{}", condition.as_str(), task.id, run_index));
164 fs::create_dir_all(&artifact_dir)
165 .with_context(|| format!("create artifact dir {}", artifact_dir.display()))?;
166 prepare_repo(fixture, &repo_dir)?;
167 let condition_input_root = match condition {
168 BenchCondition::CuratedFileBudgeted => options.curator_root.as_deref(),
169 BenchCondition::RememE2e => options.memory_config.as_deref(),
170 _ => None,
171 };
172 let setup = apply_condition(
173 condition,
174 fixture,
175 task,
176 &repo_dir,
177 &data_dir,
178 condition_input_root.map(Path::new),
179 )
180 .await?;
181 commit_condition_inputs(&repo_dir)?;
182 let prompt = build_prompt(task, setup.prompt_note.as_deref());
183
184 let runner_outcome = invoke_agent(
185 options,
186 &repo_dir,
187 &run_root,
188 &setup.env,
189 &prompt,
190 task.timeout_ms,
191 )
192 .context("invoke coding-agent runner")?;
193 let runner_stdout = write_artifact(&artifact_dir, "runner.stdout", &runner_outcome.stdout)?;
194 let runner_stderr = write_artifact(&artifact_dir, "runner.stderr", &runner_outcome.stderr)?;
195 let (usage, turns) = if options.runner == "codex" {
196 parse_codex_jsonl_usage(&runner_outcome.stdout)
197 } else {
198 (BenchTokenUsage::default(), None)
199 };
200
201 let status = command_output("git", ["status", "--porcelain"], &repo_dir, &[], 30_000)?;
202 let changed_paths = parse_changed_paths(&status.stdout);
203 let unauthorized =
204 unauthorized_paths(&changed_paths, &task.allowed_paths, &task.forbidden_paths);
205 let diff = command_output("git", ["diff", "--binary"], &repo_dir, &[], 30_000)?;
206 let final_diff = write_artifact(&artifact_dir, "final.diff", &diff.stdout)?;
207 write_hidden_files(task, &repo_dir)?;
208
209 let mut score_commands = Vec::new();
210 let patch_pattern_failures = patch_pattern_failures(
211 &diff.stdout,
212 &task.score.required_patch_patterns,
213 &task.score.forbidden_patch_patterns,
214 );
215 let mut score_failed = !patch_pattern_failures.is_empty();
216 let forbidden_patch_failed = patch_pattern_failures
217 .iter()
218 .any(|failure| failure.starts_with("forbidden patch pattern"));
219 let mut compile_failed = false;
220 for (index, command) in task.score.commands.iter().enumerate() {
221 let (program, args) = command
222 .split_first()
223 .context("score command unexpectedly empty after validation")?;
224 let outcome = command_output(
225 program,
226 args.iter().map(String::as_str),
227 &repo_dir,
228 &[],
229 120_000,
230 )
231 .with_context(|| format!("run score command {:?}", command))?;
232 if outcome.exit_code != Some(0) || outcome.timed_out {
233 score_failed = true;
234 compile_failed |= output_indicates_compile_failure(&outcome.stdout, &outcome.stderr);
235 }
236 let stdout_artifact = write_artifact(
237 &artifact_dir,
238 &format!("score-{index}.stdout"),
239 &outcome.stdout,
240 )?;
241 let stderr_artifact = write_artifact(
242 &artifact_dir,
243 &format!("score-{index}.stderr"),
244 &outcome.stderr,
245 )?;
246 score_commands.push(CommandReport {
247 command: command.clone(),
248 exit_code: outcome.exit_code,
249 timed_out: outcome.timed_out,
250 stdout_artifact,
251 stderr_artifact,
252 });
253 }
254
255 let mut memory_contract = condition
256 .uses_remem_attribution()
257 .then(|| build_memory_attribution(&setup.memory_attribution, &runner_outcome.stdout));
258 let final_head_sha = current_git_rev(&repo_dir);
259 let failure_reason = classify_failure_reason(FailureEvidence {
260 score_failed,
261 compile_failed,
262 forbidden_patch_failed,
263 unauthorized_paths: &unauthorized,
264 memory_contract: memory_contract.as_ref(),
265 memory_input: &setup.memory_attribution,
266 runner_isolation_violation: runner_isolation_violation(
267 &runner_outcome.stdout,
268 &runner_outcome.stderr,
269 )
270 .as_deref(),
271 runner_timed_out: runner_outcome.timed_out,
272 runner_exit_code: runner_outcome.exit_code,
273 runner_stdout: &runner_outcome.stdout,
274 runner_stderr: &runner_outcome.stderr,
275 });
276 let resolved = failure_reason.is_none();
277 if let Some(attribution) = &mut memory_contract {
278 update_memory_attribution_outcome(attribution, resolved, failure_reason);
279 }
280 if !options.keep_workdirs {
281 let _ = fs::remove_dir_all(&run_root);
282 }
283
284 Ok(RunReport {
285 condition,
286 task_id: task.id.clone(),
287 run_index,
288 resolved,
289 failure_reason,
290 usage,
291 turns,
292 wall_time_ms: start.elapsed().as_millis(),
293 final_head_sha,
294 changed_paths,
295 unauthorized_path_changes: unauthorized,
296 runner_exit_code: runner_outcome.exit_code,
297 runner_timed_out: runner_outcome.timed_out,
298 runtime_contract_failure: setup.context_audit_status
299 == super::RememContextAuditStatus::ContractFailure,
300 runtime_contract_failure_reason: setup.context_audit_failure_reason.clone(),
301 context_audit_status: setup.context_audit_status,
302 context_audit_failure_reason: setup.context_audit_failure_reason,
303 remem_context_audit: setup.remem_context_audit,
304 curator_log: setup.curator_log,
305 e2e_pipeline: setup.e2e_pipeline,
306 score_commands,
307 memory_contract,
308 artifacts: RunArtifacts {
309 runner_stdout,
310 runner_stderr,
311 final_diff,
312 },
313 workdir: options
314 .keep_workdirs
315 .then(|| run_root.to_string_lossy().to_string()),
316 })
317}
318
319fn prepare_repo(fixture: &CodingBenchFixture, repo_dir: &Path) -> Result<()> {
320 fs::create_dir_all(repo_dir)
321 .with_context(|| format!("create repo dir {}", repo_dir.display()))?;
322 for (path, content) in &fixture.repo.files {
323 write_relative_file(repo_dir, path, content)?;
324 }
325 let init = command_output("git", ["init", "-b", "main"], repo_dir, &[], 30_000)?;
326 if init.exit_code != Some(0) {
327 let fallback = command_output("git", ["init"], repo_dir, &[], 30_000)?;
328 ensure_success("git init", &fallback)?;
329 ensure_success(
330 "git checkout -b main",
331 &command_output("git", ["checkout", "-b", "main"], repo_dir, &[], 30_000)?,
332 )?;
333 }
334 ensure_success(
335 "git config user.email",
336 &command_output(
337 "git",
338 ["config", "user.email", "coding-bench@example.invalid"],
339 repo_dir,
340 &[],
341 30_000,
342 )?,
343 )?;
344 ensure_success(
345 "git config user.name",
346 &command_output(
347 "git",
348 ["config", "user.name", "remem coding bench"],
349 repo_dir,
350 &[],
351 30_000,
352 )?,
353 )?;
354 ensure_success(
355 "git add",
356 &command_output("git", ["add", "."], repo_dir, &[], 30_000)?,
357 )?;
358 ensure_success(
359 "git commit",
360 &command_output(
361 "git",
362 ["commit", "-m", "initial fixture"],
363 repo_dir,
364 &[],
365 30_000,
366 )?,
367 )?;
368 Ok(())
369}
370
371fn commit_condition_inputs(repo_dir: &Path) -> Result<()> {
372 let status = command_output("git", ["status", "--porcelain"], repo_dir, &[], 30_000)?;
373 if status.stdout.trim().is_empty() {
374 return Ok(());
375 }
376 ensure_success(
377 "git add condition inputs",
378 &command_output("git", ["add", "."], repo_dir, &[], 30_000)?,
379 )?;
380 ensure_success(
381 "git commit condition inputs",
382 &command_output(
383 "git",
384 ["commit", "-m", "condition inputs"],
385 repo_dir,
386 &[],
387 30_000,
388 )?,
389 )?;
390 Ok(())
391}
392
393fn invoke_agent(
394 options: &CodingBenchOptions,
395 repo_dir: &Path,
396 run_root: &Path,
397 env: &[(String, String)],
398 prompt: &str,
399 timeout_ms: u64,
400) -> Result<CommandOutcome> {
401 match options.runner.as_str() {
402 "codex" => {
403 let isolation = prepare_codex_isolation(run_root, &options.codex_bin)?;
404 let mut runner_env = env.to_vec();
405 runner_env.extend(isolation.env.clone());
406 let args = build_codex_exec_args(options, repo_dir, prompt);
407 let mut wrapped_args = isolation.args_prefix.clone();
408 wrapped_args.extend(args);
409 let outcome = command_output(
410 &isolation.program,
411 wrapped_args.iter().map(String::as_str),
412 repo_dir,
413 &runner_env,
414 timeout_ms,
415 );
416 isolation.cleanup();
417 outcome
418 }
419 "noop" => Ok(CommandOutcome {
420 stdout: String::new(),
421 stderr: String::new(),
422 exit_code: Some(0),
423 timed_out: false,
424 }),
425 other => bail!("unsupported coding benchmark runner: {other}"),
426 }
427}
428
429fn build_codex_exec_args(
430 options: &CodingBenchOptions,
431 repo_dir: &Path,
432 prompt: &str,
433) -> Vec<String> {
434 let mut args = vec![
435 "exec".to_string(),
436 "--json".to_string(),
437 "--color".to_string(),
438 "never".to_string(),
439 "--ignore-user-config".to_string(),
440 "--ignore-rules".to_string(),
441 "--ephemeral".to_string(),
442 "--disable".to_string(),
443 "hooks".to_string(),
444 "--cd".to_string(),
445 repo_dir.to_string_lossy().to_string(),
446 "--model".to_string(),
447 options.model.clone(),
448 "--sandbox".to_string(),
449 "danger-full-access".to_string(),
450 ];
451 if !options.reasoning_effort.trim().is_empty() {
452 args.push("-c".to_string());
453 args.push(format!(
454 "model_reasoning_effort=\"{}\"",
455 toml_escape(&options.reasoning_effort)
456 ));
457 }
458 if let Some(provider) = options.provider.as_deref() {
459 args.push("-c".to_string());
460 args.push(format!("model_provider=\"{}\"", toml_escape(provider)));
461 }
462 args.push(prompt.to_string());
463 args
464}
465
466fn build_prompt(task: &CodingBenchTask, condition_note: Option<&str>) -> String {
467 let mut prompt = String::new();
468 prompt.push_str("You are running an isolated coding benchmark task.\n");
469 if let Some(note) = condition_note {
470 prompt.push_str(note);
471 prompt.push('\n');
472 }
473 prompt.push_str("Only inspect files under the repository root and context files explicitly named above. Do not inspect environment variables, parent directories, CODEX_HOME, HOME, tool caches, benchmark harness artifacts, or hidden tests.\n");
474 prompt.push_str("Modify the repository to satisfy the task. Do not inspect or depend on hidden tests. Keep edits scoped to the task.\n\n");
475 prompt.push_str("Task:\n");
476 prompt.push_str(&task.prompt);
477 prompt.push('\n');
478 prompt
479}
480
481fn ensure_selected_conditions_are_executable(conditions: &[BenchCondition]) -> Result<()> {
482 let unsupported = conditions
483 .iter()
484 .copied()
485 .filter(|condition| !condition.supports_live_execution())
486 .map(BenchCondition::as_str)
487 .collect::<Vec<_>>();
488 if unsupported.is_empty() {
489 return Ok(());
490 }
491 bail!(
492 "coding benchmark live execution is not implemented for {}; use --dry-run for GH931 primary planning or select remem_seeded_sessionstart/curated_file_expert for implemented diagnostics",
493 unsupported.join(", ")
494 )
495}
496
497fn write_hidden_files(task: &CodingBenchTask, repo_dir: &Path) -> Result<()> {
498 for (path, content) in &task.score.hidden_files {
499 write_relative_file(repo_dir, path, content)?;
500 }
501 Ok(())
502}
503
504fn write_relative_file(root: &Path, relative: &str, content: &str) -> Result<()> {
505 validate_relative_path(relative)?;
506 let path = root.join(relative);
507 if let Some(parent) = path.parent() {
508 fs::create_dir_all(parent)
509 .with_context(|| format!("create parent directory {}", parent.display()))?;
510 }
511 fs::write(&path, content).with_context(|| format!("write {}", path.display()))
512}
513
514fn write_artifact(dir: &Path, name: &str, content: &str) -> Result<String> {
515 let path = dir.join(name);
516 let mut file =
517 fs::File::create(&path).with_context(|| format!("create artifact {}", path.display()))?;
518 file.write_all(content.as_bytes())
519 .with_context(|| format!("write artifact {}", path.display()))?;
520 Ok(path.to_string_lossy().to_string())
521}
522
523fn report_artifact_root(json_out: &str, epoch: i64) -> Result<PathBuf> {
524 let parent = Path::new(json_out)
525 .parent()
526 .filter(|path| !path.as_os_str().is_empty())
527 .unwrap_or_else(|| Path::new("."));
528 let path = parent.join("artifacts").join(epoch.to_string());
529 fs::create_dir_all(&path)
530 .with_context(|| format!("create artifact root {}", path.display()))?;
531 Ok(path)
532}
533
534fn unique_temp_dir(condition: BenchCondition, task_id: &str, run_index: usize) -> PathBuf {
535 std::env::temp_dir().join(format!(
536 "remem-coding-bench-{}-{}-{}-{}-{}",
537 condition.as_str(),
538 task_id,
539 run_index,
540 std::process::id(),
541 SystemTime::now()
542 .duration_since(UNIX_EPOCH)
543 .map(|duration| duration.as_nanos())
544 .unwrap_or(0)
545 ))
546}
547
548fn current_epoch() -> i64 {
549 SystemTime::now()
550 .duration_since(UNIX_EPOCH)
551 .map(|duration| duration.as_secs() as i64)
552 .unwrap_or(0)
553}
554
555fn current_git_rev(cwd: &Path) -> Option<String> {
556 let outcome = command_output("git", ["rev-parse", "HEAD"], cwd, &[], 30_000).ok()?;
557 (outcome.exit_code == Some(0)).then(|| outcome.stdout.trim().to_string())
558}
559
560fn runner_version(options: &CodingBenchOptions) -> Option<String> {
561 if options.runner != "codex" {
562 return None;
563 }
564 let outcome = command_output(
565 &options.codex_bin,
566 ["--version"],
567 Path::new("."),
568 &[],
569 30_000,
570 )
571 .ok()?;
572 (outcome.exit_code == Some(0)).then(|| outcome.stdout.trim().to_string())
573}
574
575fn file_sha256(path: &str) -> Result<String> {
576 let bytes = fs::read(path).with_context(|| format!("read fixture for sha256 {path}"))?;
577 let mut hasher = Sha256::new();
578 hasher.update(bytes);
579 Ok(format!("{:x}", hasher.finalize()))
580}
581
582fn current_git_dirty(cwd: &Path) -> Option<bool> {
583 let outcome = command_output("git", ["status", "--porcelain"], cwd, &[], 30_000).ok()?;
584 (outcome.exit_code == Some(0)).then(|| !outcome.stdout.trim().is_empty())
585}
586
587fn report_command(options: &CodingBenchOptions) -> Vec<String> {
588 let mut command = vec![
589 "remem".to_string(),
590 "eval-coding-bench".to_string(),
591 "--fixture".to_string(),
592 options.fixture_path.clone(),
593 "--runs-per-condition".to_string(),
594 options.runs_per_condition.to_string(),
595 "--matrix".to_string(),
596 effective_matrix(options).to_string(),
597 "--task-set".to_string(),
598 options.task_set.clone(),
599 "--runner".to_string(),
600 options.runner.clone(),
601 "--model".to_string(),
602 options.model.clone(),
603 "--reasoning-effort".to_string(),
604 options.reasoning_effort.clone(),
605 "--json-out".to_string(),
606 options.json_out.clone(),
607 ];
608 if options.codex_bin != "codex" {
609 command.push("--codex-bin".to_string());
610 command.push(options.codex_bin.clone());
611 }
612 if let Some(provider) = &options.provider {
613 command.push("--provider".to_string());
614 command.push(provider.clone());
615 }
616 if let Some(condition) = &options.condition {
617 command.push("--condition".to_string());
618 command.push(condition.clone());
619 }
620 if let Some(task) = &options.task {
621 command.push("--task".to_string());
622 command.push(task.clone());
623 }
624 if options.ignore_budget {
625 command.push("--ignore-budget".to_string());
626 }
627 if options.keep_workdirs {
628 command.push("--keep-workdirs".to_string());
629 }
630 command
631}
632
633fn toml_escape(value: &str) -> String {
634 value.replace('\\', "\\\\").replace('"', "\\\"")
635}
636
637#[cfg(test)]
638mod tests {
639 use super::*;
640 use std::time::Duration;
641
642 #[test]
643 fn codex_runner_ignores_host_config_rules_hooks_and_session_files() {
644 let options = CodingBenchOptions {
645 fixture_path: "eval/coding-bench/fixtures/tasks.json".to_string(),
646 runs_per_condition: 1,
647 json_out: "/tmp/remem-coding-bench.json".to_string(),
648 condition: None,
649 matrix: "primary".to_string(),
650 task: None,
651 task_set: "full".to_string(),
652 keep_workdirs: false,
653 dry_run: false,
654 runner: "codex".to_string(),
655 codex_bin: "codex".to_string(),
656 model: "gpt-5.5".to_string(),
657 provider: Some("codexapi".to_string()),
658 reasoning_effort: "medium".to_string(),
659 ignore_budget: true,
660 curator_root: None,
661 memory_config: None,
662 run_phase: "local".to_string(),
663 matrix_namespace: "local".to_string(),
664 verify_live_approval_only: false,
665 live_approval: None,
666 approval_trust_root: None,
667 supervisor_attestation: None,
668 supervisor_bin: None,
669 };
670 let args = build_codex_exec_args(&options, Path::new("/tmp/remem-bench-repo"), "prompt");
671
672 assert!(args.contains(&"--ignore-user-config".to_string()));
673 assert!(args.contains(&"--ignore-rules".to_string()));
674 assert!(args.contains(&"--ephemeral".to_string()));
675 assert!(args
676 .windows(2)
677 .any(|window| window == ["--disable", "hooks"]));
678 assert!(args
679 .windows(2)
680 .any(|window| window == ["--sandbox", "danger-full-access"]));
681 assert!(!args.contains(&"--dangerously-bypass-approvals-and-sandbox".to_string()));
682 assert!(!args.contains(&"--dangerously-bypass-hook-trust".to_string()));
683 }
684
685 #[cfg(unix)]
686 #[test]
687 fn command_output_timeout_terminates_process_group_children() -> Result<()> {
688 let start = Instant::now();
689 let outcome = command_output("sh", ["-c", "sleep 10 & wait"], Path::new("."), &[], 100)?;
690 assert!(outcome.timed_out);
691 assert!(
692 start.elapsed() < Duration::from_secs(3),
693 "timeout should not wait for a grandchild sleep to exit"
694 );
695 Ok(())
696 }
697}