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