use serde_json::json;
use std::collections::BTreeSet;
use supercov_engine::{assertion_map::*, assertion_store::assess, coverage_report::*};
fn anchor(file: &str, text: &str, needle: &str) -> Anchor {
let start = text.find(needle).unwrap();
Anchor::new(file, text, start, start + needle.len())
}
fn fixture() -> (Inputs, AssertionMap, State) {
let test = "import assert from 'node:assert/strict';\nassert.equal(result, 1);\n";
let source = "return 1;\n";
let at = anchor("test.js", test, "assert.equal(result, 1)");
let inputs = Inputs {
schema_version: 1,
language: "javascript".into(),
context_digest: "context".into(),
files: Files::from([
("test.js".into(), test.into()),
("src/a.js".into(), source.into()),
("src/b.js".into(), source.into()),
("src/helper.js".into(), "helper();\n".into()),
]),
assertions: vec![InventorySite {
at,
operation: "assert.equal".into(),
}],
limitations: vec![],
};
let (mut map, state) = seed(&inputs, "archive");
let a = &mut map.assertions[0];
a.observes = vec!["result equals one".into()];
a.flows = ["a", "b"]
.into_iter()
.map(|id| Flow {
id: id.into(),
basis: None,
questions: vec![],
explanation: format!("Author judgement for {id}"),
applies_to: vec![TestSelector {
file: "test.js".into(),
name: "test".into(),
}],
nodes: vec![Node {
id: "return".into(),
at: anchor(&format!("src/{id}.js"), source, "return 1;"),
role: "value".into(),
meaning: String::new(),
}],
edges: vec![Edge {
from: "return".into(),
to: "$assertion".into(),
kind: "data".into(),
basis: String::new(),
}],
counts_as_asserted: vec!["return".into()],
watch: vec![format!("src/{id}.js"), "test.js".into()],
})
.collect();
acknowledge(&mut map, &state, &inputs, &BTreeSet::new(), true, false).unwrap();
(inputs, map, state)
}
fn acknowledge(
map: &mut AssertionMap,
state: &State,
inputs: &Inputs,
selected: &BTreeSet<String>,
all: bool,
changes: bool,
) -> Result<(), String> {
if state.inputs_digest != inputs.identity() {
return Err("inputs changed".into());
}
if changes {
for c in &state.changes {
let mut response = ChangeAssessment {
id: c.id.clone(),
basis: None,
affected_flows: c
.known_flows
.iter()
.filter(|key| {
map.assertions
.iter()
.any(|a| a.flows.iter().any(|f| flow_key(a, f) == **key))
})
.cloned()
.collect(),
explanation: "Fixture agent inspected the change".into(),
};
response.basis = Some(expected_change_basis(c, &response, &inputs.manifest()));
map.change_assessments.retain(|r| r.id != c.id);
map.change_assessments.push(response);
}
}
let snapshot = map.clone();
for a in &mut map.assertions {
let prior = snapshot.assertions.iter().find(|p| p.id == a.id).unwrap();
for f in &mut a.flows {
if all || selected.contains(&flow_key(prior, f)) {
if !validate_flow(f, &inputs.files).is_empty()
|| prior.at.offset(&inputs.files).is_none()
{
return Err("invalid reference".into());
}
f.basis = Some(expected_basis(
prior,
f,
&snapshot,
state,
&inputs.manifest(),
));
}
}
}
Ok(())
}
fn current(map: &AssertionMap, state: &State, inputs: &Inputs) -> usize {
map.assertions
.iter()
.flat_map(|a| a.flows.iter().map(move |f| (a, f)))
.filter(|(a, f)| reasons(a, f, map, state, inputs).is_empty())
.count()
}
#[test]
fn serialized_manifest_reuses_analysis_without_previous_source() {
let (old, map, state) = fixture();
let encoded = serde_json::to_vec(&old.manifest()).unwrap();
let old_manifest: InputManifest = serde_json::from_slice(&encoded).unwrap();
let mut new = old.clone();
drop(old);
new.files.insert("src/a.js".into(), "return 2;\n".into());
let (next, state) = carry(&map, &state, &old_manifest, &new, "new", false).unwrap();
assert_eq!(next.assertions[0].id, map.assertions[0].id);
assert_eq!(next.assertions[0].flows, map.assertions[0].flows);
assert_eq!(current(&next, &state, &new), 1);
assert_eq!(state.changes.len(), 1);
let report = assess(&next, &state, &new, &report_fixture(&["b"], true), true);
assert_eq!(report["summary"]["statements"]["asserted"], 1);
}
#[test]
fn test_setup_changes_dirty_flows_even_without_an_explicit_test_watch() {
let (old, mut map, state) = fixture();
for flow in &mut map.assertions[0].flows {
flow.watch.retain(|w| w != "test.js");
}
acknowledge(&mut map, &state, &old, &BTreeSet::new(), true, false).unwrap();
let mut new = old.clone();
new.files
.get_mut("test.js")
.unwrap()
.push_str("setupChanged();\n");
let (next, state) = carry(&map, &state, &old.manifest(), &new, "new", false).unwrap();
assert_eq!(current(&next, &state, &new), 0);
assert_eq!(next.assertions[0].flows, map.assertions[0].flows);
}
#[test]
fn changing_a_node_location_requires_review_even_when_its_text_is_identical() {
let (mut inputs, mut map, mut state) = fixture();
inputs
.files
.insert("src/a.js".into(), "return 1;\nreturn 1;\n".into());
state.inputs_digest = inputs.identity();
acknowledge(&mut map, &state, &inputs, &BTreeSet::new(), true, false).unwrap();
map.assertions[0].flows[0].nodes[0].at.line = 2;
assert!(validate(&map, &inputs).is_empty());
let (next, state) = carry(&map, &state, &inputs.manifest(), &inputs, "new", false).unwrap();
assert_eq!(current(&next, &state, &inputs), 1);
}
#[test]
fn input_archive_contains_hashes_and_requires_matching_current_files() {
use supercov_engine::assertion_inputs::{append, capture, current_sources};
let root = std::env::temp_dir().join(format!("supercov-manifest-{}", std::process::id()));
std::fs::create_dir_all(&root).unwrap();
let source = "// source-only-sentinel-not-an-assertion\nimport assert from 'node:assert/strict';\nassert.equal(1, 1);\n";
std::fs::write(root.join("test.js"), source).unwrap();
let inputs = capture(&root, "javascript", ["test.js".into()]).unwrap();
let entries = append(vec![], &inputs).unwrap();
let encoded = &entries[0].contents;
assert!(!String::from_utf8_lossy(encoded).contains("source-only-sentinel"));
let value: serde_json::Value = serde_json::from_slice(encoded).unwrap();
assert_eq!(value["schemaVersion"], 2);
assert_eq!(value["files"]["test.js"]["bytes"], source.len());
assert!(value["files"]["test.js"]["sha256"].as_str().unwrap().len() == 64);
let manifest: InputManifest = serde_json::from_slice(encoded).unwrap();
assert_eq!(
current_sources(&root, &manifest).unwrap().files,
inputs.files
);
std::fs::write(root.join("test.js"), source.replace("1, 1", "2, 2")).unwrap();
assert!(
current_sources(&root, &manifest)
.unwrap_err()
.contains("Current source differs")
);
std::fs::remove_file(root.join("test.js")).unwrap();
assert!(current_sources(&root, &manifest).is_err());
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn unchanged_files_reuse_review_and_blank_line_moves_preserve_dirty_mappings() {
let (old, map, state) = fixture();
let (same, same_state) = carry(&map, &state, &old.manifest(), &old, "same", false).unwrap();
assert_eq!(current(&same, &same_state, &old), 2);
let mut new = old.clone();
for text in new.files.values_mut() {
*text = format!("\n\n{text}");
}
for site in &mut new.assertions {
site.at.line += 2;
}
let (carried, state) = carry(&map, &state, &old.manifest(), &new, "new", false).unwrap();
assert_eq!(current(&carried, &state, &new), 0);
assert!(
state
.changes
.iter()
.any(|c| c.file.as_deref() == Some("src/helper.js"))
);
assert_eq!(map.assertions[0].id, carried.assertions[0].id);
assert_eq!(carried.assertions[0].at.line, 4);
}
#[test]
fn changed_flow_stays_dirty_through_carries_and_partial_review() {
let (old, map, state) = fixture();
let mut new = old.clone();
new.files.insert("src/a.js".into(), "return 2;\n".into());
let (mut map, state) = carry(&map, &state, &old.manifest(), &new, "two", false).unwrap();
assert_eq!(current(&map, &state, &new), 1);
let (next, state) = carry(&map, &state, &new.manifest(), &new, "three", false).unwrap();
map = next;
assert_eq!(current(&map, &state, &new), 1);
let selected = BTreeSet::from([flow_key(&map.assertions[0], &map.assertions[0].flows[1])]);
acknowledge(&mut map, &state, &new, &selected, false, false).unwrap();
assert_eq!(current(&map, &state, &new), 1);
assert!(acknowledge(&mut map, &state, &new, &BTreeSet::new(), true, false).is_err());
map.assertions[0].flows[0].nodes[0].at.text = "return 2;".into();
acknowledge(&mut map, &state, &new, &BTreeSet::new(), true, false).unwrap();
assert_eq!(current(&map, &state, &new), 2);
}
#[test]
fn changed_assertion_keeps_its_explanation_as_a_dirty_identity_suggestion() {
let (old, map, state) = fixture();
let mut new = old.clone();
new.files
.get_mut("test.js")
.unwrap()
.replace_range(.., &old.files["test.js"].replace("result, 1", "result, 2"));
new.assertions[0].at.text = "assert.equal(result, 2)".into();
let (next, state) = carry(&map, &state, &old.manifest(), &new, "two", false).unwrap();
assert_eq!(next.assertions.len(), 1);
assert_eq!(next.assertions[0].id, map.assertions[0].id);
assert_eq!(
next.assertions[0].flows[0].explanation,
map.assertions[0].flows[0].explanation
);
assert_eq!(current(&next, &state, &new), 0);
}
#[test]
fn newly_declared_watch_missing_in_old_snapshot_does_not_panic_on_carry() {
let (old, mut map, state) = fixture();
let mut new = old.clone();
map.assertions[0].flows[0].watch.push("new.js".into());
new.files.insert("new.js".into(), "new();".into());
let (next, state) = carry(&map, &state, &old.manifest(), &new, "two", false).unwrap();
assert_eq!(current(&next, &state, &new), 1);
}
#[test]
fn revert_does_not_implicitly_acknowledge_review() {
let (old, map, state) = fixture();
let mut new = old.clone();
new.files.insert("src/a.js".into(), "return 2;\n".into());
let (map, state) = carry(&map, &state, &old.manifest(), &new, "two", false).unwrap();
let (map, state) = carry(&map, &state, &new.manifest(), &old, "three", false).unwrap();
assert_eq!(current(&map, &state, &old), 1);
}
#[test]
fn map_edits_require_acknowledgement_even_with_unchanged_sources() {
let (inputs, mut map, state) = fixture();
map.assertions[0].flows[0].explanation.push_str(" revised");
assert_eq!(current(&map, &state, &inputs), 1);
let (map, state) = carry(&map, &state, &inputs.manifest(), &inputs, "two", false).unwrap();
assert_eq!(current(&map, &state, &inputs), 1);
}
#[test]
fn assertion_meaning_edit_invalidates_every_flow() {
let (inputs, mut map, state) = fixture();
map.assertions[0].observes.push("another property".into());
assert_eq!(current(&map, &state, &inputs), 0);
}
#[test]
fn deleted_or_changed_assertions_keep_their_explanations() {
let (old, map, state) = fixture();
let mut new = old.clone();
new.assertions.clear();
new.files.insert("test.js".into(), String::new());
let (next, _) = carry(&map, &state, &old.manifest(), &new, "two", false).unwrap();
assert!(next.assertions.is_empty());
assert_eq!(next.retired_assertions[0].assertion, map.assertions[0]);
}
#[test]
fn new_assertions_are_unmapped_and_do_not_steal_old_ids() {
let (old, map, state) = fixture();
let mut new = old.clone();
new.files
.get_mut("test.js")
.unwrap()
.push_str("assert.equal(other, 2);\n");
new.assertions.push(InventorySite {
at: anchor("test.js", &new.files["test.js"], "assert.equal(other, 2)"),
operation: "assert.equal".into(),
});
let (next, _) = carry(&map, &state, &old.manifest(), &new, "two", false).unwrap();
assert_eq!(next.assertions.len(), 2);
assert_eq!(next.assertions[0].id, map.assertions[0].id);
assert!(next.assertions[1].flows.is_empty());
}
#[test]
fn ambiguous_duplicate_is_never_matched_by_distance() {
let old = Files::from([("a.js".into(), "before(); x(); after();".into())]);
let at = anchor("a.js", &old["a.js"], "x()");
let new = Files::from([("a.js".into(), "changed(); x(); x(); after();".into())]);
let hashes = old
.iter()
.map(|(p, s)| (p.clone(), FileFingerprint::of(s)))
.collect();
assert!(relocate(&at, &hashes, &new).is_none());
}
#[test]
fn unique_file_rename_rebases_and_ambiguous_rename_dirties() {
let (mut old, mut map, state) = fixture();
old.files
.insert("src/a.js".into(), "return 1;\n// unique".into());
let mut state = state;
acknowledge(&mut map, &state, &old, &BTreeSet::new(), true, false).unwrap_err();
state.inputs_digest = old.identity();
acknowledge(&mut map, &state, &old, &BTreeSet::new(), true, false).unwrap();
let mut new = old.clone();
let source = new.files.remove("src/a.js").unwrap();
new.files.insert("src/renamed.js".into(), source.clone());
let (next, next_state) = carry(&map, &state, &old.manifest(), &new, "two", false).unwrap();
assert_eq!(current(&next, &next_state, &new), 1);
assert_eq!(next_state.changes.len(), 2);
new.files.insert("src/other.js".into(), source);
let (next, next_state) = carry(&map, &state, &old.manifest(), &new, "two", false).unwrap();
assert_eq!(current(&next, &next_state, &new), 1);
}
#[test]
fn unwatched_changes_and_new_files_queue_scope_review() {
let (old, map, state) = fixture();
let mut new = old.clone();
new.files
.insert("src/helper.js".into(), "changed();\n".into());
new.files.insert("src/new.js".into(), "newThing();".into());
let (mut map, state) = carry(&map, &state, &old.manifest(), &new, "two", false).unwrap();
assert_eq!(current(&map, &state, &new), 2);
assert_eq!(state.changes.len(), 2);
acknowledge(&mut map, &state, &new, &BTreeSet::new(), false, true).unwrap();
assert!(
state
.changes
.iter()
.all(|c| change_current(&map, c, &new.manifest()))
);
}
#[test]
fn context_change_dirties_all_without_new_tracing() {
let (inputs, map, state) = fixture();
let (map, state) = carry(&map, &state, &inputs.manifest(), &inputs, "two", true).unwrap();
assert_eq!(current(&map, &state, &inputs), 0);
}
#[test]
fn whole_file_changes_queue_impact_even_when_already_watched() {
let (mut old, mut map, mut state) = fixture();
old.files
.insert("src/a.js".into(), "setup();\nreturn 1;\n".into());
map.assertions[0].flows[0].nodes[0].at.line = 2;
state.inputs_digest = old.identity();
acknowledge(&mut map, &state, &old, &BTreeSet::new(), true, false).unwrap();
let mut new = old.clone();
new.files
.insert("src/a.js".into(), "setupChanged();\nreturn 1;\n".into());
let (map, state) = carry(&map, &state, &old.manifest(), &new, "two", false).unwrap();
assert_eq!(current(&map, &state, &new), 1);
assert!(
state
.changes
.iter()
.any(|c| c.file.as_deref() == Some("src/a.js"))
);
}
#[test]
fn unicode_and_multiline_anchors_use_utf8_byte_columns() {
let source = "🙂; assert(\n true\n);";
let at = anchor("test.js", source, "assert(\n true\n)");
assert_eq!(at.column, 7);
assert_eq!(
at.offset(&Files::from([("test.js".into(), source.into())])),
Some(6)
);
let mut invalid = at;
invalid.column = 2;
assert!(
invalid
.offset(&Files::from([("test.js".into(), source.into())]))
.is_none()
);
}
#[test]
fn validation_rejects_dangling_edges_unknown_credit_duplicate_ids_and_bad_paths() {
let (inputs, map, _) = fixture();
for kind in 0..4 {
let mut map = map.clone();
let f = &mut map.assertions[0].flows[0];
match kind {
0 => f.edges.push(Edge {
from: "absent".into(),
to: "return".into(),
kind: "agent-defined".into(),
basis: String::new(),
}),
1 => f.counts_as_asserted.push("absent".into()),
2 => f.nodes.push(f.nodes[0].clone()),
_ => f.nodes[0].at.file = "../outside".into(),
};
assert!(!validate(&map, &inputs).is_empty());
}
}
fn report_fixture(hits: &[&str], assertion_passed: bool) -> CoverageReport {
let phase = json!({"id":"phase","kind":"assertion","operation":"assert.equal","source":"test.js:2:1","startedAtMs":0,"endedAtMs":1,"status":if assertion_passed {"passed"}else{"failed"}});
let raw = json!({"testId":"test","test":"test","testFile":"test.js","retry":0,"status":"passed","flaky":false,"provenance":{"runner":"node:test","kind":"unit","source":"test"},"role":"test","phases":[phase],"runtime":[{"hits":hits}],"browser":[],"server":[]});
let request: CoverageReportRequest=serde_json::from_value(json!({"runId":"run","generatedAt":"now","testExitCode":0,
"manifest":{"decisions":[],"branches":[],"points":[
{"id":"a","kind":"statement","file":"src/a.js","line":1,"column":1,"source":"return 1;"},
{"id":"b","kind":"statement","file":"src/b.js","line":1,"column":1,"source":"return 1;"}]},"rawResults":[raw]})).unwrap();
analyze_coverage_results(&request).unwrap()
}
#[test]
fn shared_setup_is_visible_but_never_borrowed_as_same_test_evidence() {
let (inputs, map, state) = fixture();
let mut coverage = report_fixture(&["a", "b"], true);
for view in [&mut coverage.view, &mut coverage.filters.passed] {
let mut setup = view.tests[0].clone();
setup.id = "setup".into();
setup.name = "shared setup".into();
setup.role = "setup".into();
view.tests[0].hits.clear();
view.tests[0].lines.clear();
view.tests.push(setup);
for point in &mut view.points {
point.tests = vec!["setup".into()];
}
}
let report = assess(&map, &state, &inputs, &coverage, true);
assert_eq!(report["summary"]["statements"]["asserted"], 0);
assert_eq!(
report["statements"][0]["executionEvidence"]["anyExecution"],
true
);
assert_eq!(
report["statements"][0]["executionEvidence"]["passingTests"],
json!([])
);
assert_eq!(
report["statements"][0]["executionEvidence"]["outsidePassingTests"],
json!(["setup"])
);
assert!(
report["assertions"][0]["flows"][0]["nodeCredit"][0]["reasons"]
.as_array()
.unwrap()
.iter()
.any(|r| r["code"] == "shared_setup_execution")
);
for phase in &mut coverage.filters.passed.phases {
phase.test = "setup".into();
}
let report = assess(&map, &state, &inputs, &coverage, true);
assert_eq!(report["summary"]["unobservedAssertions"], 1);
}
#[test]
fn score_requires_explicit_credit_current_review_and_passing_site() {
let (inputs, mut map, state) = fixture();
let coverage = report_fixture(&["a", "b"], true);
assert_eq!(
assess(&map, &state, &inputs, &coverage, true)["summary"]["lines"]["percentage"],
100.0
);
let failed_site = report_fixture(&["a", "b"], false);
assert_eq!(
assess(&map, &state, &inputs, &failed_site, true)["summary"]["lines"]["asserted"],
0
);
map.assertions[0].flows[0].counts_as_asserted.clear();
assert_eq!(
assess(&map, &state, &inputs, &coverage, true)["summary"]["lines"]["asserted"],
1
);
assert_eq!(
assess(&map, &state, &inputs, &coverage, false)["summary"]["lines"]["asserted"],
0
);
}
#[test]
fn absence_claims_do_not_credit_unexecuted_code_and_scope_blocks_credit() {
let (inputs, map, mut state) = fixture();
let coverage = report_fixture(&["a"], true);
assert_eq!(
assess(&map, &state, &inputs, &coverage, true)["summary"]["lines"]["percentage"],
50.0
);
add_change(
&mut state,
None,
None,
None,
"unknown helper".into(),
BTreeSet::new(),
);
assert_eq!(
assess(&map, &state, &inputs, &coverage, true)["summary"]["statements"]["percentage"],
serde_json::Value::Null
);
}
#[test]
fn unsupported_runtime_identity_does_not_borrow_test_level_assertions() {
let (inputs, map, state) = fixture();
let mut coverage = report_fixture(&["a", "b"], true);
coverage.filters.passed.phases[0].phase.source = Some("test.js::whole_test".into());
assert_eq!(
assess(&map, &state, &inputs, &coverage, true)["summary"]["lines"]["asserted"],
0
);
}
#[test]
fn unmeasured_statements_never_enter_the_primary_denominator_or_numerator() {
let (inputs, map, state) = fixture();
let mut coverage = report_fixture(&["a", "b"], true);
coverage.filters.passed.points[1].measured = false;
let report = assess(&map, &state, &inputs, &coverage, true);
assert_eq!(report["summary"]["statements"]["total"], 1);
assert_eq!(report["summary"]["statements"]["asserted"], 1);
assert_eq!(report["summary"]["statements"]["percentage"], 100.0);
}
#[test]
fn execution_from_another_test_cannot_supply_credit() {
let (inputs, map, state) = fixture();
let mut coverage = report_fixture(&["a", "b"], true);
for p in &mut coverage.filters.passed.points {
p.tests = vec!["different test".into()];
}
assert_eq!(
assess(&map, &state, &inputs, &coverage, true)["summary"]["lines"]["asserted"],
0
);
}
#[test]
fn node_credit_distinguishes_context_unexecuted_and_other_test_evidence() {
let (inputs, mut map, state) = fixture();
let flow = &mut map.assertions[0].flows[0];
flow.nodes.push(Node {
id: "context".into(),
at: flow.nodes[0].at.clone(),
role: String::new(),
meaning: "Background explanation only".into(),
});
acknowledge(&mut map, &state, &inputs, &BTreeSet::new(), true, false).unwrap();
let original = map.clone();
let report = assess(&map, &state, &inputs, &report_fixture(&["a"], true), true);
let flows = &report["assertions"][0]["flows"];
let credited = &flows[0]["nodeCredit"][0];
assert_eq!(credited["nodeId"], "return");
assert_eq!(credited["status"], "credited");
assert_eq!(credited["statementIds"], json!(["a"]));
assert_eq!(credited["matchingTests"], json!(["test"]));
assert_eq!(credited["reasons"][0]["code"], "same_test_execution");
assert_eq!(flows[0]["nodeCredit"][1]["status"], "context");
assert_eq!(
flows[0]["nodeCredit"][1]["reasons"][0]["code"],
"context_only"
);
assert_eq!(flows[1]["nodeCredit"][0]["status"], "notCredited");
assert_eq!(
flows[1]["nodeCredit"][0]["reasons"][0]["code"],
"no_passing_execution"
);
assert_eq!(report["summary"]["statements"]["asserted"], 1);
let mut coverage = report_fixture(&["a", "b"], true);
coverage.filters.passed.points[1].tests = vec!["another test".into()];
let report = assess(&map, &state, &inputs, &coverage, true);
let flow = &report["assertions"][0]["flows"][1];
assert_eq!(flow["current"], true);
assert_eq!(flow["eligible"], true);
assert_eq!(
flow["nodeCredit"][0]["reasons"][0]["code"],
"no_same_test_execution"
);
assert_eq!(flow["nodeCredit"][0]["matchingTests"], json!([]));
assert_eq!(report["summary"]["statements"]["asserted"], 1);
assert_eq!(
map, original,
"Credit diagnostics must not mutate authored claims"
);
}
#[test]
fn node_credit_reports_reference_freshness_and_run_blockers() {
let (inputs, map, state) = fixture();
let coverage = report_fixture(&["a", "b"], true);
for (case, expected) in [
(0, "flow_needs_attention"),
(1, "no_measured_statement"),
(2, "invalid_source_anchor"),
(3, "no_passing_assertion"),
(4, "run_failed"),
(5, "invalid_map_identity"),
] {
let mut map = map.clone();
let mut state = state.clone();
let mut coverage = coverage.clone();
match case {
0 => map.assertions[0].flows[0].basis = None,
1 => {
map.assertions[0].flows[0].nodes[0].at.text = "return".into();
acknowledge(&mut map, &state, &inputs, &BTreeSet::new(), true, false).unwrap();
}
2 => map.assertions[0].flows[0].nodes[0].at.line = 99,
3 => coverage.filters.passed.phases.clear(),
4 => (),
5 => state.inputs_digest = "wrong-inputs".into(),
_ => unreachable!(),
}
let report = assess(&map, &state, &inputs, &coverage, case != 4);
let node = &report["assertions"][0]["flows"][0]["nodeCredit"][0];
assert_eq!(node["status"], "notCredited", "{expected}");
assert!(
node["reasons"]
.as_array()
.unwrap()
.iter()
.any(|r| r["code"] == expected),
"{node}"
);
}
}
#[test]
fn frozen_inputs_accept_absolute_relative_and_windows_verbatim_paths_once() {
let temporary = std::env::temp_dir().join(format!("supercov-map-paths-{}", std::process::id()));
let directory = temporary.join("project");
std::fs::create_dir_all(&directory).unwrap();
let canonical = std::fs::canonicalize(&directory).unwrap();
#[cfg(windows)]
let root = std::path::PathBuf::from(canonical.to_str().unwrap().strip_prefix(r"\\?\").unwrap());
#[cfg(not(windows))]
let root = canonical.clone();
let source = "import assert from 'node:assert/strict'; assert.equal(1, 1);";
std::fs::write(root.join("test.js"), source).unwrap();
let inputs = supercov_engine::assertion_inputs::capture(
&root,
"javascript",
[
"test.js".into(),
root.join("test.js"),
canonical.join("test.js"),
],
)
.unwrap();
assert_eq!(inputs.files.len(), 1);
assert_eq!(inputs.assertions.len(), 1);
assert_eq!(inputs.assertions[0].at.file, "test.js");
assert_eq!(inputs.files["test.js"], source);
let outside = root.parent().unwrap().join("outside.js");
std::fs::write(&outside, source).unwrap();
assert!(
supercov_engine::assertion_inputs::capture(&root, "javascript", [outside])
.unwrap_err()
.contains("assertion input outside project")
);
std::fs::remove_dir_all(temporary).unwrap();
}
#[test]
fn syntax_inventory_spans_work_across_all_language_adapters() {
let root = std::env::temp_dir().join(format!("supercov-map-languages-{}", std::process::id()));
std::fs::create_dir_all(&root).unwrap();
for (file, source, expected) in [
(
"test.js",
"import assert from 'node:assert/strict'; assert.equal(1, 1);",
1,
),
("test.py", "assert True\nself.assertEqual(1, 1)\n", 2),
("test.rb", "assert_equal 1, 1\nexpect(1).to eq(1)\n", 2),
("test.rs", "fn main() { assert_eq!(1, 1); }", 1),
] {
std::fs::write(root.join(file), source).unwrap();
let inputs =
supercov_engine::assertion_inputs::capture(&root, "fixture", [file.into()]).unwrap();
assert_eq!(inputs.assertions.len(), expected, "{file}");
assert!(
inputs
.assertions
.iter()
.all(|s| s.at.offset(&inputs.files).is_some())
);
}
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn syntax_diagnostics_locate_nested_errors_and_reject_ambiguous_json() {
let (_, map, _) = fixture();
let mut value = serde_json::to_value(map).unwrap();
value["assertions"][0]["flows"][0]["countsAsAsserted"] = json!(true);
let error = parse(&serde_json::to_vec_pretty(&value).unwrap()).unwrap_err();
assert_eq!(error.pointer, "/assertions/0/flows/0/countsAsAsserted");
assert!(error.line > 1);
for text in [
r#"{"assertions":[],"assertions":[]}"#,
r#"{"assertions":[],"unknown":1}"#,
r#"{"assertions":[],"schemaVersion":1}"#,
r#"{"assertions":[]} {}"#,
] {
assert!(parse(text.as_bytes()).is_err(), "{text}");
}
assert!(parse(br#"{"schemaVersion":2,"assertions":[]}"#).is_ok());
assert!(parse(br#"{"assertions":[]}"#).is_err());
}
#[test]
fn javascript_inventory_includes_async_operands_and_fixture_modules() {
use supercov_engine::js_instrumenter::{
assertion_ranges_with_expect_modules, instrument_node_assertion_phases_with_expect_modules,
};
let source = "import { expect as check } from '@acme/fixtures';\nasync function test() { check(await value()).toBe(1); check(value()).toBe(1); }\n";
let modules = vec!["@acme/fixtures".into()];
let inventory = assertion_ranges_with_expect_modules("test.ts", source, &modules).unwrap();
assert_eq!(inventory.len(), 2, "await sites must stay inventoried");
assert!(source[inventory[0].0..inventory[0].1].contains("await"));
let transformed =
instrument_node_assertion_phases_with_expect_modules(source, "test.ts", &modules).unwrap();
assert_eq!(
transformed.assertions, 2,
"callee binding preserves await syntax"
);
let cjs = "const { expect: check } = require('@jest/globals'); check(1).toBe(1); function f(check) { check(2).toBe(2); }";
assert_eq!(
assertion_ranges_with_expect_modules("test.cjs", cjs, &[])
.unwrap()
.len(),
1
);
}
#[test]
fn a_coordinate_cannot_substitute_for_exact_assertion_identity() {
let (inputs, mut map, state) = fixture();
let coverage = report_fixture(&["a", "b"], true);
map.assertions[0].at.text.push(';');
assert!(
validate(&map, &inputs).is_empty(),
"source exists but it is a different anchor"
);
acknowledge(&mut map, &state, &inputs, &BTreeSet::new(), true, false).unwrap();
let report = assess(&map, &state, &inputs, &coverage, true);
assert_eq!(report["summary"]["statements"]["asserted"], 0);
assert_eq!(report["summary"]["unobservedAssertions"], 1);
}
#[test]
fn statement_view_exposes_exact_anchors_and_flow_credit() {
let (inputs, map, state) = fixture();
let report = assess(&map, &state, &inputs, &report_fixture(&["a"], true), true);
let rows = report["statements"].as_array().unwrap();
assert_eq!(rows.len(), 2);
assert_eq!(rows[0]["at"]["text"], "return 1;");
assert_eq!(
rows[0]["flows"][0],
flow_key(&map.assertions[0], &map.assertions[0].flows[0])
);
assert_eq!(rows[1]["declared"], true);
assert_eq!(rows[1]["asserted"], false);
}
#[test]
fn crediting_a_control_statement_does_not_credit_its_nested_body() {
let (mut inputs, mut map, mut state) = fixture();
let source = "if (true) { return 1; }\n";
inputs.files.insert("src/a.js".into(), source.into());
map.assertions[0].flows[0].nodes[0].at = anchor("src/a.js", source, source.trim());
state.inputs_digest = inputs.identity();
acknowledge(&mut map, &state, &inputs, &BTreeSet::new(), true, false).unwrap();
let mut coverage = report_fixture(&["a", "b"], true);
let mut inner = coverage.filters.passed.points[0].clone();
inner.meta.id = "inner".into();
inner.meta.column = 13;
coverage.filters.passed.points[0].meta.source = source.trim().into();
coverage.filters.passed.points.push(inner);
let report = assess(&map, &state, &inputs, &coverage, true);
assert_eq!(report["summary"]["statements"]["asserted"], 2);
assert_eq!(report["summary"]["statements"]["total"], 3);
assert_eq!(report["statements"][2]["asserted"], false);
assert_eq!(
report["assertions"][0]["flows"][0]["nodeCredit"][0]["statementIds"],
json!(["a"])
);
}
#[test]
fn basis_syntax_requires_an_explicit_null_or_versioned_token() {
let (_, map, _) = fixture();
let base = serde_json::to_value(map).unwrap();
for value in [
json!(""),
json!("scov2:ABC"),
json!("scov1:0000"),
json!(42),
] {
let mut map = base.clone();
map["assertions"][0]["flows"][0]["basis"] = value;
assert!(parse(&serde_json::to_vec(&map).unwrap()).is_err());
}
let mut missing = base.clone();
missing["assertions"][0]["flows"][0]
.as_object_mut()
.unwrap()
.remove("basis");
assert!(parse(&serde_json::to_vec(&missing).unwrap()).is_err());
missing["assertions"][0]["flows"][0]["basis"] = json!(null);
assert!(parse(&serde_json::to_vec(&missing).unwrap()).is_ok());
}
#[test]
fn counted_nodes_need_an_authored_path_to_the_exact_assertion_sink() {
let (inputs, mut map, _) = fixture();
let flow = &mut map.assertions[0].flows[0];
flow.edges[0].to = "return".into(); assert!(
validate_flow(flow, &inputs.files)
.iter()
.any(|e| e.contains("no authored path"))
);
flow.edges.push(Edge {
from: "return".into(),
to: "$assertion".into(),
kind: "data".into(),
basis: String::new(),
});
assert!(validate_flow(flow, &inputs.files).is_empty());
flow.nodes.push(Node {
id: "context".into(),
at: flow.nodes[0].at.clone(),
role: String::new(),
meaning: String::new(),
});
assert!(validate_flow(flow, &inputs.files).is_empty());
flow.counts_as_asserted.push("context".into());
assert!(!validate_flow(flow, &inputs.files).is_empty());
}
#[test]
fn empty_selectors_never_mean_all_tests_and_zero_credit_is_distinct_from_no_work() {
let (inputs, mut map, state) = fixture();
let coverage = report_fixture(&["a", "b"], true);
for f in &mut map.assertions[0].flows {
f.applies_to.clear();
}
acknowledge(&mut map, &state, &inputs, &BTreeSet::new(), true, false).unwrap();
let report = assess(&map, &state, &inputs, &coverage, true);
assert_eq!(report["summary"]["status"], "pending");
assert_eq!(report["summary"]["statements"]["percentage"], json!(null));
for f in &mut map.assertions[0].flows {
f.applies_to = vec![TestSelector {
file: "test.js".into(),
name: "test".into(),
}];
f.counts_as_asserted.clear();
}
acknowledge(&mut map, &state, &inputs, &BTreeSet::new(), true, false).unwrap();
let report = assess(&map, &state, &inputs, &coverage, true);
assert_eq!(report["summary"]["status"], "available");
assert_eq!(report["summary"]["statements"]["percentage"], 0.0);
assert_eq!(
report["summary"]["observedAssertionsWithoutCurrentExplanation"],
0
);
map.assertions[0].flows.clear();
assert_eq!(
assess(&map, &state, &inputs, &coverage, true)["summary"]["status"],
"notAssessed"
);
}
#[test]
fn qualified_selectors_reject_ambiguous_names_and_missing_siblings_do_not_erase_evidence() {
let (inputs, mut map, state) = fixture();
let mut coverage = report_fixture(&["a", "b"], true);
for f in &mut map.assertions[0].flows {
f.applies_to.push(TestSelector {
file: "test.js".into(),
name: "missing case".into(),
});
}
acknowledge(&mut map, &state, &inputs, &BTreeSet::new(), true, false).unwrap();
let report = assess(&map, &state, &inputs, &coverage, true);
assert_eq!(report["summary"]["statements"]["asserted"], 2);
assert_eq!(
report["assertions"][0]["flows"][0]["selectors"][1]["status"],
"missing"
);
let mut other = coverage.view.tests[0].clone();
other.id = "another-test".into();
other.file = Some("another.js".into());
coverage.view.tests.push(other.clone());
assert_eq!(
assess(&map, &state, &inputs, &coverage, true)["summary"]["statements"]["asserted"],
2
);
other.file = Some("test.js".into());
coverage.view.tests.push(other);
let report = assess(&map, &state, &inputs, &coverage, true);
assert_eq!(report["summary"]["statements"]["asserted"], 0);
assert_eq!(
report["assertions"][0]["flows"][0]["selectors"][0]["status"],
"ambiguous"
);
}
#[test]
fn a_selected_test_file_is_an_implicit_dependency_and_questions_are_per_flow() {
let (mut inputs, mut map, mut state) = fixture();
inputs
.files
.insert("tests/shared.js".into(), "// setup".into());
state.inputs_digest = inputs.identity();
map.assertions[0].flows[0].applies_to.push(TestSelector {
file: "tests/shared.js".into(),
name: "shared".into(),
});
acknowledge(&mut map, &state, &inputs, &BTreeSet::new(), true, false).unwrap();
let mut new = inputs.clone();
new.files
.insert("tests/shared.js".into(), "// edited setup".into());
let (mut next, state) = carry(&map, &state, &inputs.manifest(), &new, "new", false).unwrap();
assert_eq!(current(&next, &state, &new), 1);
next.assertions[0]
.questions
.push("More flows may exist".into());
assert_eq!(current(&next, &state, &new), 1);
next.assertions[0].flows[1]
.questions
.push("Which guard controls this return?".into());
acknowledge(&mut next, &state, &new, &BTreeSet::new(), true, true).unwrap();
assert_eq!(current(&next, &state, &new), 1);
assert!(
reasons(
&next.assertions[0],
&next.assertions[0].flows[1],
&next,
&state,
&new
)
.contains("flow has unresolved questions")
);
}
#[test]
fn change_impact_can_invalidate_an_unwatched_sibling_and_folds_without_churning_tokens() {
let (old, map, state) = fixture();
let mut new = old.clone();
new.files
.insert("src/helper.js".into(), "changed();\n".into());
let (mut map, state) = carry(&map, &state, &old.manifest(), &new, "two", false).unwrap();
assert_eq!(current(&map, &state, &new), 2);
let before = assess(&map, &state, &new, &report_fixture(&["a", "b"], true), true);
assert_eq!(before["summary"]["status"], "pending");
assert_eq!(before["summary"]["statements"]["asserted"], 2); let change = &state.changes[0];
let key = flow_key(&map.assertions[0], &map.assertions[0].flows[0]);
let mut response = ChangeAssessment {
id: change.id.clone(),
basis: None,
affected_flows: vec![key],
explanation: "Helper affects the first claim despite its missing watch".into(),
};
response.basis = Some(expected_change_basis(change, &response, &new.manifest()));
map.change_assessments.push(response);
assert_eq!(current(&map, &state, &new), 1);
acknowledge(&mut map, &state, &new, &BTreeSet::new(), true, false).unwrap();
let tokens = map.assertions[0]
.flows
.iter()
.map(|f| f.basis.clone())
.collect::<Vec<_>>();
let (next, next_state) = carry(&map, &state, &new.manifest(), &new, "three", false).unwrap();
assert_eq!(current(&next, &next_state, &new), 2);
assert!(next.change_assessments.is_empty());
assert!(next_state.changes.is_empty());
assert_eq!(
tokens,
next.assertions[0]
.flows
.iter()
.map(|f| f.basis.clone())
.collect::<Vec<_>>()
);
let mut later = new.clone();
later
.files
.insert("brand-new.js".into(), "// unrelated".into());
let (later_map, later_state) =
carry(&next, &next_state, &new.manifest(), &later, "four", false).unwrap();
assert_eq!(current(&later_map, &later_state, &later), 2);
assert_eq!(later_state.changes.len(), 1);
assert_eq!(later_state.changes[0].file.as_deref(), Some("brand-new.js"));
}
#[test]
fn missing_impact_responses_cannot_clear_changes_and_known_dependencies_cannot_be_omitted() {
let (old, map, state) = fixture();
let mut new = old.clone();
new.files
.get_mut("src/a.js")
.unwrap()
.push_str("// changed");
let (mut map, state) = carry(&map, &state, &old.manifest(), &new, "two", false).unwrap();
let c = &state.changes[0];
let mut r = ChangeAssessment {
id: c.id.clone(),
basis: None,
affected_flows: vec![],
explanation: "No effect".into(),
};
r.basis = Some(expected_change_basis(c, &r, &new.manifest()));
map.change_assessments.push(r);
assert!(!change_current(&map, c, &new.manifest()));
assert_eq!(validation(&map, &state, &new)["valid"], false);
acknowledge(&mut map, &state, &new, &BTreeSet::new(), true, true).unwrap();
assert!(change_current(&map, c, &new.manifest()));
map.change_assessments.clear();
let (next, next_state) = carry(&map, &state, &new.manifest(), &new, "three", false).unwrap();
assert_eq!(next_state.changes.len(), 1);
assert_eq!(
assess(
&next,
&next_state,
&new,
&report_fixture(&["a", "b"], true),
true
)["summary"]["status"],
"pending"
);
map.assertions[0].flows.remove(0);
let mut r = ChangeAssessment {
id: c.id.clone(),
basis: None,
affected_flows: vec![],
explanation: "The affected claim was explicitly removed".into(),
};
r.basis = Some(expected_change_basis(c, &r, &new.manifest()));
map.change_assessments.push(r);
assert!(change_current(&map, c, &new.manifest()));
}
#[test]
fn validation_only_reads_and_basis_encoding_is_stable() {
let (inputs, map, state) = fixture();
let before = serde_json::to_vec(&(&map, &state)).unwrap();
let v = validation(&map, &state, &inputs);
assert_eq!(v["valid"], true);
assert_eq!(before, serde_json::to_vec(&(&map, &state)).unwrap());
let a = &map.assertions[0];
let f = &a.flows[0];
assert_eq!(
expected_basis(a, f, &map, &state, &inputs.manifest()),
"scov2:2469cd73a9d32689c8e28a4d24ef2c5a846f7c8407407013a3adbdeb88c6e68b"
);
}
#[test]
fn editor_schema_accepts_explicit_null_basis_and_requires_the_field() {
let schema = schema();
for name in ["Flow", "ChangeAssessment"] {
assert_eq!(
schema["$defs"][name]["properties"]["basis"]["type"],
json!(["string", "null"])
);
assert!(
schema["$defs"][name]["required"]
.as_array()
.unwrap()
.contains(&json!("basis"))
);
}
}