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