Skip to main content

supercov_engine/
javascript_run.rs

1//! Rust-owned JavaScript execution for the public Supercov engine.
2
3use std::{
4    collections::BTreeMap,
5    ffi::OsString,
6    path::{Path, PathBuf},
7    time::{Instant, SystemTime, UNIX_EPOCH},
8};
9
10use serde::{Deserialize, Serialize};
11use supercov_contracts::{
12    AttributionPrecision, ExecutionModel, FrontendAttribution, FrontendLimitation,
13    FrontendLimitationScope, FrontendRunDeclaration, FrontendRunnerDeclaration,
14    LANGUAGE_FRONTEND_PROTOCOL_VERSION, StructuralSource,
15};
16
17use crate::{
18    coverage_report::{PersistedCoverageModel, RawTestResult, javascript_coverage_model},
19    evidence_archive::{
20        EvidenceArchiveEntry, EvidenceArchiveSource, collect_sources, write_archive,
21    },
22    integrity::{FrontendIntegrityInputs, create_run_integrity},
23    javascript_frontend::{
24        javascript_frontend_reuse_paths, load_cached_javascript_frontend,
25        prepare_javascript_frontend, read_javascript_frontend_cache,
26    },
27    lifecycle::{
28        ProjectLock, RunState, RunStateStatus, finalize_published_run, interrupt_run_state,
29        publish_run, recover_abandoned_runs, remove_stored_tree_deferred, update_run_state,
30        write_run_state,
31    },
32    orchestration::{
33        ExecutionPhase, ExecutionPlan, OrchestrationError, PhaseKind, execute_plan_with_supervisor,
34    },
35    process_supervision::{
36        CommandSpec, ForwardedSignal, ProcessSupervisor, SupervisionOptions, positive_milliseconds,
37    },
38    project_discovery::discover_coverage_project,
39    run_store::{RawEvidenceMetadata, RunIntegrity, RunMetadata, RunTimings},
40    workspace::{
41        cached_workspace_path, prepare_cached_workspace, prune_cached_workspace_sources,
42        sync_command_outputs, workspace_output_baseline,
43    },
44};
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase", deny_unknown_fields)]
48pub struct DirectJavascriptRunRequest {
49    pub root: PathBuf,
50    pub command: Vec<String>,
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub run_id: Option<String>,
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub started_at: Option<String>,
55    #[serde(skip)]
56    pub watchdog_program: Option<PathBuf>,
57}
58
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase", deny_unknown_fields)]
61pub struct DirectJavascriptRunResult {
62    pub run_id: String,
63    pub run_directory: PathBuf,
64    pub workspace: PathBuf,
65    pub exit_code: i32,
66    pub assertion_calls: usize,
67    pub recovered_runs: Vec<String>,
68    pub metadata: RunMetadata,
69    /// The runs of servers the suite drove, published beside this one, by
70    /// language.
71    pub server_runs: Vec<(String, PathBuf)>,
72    /// The project's build script, when the command did not run it. The copy
73    /// starts without build output, so a suite that needs it fails; the
74    /// caller says so when the failure is not a test's.
75    #[serde(skip)]
76    pub unrun_build: Vec<String>,
77}
78
79#[derive(Debug)]
80pub enum DirectJavascriptRunError {
81    Interrupted {
82        signal: ForwardedSignal,
83        exit_code: i32,
84        timings: RunTimings,
85        total_ms: f64,
86    },
87    Failed(String),
88}
89
90impl std::fmt::Display for DirectJavascriptRunError {
91    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        match self {
93            Self::Interrupted { signal, .. } => {
94                write!(formatter, "interrupted by {}", signal_name(*signal))
95            }
96            Self::Failed(message) => formatter.write_str(message),
97        }
98    }
99}
100
101impl std::error::Error for DirectJavascriptRunError {}
102
103impl From<String> for DirectJavascriptRunError {
104    fn from(value: String) -> Self {
105        Self::Failed(value)
106    }
107}
108
109fn signal_name(signal: ForwardedSignal) -> &'static str {
110    match signal {
111        ForwardedSignal::Sighup => "SIGHUP",
112        ForwardedSignal::Sigint => "SIGINT",
113        ForwardedSignal::Sigterm => "SIGTERM",
114    }
115}
116
117fn javascript_runner_declaration(
118    runner: String,
119    results: &[&RawTestResult],
120) -> FrontendRunnerDeclaration {
121    let has_observations = |raw: &&RawTestResult| {
122        !raw.phases.is_empty()
123            || !raw.server.is_empty()
124            || raw.runtime.iter().chain(&raw.browser).any(|snapshot| {
125                !snapshot.decisions.is_empty()
126                    || !snapshot.hits.is_empty()
127                    || !snapshot.events.is_empty()
128            })
129    };
130    let exact_test = results.iter().all(|raw| {
131        raw.test_id
132            .as_deref()
133            .is_some_and(|value| !value.is_empty())
134            && raw
135                .scope
136                .as_ref()
137                .is_none_or(|scope| raw.test_id.as_deref() == Some(scope.test_id.as_str()))
138    });
139    let exact_worker = results.iter().all(|raw| {
140        !has_observations(raw)
141            || raw
142                .scope
143                .as_ref()
144                .is_some_and(|scope| !scope.worker_id.is_empty())
145    });
146    let exact_retry = results.iter().all(|raw| {
147        raw.retry.is_some()
148            && raw
149                .scope
150                .as_ref()
151                .is_none_or(|scope| raw.retry == Some(scope.retry))
152    });
153    let contextual = !results.is_empty() && exact_test && exact_worker && exact_retry;
154    let precision = |exact| {
155        if exact {
156            AttributionPrecision::Exact
157        } else {
158            AttributionPrecision::Unavailable
159        }
160    };
161    let attribution = FrontendAttribution {
162        run: AttributionPrecision::Exact,
163        worker: precision(contextual),
164        test: precision(contextual),
165        retry: precision(contextual),
166        phase: precision(contextual),
167        action: precision(contextual),
168        assertion: precision(contextual),
169    };
170    let mut limitations = Vec::new();
171    if !contextual {
172        for (suffix, scope) in [
173            ("worker", FrontendLimitationScope::Worker),
174            ("test", FrontendLimitationScope::Test),
175            ("retry", FrontendLimitationScope::Retry),
176            ("phase", FrontendLimitationScope::Phase),
177            ("action", FrontendLimitationScope::Action),
178            ("assertion", FrontendLimitationScope::Assertion),
179        ] {
180            limitations.push(FrontendLimitation {
181                id: format!("{runner}-no-{suffix}").replace(':', "-"),
182                scopes: vec![scope],
183                reason: format!(
184                    "Runner {runner} did not expose exact {suffix} identity for every result"
185                ),
186            });
187        }
188    }
189    FrontendRunnerDeclaration {
190        runner,
191        execution_model: if contextual {
192            ExecutionModel::ParallelContextPropagated
193        } else {
194            ExecutionModel::ParallelUnattributed
195        },
196        attribution,
197        limitations,
198    }
199}
200
201fn javascript_archive_entries(
202    mut entries: Vec<EvidenceArchiveEntry>,
203    manifest: &crate::javascript_frontend::JavascriptManifest,
204    run_id: &str,
205    exit_code: i32,
206) -> Result<Vec<EvidenceArchiveEntry>, String> {
207    let mut results = Vec::new();
208    for entry in &entries {
209        if entry.path == "mcdc.json" || entry.path.ends_with("/mcdc.json") {
210            results.push(
211                serde_json::from_slice::<RawTestResult>(&entry.contents)
212                    .map_err(|error| format!("invalid {}: {error}", entry.path))?,
213            );
214        } else if entry.path == "mcdc.jsonl" || entry.path.ends_with(".mcdc.jsonl") {
215            let contents = entry
216                .contents
217                .strip_suffix(b"\n")
218                .ok_or_else(|| format!("{} does not end with a newline", entry.path))?;
219            for (index, line) in contents.split(|byte| *byte == b'\n').enumerate() {
220                if line.is_empty() {
221                    return Err(format!(
222                        "{} contains a blank record at line {}",
223                        entry.path,
224                        index + 1
225                    ));
226                }
227                results.push(
228                    serde_json::from_slice::<RawTestResult>(line).map_err(|error| {
229                        format!(
230                            "invalid {} record at line {}: {error}",
231                            entry.path,
232                            index + 1
233                        )
234                    })?,
235                );
236            }
237        }
238    }
239    if results.is_empty() {
240        let status = match exit_code {
241            0 => "passed",
242            supercov_contracts::COMMAND_TIMEOUT_EXIT_CODE => "timedOut",
243            _ => "failed",
244        };
245        let command_result = RawTestResult {
246            span: None,
247            test_id: Some(format!("command:{run_id}")),
248            scope: None,
249            test: "Test command".into(),
250            test_file: None,
251            title: Some("Test command".into()),
252            retry: Some(0),
253            status: Some(status.into()),
254            expected_status: None,
255            flaky: false,
256            provenance: crate::coverage_report::TestProvenance {
257                runner: "command".into(),
258                kind: "setup".into(),
259                project: None,
260                source: "engine".into(),
261                tags: Vec::new(),
262            },
263            role: "setup".into(),
264            attribution: crate::coverage_report::ATTRIBUTION_EXACT.into(),
265            phases: vec![],
266            runtime: vec![],
267            browser: vec![],
268            server: vec![],
269        };
270        entries.push(EvidenceArchiveEntry {
271            path: "results/command/mcdc.json".into(),
272            contents: serde_json::to_vec(&command_result).map_err(|error| error.to_string())?,
273        });
274        results.push(command_result);
275    }
276    let mut by_runner = BTreeMap::<String, Vec<&RawTestResult>>::new();
277    for result in &results {
278        by_runner
279            .entry(result.provenance.runner.clone())
280            .or_default()
281            .push(result);
282    }
283    if entries
284        .iter()
285        .any(|entry| entry.path.starts_with("server/background/") && entry.path.ends_with(".jsonl"))
286    {
287        by_runner.entry("background".into()).or_default();
288    }
289    let declaration = FrontendRunDeclaration {
290        protocol_version: LANGUAGE_FRONTEND_PROTOCOL_VERSION,
291        frontend_id: "javascript".into(),
292        frontend_version: "javascript-owned-v1".into(),
293        language: "javascript".into(),
294        structural_source: StructuralSource::OwnedProbes,
295        runners: by_runner
296            .into_iter()
297            .map(|(runner, results)| javascript_runner_declaration(runner, &results))
298            .collect(),
299        structural_limitations: manifest
300            .limitations
301            .iter()
302            .map(|limitation| limitation.id.clone())
303            .collect(),
304    };
305    entries.push(EvidenceArchiveEntry {
306        path: "coverage-model.json".into(),
307        contents: serde_json::to_vec(
308            &PersistedCoverageModel::from_declaration(&javascript_coverage_model())
309                .expect("JavaScript coverage model is contract-valid"),
310        )
311        .map_err(|error| error.to_string())?,
312    });
313    entries.push(EvidenceArchiveEntry {
314        path: "frontend.json".into(),
315        contents: serde_json::to_vec(&declaration).map_err(|error| error.to_string())?,
316    });
317    Ok(entries)
318}
319
320struct RunCleanup {
321    root: PathBuf,
322    run_id: String,
323    started_at: String,
324    lock: ProjectLock,
325    workspace: Option<PathBuf>,
326    state_written: bool,
327    terminal_recorded: bool,
328    /// Leave the instrumented copy in place: its build failed, and what the
329    /// build read is the only evidence of why.
330    keep_workspace: bool,
331}
332
333impl RunCleanup {
334    fn lock(&self) -> &ProjectLock {
335        &self.lock
336    }
337
338    fn set_workspace(&mut self, workspace: PathBuf) {
339        self.workspace = Some(workspace);
340    }
341
342    fn mark_state_written(&mut self) {
343        self.state_written = true;
344    }
345
346    fn mark_terminal(&mut self) {
347        self.terminal_recorded = true;
348    }
349}
350
351impl Drop for RunCleanup {
352    fn drop(&mut self) {
353        if self.state_written && !self.terminal_recorded {
354            let _ = update_run_state(
355                &self.root,
356                &self.run_id,
357                RunStateStatus::Failed,
358                &self.started_at,
359                Some("Rust run exited before reaching a terminal lifecycle state".into()),
360            );
361        }
362        if let Some(workspace) = &self.workspace {
363            let _ = remove_stored_tree_deferred(
364                &self.root,
365                &workspace.join(".supercov/evidence").join(&self.run_id),
366            );
367            let _ = remove_stored_tree_deferred(
368                &self.root,
369                &workspace
370                    .join(".supercov/server-evidence")
371                    .join(&self.run_id),
372            );
373            let keep_workspace = self.keep_workspace
374                || std::env::var("SUPERCOV_KEEP_WORKSPACE").is_ok_and(|value| !value.is_empty());
375            if !keep_workspace {
376                let _ = prune_cached_workspace_sources(&self.root, &self.lock);
377            }
378        }
379        let _ = self.lock.release();
380    }
381}
382
383fn now_nonce() -> u128 {
384    SystemTime::now()
385        .duration_since(UNIX_EPOCH)
386        .unwrap_or_default()
387        .as_millis()
388}
389
390fn elapsed_ms(started: Instant) -> f64 {
391    started.elapsed().as_secs_f64() * 1000.0
392}
393
394fn rounded_millisecond(value: f64) -> f64 {
395    (value * 10.0).round() / 10.0
396}
397
398fn supervision_options() -> Result<SupervisionOptions, String> {
399    let defaults = SupervisionOptions::default();
400    Ok(SupervisionOptions {
401        diagnostic_interval: positive_milliseconds(
402            std::env::var("SUPERCOV_DIAGNOSTIC_INTERVAL_MS")
403                .ok()
404                .as_deref(),
405            "SUPERCOV_DIAGNOSTIC_INTERVAL_MS",
406        )
407        .map_err(|error| error.to_string())?
408        .unwrap_or(defaults.diagnostic_interval),
409        timeout: positive_milliseconds(
410            std::env::var("SUPERCOV_COMMAND_TIMEOUT_MS").ok().as_deref(),
411            "SUPERCOV_COMMAND_TIMEOUT_MS",
412        )
413        .map_err(|error| error.to_string())?,
414        termination_grace: defaults.termination_grace,
415    })
416}
417
418fn remove_derived_pnpm_config(environment: &mut BTreeMap<OsString, OsString>) {
419    // `npx supercov` is itself launched by npm, which exports project `.npmrc`
420    // keys as `npm_config_*`. pnpm-only keys then make every nested npm process
421    // print a second set of "Unknown env config" warnings. They have no npm
422    // semantics (npm 11 reports them as unknown), so do not leak those derived
423    // environment aliases into the user's already-configured test command.
424    const PNPM_ONLY_NPM_CONFIG: &[&str] = &[
425        "npm_config_auto_install_peers",
426        "npm_config_enable_pre_post_scripts",
427        "npm_config_shamefully_hoist",
428    ];
429    environment.retain(|key, _| {
430        let key = key.to_string_lossy().to_ascii_lowercase();
431        !PNPM_ONLY_NPM_CONFIG.contains(&key.as_str())
432    });
433}
434
435fn environment_with(values: BTreeMap<String, String>) -> Vec<(OsString, OsString)> {
436    let mut environment = std::env::vars_os().collect::<BTreeMap<_, _>>();
437    remove_derived_pnpm_config(&mut environment);
438    for (key, value) in values {
439        environment.retain(|existing, _| !existing.to_string_lossy().eq_ignore_ascii_case(&key));
440        environment.insert(key.into(), value.into());
441    }
442    environment.into_iter().collect()
443}
444
445/// Where Playwright is told to keep the test files it compiles.
446const PLAYWRIGHT_CACHE_VARIABLE: &str = "PWTEST_CACHE_DIR";
447
448/// Beside the copies whose files it holds, and removed with them.
449pub fn playwright_transform_cache(root: &Path) -> PathBuf {
450    crate::workspace::workspace_container(root).join("caches/playwright-transform")
451}
452
453fn node_options(preload: &Path) -> String {
454    // The register import must be ABSOLUTE: node resolves a relative
455    // `--import` against each child process's OWN working directory, and
456    // monorepo runners spawn tasks inside package directories. turbo running
457    // `packages/react#build` resolved `.supercov/register.mjs` against
458    // `packages/react/` and aborted every task with ERR_MODULE_NOT_FOUND.
459    let preload = std::path::absolute(preload).unwrap_or_else(|_| preload.to_path_buf());
460    // And it must be a URL, not a path: Node reads a bare `C:\...` as a URL
461    // with scheme `c:` and refuses it, so on Windows the first Node child died
462    // at startup and the suite reported only that its exit code was 1. The
463    // JavaScript runtime already passes this import as a file URL; so does
464    // this.
465    [
466        std::env::var("NODE_OPTIONS").ok(),
467        Some("--enable-source-maps".into()),
468        Some(format!("--import={}", crate::workspace::file_url(&preload))),
469    ]
470    .into_iter()
471    .flatten()
472    .filter(|value| !value.is_empty())
473    .collect::<Vec<_>>()
474    .join(" ")
475}
476
477/// Fingerprint the current JavaScript project using the same discovery and
478/// runtime-shim inputs as a Rust-owned execution. Query callers deliberately
479/// treat failure as "staleness unavailable", matching the frozen CLI contract.
480pub fn current_javascript_integrity(
481    root: &Path,
482    command: &[String],
483    source_roots: Option<&[String]>,
484) -> Result<RunIntegrity, String> {
485    let mut environment = std::env::vars().collect::<BTreeMap<_, _>>();
486    // The roots the run was measured under, not this shell's. A run recorded
487    // with the variable and queried from a shell without it discovered a
488    // different scope and read as stale -- or, where automatic discovery found
489    // nothing, stopped being checked at all.
490    match source_roots.filter(|roots| !roots.is_empty()) {
491        Some(roots) => {
492            environment.insert(
493                crate::source_discovery::SOURCE_ROOTS_VARIABLE.into(),
494                roots.join(","),
495            );
496        }
497        None => {
498            environment.remove(crate::source_discovery::SOURCE_ROOTS_VARIABLE);
499        }
500    }
501    let project = discover_coverage_project(root, &environment, command)
502        .map_err(|error| error.to_string())?;
503    javascript_integrity_for_project(root, &project)
504}
505
506fn javascript_integrity_for_project(
507    root: &Path,
508    project: &crate::project_discovery::CoverageProject,
509) -> Result<RunIntegrity, String> {
510    let frontend = FrontendIntegrityInputs::embedded_javascript();
511    create_run_integrity(root, project, &frontend).map_err(|error| error.to_string())
512}
513
514/// Execute one JavaScript suite with every language-neutral stage owned by
515/// Rust. Target-language runtime and runner adapters remain generated shims.
516pub fn run_direct_javascript(
517    request: &DirectJavascriptRunRequest,
518    diagnostics: &mut dyn std::io::Write,
519) -> Result<DirectJavascriptRunResult, DirectJavascriptRunError> {
520    if request.command.is_empty() {
521        return Err(DirectJavascriptRunError::Failed(
522            "test command must not be empty".into(),
523        ));
524    }
525    let total_started = Instant::now();
526    let initialization_started = Instant::now();
527    let root = crate::workspace::canonicalize_simplified(&request.root)
528        .map_err(|error| format!("{}: {error}", request.root.display()))?;
529    let nonce = now_nonce();
530    let run_id = request
531        .run_id
532        .clone()
533        .unwrap_or_else(|| format!("rust-{nonce}"));
534    let started_at = request
535        .started_at
536        .clone()
537        .unwrap_or_else(|| format!("unix-ms-{nonce}"));
538    let lock =
539        ProjectLock::acquire(&root, &run_id, &started_at).map_err(|error| error.to_string())?;
540    let mut cleanup = RunCleanup {
541        root: root.clone(),
542        run_id: run_id.clone(),
543        started_at: started_at.clone(),
544        lock,
545        workspace: None,
546        state_written: false,
547        terminal_recorded: false,
548        keep_workspace: false,
549    };
550    let recovered_runs =
551        recover_abandoned_runs(&root, &started_at).map_err(|error| error.to_string())?;
552    if !recovered_runs.is_empty() {
553        writeln!(
554            diagnostics,
555            "[supercov] recovered abandoned run(s): {}",
556            recovered_runs.join(", ")
557        )
558        .map_err(|error| error.to_string())?;
559    }
560    let environment = std::env::vars().collect::<BTreeMap<_, _>>();
561    let source_roots = crate::source_discovery::configured_source_roots(&environment);
562    let project = crate::project_discovery::discover_coverage_project_of_any_size(
563        &root,
564        &environment,
565        &request.command,
566    )
567    .map_err(|error| error.to_string())?;
568    // Browser tests may drive a server in another language -- Python, Ruby,
569    // Go, Java/Kotlin, a Rust Worker -- whose source the probes measure in
570    // the copy the suite runs in. Without one, a project
571    // with no JavaScript source has nothing to measure.
572    let browser_suite = project.playwright_config.is_some();
573    let serves_python = browser_suite
574        && crate::python_project::discover_python_files(&root)
575            .is_ok_and(|files| !files.sources.is_empty());
576    let serves_ruby = browser_suite
577        && crate::ruby_project::discover_ruby_files(&root)
578            .is_ok_and(|files| !files.sources.is_empty());
579    let serves_go = browser_suite
580        && (root.join("go.mod").is_file()
581            || !crate::go_project::workspace_modules(&root).is_empty());
582    let serves_rust_worker = browser_suite && crate::rust_worker::RustWorkerServer::present(&root);
583    // A wasm library any JavaScript suite may load, not only a browser one.
584    let serves_rust_wasm = crate::rust_wasm::RustWasmServer::present(&root);
585    let serves_jvm = browser_suite
586        && [
587            "pom.xml",
588            "build.gradle",
589            "build.gradle.kts",
590            "src/main/java",
591            "src/main/kotlin",
592        ]
593        .iter()
594        .any(|name| root.join(name).exists());
595    if project.source_files.is_empty()
596        && !serves_python
597        && !serves_ruby
598        && !serves_go
599        && !serves_jvm
600        && !serves_rust_worker
601        && !serves_rust_wasm
602    {
603        return Err(
604            crate::project_discovery::ProjectDiscoveryError::NoSourceFiles
605                .to_string()
606                .into(),
607        );
608    }
609    let integrity = javascript_integrity_for_project(&root, &project)?;
610    let source_inputs = crate::source_capture::capture(
611        &root,
612        "javascript",
613        crate::integrity::javascript_assertion_paths(&root, &project).map_err(|e| e.to_string())?,
614    )?;
615    // The root too, which a moved project must not reuse. And the files that
616    // are instrumented: the fingerprint digests every first-party file
617    // whatever its classification, so SUPERCOV_SOURCE_ROOTS set on a project
618    // that had already run was silently ignored until the workspace was
619    // deleted.
620    let frontend_cache_key = format!(
621        "{}:{}:{}:{}:{}",
622        integrity.fingerprint.combined,
623        integrity.fingerprint.execution,
624        crate::source_manifest::digest(&request.command),
625        crate::source_manifest::digest(&project.root.to_string_lossy()),
626        crate::source_manifest::digest(&(&project.source_files, &source_roots))
627    );
628    let prior_workspace =
629        cached_workspace_path(&root, cleanup.lock()).map_err(|error| error.to_string())?;
630    let reusable_frontend = read_javascript_frontend_cache(&prior_workspace, &frontend_cache_key);
631    let cached_paths = reusable_frontend
632        .as_ref()
633        .map(javascript_frontend_reuse_paths)
634        .unwrap_or_default();
635    let initialization_ms = elapsed_ms(initialization_started);
636    // A server's plan is prepared while the workspace is copied and the
637    // JavaScript instrumented: parsing a large Python tree took a third of a
638    // second of every run, server or not.
639    let prepare_servers = |workspace: PathBuf| {
640        let (root, run_id, command) = (root.clone(), run_id.clone(), request.command.clone());
641        move || -> Result<Vec<Server>, String> {
642            let mut servers = Vec::new();
643            if serves_python
644                && let Some(server) =
645                    crate::python_run::PythonServer::prepare(&root, &workspace, &run_id, &command)?
646            {
647                servers.push(Server::Python(server));
648            }
649            if serves_ruby
650                && let Some(server) =
651                    crate::ruby_run::RubyServer::prepare(&root, &workspace, &run_id, &command)?
652            {
653                servers.push(Server::Ruby(server));
654            }
655            Ok(servers)
656        }
657    };
658    let preparing_servers = std::thread::spawn(prepare_servers(prior_workspace.clone()));
659
660    let workspace_started = Instant::now();
661    let workspace_progress = crate::progress::ProgressLine::start("preparing isolated workspace");
662    let workspace = prepare_cached_workspace(&root, cleanup.lock(), &cached_paths)
663        .map_err(|error| error.to_string())?;
664    drop(workspace_progress);
665    cleanup.set_workspace(workspace.clone());
666    // The workspace this one replaced is in the trash now.
667    crate::lifecycle::sweep_trash_in_background(&root);
668    if let Err(error) = crate::workspace::mirror_git_repository(&root, &workspace)
669        && std::env::var_os("SUPERCOV_DEBUG").is_some()
670    {
671        eprintln!("[supercov] the workspace has no git repository of its own: {error}");
672    }
673    let workspace_preparation_ms = elapsed_ms(workspace_started);
674    writeln!(
675        diagnostics,
676        "[supercov] instrumenting isolated workspace {}",
677        workspace.display()
678    )
679    .map_err(|error| error.to_string())?;
680    cleanup.mark_state_written();
681    write_run_state(
682        &root,
683        &RunState {
684            id: run_id.clone(),
685            pid: std::process::id(),
686            root: root.display().to_string(),
687            workspace: workspace.display().to_string(),
688            started_at: started_at.clone(),
689            updated_at: started_at.clone(),
690            status: RunStateStatus::Preparing,
691            signal: None,
692            error: None,
693        },
694    )
695    .map_err(|error| error.to_string())?;
696
697    let adapter_started = Instant::now();
698    let instrumentation_progress = crate::progress::ProgressLine::start("instrumenting sources");
699    let collector_id = format!("collector-{}", integrity.fingerprint.execution);
700    let frontend = if let Some(cache) = &reusable_frontend {
701        load_cached_javascript_frontend(&workspace, cache)
702    } else {
703        prepare_javascript_frontend(
704            &workspace,
705            &project,
706            &collector_id,
707            &frontend_cache_key,
708            &request.command,
709        )
710    }
711    .map_err(|error| error.to_string())?;
712    crate::workspace::hide_rewritten_files(
713        &workspace,
714        &crate::javascript_frontend::rewritten_files(&workspace),
715    );
716    drop(instrumentation_progress);
717    let adapter_setup_ms = elapsed_ms(adapter_started);
718    if let Some(detail) = crate::javascript_frontend::setup_timing_detail() {
719        writeln!(diagnostics, "[supercov] {detail}").map_err(|error| error.to_string())?;
720    }
721
722    let mut servers = preparing_servers
723        .join()
724        .unwrap_or_else(|panic| std::panic::resume_unwind(panic))?;
725    // Prepared for where the workspace was to be; one moved since is
726    // prepared again for where it is.
727    if workspace != prior_workspace && !servers.is_empty() {
728        servers = prepare_servers(workspace.clone())()?;
729    }
730    // Go, Java and Kotlin are rewritten in the copy, so they wait for it.
731    if serves_go
732        && let Some(server) =
733            crate::go_run::GoServer::prepare(&root, &workspace, &run_id, &request.command)?
734    {
735        servers.push(Server::Go(server));
736    }
737    if serves_jvm
738        && let Some(server) =
739            crate::jvm_run::JvmServer::prepare(&root, &workspace, &run_id, &request.command)?
740    {
741        servers.push(Server::Jvm(server));
742    }
743    // A Rust Worker is instrumented in the copy too, and `wrangler dev`
744    // builds it there when the suite starts it.
745    if serves_rust_worker {
746        servers.extend(
747            crate::rust_worker::RustWorkerServer::prepare(
748                &root,
749                &workspace,
750                &run_id,
751                &request.command,
752                diagnostics,
753            )?
754            .into_iter()
755            .map(Server::RustWorker),
756        );
757    }
758    if serves_rust_wasm {
759        servers.extend(
760            crate::rust_wasm::RustWasmServer::prepare(
761                &root,
762                &workspace,
763                &workspace.join(".supercov/server-evidence"),
764                &run_id,
765                &request.command,
766            )?
767            .into_iter()
768            .map(Server::RustWasm),
769        );
770    }
771    if project.source_files.is_empty() && servers.is_empty() {
772        return Err(
773            crate::project_discovery::ProjectDiscoveryError::NoSourceFiles
774                .to_string()
775                .into(),
776        );
777    }
778    for server in &servers {
779        writeln!(
780            diagnostics,
781            "[supercov] measuring {} {language} source file(s) in any {language} server the tests start",
782            server.source_files(),
783            language = server.language(),
784        )
785        .map_err(|error| error.to_string())?;
786    }
787
788    let evidence_relative = format!(".supercov/evidence/{run_id}");
789    let evidence_directory = workspace.join(&evidence_relative);
790    let server_evidence_root = workspace.join(".supercov/server-evidence");
791    let diagnostic_owner = workspace.join(format!(".supercov/diagnostic-owner-{run_id}"));
792    let mut overrides: BTreeMap<String, String> = BTreeMap::from([
793        ("NODE_OPTIONS".into(), node_options(&frontend.preload_path)),
794        ("SUPERCOV_CJS_INTERCEPT".into(), "1".into()),
795        ("SUPERCOV_DIRECT_INSTRUMENTATION".into(), "1".into()),
796        // tap measures its own coverage and fails a run short of 100%. Under
797        // Supercov it measures the instrumented copies, whose probes are
798        // branches the suite was never meant to cover: minimatch passed all
799        // 6,236 tests and still failed. Its report stays; only the gate goes.
800        ("TAP_ALLOW_INCOMPLETE_COVERAGE".into(), "1".into()),
801        // Must be absolute: monorepo runners spawn test processes with a
802        // package directory as cwd, and a relative evidence directory made
803        // every per-test record land beside the package, never collected.
804        (
805            "SUPERCOV_EVIDENCE_DIR".into(),
806            evidence_directory.display().to_string(),
807        ),
808        (
809            "SUPERCOV_DIAGNOSTIC_OWNER_FILE".into(),
810            diagnostic_owner.display().to_string(),
811        ),
812        (
813            "SUPERCOV_EXECUTION_FINGERPRINT".into(),
814            integrity.fingerprint.execution.clone(),
815        ),
816        (
817            "SUPERCOV_EXECUTION_LOG".into(),
818            evidence_directory
819                .join("execution.jsonl")
820                .display()
821                .to_string(),
822        ),
823        (
824            "SUPERCOV_MANIFEST".into(),
825            frontend.manifest_path.display().to_string(),
826        ),
827        (
828            "SUPERCOV_PROJECT_ROOT".into(),
829            workspace.display().to_string(),
830        ),
831        ("SUPERCOV_RUN_ID".into(), run_id.clone()),
832        (
833            "SUPERCOV_SERVER_EVIDENCE_ROOT".into(),
834            server_evidence_root.display().to_string(),
835        ),
836        (
837            "SUPERCOV_SOURCE_PROJECT_ROOT".into(),
838            root.display().to_string(),
839        ),
840    ]);
841    // Playwright keeps each test file it compiles in the system's temporary
842    // directory, the code in one file and its source map in another, and
843    // takes the code without looking for the map. macOS removes files there
844    // that nothing has read for three days, and the two are not read alike:
845    // the maps went, and every frame and test location of a suite came out
846    // at the compiled code's lines (`app.spec.ts:50:1` for line 17, a code
847    // frame of other tests' lines) until the code was three days unread too.
848    // The copy's compiled files are kept with the copy instead. A cache
849    // someone chose is theirs.
850    if std::env::var_os(PLAYWRIGHT_CACHE_VARIABLE).is_none_or(|value| value.is_empty()) {
851        overrides.insert(
852            PLAYWRIGHT_CACHE_VARIABLE.into(),
853            playwright_transform_cache(&root).display().to_string(),
854        );
855    }
856    // A wrapped npm/pnpm/yarn script can launch either runner several process
857    // generations later. The preload is the discovery boundary, so always
858    // provide both generated configs; each runner ignores the unrelated one.
859    overrides.insert(
860        "SUPERCOV_GENERATED_VITEST_CONFIG".into(),
861        frontend.vitest_config_path.display().to_string(),
862    );
863    overrides.insert(
864        "SUPERCOV_GENERATED_PLAYWRIGHT_CONFIG".into(),
865        frontend.playwright_config_path.display().to_string(),
866    );
867    overrides.insert(
868        "SUPERCOV_PLAYWRIGHT_MODULE".into(),
869        project.playwright_module.clone(),
870    );
871    overrides.insert(
872        "SUPERCOV_PLAYWRIGHT_TEST_EXPORT".into(),
873        project.playwright_test_export.clone(),
874    );
875    overrides.insert(
876        "SUPERCOV_PLAYWRIGHT_WRAPPER".into(),
877        "./.supercov/node_modules/playwright.mjs".into(),
878    );
879    if let Some(original) = project
880        .playwright_config
881        .as_ref()
882        .and_then(|path| path.strip_prefix(&root).ok())
883        .map(|path| workspace.join(path))
884    {
885        overrides.insert(
886            "SUPERCOV_ORIGINAL_PLAYWRIGHT_CONFIG".into(),
887            original.display().to_string(),
888        );
889    }
890    for server in &servers {
891        overrides.extend(server.environment());
892    }
893    // `uv run` syncs the environment before it runs, and the copy's is the
894    // project's own, linked in: a sync rewrote the project's editable install
895    // to point at the copy, and regenerated a `hatch-vcs` version file that
896    // then came back to the project as a change. The environment is used as
897    // it is; a command that wants it synced syncs it first, in the project.
898    if crate::workspace::links_a_virtual_environment(&workspace)
899        && std::env::var_os("UV_NO_SYNC").is_none()
900    {
901        overrides.insert("UV_NO_SYNC".into(), "1".into());
902    }
903    // What each process a browser suite starts loads, reads and serves, so a
904    // change to a mock server's files affects only the tests it served
905    // (support_reach.rs).
906    let support_directory = workspace.join(".supercov/support").join(&run_id);
907    if project.playwright_config.is_some() {
908        overrides.insert(
909            crate::support_reach::DIRECTORY_VARIABLE.into(),
910            support_directory.display().to_string(),
911        );
912    }
913    // A tool that judges source as text, started without a package manager
914    // or installed outside the project, is found through PATH.
915    if let Some(path) = crate::source_tools::path_with_launchers(&workspace)
916        && let Some(path) = path.to_str()
917    {
918        overrides.insert("PATH".into(), path.to_owned());
919    }
920    // A server built from the copy runs in a container nothing of the run
921    // reaches: the suite's Docker clients reach the daemon through a relay
922    // that hands it the run's settings (`start_docker_relay`).
923    #[cfg(unix)]
924    let docker_state = root.join(".supercov/work").join(&run_id).join("docker");
925    #[cfg(unix)]
926    let mut docker_proxy = if !project.source_files.is_empty() || !servers.is_empty() {
927        start_docker_relay(
928            &root,
929            &workspace,
930            &docker_state,
931            &servers,
932            &environment,
933            &mut overrides,
934            diagnostics,
935        )?
936    } else {
937        None
938    };
939    // What bootstrap.cjs installs in a process that runs the workspace without
940    // these settings: one started in a container or VM the workspace is
941    // mounted into. It moves `root` to where it sees the workspace from.
942    let settings = serde_json::json!({
943        "root": workspace.display().to_string(),
944        "environment": overrides
945            .iter()
946            .filter(|(name, _)| name.starts_with("SUPERCOV_"))
947            .collect::<BTreeMap<_, _>>(),
948    });
949    let settings_path = frontend.preload_path.with_file_name("run.json");
950    std::fs::write(
951        &settings_path,
952        serde_json::to_vec(&settings).map_err(|error| error.to_string())?,
953    )
954    .map_err(|error| format!("{}: {error}", settings_path.display()))?;
955    // What the tools of a command before this one changed in the source has
956    // been applied or let go; this command starts from the project as it is.
957    let _ = crate::source_tools::clear_source_changes(&workspace);
958    // The command, and nothing before it.
959    let plan = ExecutionPlan {
960        preparation: Vec::new(),
961        test: ExecutionPhase {
962            name: "test".into(),
963            kind: PhaseKind::Test,
964            command: CommandSpec {
965                program: request.command[0].clone().into(),
966                arguments: request.command[1..].iter().map(OsString::from).collect(),
967                cwd: workspace.clone(),
968                environment: Some(environment_with(overrides)),
969                captured_output: None,
970                console: None,
971            },
972        },
973    };
974    let options = supervision_options()?;
975    let watchdog_program = request.watchdog_program.as_ref().ok_or_else(|| {
976        DirectJavascriptRunError::Failed(
977            "the JavaScript run is missing its crash-containment executable".into(),
978        )
979    })?;
980    let supervisor = ProcessSupervisor::new_crash_safe(watchdog_program)
981        .map_err(|error| DirectJavascriptRunError::Failed(error.to_string()))?;
982    let output_baseline = std::cell::RefCell::new(None);
983    let execution = match execute_plan_with_supervisor(
984        &supervisor,
985        &plan,
986        options,
987        diagnostics,
988        |phase, diagnostics| {
989            {
990                // The snapshot boundary sits before the user's command, so
991                // only the command's own effects flow back to the real
992                // project afterwards.
993                *output_baseline.borrow_mut() =
994                    Some(workspace_output_baseline(&workspace).map_err(|error| {
995                        OrchestrationError::PhaseSetup {
996                            phase: phase.name.clone(),
997                            reason: error.to_string(),
998                        }
999                    })?);
1000                if frontend.assertion_calls > 0 {
1001                    writeln!(
1002                        diagnostics,
1003                        "[supercov] attributed {} native node:assert call(s)",
1004                        frontend.assertion_calls
1005                    )
1006                    .map_err(|error| OrchestrationError::PhaseSetup {
1007                        phase: phase.name.clone(),
1008                        reason: error.to_string(),
1009                    })?;
1010                }
1011                writeln!(
1012                    diagnostics,
1013                    "[supercov] running in isolated workspace: {}",
1014                    request.command.join(" ")
1015                )
1016                .map_err(|error| OrchestrationError::PhaseSetup {
1017                    phase: phase.name.clone(),
1018                    reason: error.to_string(),
1019                })?;
1020            }
1021            update_run_state(&root, &run_id, RunStateStatus::Testing, &started_at, None).map_err(
1022                |error| OrchestrationError::PhaseSetup {
1023                    phase: phase.name.clone(),
1024                    reason: error.to_string(),
1025                },
1026            )?;
1027            Ok(())
1028        },
1029    ) {
1030        Ok(execution) => execution,
1031        Err(error) => {
1032            let message = error.to_string();
1033            let state_updated = update_run_state(
1034                &root,
1035                &run_id,
1036                RunStateStatus::Failed,
1037                &started_at,
1038                Some(message.clone()),
1039            )
1040            .is_ok();
1041            if state_updated {
1042                cleanup.mark_terminal();
1043            }
1044            return Err(message.into());
1045        }
1046    };
1047    // A server left running writes its last window when stopped.
1048    #[cfg(unix)]
1049    if let Some(proxy) = docker_proxy.take() {
1050        proxy.finish();
1051        for note in crate::docker_proxy::notes(&docker_state.join("containers.jsonl")) {
1052            writeln!(diagnostics, "[supercov] {note}").map_err(|error| error.to_string())?;
1053        }
1054    }
1055    let test_command_ms = execution
1056        .phases
1057        .iter()
1058        .find(|phase| phase.kind == PhaseKind::Test)
1059        .map_or(0.0, |phase| phase.duration_ms as f64);
1060    if let Some(signal) = execution.interrupted_signal {
1061        interrupt_run_state(&root, &run_id, &started_at, signal_name(signal))
1062            .map_err(|error| error.to_string())?;
1063        cleanup.mark_terminal();
1064        return Err(DirectJavascriptRunError::Interrupted {
1065            signal,
1066            exit_code: execution.exit_code,
1067            timings: RunTimings {
1068                initialization_ms: rounded_millisecond(initialization_ms),
1069                workspace_preparation_ms: rounded_millisecond(workspace_preparation_ms),
1070                adapter_setup_ms: rounded_millisecond(adapter_setup_ms),
1071                instrumented_build_ms: 0.0,
1072                test_command_ms: rounded_millisecond(test_command_ms),
1073                evidence_publication_ms: 0.0,
1074            },
1075            total_ms: rounded_millisecond(elapsed_ms(total_started)),
1076        });
1077    }
1078    // Leave the real working tree as the command alone would have: updated
1079    // snapshots, generated fixtures, and reports belong in the repository,
1080    // not in the cache. Deletions and instrumented-source rewrites are
1081    // reported, never propagated.
1082    if let Some(baseline) = output_baseline.borrow().as_ref() {
1083        let protected = project
1084            .source_files
1085            .iter()
1086            .map(PathBuf::from)
1087            .collect::<std::collections::BTreeSet<_>>();
1088        let outputs = sync_command_outputs(&root, &workspace, baseline, &protected)
1089            .map_err(|error| error.to_string())?;
1090        if outputs.synced > 0 {
1091            let _ = writeln!(
1092                diagnostics,
1093                "[supercov] synced {} file(s) the command created or changed back to the project: {}",
1094                outputs.synced,
1095                crate::workspace::summarize_paths(&outputs.synced_paths)
1096            );
1097        }
1098        if !outputs.skipped_instrumented.is_empty() {
1099            let _ = writeln!(
1100                diagnostics,
1101                "[supercov] {} file(s) stayed in the isolated workspace: they are instrumented copies, or were built from them, and must not overwrite your project: {}",
1102                outputs.skipped_instrumented.len(),
1103                crate::workspace::summarize_paths(&outputs.skipped_instrumented)
1104            );
1105        }
1106        // A formatter or a fixer the command ran changed source files. The
1107        // tests ran the copy instrumented from the text before, so that is
1108        // what this run measured; the project gets the change, as it does
1109        // when the command runs alone.
1110        match crate::source_tools::apply_source_changes(&root, &workspace) {
1111            Ok(changes) => {
1112                if !changes.applied.is_empty() {
1113                    let _ = writeln!(
1114                        diagnostics,
1115                        "[supercov] the command's tools changed {} source file(s), now changed in the project too: {}; this run measured them as they were before",
1116                        changes.applied.len(),
1117                        crate::workspace::summarize_paths(&changes.applied)
1118                    );
1119                }
1120                if !changes.kept_back.is_empty() {
1121                    let _ = writeln!(
1122                        diagnostics,
1123                        "[supercov] the command's tools changed {} source file(s) that were edited in the project while it ran; those changes were not applied: {}",
1124                        changes.kept_back.len(),
1125                        crate::workspace::summarize_paths(&changes.kept_back)
1126                    );
1127                }
1128            }
1129            Err(error) => {
1130                let _ = writeln!(
1131                    diagnostics,
1132                    "[supercov] the source changes the command's tools made could not be applied to the project: {error}"
1133                );
1134            }
1135        }
1136        if !outputs.deleted_in_workspace.is_empty() {
1137            let _ = writeln!(
1138                diagnostics,
1139                "[supercov] the command deleted {} file(s) in the isolated workspace; deletions are not propagated to the project: {}",
1140                outputs.deleted_in_workspace.len(),
1141                crate::workspace::summarize_paths(&outputs.deleted_in_workspace)
1142            );
1143        }
1144    }
1145    update_run_state(
1146        &root,
1147        &run_id,
1148        RunStateStatus::Publishing,
1149        &started_at,
1150        None,
1151    )
1152    .map_err(|error| error.to_string())?;
1153
1154    let publication_started = Instant::now();
1155    // Read before the evidence is archived and removed.
1156    let served = if servers.is_empty() && !support_directory.is_dir() {
1157        Vec::new()
1158    } else {
1159        crate::served_tests::read(&evidence_directory)
1160    };
1161    let support = crate::support_reach::read(&support_directory, &workspace, &served);
1162    if project.source_files.is_empty() {
1163        // Only the servers were measured: the first published is the run,
1164        // any other beside it.
1165        let metadata = RunMetadata {
1166            id: run_id.clone(),
1167            started_at: started_at.clone(),
1168            duration_ms: rounded_millisecond(elapsed_ms(total_started)),
1169            command: request.command.clone(),
1170            test_exit_code: Some(execution.exit_code),
1171            integrity: integrity.clone(),
1172            raw_evidence: RawEvidenceMetadata::default(),
1173            isolated_build: Some(true),
1174            instrumented_build_cache: None,
1175            timings: Some(RunTimings {
1176                initialization_ms: rounded_millisecond(initialization_ms),
1177                workspace_preparation_ms: rounded_millisecond(workspace_preparation_ms),
1178                adapter_setup_ms: rounded_millisecond(adapter_setup_ms),
1179                instrumented_build_ms: 0.0,
1180                test_command_ms: rounded_millisecond(test_command_ms),
1181                evidence_publication_ms: 0.0,
1182            }),
1183            merged: None,
1184            parents: None,
1185            source_roots: source_roots.clone(),
1186            copy: Some(cleanup.lock().copy()),
1187            merged_across: None,
1188            recorded_by: None,
1189            companions: None,
1190        };
1191        let mut published = None;
1192        let mut server_runs = Vec::new();
1193        let mut failure = None;
1194        for server in servers {
1195            let id = if published.is_none() {
1196                run_id.clone()
1197            } else {
1198                companion_run_id(&run_id, server_runs.len() + 1)
1199            };
1200            let language = server.language();
1201            match server.publish(
1202                &root,
1203                &run_id,
1204                RunMetadata {
1205                    id: id.clone(),
1206                    ..metadata.clone()
1207                },
1208                &served,
1209                diagnostics,
1210            ) {
1211                Ok(Some(run)) if published.is_none() => published = Some(run),
1212                Ok(Some((directory, _))) => {
1213                    finalize_published_run(&root, &id).map_err(|error| error.to_string())?;
1214                    server_runs.push((language.to_owned(), directory));
1215                }
1216                Ok(None) => {}
1217                Err(error) => {
1218                    let _ = writeln!(
1219                        diagnostics,
1220                        "[supercov] the {language} server's run could not be published: {error}"
1221                    );
1222                    failure.get_or_insert(error);
1223                }
1224            }
1225        }
1226        remove_stored_tree_deferred(&root, &workspace.join(".supercov/evidence"))
1227            .map_err(|error| error.to_string())?;
1228        remove_stored_tree_deferred(&root, &server_evidence_root)
1229            .map_err(|error| error.to_string())?;
1230        if published.is_none()
1231            && let Some(error) = failure
1232        {
1233            return Err(error.into());
1234        }
1235        let Some((run_directory, metadata)) = published else {
1236            let message = if execution.exit_code != 0 && served.is_empty() {
1237                format!(
1238                    "the test command exited {} before any test reported an outcome; its own output above says why, and no run was published",
1239                    execution.exit_code
1240                )
1241            } else {
1242                "the project has no JavaScript source, and no test's request reached measured server code: there was nothing to measure, and no run was published".to_owned()
1243            };
1244            let _ = update_run_state(
1245                &root,
1246                &run_id,
1247                RunStateStatus::Failed,
1248                &started_at,
1249                Some(message.clone()),
1250            );
1251            cleanup.mark_terminal();
1252            return Err(message.into());
1253        };
1254        update_run_state(
1255            &root,
1256            &run_id,
1257            if execution.exit_code == 0 {
1258                RunStateStatus::Complete
1259            } else {
1260                RunStateStatus::Failed
1261            },
1262            &started_at,
1263            None,
1264        )
1265        .map_err(|error| error.to_string())?;
1266        finalize_published_run(&root, &run_id).map_err(|error| error.to_string())?;
1267        cleanup.mark_terminal();
1268        return Ok(DirectJavascriptRunResult {
1269            run_id,
1270            run_directory,
1271            workspace,
1272            exit_code: execution.exit_code,
1273            assertion_calls: frontend.assertion_calls,
1274            recovered_runs,
1275            metadata,
1276            server_runs,
1277            unrun_build: project.unrun_build.clone(),
1278        });
1279    }
1280    let archive_path = root
1281        .join(".supercov/work")
1282        .join(&run_id)
1283        .join("evidence.raw.gz");
1284    let entries = collect_sources(&[
1285        EvidenceArchiveSource::File {
1286            file: frontend.manifest_path,
1287            path: "manifest.json".into(),
1288        },
1289        EvidenceArchiveSource::File {
1290            file: workspace.join(".supercov/statement-exclusions.json"),
1291            path: "statement-exclusions.json".into(),
1292        },
1293        EvidenceArchiveSource::Directory {
1294            directory: evidence_directory,
1295            prefix: None,
1296        },
1297        EvidenceArchiveSource::Directory {
1298            directory: server_evidence_root.join(&run_id),
1299            prefix: Some("server".into()),
1300        },
1301    ])
1302    .map_err(|error| error.to_string())?;
1303    let mut entries =
1304        javascript_archive_entries(entries, &frontend.manifest, &run_id, execution.exit_code)?;
1305    if let Some(support) = &support {
1306        entries.push(EvidenceArchiveEntry {
1307            path: crate::support_reach::ARCHIVE_PATH.into(),
1308            contents: serde_json::to_vec(support).map_err(|error| error.to_string())?,
1309        });
1310    }
1311    // The files the run's servers loaded and read are recorded with the
1312    // rest, so a change to one -- a mock's `requests.json` as much as its
1313    // code -- is seen, and judged by the tests that reached it.
1314    let mut recorded_inputs = source_inputs.clone();
1315    if let Some(support) = &support
1316        && let Ok(reached) = crate::source_capture::capture(
1317            &root,
1318            "javascript",
1319            support.files.keys().map(PathBuf::from),
1320        )
1321    {
1322        for (file, text) in reached.files {
1323            recorded_inputs.files.entry(file).or_insert(text);
1324        }
1325    }
1326    let entries = crate::source_capture::append(entries, &recorded_inputs)?;
1327    let raw = write_archive(entries, &archive_path).map_err(|error| error.to_string())?;
1328    remove_stored_tree_deferred(&root, &workspace.join(".supercov/evidence"))
1329        .map_err(|error| error.to_string())?;
1330    remove_stored_tree_deferred(&root, &server_evidence_root).map_err(|error| error.to_string())?;
1331    let evidence_publication_ms = elapsed_ms(publication_started);
1332    let timings = RunTimings {
1333        initialization_ms: rounded_millisecond(initialization_ms),
1334        workspace_preparation_ms: rounded_millisecond(workspace_preparation_ms),
1335        adapter_setup_ms: rounded_millisecond(adapter_setup_ms),
1336        instrumented_build_ms: 0.0,
1337        test_command_ms: rounded_millisecond(test_command_ms),
1338        evidence_publication_ms: rounded_millisecond(evidence_publication_ms),
1339    };
1340    let metadata = RunMetadata {
1341        id: run_id.clone(),
1342        started_at: started_at.clone(),
1343        duration_ms: rounded_millisecond(elapsed_ms(total_started)),
1344        command: request.command.clone(),
1345        test_exit_code: Some(execution.exit_code),
1346        integrity,
1347        raw_evidence: RawEvidenceMetadata {
1348            schema_version: raw.schema_version,
1349            format: raw.format.into(),
1350            file: raw.file.into(),
1351            files: raw.files,
1352            uncompressed_bytes: raw.uncompressed_bytes,
1353            compressed_bytes: raw.compressed_bytes,
1354        },
1355        isolated_build: Some(true),
1356        instrumented_build_cache: None,
1357        timings: Some(timings),
1358        merged: None,
1359        parents: None,
1360        source_roots: source_roots.clone(),
1361        copy: Some(cleanup.lock().copy()),
1362        merged_across: None,
1363        recorded_by: None,
1364        companions: None,
1365    };
1366    // Each server's run beside the browser run, under an id of its own that
1367    // sorts after it: `runs latest` stays the browser run. Published first,
1368    // so the browser run names the ones that were.
1369    let mut server_runs = Vec::new();
1370    let mut companions = Vec::new();
1371    for server in servers {
1372        let id = companion_run_id(&run_id, server_runs.len() + 1);
1373        let language = server.language();
1374        let server_metadata = RunMetadata {
1375            id: id.clone(),
1376            companions: Some(vec![crate::run_store::CompanionRun {
1377                id: run_id.clone(),
1378                language: "JavaScript".into(),
1379                role: "browser".into(),
1380            }]),
1381            ..metadata.clone()
1382        };
1383        match server.publish(&root, &run_id, server_metadata, &served, diagnostics) {
1384            Ok(Some((directory, _))) => {
1385                finalize_published_run(&root, &id).map_err(|error| error.to_string())?;
1386                server_runs.push((language.to_owned(), directory));
1387                companions.push(crate::run_store::CompanionRun {
1388                    id,
1389                    language: language.into(),
1390                    role: "server".into(),
1391                });
1392            }
1393            Ok(None) => {}
1394            // The browser run stands on its own.
1395            Err(error) => {
1396                let _ = writeln!(
1397                    diagnostics,
1398                    "[supercov] the {language} server's run could not be published: {error}"
1399                );
1400            }
1401        }
1402    }
1403    let metadata = RunMetadata {
1404        companions: (!companions.is_empty()).then_some(companions),
1405        ..metadata
1406    };
1407    let run_directory =
1408        publish_run(&root, &metadata, &archive_path).map_err(|error| error.to_string())?;
1409    let terminal_status = if execution.exit_code == 0 {
1410        RunStateStatus::Complete
1411    } else {
1412        RunStateStatus::Failed
1413    };
1414    update_run_state(&root, &run_id, terminal_status, &started_at, None)
1415        .map_err(|error| error.to_string())?;
1416    finalize_published_run(&root, &run_id).map_err(|error| error.to_string())?;
1417    cleanup.mark_terminal();
1418    Ok(DirectJavascriptRunResult {
1419        run_id,
1420        run_directory,
1421        workspace,
1422        exit_code: execution.exit_code,
1423        assertion_calls: frontend.assertion_calls,
1424        recovered_runs,
1425        metadata,
1426        server_runs,
1427        unrun_build: project.unrun_build.clone(),
1428    })
1429}
1430
1431/// The id of the `nth` run published beside `run_id` from the same command:
1432/// a public id `nth` below it when `run_id` is one, since runs that started
1433/// together are listed by id, highest first.
1434fn companion_run_id(run_id: &str, nth: usize) -> String {
1435    match run_id
1436        .strip_prefix("run_")
1437        .filter(|hex| hex.len() == 16)
1438        .and_then(|hex| u64::from_str_radix(hex, 16).ok())
1439    {
1440        Some(number) => format!("run_{:016x}", number.wrapping_sub(nth as u64)),
1441        None => format!("{run_id}-server-{nth}"),
1442    }
1443}
1444
1445/// The relay the suite's Docker clients reach the daemon through
1446/// (docker_proxy.rs). It gives each container whose image holds the copy's
1447/// code the run's settings and Supercov's directory, mounted at its path: a
1448/// Go or Java server's windows and a Node server's evidence are written where
1449/// the run reads them. None when the daemon is not a local socket.
1450#[cfg(unix)]
1451fn start_docker_relay(
1452    root: &Path,
1453    workspace: &Path,
1454    docker_state: &Path,
1455    servers: &[Server],
1456    environment: &BTreeMap<String, String>,
1457    overrides: &mut BTreeMap<String, String>,
1458    diagnostics: &mut dyn std::io::Write,
1459) -> Result<Option<crate::docker_proxy::DockerProxy>, String> {
1460    let Some(upstream) = crate::docker_proxy::upstream_socket(environment) else {
1461        return Ok(None);
1462    };
1463    let mut names = overrides
1464        .keys()
1465        .filter(|name| name.starts_with("SUPERCOV_"))
1466        .cloned()
1467        .collect::<std::collections::BTreeSet<_>>();
1468    names.extend(
1469        servers
1470            .iter()
1471            .flat_map(|server| server.environment().into_keys()),
1472    );
1473    let settings = crate::docker_proxy::ProxySettings {
1474        copy: workspace.to_owned(),
1475        mount: root.join(".supercov"),
1476        environment: names
1477            .iter()
1478            .filter_map(|name| Some((name.clone(), overrides.get(name)?.clone())))
1479            .collect(),
1480        fingerprint: servers
1481            .iter()
1482            .map(Server::source_fingerprint)
1483            .collect::<Vec<_>>()
1484            .join(","),
1485        registry: root.join(".supercov/cache/docker-images.json"),
1486        state: docker_state.join("containers.jsonl"),
1487    };
1488    match crate::docker_proxy::socket_path(docker_state)
1489        .and_then(|socket| crate::docker_proxy::DockerProxy::start(&socket, &upstream, settings))
1490    {
1491        Ok(proxy) => {
1492            overrides.extend(proxy.environment(environment));
1493            Ok(Some(proxy))
1494        }
1495        Err(error) => {
1496            writeln!(
1497                diagnostics,
1498                "[supercov] containers the tests start are not measured: the Docker relay could not start ({error})"
1499            )
1500            .map_err(|error| error.to_string())?;
1501            Ok(None)
1502        }
1503    }
1504}
1505
1506/// A server in another language a browser suite may start.
1507enum Server {
1508    Python(crate::python_run::PythonServer),
1509    Ruby(crate::ruby_run::RubyServer),
1510    Go(crate::go_run::GoServer),
1511    Jvm(crate::jvm_run::JvmServer),
1512    RustWorker(crate::rust_worker::RustWorkerServer),
1513    RustWasm(crate::rust_wasm::RustWasmServer),
1514}
1515
1516impl Server {
1517    fn language(&self) -> &'static str {
1518        match self {
1519            Self::Python(_) => "Python",
1520            Self::Ruby(_) => "Ruby",
1521            Self::Go(_) => "Go",
1522            Self::Jvm(_) => "Java/Kotlin",
1523            Self::RustWorker(_) => "Rust Worker",
1524            Self::RustWasm(_) => "Rust wasm",
1525        }
1526    }
1527
1528    fn source_files(&self) -> usize {
1529        match self {
1530            Self::Python(server) => server.source_files(),
1531            Self::Ruby(server) => server.source_files(),
1532            Self::Go(server) => server.source_files(),
1533            Self::Jvm(server) => server.source_files(),
1534            Self::RustWorker(server) => server.source_files(),
1535            Self::RustWasm(server) => server.source_files(),
1536        }
1537    }
1538
1539    /// The source this server was instrumented from: an image built from an
1540    /// earlier copy holds probes placed in other source.
1541    #[cfg(unix)]
1542    fn source_fingerprint(&self) -> String {
1543        match self {
1544            Self::Python(server) => server.source_fingerprint(),
1545            Self::Ruby(server) => server.source_fingerprint(),
1546            Self::Go(server) => server.source_fingerprint(),
1547            Self::Jvm(server) => server.source_fingerprint(),
1548            Self::RustWorker(server) => server.source_fingerprint(),
1549            Self::RustWasm(server) => server.source_fingerprint(),
1550        }
1551    }
1552
1553    fn environment(&self) -> BTreeMap<String, String> {
1554        match self {
1555            Self::Python(server) => server.environment(),
1556            Self::Ruby(server) => server.environment(),
1557            Self::Go(server) => server.environment(),
1558            Self::Jvm(server) => server.environment(),
1559            Self::RustWorker(server) => server.environment(),
1560            Self::RustWasm(server) => server.environment(),
1561        }
1562    }
1563
1564    fn publish(
1565        self,
1566        root: &Path,
1567        evidence_run_id: &str,
1568        metadata: RunMetadata,
1569        served: &[crate::served_tests::ServedTest],
1570        diagnostics: &mut dyn std::io::Write,
1571    ) -> Result<Option<(PathBuf, RunMetadata)>, String> {
1572        match self {
1573            Self::Python(server) => {
1574                server.publish(root, evidence_run_id, metadata, served, diagnostics)
1575            }
1576            Self::Ruby(server) => {
1577                server.publish(root, evidence_run_id, metadata, served, diagnostics)
1578            }
1579            Self::Go(server) => {
1580                server.publish(root, evidence_run_id, metadata, served, diagnostics)
1581            }
1582            Self::Jvm(server) => {
1583                server.publish(root, evidence_run_id, metadata, served, diagnostics)
1584            }
1585            Self::RustWorker(server) => {
1586                server.publish(root, evidence_run_id, metadata, served, diagnostics)
1587            }
1588            Self::RustWasm(server) => {
1589                server.publish(root, evidence_run_id, metadata, served, diagnostics)
1590            }
1591        }
1592    }
1593}
1594
1595#[cfg(test)]
1596mod environment_tests {
1597    use super::*;
1598
1599    // A server's run published beside a browser run sorts after it, so
1600    // `runs latest` stays the browser run; and is the same each time.
1601    #[test]
1602    fn a_companion_run_id_is_public_and_sorts_after_its_run() {
1603        for run in [
1604            "run_0123456789abcdef",
1605            "run_ffffffffffffffff",
1606            "run_8000000000000000",
1607        ] {
1608            let companion = companion_run_id(run, 1);
1609            assert_eq!(companion.len(), 20);
1610            assert!(companion.starts_with("run_"));
1611            assert!(companion.as_str() < run, "{companion} >= {run}");
1612            assert_eq!(companion, companion_run_id(run, 1));
1613            assert!(companion_run_id(run, 2) < companion);
1614        }
1615        assert_eq!(companion_run_id("rust-1", 1), "rust-1-server-1");
1616    }
1617
1618    #[test]
1619    fn nested_npm_drops_only_pnpm_derived_environment_aliases() {
1620        let mut environment = BTreeMap::from([
1621            ("npm_config_auto_install_peers".into(), "true".into()),
1622            ("NPM_CONFIG_SHAMEFULLY_HOIST".into(), "true".into()),
1623            (
1624                "npm_config_registry".into(),
1625                "https://registry.npmjs.org".into(),
1626            ),
1627            ("USER_VALUE".into(), "kept".into()),
1628        ]);
1629        remove_derived_pnpm_config(&mut environment);
1630        assert_eq!(
1631            environment,
1632            BTreeMap::from([
1633                ("USER_VALUE".into(), "kept".into()),
1634                (
1635                    "npm_config_registry".into(),
1636                    "https://registry.npmjs.org".into()
1637                ),
1638            ])
1639        );
1640    }
1641}