Skip to main content

supercov_engine/
coverage_report.rs

1//! Language-neutral reconstruction of coverage views from frozen obligations
2//! and per-attempt evidence.
3//!
4//! This module deliberately knows nothing about JavaScript or any test runner.
5//! Language frontends provide the manifest and normalized evidence records;
6//! Rust owns merging, attempt outcomes, attribution confidence, filtering and
7//! every structural coverage verdict.
8
9use std::collections::{BTreeMap, BTreeSet, HashMap};
10use std::path::{Path, PathBuf};
11
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use supercov_contracts::{
15    COVERAGE_MODEL_SCHEMA_VERSION, EVIDENCE_ARCHIVE_V3_SCHEMA_VERSION, FrontendRunDeclaration,
16};
17
18use crate::coverage_analysis::{
19    AnalysisError, BranchCoverage, CoverageCoreInput, CoverageSummary, DecisionCoverage,
20    McdcVector, PointCoverage, PointKind, analyze_core, find_witnesses_for_conditions,
21};
22use crate::evidence_archive::{EvidenceArchiveEntry, read_versioned_archive};
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase", deny_unknown_fields)]
26pub struct DecisionMeta {
27    pub id: String,
28    pub file: String,
29    pub line: usize,
30    pub column: usize,
31    pub source: String,
32    pub conditions: Vec<String>,
33    pub kind: String,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase", deny_unknown_fields)]
38pub struct PointMeta {
39    pub id: String,
40    pub kind: PointKind,
41    pub file: String,
42    pub line: usize,
43    pub column: usize,
44    pub source: String,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub label: Option<String>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase", deny_unknown_fields)]
51pub struct BranchAlternativeMeta {
52    pub id: String,
53    pub label: String,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase", deny_unknown_fields)]
58pub struct BranchMeta {
59    pub id: String,
60    pub kind: String,
61    pub file: String,
62    pub line: usize,
63    pub column: usize,
64    pub source: String,
65    pub alternatives: Vec<BranchAlternativeMeta>,
66}
67
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase", deny_unknown_fields)]
70pub struct CoverageManifest {
71    pub decisions: Vec<DecisionMeta>,
72    pub points: Vec<PointMeta>,
73    pub branches: Vec<BranchMeta>,
74    #[serde(default, skip_serializing_if = "Vec::is_empty")]
75    pub limitations: Vec<Value>,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub scope: Option<Value>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase", deny_unknown_fields)]
82pub struct DecisionSnapshot {
83    pub meta: DecisionMeta,
84    pub vectors: Vec<McdcVector>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase", deny_unknown_fields)]
89pub struct RuntimeEvent {
90    #[serde(rename = "type")]
91    pub event_type: String,
92    pub id: String,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub vector: Option<McdcVector>,
95    pub timestamp_ms: i64,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub phase_id: Option<String>,
98    pub environment: String,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
102#[serde(rename_all = "camelCase", deny_unknown_fields)]
103pub struct RuntimeSnapshot {
104    #[serde(default)]
105    pub decisions: Vec<DecisionSnapshot>,
106    #[serde(default)]
107    pub hits: Vec<String>,
108    #[serde(default)]
109    pub events: Vec<RuntimeEvent>,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(rename_all = "camelCase", deny_unknown_fields)]
114pub struct ExecutionScope {
115    pub version: usize,
116    pub run_id: String,
117    pub worker_id: String,
118    pub test_id: String,
119    pub test_key: String,
120    pub retry: usize,
121    pub attempt_id: String,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase", deny_unknown_fields)]
126pub struct ServerRecord {
127    #[serde(rename = "type")]
128    pub record_type: String,
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub meta: Option<DecisionMeta>,
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub vector: Option<McdcVector>,
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub id: Option<String>,
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub timestamp_ms: Option<i64>,
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub phase_id: Option<String>,
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub scope: Option<ExecutionScope>,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase", deny_unknown_fields)]
145pub struct CoveragePhase {
146    pub id: String,
147    pub kind: String,
148    pub operation: String,
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub source: Option<String>,
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub caused_by_phase_id: Option<String>,
153    pub started_at_ms: i64,
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub ended_at_ms: Option<i64>,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub status: Option<String>,
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub error: Option<String>,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase", deny_unknown_fields)]
164pub struct TestProvenance {
165    pub runner: String,
166    pub kind: String,
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub project: Option<String>,
169    pub source: String,
170}
171
172impl Default for TestProvenance {
173    fn default() -> Self {
174        Self {
175            runner: "unknown".into(),
176            kind: "unknown".into(),
177            project: None,
178            source: "unknown".into(),
179        }
180    }
181}
182
183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
184#[serde(rename_all = "camelCase", deny_unknown_fields)]
185pub struct RawTestResult {
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub test_id: Option<String>,
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub scope: Option<ExecutionScope>,
190    pub test: String,
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub test_file: Option<String>,
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub title: Option<String>,
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub retry: Option<usize>,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub status: Option<String>,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub expected_status: Option<String>,
201    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
202    pub flaky: bool,
203    #[serde(default)]
204    pub provenance: TestProvenance,
205    #[serde(default = "default_test_role")]
206    pub role: String,
207    #[serde(default)]
208    pub phases: Vec<CoveragePhase>,
209    #[serde(default)]
210    pub runtime: Vec<RuntimeSnapshot>,
211    #[serde(default)]
212    pub browser: Vec<RuntimeSnapshot>,
213    #[serde(default)]
214    pub server: Vec<ServerRecord>,
215}
216
217fn default_test_role() -> String {
218    "test".into()
219}
220
221#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
222#[serde(rename_all = "camelCase")]
223pub struct TestAttempt {
224    pub retry: usize,
225    pub status: String,
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub expected_status: Option<String>,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
231#[serde(rename_all = "camelCase")]
232pub struct CoverageConfidence {
233    pub level: String,
234    pub setup_only: bool,
235    pub background_only: bool,
236    pub asserted: bool,
237    pub tests: Vec<String>,
238    pub asserted_tests: Vec<String>,
239    pub runners: Vec<String>,
240    pub kinds: Vec<String>,
241    pub e2e: bool,
242}
243
244#[derive(Debug, Clone, PartialEq, Serialize)]
245#[serde(rename_all = "camelCase")]
246pub struct VectorObservation {
247    pub vector: McdcVector,
248    pub tests: Vec<String>,
249    #[serde(skip_serializing_if = "Vec::is_empty")]
250    pub phases: Vec<String>,
251    #[serde(skip_serializing_if = "Vec::is_empty")]
252    pub explicit_phases: Vec<String>,
253    pub confidence: CoverageConfidence,
254}
255
256#[derive(Debug, Clone, PartialEq, Serialize)]
257#[serde(rename_all = "camelCase")]
258pub struct ConditionResult {
259    pub index: usize,
260    pub source: String,
261    pub covered: bool,
262    pub assertion_covered: bool,
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub witness: Option<[McdcVector; 2]>,
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub witness_tests: Option<[Vec<String>; 2]>,
267}
268
269#[derive(Debug, Clone, PartialEq, Serialize)]
270#[serde(rename_all = "camelCase")]
271pub struct DecisionResult {
272    pub meta: DecisionMeta,
273    pub executed: bool,
274    pub covered: bool,
275    pub vectors: Vec<McdcVector>,
276    pub vector_observations: Vec<VectorObservation>,
277    pub conditions: Vec<ConditionResult>,
278    pub tests: Vec<String>,
279    pub confidence: CoverageConfidence,
280}
281
282#[derive(Debug, Clone, PartialEq, Serialize)]
283#[serde(rename_all = "camelCase")]
284pub struct PointResult {
285    pub meta: PointMeta,
286    pub covered: bool,
287    pub tests: Vec<String>,
288    pub phases: Vec<String>,
289    pub confidence: CoverageConfidence,
290}
291
292#[derive(Debug, Clone, PartialEq, Serialize)]
293#[serde(rename_all = "camelCase")]
294pub struct AlternativeResult {
295    pub id: String,
296    pub label: String,
297    pub covered: bool,
298    pub tests: Vec<String>,
299    pub phases: Vec<String>,
300    pub confidence: CoverageConfidence,
301}
302
303#[derive(Debug, Clone, PartialEq, Serialize)]
304#[serde(rename_all = "camelCase")]
305pub struct BranchResult {
306    pub meta: BranchMeta,
307    pub covered: bool,
308    pub alternatives: Vec<AlternativeResult>,
309}
310
311#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
312pub struct SourceLine {
313    pub file: String,
314    pub line: usize,
315}
316
317#[derive(Debug, Clone, PartialEq, Serialize)]
318#[serde(rename_all = "camelCase")]
319pub struct LineResult {
320    pub file: String,
321    pub line: usize,
322    pub covered: bool,
323    pub tests: Vec<String>,
324    pub runners: Vec<String>,
325    pub kinds: Vec<String>,
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub exclusive_kind: Option<String>,
328    pub phases: Vec<String>,
329    pub confidence: CoverageConfidence,
330}
331
332#[derive(Debug, Clone, PartialEq, Serialize)]
333#[serde(rename_all = "camelCase")]
334pub struct TestDecisionResult {
335    pub id: String,
336    pub vectors: Vec<McdcVector>,
337}
338
339#[derive(Debug, Clone, PartialEq, Serialize)]
340#[serde(rename_all = "camelCase")]
341pub struct TestCoverageResult {
342    pub id: String,
343    pub name: String,
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub file: Option<String>,
346    #[serde(skip_serializing_if = "Option::is_none")]
347    pub title: Option<String>,
348    pub retries: Vec<usize>,
349    pub attempts: Vec<TestAttempt>,
350    pub outcome: String,
351    pub provenance: TestProvenance,
352    pub role: String,
353    pub hits: Vec<String>,
354    pub decisions: Vec<TestDecisionResult>,
355    pub lines: Vec<SourceLine>,
356}
357
358#[derive(Debug, Clone, PartialEq, Serialize)]
359#[serde(rename_all = "camelCase")]
360pub struct TestFileResult {
361    pub file: String,
362    pub tests: Vec<String>,
363    pub runners: Vec<String>,
364    pub kinds: Vec<String>,
365    pub lines: Vec<SourceLine>,
366}
367
368#[derive(Debug, Clone, PartialEq, Serialize)]
369#[serde(rename_all = "camelCase")]
370pub struct PhaseResult {
371    #[serde(flatten)]
372    pub phase: CoveragePhase,
373    pub test: String,
374    pub hits: Vec<String>,
375    pub decisions: Vec<TestDecisionResult>,
376    pub lines: Vec<SourceLine>,
377    pub browser_events: usize,
378    pub server_events: usize,
379    pub explicit_events: usize,
380    pub inferred_events: usize,
381    pub explicit_browser_events: usize,
382    pub inferred_browser_events: usize,
383    pub explicit_server_events: usize,
384    pub inferred_server_events: usize,
385}
386
387#[derive(Debug, Clone, PartialEq, Serialize)]
388#[serde(rename_all = "camelCase")]
389pub struct DimensionCoverage {
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub kind: Option<String>,
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub runner: Option<String>,
394    pub tests: usize,
395    pub setups: usize,
396    pub summary: CoverageSummary,
397}
398
399#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
400#[serde(rename_all = "camelCase")]
401pub struct CoverageModel {
402    pub name: String,
403    pub completeness_meaning: String,
404    pub measured: Vec<String>,
405    pub not_measured: Vec<String>,
406}
407
408#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
409#[serde(rename_all = "camelCase", deny_unknown_fields)]
410pub struct CoverageModelDeclaration {
411    pub variant: String,
412    pub name: String,
413    pub completeness_meaning: String,
414    pub measured: Vec<String>,
415    pub not_measured: Vec<String>,
416}
417
418#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
419#[serde(rename_all = "camelCase", deny_unknown_fields)]
420pub struct PersistedCoverageModel {
421    pub schema_version: u32,
422    pub variant: String,
423    pub name: String,
424    pub completeness_meaning: String,
425    pub measured: Vec<String>,
426    pub not_measured: Vec<String>,
427}
428
429impl PersistedCoverageModel {
430    pub fn from_declaration(value: &CoverageModelDeclaration) -> Self {
431        Self {
432            schema_version: COVERAGE_MODEL_SCHEMA_VERSION,
433            variant: value.variant.clone(),
434            name: value.name.clone(),
435            completeness_meaning: value.completeness_meaning.clone(),
436            measured: value.measured.clone(),
437            not_measured: value.not_measured.clone(),
438        }
439    }
440
441    fn into_declaration(self) -> Result<CoverageModelDeclaration, &'static str> {
442        if self.schema_version != COVERAGE_MODEL_SCHEMA_VERSION {
443            return Err("unsupported coverage model schema");
444        }
445        let fields = [
446            self.variant.as_str(),
447            self.name.as_str(),
448            self.completeness_meaning.as_str(),
449        ];
450        if fields.iter().any(|field| field.trim().is_empty())
451            || self.measured.is_empty()
452            || self
453                .measured
454                .iter()
455                .chain(&self.not_measured)
456                .any(|item| item.trim().is_empty())
457            || self.measured.iter().collect::<BTreeSet<_>>().len() != self.measured.len()
458            || self.not_measured.iter().collect::<BTreeSet<_>>().len() != self.not_measured.len()
459        {
460            return Err("invalid coverage model declaration");
461        }
462        Ok(CoverageModelDeclaration {
463            variant: self.variant,
464            name: self.name,
465            completeness_meaning: self.completeness_meaning,
466            measured: self.measured,
467            not_measured: self.not_measured,
468        })
469    }
470}
471
472#[derive(Debug, Clone, PartialEq, Serialize)]
473#[serde(rename_all = "camelCase")]
474pub struct CoverageView {
475    pub generated_at: String,
476    pub variant: String,
477    #[serde(skip_serializing_if = "Option::is_none")]
478    pub scope: Option<Value>,
479    pub model: CoverageModel,
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub integrity: Option<Value>,
482    pub limitations: Vec<Value>,
483    #[serde(skip_serializing_if = "Option::is_none")]
484    pub transport: Option<TransportStats>,
485    pub summary: CoverageSummary,
486    pub coverage_by_kind: Vec<DimensionCoverage>,
487    pub coverage_by_runner: Vec<DimensionCoverage>,
488    pub decisions: Vec<DecisionResult>,
489    pub points: Vec<PointResult>,
490    pub branches: Vec<BranchResult>,
491    pub tests: Vec<TestCoverageResult>,
492    pub test_files: Vec<TestFileResult>,
493    pub phases: Vec<PhaseResult>,
494    pub lines: Vec<LineResult>,
495}
496
497#[derive(Debug, Clone, PartialEq, Serialize)]
498#[serde(rename_all = "camelCase")]
499pub struct CoverageFilters {
500    pub passed: CoverageView,
501    pub failed: CoverageView,
502}
503
504#[derive(Debug, Clone, PartialEq, Serialize)]
505#[serde(rename_all = "camelCase")]
506pub struct CoverageReport {
507    #[serde(flatten)]
508    pub view: CoverageView,
509    #[serde(skip_serializing_if = "Option::is_none")]
510    pub execution: Option<ExecutionResult>,
511    pub filters: CoverageFilters,
512}
513
514#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
515#[serde(rename_all = "camelCase")]
516pub struct ExecutionResult {
517    pub test_exit_code: Option<i32>,
518    pub valid: bool,
519}
520
521#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
522#[serde(rename_all = "camelCase")]
523pub struct TransportStats {
524    pub processes: usize,
525    pub child_launches: usize,
526    pub remote_launches: usize,
527    pub workspace_capabilities: usize,
528    pub scoped_server_records: usize,
529    pub background_server_records: usize,
530    pub corrupt_records: usize,
531    pub corrupt_files: usize,
532}
533
534#[derive(Debug, Clone, PartialEq, Deserialize)]
535#[serde(rename_all = "camelCase", deny_unknown_fields)]
536pub struct CoverageReportRequest {
537    pub run_id: String,
538    pub manifest: CoverageManifest,
539    pub raw_results: Vec<RawTestResult>,
540    pub generated_at: String,
541    #[serde(default, skip_serializing_if = "Option::is_none")]
542    pub coverage_model: Option<CoverageModelDeclaration>,
543    #[serde(skip_serializing_if = "Option::is_none")]
544    pub integrity: Option<Value>,
545    #[serde(default, deserialize_with = "deserialize_exit_code")]
546    pub test_exit_code: ExitCodeInput,
547}
548
549#[derive(Debug, Clone, PartialEq, Deserialize)]
550#[serde(rename_all = "camelCase", deny_unknown_fields)]
551pub struct ArchiveReportRequest {
552    pub archive_path: PathBuf,
553    pub run_id: String,
554    pub generated_at: String,
555    #[serde(skip_serializing_if = "Option::is_none")]
556    pub integrity: Option<Value>,
557    #[serde(default, deserialize_with = "deserialize_exit_code")]
558    pub test_exit_code: ExitCodeInput,
559}
560
561#[derive(Debug, Clone, PartialEq, Eq, Default)]
562pub enum ExitCodeInput {
563    #[default]
564    Missing,
565    Present(Option<i32>),
566}
567
568fn deserialize_exit_code<'de, D>(deserializer: D) -> Result<ExitCodeInput, D::Error>
569where
570    D: serde::Deserializer<'de>,
571{
572    Option::<i32>::deserialize(deserializer).map(ExitCodeInput::Present)
573}
574
575#[derive(Debug, Clone, PartialEq, Eq)]
576pub enum ReportError {
577    Analysis(AnalysisError),
578    DecisionAnalysis {
579        decision_id: String,
580        error: AnalysisError,
581    },
582    InvalidEvent(String),
583    InvalidServerRecord(String),
584    InvalidArchive(String),
585    MissingManifest,
586    InvalidJson {
587        path: String,
588        reason: String,
589    },
590    ScopeMismatch {
591        expected: String,
592        actual: String,
593    },
594    NoEvidence(String),
595}
596
597impl From<AnalysisError> for ReportError {
598    fn from(value: AnalysisError) -> Self {
599        Self::Analysis(value)
600    }
601}
602
603#[derive(Clone, Default)]
604struct OrderedVectors {
605    values: Vec<McdcVector>,
606    indexes: HashMap<String, usize>,
607}
608
609impl OrderedVectors {
610    fn insert(&mut self, vector: &McdcVector) -> usize {
611        let key = vector_key(vector);
612        if let Some(index) = self.indexes.get(&key) {
613            return *index;
614        }
615        let index = self.values.len();
616        self.values.push(vector.clone());
617        self.indexes.insert(key, index);
618        index
619    }
620}
621
622#[derive(Clone)]
623struct MutableObservation {
624    vector: McdcVector,
625    tests: BTreeSet<String>,
626    phases: BTreeSet<String>,
627    explicit_phases: BTreeSet<String>,
628}
629
630#[derive(Clone)]
631struct MutableTest {
632    id: String,
633    name: String,
634    file: Option<String>,
635    title: Option<String>,
636    retries: BTreeSet<usize>,
637    attempts: BTreeMap<usize, TestAttempt>,
638    runner_reported_flaky: bool,
639    provenance: TestProvenance,
640    role: String,
641    hits: BTreeSet<String>,
642    decisions: BTreeMap<String, OrderedVectors>,
643}
644
645#[derive(Clone)]
646struct MutablePhase {
647    phase: CoveragePhase,
648    test: String,
649    hits: BTreeSet<String>,
650    decisions: BTreeMap<String, OrderedVectors>,
651    browser_events: usize,
652    server_events: usize,
653    explicit_events: usize,
654    inferred_events: usize,
655    explicit_browser_events: usize,
656    inferred_browser_events: usize,
657    explicit_server_events: usize,
658    inferred_server_events: usize,
659}
660
661fn vector_key(vector: &McdcVector) -> String {
662    let mut key = String::with_capacity(vector.values.len() + 2);
663    for value in &vector.values {
664        key.push(match value {
665            None => '-',
666            Some(false) => 'F',
667            Some(true) => 'T',
668        });
669    }
670    key.push(':');
671    key.push(if vector.outcome { 'T' } else { 'F' });
672    key
673}
674
675fn sorted<T: Clone + Ord>(values: &BTreeSet<T>) -> Vec<T> {
676    values.iter().cloned().collect()
677}
678
679fn record_attempt(test: &mut MutableTest, raw: &RawTestResult) {
680    let (Some(retry), Some(raw_status)) = (raw.retry, raw.status.as_ref()) else {
681        return;
682    };
683    let previous = test.attempts.get(&retry);
684    let status = if raw_status == "unknown" {
685        previous.map_or_else(|| raw_status.clone(), |attempt| attempt.status.clone())
686    } else {
687        raw_status.clone()
688    };
689    let expected_status = raw
690        .expected_status
691        .clone()
692        .or_else(|| previous.and_then(|attempt| attempt.expected_status.clone()));
693    test.attempts.insert(
694        retry,
695        TestAttempt {
696            retry,
697            status,
698            expected_status,
699        },
700    );
701}
702
703fn test_outcome(test: &MutableTest) -> String {
704    let Some(terminal) = test.attempts.values().next_back() else {
705        return "unknown".into();
706    };
707    if terminal.status == "passed"
708        && (test.runner_reported_flaky
709            || test
710                .attempts
711                .values()
712                .take(test.attempts.len().saturating_sub(1))
713                .any(|attempt| attempt.status != "passed"))
714    {
715        "flaky".into()
716    } else {
717        terminal.status.clone()
718    }
719}
720
721fn raw_test_id(raw: &RawTestResult) -> &str {
722    raw.test_id.as_deref().unwrap_or(&raw.test)
723}
724
725pub fn passing_coverage_results(raw_results: &[RawTestResult]) -> Vec<RawTestResult> {
726    let mut attempts: BTreeMap<(String, usize), (BTreeSet<String>, bool)> = BTreeMap::new();
727    for raw in raw_results {
728        let entry = attempts
729            .entry((raw_test_id(raw).into(), raw.retry.unwrap_or(0)))
730            .or_default();
731        if let Some(status) = &raw.status {
732            entry.0.insert(status.clone());
733        }
734        entry.1 |= raw.expected_status.as_deref() == Some("failed");
735    }
736    let mut terminal_retries = BTreeMap::<String, usize>::new();
737    for (test, retry) in attempts.keys() {
738        terminal_retries
739            .entry(test.clone())
740            .and_modify(|value| *value = (*value).max(*retry))
741            .or_insert(*retry);
742    }
743    let accepted = terminal_retries
744        .into_iter()
745        .filter_map(|(test, retry)| {
746            let (statuses, expected_failure) = attempts.get(&(test.clone(), retry))?;
747            (statuses.contains("passed") && !expected_failure).then_some((test, retry))
748        })
749        .collect::<BTreeSet<_>>();
750    raw_results
751        .iter()
752        .filter(|raw| accepted.contains(&(raw_test_id(raw).into(), raw.retry.unwrap_or(0))))
753        .cloned()
754        .collect()
755}
756
757pub fn failed_coverage_results(raw_results: &[RawTestResult]) -> Vec<RawTestResult> {
758    let failed = raw_results
759        .iter()
760        .filter(|raw| raw.status.as_deref() == Some("failed"))
761        .map(|raw| (raw_test_id(raw).to_owned(), raw.retry.unwrap_or(0)))
762        .collect::<BTreeSet<_>>();
763    raw_results
764        .iter()
765        .filter(|raw| failed.contains(&(raw_test_id(raw).into(), raw.retry.unwrap_or(0))))
766        .cloned()
767        .collect()
768}
769
770fn javascript_coverage_model() -> CoverageModelDeclaration {
771    CoverageModelDeclaration {
772        variant: "masking-short-circuit".into(),
773        name: "coverage-completeness-v2".into(),
774        completeness_meaning: "Every obligation in the measured model was observed by at least one existing test; test assertions and product correctness are separate assumptions.".into(),
775        measured: [
776            "executable source lines",
777            "executable statements",
778            "function entries",
779            "true and false outcomes of if, ternary, while, do/while, and classic for decisions",
780            "true and false outcomes of every atomic condition in those decisions",
781            "masking MC/DC independence for every atomic condition in those decisions",
782            "short-circuit and right-evaluated selections for &&, ||, and ?? value expressions, including JSX",
783            "short-circuit and evaluated alternatives for logical assignments and optional chains",
784            "provided and default-evaluated parameter and destructuring values",
785            "try success and catch entry",
786            "zero and entered for-in/for-of loops",
787            "entered switch cases, defaults, and implicit no-match alternatives",
788        ]
789        .into_iter()
790        .map(str::to_owned)
791        .collect(),
792        not_measured: [
793            "all input values or semantic input partitions",
794            "all execution paths or ordering/concurrency interleavings",
795            "destructuring defaults in classic for initializers (reported as blockers when discovered)",
796            "the internal statements and decisions of runtime-generated eval/Function source",
797            "mutation score or assertion fault-detection strength",
798        ]
799        .into_iter()
800        .map(str::to_owned)
801        .collect(),
802    }
803}
804
805fn summary_for_results(
806    decisions: &[DecisionResult],
807    points: &[PointResult],
808    branches: &[BranchResult],
809    lines: &[LineResult],
810    test_ids: Option<&BTreeSet<String>>,
811) -> Result<CoverageSummary, ReportError> {
812    let includes = |tests: &[String], covered: bool| {
813        test_ids.map_or(covered, |selected| {
814            tests.iter().any(|test| selected.contains(test))
815        })
816    };
817    let input = CoverageCoreInput {
818        decisions: decisions
819            .iter()
820            .map(|decision| DecisionCoverage {
821                condition_count: decision.meta.conditions.len(),
822                vectors: decision
823                    .vector_observations
824                    .iter()
825                    .filter(|observation| includes(&observation.tests, true))
826                    .map(|observation| observation.vector.clone())
827                    .collect(),
828            })
829            .collect(),
830        points: points
831            .iter()
832            .map(|point| PointCoverage {
833                kind: point.meta.kind.clone(),
834                covered: includes(&point.tests, point.covered),
835            })
836            .collect(),
837        branches: branches
838            .iter()
839            .map(|branch| BranchCoverage {
840                kind: branch.meta.kind.clone(),
841                alternatives: branch
842                    .alternatives
843                    .iter()
844                    .map(|alternative| includes(&alternative.tests, alternative.covered))
845                    .collect(),
846            })
847            .collect(),
848        lines: lines
849            .iter()
850            .map(|line| includes(&line.tests, line.covered))
851            .collect(),
852    };
853    Ok(analyze_core(&input)?.summary)
854}
855
856/// Recompute every structural coverage metric for an arbitrary set of test,
857/// setup, and background evidence identities. MC/DC witnesses are rebuilt
858/// from the selected observations; existing aggregate verdicts are never
859/// reused.
860pub fn coverage_summary_for_tests(
861    view: &CoverageView,
862    test_ids: &BTreeSet<String>,
863) -> Result<CoverageSummary, ReportError> {
864    summary_for_results(
865        &view.decisions,
866        &view.points,
867        &view.branches,
868        &view.lines,
869        Some(test_ids),
870    )
871}
872
873fn confidence_for(
874    test_ids: impl IntoIterator<Item = String>,
875    phase_ids: impl IntoIterator<Item = String>,
876    explicit_phase_ids: impl IntoIterator<Item = String>,
877    tests: &HashMap<String, MutableTest>,
878    phases: &HashMap<String, MutablePhase>,
879    asserted_phase_ids: &BTreeSet<String>,
880) -> CoverageConfidence {
881    let test_ids = test_ids.into_iter().collect::<BTreeSet<_>>();
882    let phase_ids = phase_ids.into_iter().collect::<BTreeSet<_>>();
883    let explicit_phase_ids = explicit_phase_ids.into_iter().collect::<BTreeSet<_>>();
884    let asserted_phases = explicit_phase_ids
885        .iter()
886        .filter(|id| asserted_phase_ids.contains(*id))
887        .collect::<Vec<_>>();
888    let asserted_tests = asserted_phases
889        .iter()
890        .filter_map(|id| phases.get(*id).map(|phase| phase.test.clone()))
891        .collect::<BTreeSet<_>>();
892    let provenances = test_ids
893        .iter()
894        .filter_map(|id| tests.get(id).map(|test| &test.provenance))
895        .collect::<Vec<_>>();
896    let roles = test_ids
897        .iter()
898        .filter_map(|id| tests.get(id).map(|test| test.role.as_str()))
899        .collect::<Vec<_>>();
900    let phase_kinds = phase_ids
901        .iter()
902        .filter_map(|id| phases.get(id).map(|phase| phase.phase.kind.as_str()))
903        .collect::<Vec<_>>();
904    let only = |phase_kind: &str, role: &str| {
905        if phase_kinds.is_empty() {
906            !roles.is_empty() && roles.iter().all(|value| *value == role)
907        } else {
908            phase_kinds.iter().all(|value| *value == phase_kind)
909        }
910    };
911    let has_action = explicit_phase_ids.iter().any(|id| {
912        phases
913            .get(id)
914            .is_some_and(|phase| phase.phase.kind == "action")
915    });
916    let level = if test_ids.is_empty() {
917        "unexecuted"
918    } else if !asserted_tests.is_empty() {
919        "asserted"
920    } else if has_action {
921        "action"
922    } else {
923        "executed"
924    };
925    let runners = provenances
926        .iter()
927        .map(|provenance| provenance.runner.clone())
928        .collect::<BTreeSet<_>>();
929    let kinds = provenances
930        .iter()
931        .map(|provenance| provenance.kind.clone())
932        .collect::<BTreeSet<_>>();
933    CoverageConfidence {
934        level: level.into(),
935        setup_only: only("setup", "setup"),
936        background_only: only("background", "background"),
937        asserted: !asserted_tests.is_empty(),
938        tests: sorted(&test_ids),
939        asserted_tests: sorted(&asserted_tests),
940        runners: sorted(&runners),
941        e2e: kinds.contains("e2e"),
942        kinds: sorted(&kinds),
943    }
944}
945
946fn add_reference(map: &mut HashMap<String, BTreeSet<String>>, id: &str, value: &str) {
947    map.entry(id.into()).or_default().insert(value.into());
948}
949
950pub fn create_coverage_view(
951    manifest: &CoverageManifest,
952    raw_results: &[RawTestResult],
953    generated_at: &str,
954) -> Result<CoverageView, ReportError> {
955    create_coverage_view_with_model(
956        manifest,
957        raw_results,
958        generated_at,
959        &javascript_coverage_model(),
960    )
961}
962
963fn create_coverage_view_with_model(
964    manifest: &CoverageManifest,
965    raw_results: &[RawTestResult],
966    generated_at: &str,
967    coverage_model: &CoverageModelDeclaration,
968) -> Result<CoverageView, ReportError> {
969    let mut decision_metadata = manifest.decisions.clone();
970    let decision_indexes = decision_metadata
971        .iter()
972        .enumerate()
973        .map(|(index, meta)| (meta.id.clone(), index))
974        .collect::<HashMap<_, _>>();
975    let manifest_files = manifest
976        .decisions
977        .iter()
978        .map(|meta| meta.file.clone())
979        .chain(manifest.points.iter().map(|meta| meta.file.clone()))
980        .chain(manifest.branches.iter().map(|meta| meta.file.clone()))
981        .chain(
982            manifest
983                .scope
984                .as_ref()
985                .and_then(|scope| scope.get("entries"))
986                .and_then(Value::as_array)
987                .into_iter()
988                .flatten()
989                .filter(|entry| entry.get("status").and_then(Value::as_str) == Some("included"))
990                .filter_map(|entry| entry.get("file").and_then(Value::as_str).map(str::to_owned)),
991        )
992        .collect::<BTreeSet<_>>();
993    let mut vectors_by_decision = HashMap::<String, Vec<MutableObservation>>::new();
994    let mut vector_indexes = HashMap::<String, HashMap<String, usize>>::new();
995    let mut tests_by_decision = HashMap::<String, BTreeSet<String>>::new();
996    let mut tests_by_hit = HashMap::<String, BTreeSet<String>>::new();
997    let mut tests_by_id = HashMap::<String, MutableTest>::new();
998    let mut test_order = Vec::<String>::new();
999    let mut phases_by_id = HashMap::<String, MutablePhase>::new();
1000    let mut phases_by_hit = HashMap::<String, BTreeSet<String>>::new();
1001    let mut explicit_phases_by_hit = HashMap::<String, BTreeSet<String>>::new();
1002
1003    for raw in raw_results {
1004        let id = raw_test_id(raw).to_owned();
1005        if !tests_by_id.contains_key(&id) {
1006            test_order.push(id.clone());
1007            tests_by_id.insert(
1008                id.clone(),
1009                MutableTest {
1010                    id: id.clone(),
1011                    name: raw.test.clone(),
1012                    file: raw.test_file.clone(),
1013                    title: raw.title.clone(),
1014                    retries: raw.retry.into_iter().collect(),
1015                    attempts: BTreeMap::new(),
1016                    runner_reported_flaky: raw.flaky,
1017                    provenance: raw.provenance.clone(),
1018                    role: raw.role.clone(),
1019                    hits: BTreeSet::new(),
1020                    decisions: BTreeMap::new(),
1021                },
1022            );
1023        }
1024        let test = tests_by_id.get_mut(&id).expect("test was inserted");
1025        if let Some(retry) = raw.retry {
1026            test.retries.insert(retry);
1027        }
1028        test.runner_reported_flaky |= raw.flaky;
1029        record_attempt(test, raw);
1030
1031        let mut ordered_phases = raw.phases.clone();
1032        ordered_phases.sort_by_key(|phase| phase.started_at_ms);
1033        for phase in &ordered_phases {
1034            phases_by_id.insert(
1035                phase.id.clone(),
1036                MutablePhase {
1037                    phase: phase.clone(),
1038                    test: id.clone(),
1039                    hits: BTreeSet::new(),
1040                    decisions: BTreeMap::new(),
1041                    browser_events: 0,
1042                    server_events: 0,
1043                    explicit_events: 0,
1044                    inferred_events: 0,
1045                    explicit_browser_events: 0,
1046                    inferred_browser_events: 0,
1047                    explicit_server_events: 0,
1048                    inferred_server_events: 0,
1049                },
1050            );
1051        }
1052
1053        let correlate = |event: &RuntimeEvent| {
1054            event.phase_id.clone().or_else(|| {
1055                ordered_phases
1056                    .iter()
1057                    .take_while(|phase| phase.started_at_ms <= event.timestamp_ms)
1058                    .last()
1059                    .map(|phase| phase.id.clone())
1060            })
1061        };
1062
1063        let snapshots = raw.runtime.iter().chain(&raw.browser);
1064        for snapshot in snapshots {
1065            for decision in &snapshot.decisions {
1066                let Some(index) = decision_indexes.get(&decision.meta.id).copied() else {
1067                    if manifest_files.contains(&decision.meta.file) {
1068                        return Err(ReportError::InvalidServerRecord(format!(
1069                            "decision {} is absent from the frozen manifest",
1070                            decision.meta.id
1071                        )));
1072                    }
1073                    continue;
1074                };
1075                if decision_metadata[index] != decision.meta {
1076                    return Err(ReportError::InvalidServerRecord(format!(
1077                        "decision {} metadata differs from the frozen manifest",
1078                        decision.meta.id
1079                    )));
1080                }
1081                for vector in &decision.vectors {
1082                    let key = vector_key(vector);
1083                    let indexes = vector_indexes.entry(decision.meta.id.clone()).or_default();
1084                    let observations = vectors_by_decision
1085                        .entry(decision.meta.id.clone())
1086                        .or_default();
1087                    let observation_index = *indexes.entry(key.clone()).or_insert_with(|| {
1088                        observations.push(MutableObservation {
1089                            vector: vector.clone(),
1090                            tests: BTreeSet::new(),
1091                            phases: BTreeSet::new(),
1092                            explicit_phases: BTreeSet::new(),
1093                        });
1094                        observations.len() - 1
1095                    });
1096                    observations[observation_index].tests.insert(id.clone());
1097                    tests_by_id
1098                        .get_mut(&id)
1099                        .expect("registered test")
1100                        .decisions
1101                        .entry(decision.meta.id.clone())
1102                        .or_default()
1103                        .insert(vector);
1104                }
1105                if !decision.vectors.is_empty() {
1106                    add_reference(&mut tests_by_decision, &decision.meta.id, &id);
1107                }
1108            }
1109            for hit in &snapshot.hits {
1110                add_reference(&mut tests_by_hit, hit, &id);
1111                tests_by_id
1112                    .get_mut(&id)
1113                    .expect("registered test")
1114                    .hits
1115                    .insert(hit.clone());
1116            }
1117            for event in &snapshot.events {
1118                let explicit = event.phase_id.is_some();
1119                let Some(phase_id) = correlate(event) else {
1120                    continue;
1121                };
1122                let Some(phase) = phases_by_id.get_mut(&phase_id) else {
1123                    continue;
1124                };
1125                if event.environment == "browser" {
1126                    phase.browser_events += 1;
1127                    if explicit {
1128                        phase.explicit_browser_events += 1;
1129                    } else {
1130                        phase.inferred_browser_events += 1;
1131                    }
1132                } else {
1133                    phase.server_events += 1;
1134                    if explicit {
1135                        phase.explicit_server_events += 1;
1136                    } else {
1137                        phase.inferred_server_events += 1;
1138                    }
1139                }
1140                if explicit {
1141                    phase.explicit_events += 1;
1142                } else {
1143                    phase.inferred_events += 1;
1144                }
1145                if event.event_type == "hit" {
1146                    phase.hits.insert(event.id.clone());
1147                    add_reference(&mut phases_by_hit, &event.id, &phase_id);
1148                    if explicit {
1149                        add_reference(&mut explicit_phases_by_hit, &event.id, &phase_id);
1150                    }
1151                } else if event.event_type == "decision" {
1152                    let vector = event
1153                        .vector
1154                        .as_ref()
1155                        .ok_or_else(|| ReportError::InvalidEvent(event.id.clone()))?;
1156                    phase
1157                        .decisions
1158                        .entry(event.id.clone())
1159                        .or_default()
1160                        .insert(vector);
1161                    if let Some(index) = vector_indexes
1162                        .get(&event.id)
1163                        .and_then(|indexes| indexes.get(&vector_key(vector)))
1164                        .copied()
1165                        && let Some(observation) = vectors_by_decision
1166                            .get_mut(&event.id)
1167                            .and_then(|observations| observations.get_mut(index))
1168                    {
1169                        observation.phases.insert(phase_id.clone());
1170                        if explicit {
1171                            observation.explicit_phases.insert(phase_id.clone());
1172                        }
1173                    }
1174                } else {
1175                    return Err(ReportError::InvalidEvent(event.event_type.clone()));
1176                }
1177            }
1178        }
1179
1180        for record in &raw.server {
1181            let (record_id, decision) = if record.record_type == "decision" {
1182                let meta = record
1183                    .meta
1184                    .as_ref()
1185                    .ok_or_else(|| ReportError::InvalidServerRecord("missing meta".into()))?;
1186                let vector = record
1187                    .vector
1188                    .as_ref()
1189                    .ok_or_else(|| ReportError::InvalidServerRecord("missing vector".into()))?;
1190                let Some(index) = decision_indexes.get(&meta.id).copied() else {
1191                    if manifest_files.contains(&meta.file) {
1192                        return Err(ReportError::InvalidServerRecord(format!(
1193                            "decision {} is absent from the frozen manifest",
1194                            meta.id
1195                        )));
1196                    }
1197                    continue;
1198                };
1199                if decision_metadata[index] != *meta {
1200                    return Err(ReportError::InvalidServerRecord(format!(
1201                        "decision {} metadata differs from the frozen manifest",
1202                        meta.id
1203                    )));
1204                }
1205                let key = vector_key(vector);
1206                let indexes = vector_indexes.entry(meta.id.clone()).or_default();
1207                let observations = vectors_by_decision.entry(meta.id.clone()).or_default();
1208                let index = *indexes.entry(key).or_insert_with(|| {
1209                    observations.push(MutableObservation {
1210                        vector: vector.clone(),
1211                        tests: BTreeSet::new(),
1212                        phases: BTreeSet::new(),
1213                        explicit_phases: BTreeSet::new(),
1214                    });
1215                    observations.len() - 1
1216                });
1217                observations[index].tests.insert(id.clone());
1218                tests_by_id
1219                    .get_mut(&id)
1220                    .expect("registered test")
1221                    .decisions
1222                    .entry(meta.id.clone())
1223                    .or_default()
1224                    .insert(vector);
1225                add_reference(&mut tests_by_decision, &meta.id, &id);
1226                (meta.id.clone(), Some(vector.clone()))
1227            } else if record.record_type == "hit" {
1228                let hit = record
1229                    .id
1230                    .as_ref()
1231                    .ok_or_else(|| ReportError::InvalidServerRecord("missing hit id".into()))?;
1232                add_reference(&mut tests_by_hit, hit, &id);
1233                tests_by_id
1234                    .get_mut(&id)
1235                    .expect("registered test")
1236                    .hits
1237                    .insert(hit.clone());
1238                (hit.clone(), None)
1239            } else {
1240                return Err(ReportError::InvalidServerRecord(record.record_type.clone()));
1241            };
1242            let Some(timestamp_ms) = record.timestamp_ms else {
1243                continue;
1244            };
1245            let event = RuntimeEvent {
1246                event_type: record.record_type.clone(),
1247                id: record_id,
1248                vector: decision,
1249                timestamp_ms,
1250                phase_id: record.phase_id.clone(),
1251                environment: "server".into(),
1252            };
1253            let explicit = event.phase_id.is_some();
1254            let phase_id = correlate(&event);
1255            let Some(phase_id) = phase_id else { continue };
1256            let Some(phase) = phases_by_id.get_mut(&phase_id) else {
1257                continue;
1258            };
1259            phase.server_events += 1;
1260            if explicit {
1261                phase.explicit_events += 1;
1262                phase.explicit_server_events += 1;
1263            } else {
1264                phase.inferred_events += 1;
1265                phase.inferred_server_events += 1;
1266            }
1267            if event.event_type == "hit" {
1268                phase.hits.insert(event.id.clone());
1269                add_reference(&mut phases_by_hit, &event.id, &phase_id);
1270                if explicit {
1271                    add_reference(&mut explicit_phases_by_hit, &event.id, &phase_id);
1272                }
1273            } else if let Some(vector) = &event.vector {
1274                phase
1275                    .decisions
1276                    .entry(event.id.clone())
1277                    .or_default()
1278                    .insert(vector);
1279                if let Some(index) = vector_indexes
1280                    .get(&event.id)
1281                    .and_then(|indexes| indexes.get(&vector_key(vector)))
1282                    .copied()
1283                    && let Some(observation) = vectors_by_decision
1284                        .get_mut(&event.id)
1285                        .and_then(|observations| observations.get_mut(index))
1286                {
1287                    observation.phases.insert(phase_id.clone());
1288                    if explicit {
1289                        observation.explicit_phases.insert(phase_id.clone());
1290                    }
1291                }
1292            }
1293        }
1294    }
1295
1296    let mut asserted_phase_ids = BTreeSet::new();
1297    for phase in phases_by_id.values() {
1298        if phase.phase.kind == "assertion" && phase.phase.status.as_deref() == Some("passed") {
1299            asserted_phase_ids.insert(phase.phase.id.clone());
1300            if let Some(cause) = &phase.phase.caused_by_phase_id {
1301                asserted_phase_ids.insert(cause.clone());
1302            }
1303        }
1304    }
1305
1306    decision_metadata.sort_by(|left, right| {
1307        left.file
1308            .cmp(&right.file)
1309            .then(left.line.cmp(&right.line))
1310            .then(left.column.cmp(&right.column))
1311    });
1312    let mut decisions = Vec::with_capacity(decision_metadata.len());
1313    for meta in decision_metadata {
1314        let mutable = vectors_by_decision.remove(&meta.id).unwrap_or_default();
1315        let mut observations = Vec::with_capacity(mutable.len());
1316        for observation in mutable {
1317            let confidence = confidence_for(
1318                sorted(&observation.tests),
1319                sorted(&observation.phases),
1320                sorted(&observation.explicit_phases),
1321                &tests_by_id,
1322                &phases_by_id,
1323                &asserted_phase_ids,
1324            );
1325            observations.push(VectorObservation {
1326                vector: observation.vector,
1327                tests: sorted(&observation.tests),
1328                phases: sorted(&observation.phases),
1329                explicit_phases: sorted(&observation.explicit_phases),
1330                confidence,
1331            });
1332        }
1333        let vectors = observations
1334            .iter()
1335            .map(|observation| observation.vector.clone())
1336            .collect::<Vec<_>>();
1337        let witnesses =
1338            find_witnesses_for_conditions(&vectors, meta.conditions.len()).map_err(|error| {
1339                ReportError::DecisionAnalysis {
1340                    decision_id: meta.id.clone(),
1341                    error,
1342                }
1343            })?;
1344        let mut conditions = Vec::with_capacity(meta.conditions.len());
1345        for (index, source) in meta.conditions.iter().enumerate() {
1346            let witness = witnesses[index].map(|witness| {
1347                [
1348                    vectors[witness.first].clone(),
1349                    vectors[witness.second].clone(),
1350                ]
1351            });
1352            let witness_tests = witnesses[index].map(|witness| {
1353                [
1354                    observations[witness.first].tests.clone(),
1355                    observations[witness.second].tests.clone(),
1356                ]
1357            });
1358            let assertion_covered = (0..observations.len()).any(|left| {
1359                ((left + 1)..observations.len()).any(|right| {
1360                    observations[left].confidence.asserted
1361                        && observations[right].confidence.asserted
1362                        && crate::coverage_analysis::is_independence_pair(
1363                            &observations[left].vector,
1364                            &observations[right].vector,
1365                            index,
1366                        )
1367                })
1368            });
1369            conditions.push(ConditionResult {
1370                index,
1371                source: source.clone(),
1372                covered: witness.is_some(),
1373                assertion_covered,
1374                witness,
1375                witness_tests,
1376            });
1377        }
1378        let decision_tests = tests_by_decision.remove(&meta.id).unwrap_or_default();
1379        let confidence = confidence_for(
1380            sorted(&decision_tests),
1381            observations
1382                .iter()
1383                .flat_map(|observation| observation.phases.clone()),
1384            observations
1385                .iter()
1386                .flat_map(|observation| observation.explicit_phases.clone()),
1387            &tests_by_id,
1388            &phases_by_id,
1389            &asserted_phase_ids,
1390        );
1391        decisions.push(DecisionResult {
1392            executed: !vectors.is_empty(),
1393            covered: conditions.iter().all(|condition| condition.covered),
1394            meta,
1395            vectors,
1396            vector_observations: observations,
1397            conditions,
1398            tests: sorted(&decision_tests),
1399            confidence,
1400        });
1401    }
1402
1403    let points = manifest
1404        .points
1405        .iter()
1406        .cloned()
1407        .map(|meta| {
1408            let tests = tests_by_hit.get(&meta.id).cloned().unwrap_or_default();
1409            let phases = phases_by_hit.get(&meta.id).cloned().unwrap_or_default();
1410            let explicit = explicit_phases_by_hit
1411                .get(&meta.id)
1412                .cloned()
1413                .unwrap_or_default();
1414            PointResult {
1415                covered: tests_by_hit.contains_key(&meta.id),
1416                confidence: confidence_for(
1417                    sorted(&tests),
1418                    sorted(&phases),
1419                    sorted(&explicit),
1420                    &tests_by_id,
1421                    &phases_by_id,
1422                    &asserted_phase_ids,
1423                ),
1424                meta,
1425                tests: sorted(&tests),
1426                phases: sorted(&phases),
1427            }
1428        })
1429        .collect::<Vec<_>>();
1430
1431    let branches = manifest
1432        .branches
1433        .iter()
1434        .cloned()
1435        .map(|meta| {
1436            let alternatives = meta
1437                .alternatives
1438                .iter()
1439                .map(|alternative| {
1440                    let tests = tests_by_hit
1441                        .get(&alternative.id)
1442                        .cloned()
1443                        .unwrap_or_default();
1444                    let phases = phases_by_hit
1445                        .get(&alternative.id)
1446                        .cloned()
1447                        .unwrap_or_default();
1448                    let explicit = explicit_phases_by_hit
1449                        .get(&alternative.id)
1450                        .cloned()
1451                        .unwrap_or_default();
1452                    AlternativeResult {
1453                        id: alternative.id.clone(),
1454                        label: alternative.label.clone(),
1455                        covered: tests_by_hit.contains_key(&alternative.id),
1456                        tests: sorted(&tests),
1457                        phases: sorted(&phases),
1458                        confidence: confidence_for(
1459                            sorted(&tests),
1460                            sorted(&phases),
1461                            sorted(&explicit),
1462                            &tests_by_id,
1463                            &phases_by_id,
1464                            &asserted_phase_ids,
1465                        ),
1466                    }
1467                })
1468                .collect::<Vec<_>>();
1469            BranchResult {
1470                covered: alternatives.iter().all(|alternative| alternative.covered),
1471                meta,
1472                alternatives,
1473            }
1474        })
1475        .collect::<Vec<_>>();
1476
1477    let mut line_aggregates =
1478        BTreeMap::<SourceLine, (bool, BTreeSet<String>, BTreeSet<String>, BTreeSet<String>)>::new();
1479    for point in &points {
1480        let aggregate = line_aggregates
1481            .entry(SourceLine {
1482                file: point.meta.file.clone(),
1483                line: point.meta.line,
1484            })
1485            .or_default();
1486        aggregate.0 |= point.covered;
1487        aggregate.1.extend(point.tests.clone());
1488        aggregate.2.extend(point.phases.clone());
1489        aggregate.3.extend(
1490            explicit_phases_by_hit
1491                .get(&point.meta.id)
1492                .into_iter()
1493                .flatten()
1494                .cloned(),
1495        );
1496    }
1497    let lines = line_aggregates
1498        .into_iter()
1499        .map(|(location, (covered, test_ids, phase_ids, explicit_ids))| {
1500            let provenances = test_ids
1501                .iter()
1502                .filter_map(|id| tests_by_id.get(id).map(|test| &test.provenance))
1503                .collect::<Vec<_>>();
1504            let runners = provenances
1505                .iter()
1506                .map(|provenance| provenance.runner.clone())
1507                .collect::<BTreeSet<_>>();
1508            let kinds = provenances
1509                .iter()
1510                .map(|provenance| provenance.kind.clone())
1511                .collect::<BTreeSet<_>>();
1512            LineResult {
1513                file: location.file,
1514                line: location.line,
1515                covered,
1516                tests: sorted(&test_ids),
1517                runners: sorted(&runners),
1518                exclusive_kind: (kinds.len() == 1).then(|| kinds.first().unwrap().clone()),
1519                phases: sorted(&phase_ids),
1520                confidence: confidence_for(
1521                    sorted(&test_ids),
1522                    sorted(&phase_ids),
1523                    sorted(&explicit_ids),
1524                    &tests_by_id,
1525                    &phases_by_id,
1526                    &asserted_phase_ids,
1527                ),
1528                kinds: sorted(&kinds),
1529            }
1530        })
1531        .collect::<Vec<_>>();
1532
1533    let point_locations = manifest
1534        .points
1535        .iter()
1536        .map(|point| {
1537            (
1538                point.id.clone(),
1539                SourceLine {
1540                    file: point.file.clone(),
1541                    line: point.line,
1542                },
1543            )
1544        })
1545        .collect::<HashMap<_, _>>();
1546    test_order.sort_by(|left, right| tests_by_id[left].name.cmp(&tests_by_id[right].name));
1547    let tests = test_order
1548        .into_iter()
1549        .map(|id| {
1550            let test = tests_by_id.get(&id).expect("test order references test");
1551            let lines = test
1552                .hits
1553                .iter()
1554                .filter_map(|hit| point_locations.get(hit).cloned())
1555                .collect::<BTreeSet<_>>();
1556            TestCoverageResult {
1557                id: test.id.clone(),
1558                name: test.name.clone(),
1559                file: test.file.clone(),
1560                title: test.title.clone(),
1561                retries: sorted(&test.retries),
1562                attempts: test.attempts.values().cloned().collect(),
1563                outcome: test_outcome(test),
1564                provenance: test.provenance.clone(),
1565                role: test.role.clone(),
1566                hits: sorted(&test.hits),
1567                decisions: test
1568                    .decisions
1569                    .iter()
1570                    .map(|(id, vectors)| TestDecisionResult {
1571                        id: id.clone(),
1572                        vectors: vectors.values.clone(),
1573                    })
1574                    .collect(),
1575                lines: lines.into_iter().collect(),
1576            }
1577        })
1578        .collect::<Vec<_>>();
1579
1580    let mut test_files = BTreeMap::<
1581        String,
1582        (
1583            BTreeSet<String>,
1584            BTreeSet<String>,
1585            BTreeSet<String>,
1586            BTreeSet<SourceLine>,
1587        ),
1588    >::new();
1589    for test in &tests {
1590        let aggregate = test_files
1591            .entry(
1592                test.file
1593                    .clone()
1594                    .unwrap_or_else(|| "(unknown test file)".into()),
1595            )
1596            .or_default();
1597        aggregate.0.insert(test.id.clone());
1598        aggregate.1.insert(test.provenance.runner.clone());
1599        aggregate.2.insert(test.provenance.kind.clone());
1600        aggregate.3.extend(test.lines.clone());
1601    }
1602    let test_files = test_files
1603        .into_iter()
1604        .map(|(file, (tests, runners, kinds, lines))| TestFileResult {
1605            file,
1606            tests: sorted(&tests),
1607            runners: sorted(&runners),
1608            kinds: sorted(&kinds),
1609            lines: lines.into_iter().collect(),
1610        })
1611        .collect::<Vec<_>>();
1612
1613    let mut phases = phases_by_id.values().cloned().collect::<Vec<_>>();
1614    phases.sort_by(|left, right| {
1615        left.phase
1616            .started_at_ms
1617            .cmp(&right.phase.started_at_ms)
1618            .then(left.phase.id.cmp(&right.phase.id))
1619    });
1620    let phases = phases
1621        .into_iter()
1622        .map(|phase| {
1623            let lines = phase
1624                .hits
1625                .iter()
1626                .filter_map(|hit| point_locations.get(hit).cloned())
1627                .collect::<BTreeSet<_>>();
1628            PhaseResult {
1629                phase: phase.phase,
1630                test: phase.test,
1631                hits: sorted(&phase.hits),
1632                decisions: phase
1633                    .decisions
1634                    .into_iter()
1635                    .map(|(id, vectors)| TestDecisionResult {
1636                        id,
1637                        vectors: vectors.values,
1638                    })
1639                    .collect(),
1640                lines: lines.into_iter().collect(),
1641                browser_events: phase.browser_events,
1642                server_events: phase.server_events,
1643                explicit_events: phase.explicit_events,
1644                inferred_events: phase.inferred_events,
1645                explicit_browser_events: phase.explicit_browser_events,
1646                inferred_browser_events: phase.inferred_browser_events,
1647                explicit_server_events: phase.explicit_server_events,
1648                inferred_server_events: phase.inferred_server_events,
1649            }
1650        })
1651        .collect::<Vec<_>>();
1652
1653    let mut summary = summary_for_results(&decisions, &points, &branches, &lines, None)?;
1654    if !manifest.limitations.is_empty() {
1655        summary.coverage_complete = false;
1656        summary.completeness_blocked = Some(true);
1657    }
1658
1659    let dimension_coverage = |field: &str| -> Result<Vec<DimensionCoverage>, ReportError> {
1660        let values = tests
1661            .iter()
1662            .map(|test| {
1663                if field == "kind" {
1664                    test.provenance.kind.clone()
1665                } else {
1666                    test.provenance.runner.clone()
1667                }
1668            })
1669            .collect::<BTreeSet<_>>();
1670        values
1671            .into_iter()
1672            .map(|value| {
1673                let selected = tests
1674                    .iter()
1675                    .filter(|test| {
1676                        if field == "kind" {
1677                            test.provenance.kind == value
1678                        } else {
1679                            test.provenance.runner == value
1680                        }
1681                    })
1682                    .map(|test| test.id.clone())
1683                    .collect::<BTreeSet<_>>();
1684                Ok(DimensionCoverage {
1685                    kind: (field == "kind").then(|| value.clone()),
1686                    runner: (field == "runner").then(|| value.clone()),
1687                    tests: tests
1688                        .iter()
1689                        .filter(|test| selected.contains(&test.id) && test.role == "test")
1690                        .count(),
1691                    setups: tests
1692                        .iter()
1693                        .filter(|test| selected.contains(&test.id) && test.role == "setup")
1694                        .count(),
1695                    summary: summary_for_results(
1696                        &decisions,
1697                        &points,
1698                        &branches,
1699                        &lines,
1700                        Some(&selected),
1701                    )?,
1702                })
1703            })
1704            .collect()
1705    };
1706
1707    Ok(CoverageView {
1708        generated_at: generated_at.into(),
1709        variant: coverage_model.variant.clone(),
1710        scope: manifest.scope.clone(),
1711        model: CoverageModel {
1712            name: coverage_model.name.clone(),
1713            completeness_meaning: coverage_model.completeness_meaning.clone(),
1714            measured: coverage_model.measured.clone(),
1715            not_measured: coverage_model.not_measured.clone(),
1716        },
1717        integrity: None,
1718        limitations: manifest.limitations.clone(),
1719        transport: None,
1720        summary,
1721        coverage_by_kind: dimension_coverage("kind")?,
1722        coverage_by_runner: dimension_coverage("runner")?,
1723        decisions,
1724        points,
1725        branches,
1726        tests,
1727        test_files,
1728        phases,
1729        lines,
1730    })
1731}
1732
1733pub fn analyze_coverage_results(
1734    request: &CoverageReportRequest,
1735) -> Result<CoverageReport, ReportError> {
1736    if let Some(scope) = request
1737        .raw_results
1738        .iter()
1739        .filter_map(|raw| raw.scope.as_ref())
1740        .find(|scope| scope.run_id != request.run_id)
1741    {
1742        return Err(ReportError::ScopeMismatch {
1743            expected: request.run_id.clone(),
1744            actual: scope.run_id.clone(),
1745        });
1746    }
1747    if request.raw_results.is_empty() {
1748        return Err(ReportError::NoEvidence(request.run_id.clone()));
1749    }
1750    let default_model = javascript_coverage_model();
1751    let coverage_model = request.coverage_model.as_ref().unwrap_or(&default_model);
1752    let view = create_coverage_view_with_model(
1753        &request.manifest,
1754        &request.raw_results,
1755        &request.generated_at,
1756        coverage_model,
1757    )?;
1758    let passed = create_coverage_view_with_model(
1759        &request.manifest,
1760        &passing_coverage_results(&request.raw_results),
1761        &request.generated_at,
1762        coverage_model,
1763    )?;
1764    let failed = create_coverage_view_with_model(
1765        &request.manifest,
1766        &failed_coverage_results(&request.raw_results),
1767        &request.generated_at,
1768        coverage_model,
1769    )?;
1770    let execution = match request.test_exit_code {
1771        ExitCodeInput::Missing => None,
1772        ExitCodeInput::Present(test_exit_code) => Some(ExecutionResult {
1773            valid: test_exit_code == Some(0),
1774            test_exit_code,
1775        }),
1776    };
1777    let mut view = view;
1778    let mut passed = passed;
1779    let mut failed = failed;
1780    if let Some(integrity) = &request.integrity {
1781        view.integrity = Some(integrity.clone());
1782        passed.integrity = Some(integrity.clone());
1783        failed.integrity = Some(integrity.clone());
1784    }
1785    Ok(CoverageReport {
1786        view,
1787        execution,
1788        filters: CoverageFilters { passed, failed },
1789    })
1790}
1791
1792fn parse_entry<T: for<'de> Deserialize<'de>>(
1793    entry: &EvidenceArchiveEntry,
1794) -> Result<T, ReportError> {
1795    serde_json::from_slice(&entry.contents).map_err(|error| ReportError::InvalidJson {
1796        path: entry.path.clone(),
1797        reason: error.to_string(),
1798    })
1799}
1800
1801fn parse_json_lines<'a, T: for<'de> Deserialize<'de>>(
1802    entries: impl Iterator<Item = &'a EvidenceArchiveEntry>,
1803) -> (Vec<T>, usize, usize) {
1804    let mut records = Vec::new();
1805    let mut corrupt_records = 0;
1806    let mut corrupt_files = BTreeSet::new();
1807    for entry in entries {
1808        for line in entry.contents.split(|byte| *byte == b'\n') {
1809            if line.is_empty() {
1810                continue;
1811            }
1812            match serde_json::from_slice(line) {
1813                Ok(record) => records.push(record),
1814                Err(_) => {
1815                    corrupt_records += 1;
1816                    corrupt_files.insert(entry.path.clone());
1817                }
1818            }
1819        }
1820    }
1821    (records, corrupt_records, corrupt_files.len())
1822}
1823
1824fn is_mcdc_result(path: &str) -> bool {
1825    path == "mcdc.json" || path.ends_with("/mcdc.json")
1826}
1827
1828pub fn analyze_coverage_archive(
1829    request: &ArchiveReportRequest,
1830) -> Result<CoverageReport, ReportError> {
1831    let archive = read_versioned_archive(Path::new(&request.archive_path))
1832        .map_err(|error| ReportError::InvalidArchive(error.to_string()))?;
1833    let entries = archive.entries;
1834    let manifest = entries
1835        .iter()
1836        .find(|entry| entry.path == "manifest.json")
1837        .ok_or(ReportError::MissingManifest)
1838        .and_then(parse_entry::<CoverageManifest>)?;
1839    let mut raw_results = entries
1840        .iter()
1841        .filter(|entry| is_mcdc_result(&entry.path))
1842        .map(parse_entry::<RawTestResult>)
1843        .collect::<Result<Vec<_>, _>>()?;
1844
1845    let (scoped_records, scoped_corrupt, scoped_corrupt_files) =
1846        parse_json_lines::<ServerRecord>(entries.iter().filter(|entry| {
1847            entry.path.starts_with("server/")
1848                && !entry.path.starts_with("server/background/")
1849                && entry.path.ends_with(".jsonl")
1850        }));
1851    for record in &scoped_records {
1852        let Some(scope) = &record.scope else { continue };
1853        let Some(raw) = raw_results
1854            .iter_mut()
1855            .find(|raw| raw_test_id(raw) == scope.test_id && raw.retry.unwrap_or(0) == scope.retry)
1856        else {
1857            continue;
1858        };
1859        if !raw.server.contains(record) {
1860            raw.server.push(record.clone());
1861        }
1862    }
1863
1864    let (background_records, background_corrupt, background_corrupt_files) =
1865        parse_json_lines::<ServerRecord>(entries.iter().filter(|entry| {
1866            entry.path.starts_with("server/background/") && entry.path.ends_with(".jsonl")
1867        }));
1868    if !background_records.is_empty() {
1869        raw_results.push(RawTestResult {
1870            test_id: Some(format!("background:{}", request.run_id)),
1871            scope: None,
1872            test: "Background / unattributed".into(),
1873            test_file: None,
1874            title: Some("Background / unattributed".into()),
1875            retry: None,
1876            status: Some("unknown".into()),
1877            expected_status: None,
1878            flaky: false,
1879            provenance: TestProvenance {
1880                runner: "background".into(),
1881                kind: "background".into(),
1882                project: None,
1883                source: "explicit".into(),
1884            },
1885            role: "background".into(),
1886            phases: vec![],
1887            runtime: vec![],
1888            browser: vec![],
1889            server: background_records.clone(),
1890        });
1891    }
1892
1893    let (execution_events, execution_corrupt, execution_corrupt_files) =
1894        parse_json_lines::<Value>(entries.iter().filter(|entry| {
1895            entry.path.starts_with("execution.") && entry.path.ends_with(".jsonl")
1896        }));
1897    let count_event = |name: &str| {
1898        execution_events
1899            .iter()
1900            .filter(|event| event.get("event").and_then(Value::as_str) == Some(name))
1901            .count()
1902    };
1903    let transport = TransportStats {
1904        processes: count_event("process"),
1905        child_launches: count_event("child-launch"),
1906        remote_launches: count_event("remote-launch"),
1907        workspace_capabilities: count_event("workspace-capability"),
1908        scoped_server_records: scoped_records.len(),
1909        background_server_records: background_records.len(),
1910        corrupt_records: scoped_corrupt + background_corrupt + execution_corrupt,
1911        corrupt_files: scoped_corrupt_files + background_corrupt_files + execution_corrupt_files,
1912    };
1913    let (frontend, coverage_model) = if archive.schema_version == EVIDENCE_ARCHIVE_V3_SCHEMA_VERSION
1914    {
1915        let frontend = entries
1916            .iter()
1917            .find(|entry| entry.path == "frontend.json")
1918            .ok_or_else(|| ReportError::InvalidArchive("missing frontend.json".into()))
1919            .and_then(parse_entry::<FrontendRunDeclaration>)?;
1920        let persisted = entries
1921            .iter()
1922            .find(|entry| entry.path == "coverage-model.json")
1923            .ok_or_else(|| ReportError::InvalidArchive("missing coverage-model.json".into()))
1924            .and_then(parse_entry::<PersistedCoverageModel>)?;
1925        let model = persisted
1926            .into_declaration()
1927            .map_err(|reason| ReportError::InvalidJson {
1928                path: "coverage-model.json".into(),
1929                reason: reason.into(),
1930            })?;
1931        (Some(frontend), Some(model))
1932    } else {
1933        (None, None)
1934    };
1935    let normalized = CoverageReportRequest {
1936        run_id: request.run_id.clone(),
1937        manifest,
1938        raw_results,
1939        generated_at: request.generated_at.clone(),
1940        coverage_model,
1941        integrity: request.integrity.clone(),
1942        test_exit_code: request.test_exit_code.clone(),
1943    };
1944    let mut report = if let Some(frontend) = frontend {
1945        crate::frontend_protocol::analyze_frontend_results(&frontend, &normalized)
1946            .map_err(|error| ReportError::InvalidArchive(error.to_string()))?
1947    } else {
1948        analyze_coverage_results(&normalized)?
1949    };
1950    report.view.transport = Some(transport.clone());
1951    report.filters.passed.transport = Some(transport.clone());
1952    report.filters.failed.transport = Some(transport);
1953    Ok(report)
1954}
1955
1956#[cfg(test)]
1957mod tests {
1958    use std::{fs, time::SystemTime};
1959
1960    use crate::evidence_archive::{EvidenceArchiveEntry, write_archive};
1961
1962    use super::*;
1963
1964    fn point(id: &str, line: usize) -> PointMeta {
1965        PointMeta {
1966            id: id.into(),
1967            kind: PointKind::Statement,
1968            file: "src/app.js".into(),
1969            line,
1970            column: 0,
1971            source: "work();".into(),
1972            label: None,
1973        }
1974    }
1975
1976    fn raw(id: &str, retry: usize, status: &str, hits: &[&str]) -> RawTestResult {
1977        RawTestResult {
1978            test_id: Some(id.into()),
1979            scope: None,
1980            test: id.into(),
1981            test_file: Some("tests/app.test.js".into()),
1982            title: None,
1983            retry: Some(retry),
1984            status: Some(status.into()),
1985            expected_status: None,
1986            flaky: false,
1987            provenance: TestProvenance {
1988                runner: "node:test".into(),
1989                kind: "unit".into(),
1990                project: None,
1991                source: "runner-default".into(),
1992            },
1993            role: "test".into(),
1994            phases: vec![],
1995            runtime: vec![RuntimeSnapshot {
1996                decisions: vec![],
1997                hits: hits.iter().map(|hit| (*hit).into()).collect(),
1998                events: vec![],
1999            }],
2000            browser: vec![],
2001            server: vec![],
2002        }
2003    }
2004
2005    #[test]
2006    fn report_retains_unexecuted_manifest_conditions() {
2007        let manifest = CoverageManifest {
2008            decisions: vec![DecisionMeta {
2009                id: "decision".into(),
2010                file: "src/app.js".into(),
2011                line: 1,
2012                column: 0,
2013                source: "left && right".into(),
2014                conditions: vec!["left".into(), "right".into()],
2015                kind: "if".into(),
2016            }],
2017            points: vec![],
2018            branches: vec![],
2019            limitations: vec![],
2020            scope: None,
2021        };
2022        let view =
2023            create_coverage_view(&manifest, &[raw("test", 0, "passed", &[])], "time").unwrap();
2024        assert_eq!(view.summary.conditions, 2);
2025        assert_eq!(view.summary.covered_conditions, 0);
2026        assert_eq!(view.decisions[0].conditions.len(), 2);
2027    }
2028
2029    #[test]
2030    fn frozen_manifest_ignores_out_of_scope_synthetic_decisions() {
2031        let manifest = CoverageManifest {
2032            decisions: vec![DecisionMeta {
2033                id: "application-decision".into(),
2034                file: "src/app.js".into(),
2035                line: 1,
2036                column: 0,
2037                source: "left && right".into(),
2038                conditions: vec!["left".into(), "right".into()],
2039                kind: "if".into(),
2040            }],
2041            points: vec![],
2042            branches: vec![],
2043            limitations: vec![],
2044            scope: None,
2045        };
2046        let mut attempt = raw("test", 0, "passed", &[]);
2047        attempt.runtime[0].decisions.push(DecisionSnapshot {
2048            meta: DecisionMeta {
2049                id: "synthetic-fixture".into(),
2050                file: "fixtures/generated.js".into(),
2051                line: 1,
2052                column: 0,
2053                source: "a && b && c".into(),
2054                conditions: vec!["a".into(), "b".into(), "c".into()],
2055                kind: "if".into(),
2056            },
2057            vectors: vec![McdcVector {
2058                values: vec![Some(true), Some(true), Some(true)],
2059                outcome: true,
2060            }],
2061        });
2062
2063        let view = create_coverage_view(&manifest, &[attempt], "time").unwrap();
2064        assert_eq!(view.decisions.len(), 1);
2065        assert_eq!(view.decisions[0].meta.id, "application-decision");
2066        assert!(view.decisions[0].vectors.is_empty());
2067    }
2068
2069    #[test]
2070    fn frozen_manifest_rejects_in_scope_unknown_or_changed_decisions() {
2071        let expected = DecisionMeta {
2072            id: "application-decision".into(),
2073            file: "src/app.js".into(),
2074            line: 1,
2075            column: 0,
2076            source: "left && right".into(),
2077            conditions: vec!["left".into(), "right".into()],
2078            kind: "if".into(),
2079        };
2080        let manifest = CoverageManifest {
2081            decisions: vec![expected.clone()],
2082            points: vec![],
2083            branches: vec![],
2084            limitations: vec![],
2085            scope: Some(serde_json::json!({
2086                "entries": [{ "file": "src/empty.js", "status": "included" }]
2087            })),
2088        };
2089        let vector = McdcVector {
2090            values: vec![Some(true), Some(true)],
2091            outcome: true,
2092        };
2093
2094        let mut unknown = raw("test", 0, "passed", &[]);
2095        unknown.runtime[0].decisions.push(DecisionSnapshot {
2096            meta: DecisionMeta {
2097                id: "unknown-decision".into(),
2098                file: "src/empty.js".into(),
2099                ..expected.clone()
2100            },
2101            vectors: vec![vector.clone()],
2102        });
2103        assert!(matches!(
2104            create_coverage_view(&manifest, &[unknown], "time"),
2105            Err(ReportError::InvalidServerRecord(reason))
2106                if reason.contains("absent from the frozen manifest")
2107        ));
2108
2109        let mut changed = raw("test", 0, "passed", &[]);
2110        changed.runtime[0].decisions.push(DecisionSnapshot {
2111            meta: DecisionMeta {
2112                source: "left || right".into(),
2113                ..expected
2114            },
2115            vectors: vec![vector],
2116        });
2117        assert!(matches!(
2118            create_coverage_view(&manifest, &[changed], "time"),
2119            Err(ReportError::InvalidServerRecord(reason))
2120                if reason.contains("differs from the frozen manifest")
2121        ));
2122    }
2123
2124    #[test]
2125    fn timestamp_overlap_cannot_upgrade_assertion_confidence() {
2126        let manifest = CoverageManifest {
2127            decisions: vec![],
2128            points: vec![point("hit", 1)],
2129            branches: vec![],
2130            limitations: vec![],
2131            scope: None,
2132        };
2133        let phase = CoveragePhase {
2134            id: "assertion".into(),
2135            kind: "assertion".into(),
2136            operation: "equal".into(),
2137            source: None,
2138            caused_by_phase_id: None,
2139            started_at_ms: 100,
2140            ended_at_ms: Some(120),
2141            status: Some("passed".into()),
2142            error: None,
2143        };
2144        let mut attempt = raw("test", 0, "passed", &["hit"]);
2145        attempt.phases.push(phase.clone());
2146        attempt.runtime[0].events.push(RuntimeEvent {
2147            event_type: "hit".into(),
2148            id: "hit".into(),
2149            vector: None,
2150            timestamp_ms: 110,
2151            phase_id: None,
2152            environment: "server".into(),
2153        });
2154        let inferred = create_coverage_view(&manifest, &[attempt.clone()], "time").unwrap();
2155        assert_eq!(inferred.points[0].confidence.level, "executed");
2156        assert!(!inferred.points[0].confidence.asserted);
2157
2158        attempt.runtime[0].events[0].phase_id = Some(phase.id);
2159        let explicit = create_coverage_view(&manifest, &[attempt], "time").unwrap();
2160        assert_eq!(explicit.points[0].confidence.level, "asserted");
2161        assert!(explicit.points[0].confidence.asserted);
2162    }
2163
2164    #[test]
2165    fn verified_view_uses_only_the_terminal_successful_attempt() {
2166        let manifest = CoverageManifest {
2167            decisions: vec![],
2168            points: vec![point("failed", 1), point("passed", 2), point("expected", 3)],
2169            branches: vec![],
2170            limitations: vec![],
2171            scope: None,
2172        };
2173        let failed = raw("flaky", 0, "failed", &["failed"]);
2174        let mut passed = raw("flaky", 1, "passed", &["passed"]);
2175        passed.flaky = true;
2176        let mut expected = raw("expected-failure", 0, "passed", &["expected"]);
2177        expected.expected_status = Some("failed".into());
2178        let request = CoverageReportRequest {
2179            run_id: "run".into(),
2180            manifest,
2181            raw_results: vec![failed, passed, expected],
2182            generated_at: "time".into(),
2183            coverage_model: None,
2184            integrity: None,
2185            test_exit_code: ExitCodeInput::Missing,
2186        };
2187        let report = analyze_coverage_results(&request).unwrap();
2188        assert!(!report.filters.passed.points[0].covered);
2189        assert!(report.filters.passed.points[1].covered);
2190        assert!(!report.filters.passed.points[2].covered);
2191        assert!(report.filters.failed.points[0].covered);
2192        assert_eq!(report.view.tests[1].outcome, "flaky");
2193    }
2194
2195    fn archive(entries: Vec<EvidenceArchiveEntry>) -> PathBuf {
2196        let nonce = SystemTime::now()
2197            .duration_since(SystemTime::UNIX_EPOCH)
2198            .unwrap()
2199            .as_nanos();
2200        let root = std::env::temp_dir().join(format!(
2201            "supercov-rust-report-{}-{nonce}",
2202            std::process::id()
2203        ));
2204        fs::create_dir_all(&root).unwrap();
2205        let path = root.join("evidence.raw.gz");
2206        write_archive(entries, &path).unwrap();
2207        path
2208    }
2209
2210    #[test]
2211    fn archive_analysis_keeps_valid_lines_and_reports_transport_corruption() {
2212        let manifest = CoverageManifest {
2213            decisions: vec![],
2214            points: vec![point("background-hit", 1)],
2215            branches: vec![],
2216            limitations: vec![],
2217            scope: None,
2218        };
2219        let path = archive(vec![
2220            EvidenceArchiveEntry {
2221                path: "manifest.json".into(),
2222                contents: serde_json::to_vec(&manifest).unwrap(),
2223            },
2224            EvidenceArchiveEntry {
2225                path: "server/background/worker.jsonl".into(),
2226                contents: b"{\"type\":\"hit\",\"id\":\"background-hit\"}\nnot-json\n".to_vec(),
2227            },
2228        ]);
2229        let report = analyze_coverage_archive(&ArchiveReportRequest {
2230            archive_path: path.clone(),
2231            run_id: "run".into(),
2232            generated_at: "time".into(),
2233            integrity: None,
2234            test_exit_code: ExitCodeInput::Missing,
2235        })
2236        .unwrap();
2237        assert!(report.view.points[0].covered);
2238        assert_eq!(report.view.tests[0].role, "background");
2239        assert_eq!(report.view.transport.as_ref().unwrap().corrupt_records, 1);
2240        assert_eq!(report.view.transport.as_ref().unwrap().corrupt_files, 1);
2241        fs::remove_dir_all(path.parent().unwrap()).unwrap();
2242    }
2243
2244    #[test]
2245    fn archive_analysis_rejects_cross_run_evidence() {
2246        let manifest = CoverageManifest {
2247            decisions: vec![],
2248            points: vec![],
2249            branches: vec![],
2250            limitations: vec![],
2251            scope: None,
2252        };
2253        let mut result = raw("test", 0, "passed", &[]);
2254        result.scope = Some(ExecutionScope {
2255            version: 1,
2256            run_id: "other-run".into(),
2257            worker_id: "worker".into(),
2258            test_id: "test".into(),
2259            test_key: "key".into(),
2260            retry: 0,
2261            attempt_id: "attempt".into(),
2262        });
2263        let path = archive(vec![
2264            EvidenceArchiveEntry {
2265                path: "manifest.json".into(),
2266                contents: serde_json::to_vec(&manifest).unwrap(),
2267            },
2268            EvidenceArchiveEntry {
2269                path: "worker/mcdc.json".into(),
2270                contents: serde_json::to_vec(&result).unwrap(),
2271            },
2272        ]);
2273        assert_eq!(
2274            analyze_coverage_archive(&ArchiveReportRequest {
2275                archive_path: path.clone(),
2276                run_id: "run".into(),
2277                generated_at: "time".into(),
2278                integrity: None,
2279                test_exit_code: ExitCodeInput::Missing,
2280            }),
2281            Err(ReportError::ScopeMismatch {
2282                expected: "run".into(),
2283                actual: "other-run".into(),
2284            })
2285        );
2286        fs::remove_dir_all(path.parent().unwrap()).unwrap();
2287    }
2288
2289    #[test]
2290    fn explicit_null_exit_code_remains_distinct_from_an_absent_exit_code() {
2291        let request: CoverageReportRequest = serde_json::from_value(serde_json::json!({
2292            "runId": "run",
2293            "manifest": { "decisions": [], "points": [], "branches": [] },
2294            "rawResults": [{
2295                "test": "test",
2296                "status": "passed",
2297                "browser": [],
2298                "server": []
2299            }],
2300            "generatedAt": "time",
2301            "testExitCode": null
2302        }))
2303        .unwrap();
2304        let report = analyze_coverage_results(&request).unwrap();
2305        assert_eq!(
2306            report.execution,
2307            Some(ExecutionResult {
2308                test_exit_code: None,
2309                valid: false,
2310            })
2311        );
2312    }
2313}