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