diff --git a/src/dynamic/runner.rs b/src/dynamic/runner.rs index faa51a33..2a5f3a4c 100644 --- a/src/dynamic/runner.rs +++ b/src/dynamic/runner.rs @@ -124,6 +124,24 @@ impl From for RunError { } } +/// Detect the conventional harness import-error signal: exit code 77 plus +/// the `NYX_IMPORT_ERROR:` marker on stderr. Per-lang harness preambles in +/// `src/dynamic/lang/{js_shared,ruby,php}.rs` emit this when the fixture's +/// top-level `require` / `import` / `use` fails at runtime (missing npm, +/// gem, or composer dep; unparseable syntax). Treated as a build failure +/// upstream so the SKIP-on-`BuildFailed` branch in e2e corpus tests catches +/// missing host deps instead of failing the assertion. +fn is_runtime_import_error(outcome: &sandbox::SandboxOutcome) -> bool { + if outcome.exit_code != Some(77) { + return false; + } + let needle = b"NYX_IMPORT_ERROR:"; + outcome + .stderr + .windows(needle.len()) + .any(|w| w == needle) +} + /// Build harness (with retry), run every payload, stop at first confirmed trigger. /// /// "Confirmed trigger" = `oracle_fired && sink_hit` (§4.1). @@ -427,6 +445,23 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result, stderr: &[u8]) -> sandbox::SandboxOutcome { + sandbox::SandboxOutcome { + exit_code, + stdout: Vec::new(), + stderr: stderr.to_vec(), + timed_out: false, + oob_callback_seen: false, + sink_hit: false, + duration: std::time::Duration::ZERO, + hardening_outcome: None, + } + } + + #[test] + fn import_error_detects_exit_77_with_marker() { + let outcome = outcome_with(Some(77), b"NYX_IMPORT_ERROR: Cannot find module 'express'\n"); + assert!(is_runtime_import_error(&outcome)); + } + + #[test] + fn import_error_ignores_clean_exit() { + let outcome = outcome_with(Some(0), b"NYX_IMPORT_ERROR: bogus\n"); + assert!(!is_runtime_import_error(&outcome)); + } + + #[test] + fn import_error_ignores_other_nonzero_exits() { + let outcome = outcome_with(Some(1), b"some other crash\n"); + assert!(!is_runtime_import_error(&outcome)); + } + + #[test] + fn import_error_ignores_exit_77_without_marker() { + let outcome = outcome_with(Some(77), b"crash but no marker\n"); + assert!(!is_runtime_import_error(&outcome)); + } + + #[test] + fn import_error_ignores_signal_no_exit_code() { + let outcome = outcome_with(None, b"NYX_IMPORT_ERROR: spurious\n"); + assert!(!is_runtime_import_error(&outcome)); + } + + #[test] + fn import_error_matches_marker_embedded_in_other_stderr() { + let outcome = outcome_with( + Some(77), + b"some preamble\nNYX_IMPORT_ERROR: real failure\nmore noise\n", + ); + assert!(is_runtime_import_error(&outcome)); + } }