// tests/ambient_isolation_acceptance.rs --- integration tests must not
// read or write the developer's real ambient roots.

//! Acceptance for `docs/test-ambient-config-isolation-framing.md`.
//!
//! # The defect
//!
//! `src/editor.rs` guards user-config loading with `#[cfg(not(test))]`.
//! `cfg(test)` is set only while compiling the crate's *own* unit tests;
//! an integration test in `tests/` links `pmacs` as an ordinary
//! dependency, so the guard is inactive for all of them. `cargo test
//! --lib` is protected, `cargo test --test <name>` is not. And config
//! loading is only the read half: `EditorState::new` materializes
//! bundled packages into `$XDG_DATA_HOME/pmacs` (else
//! `$HOME/.local/share`) **unconditionally**, outside every `cfg` guard,
//! creating directories and writing files.
//!
//! # The census (framing acceptance 1), read at `54a092e`
//!
//! Method: every occurrence was listed with its enclosing context and
//! read. A grep for the bare name over-counts — the framing's revision 1
//! reported 18 by grepping `Editor::new`, which does not even match the
//! real constructor `EditorState::new`.
//!
//! **In-process construction — 342 sites in 66 of 97 files.**
//!
//! * `EditorState::new()` — 334 textual occurrences, of which **330 are
//!   calls**. The other 4 are prose: `persistence_acceptance.rs:6` and
//!   `:76`, `m7_11_acceptance.rs:92`, `m8_2_acceptance.rs:75`. A fifth
//!   file, `m5_6_acceptance.rs:94`, names the constructor only to say it
//!   deliberately does **not** use it — the third place in the tree
//!   documenting this same `cfg(test)` gap.
//! * `EditorState::open(` — 14 textual occurrences in 3 files, of which
//!   **12 are calls** (`journey_acceptance` 7, `m4_acceptance` 4,
//!   `theme_faces_acceptance` 1). The other 2 are assertion-message
//!   strings in `journey_acceptance.rs:2030,2038`.
//! * Only 329 of the 330 `new()` calls are `let` bindings; the odd one
//!   is `m8_1_acceptance.rs:39`, a bare tail expression in a
//!   `fresh_editor()` helper. Sites, not files, are the unit.
//!
//! **Spawned `pmacs` — 14 sites in 8 files.** `grep CARGO_BIN_EXE_pmacs`
//! reports 36 sites in 26 files, but 18 of those are the *sibling*
//! binaries `CARGO_BIN_EXE_pmacs_fake_lsp` / `_fake_mcp`, which are LSP
//! and MCP stubs and not pmacs at all. Reading each occurrence:
//!
//! * `tests/common/daemon.rs:158` — the shared `--daemon` harness.
//! * `tests/common/pty.rs:111` — the shared real-PTY spawner.
//! * `m5_7_acceptance.rs` ×5, `m5_8_acceptance.rs` ×3,
//!   `m5_5_acceptance.rs` ×1, `m5_perf_acceptance.rs` ×1,
//!   `gpu_invocation_acceptance.rs` ×2 — direct `Command::new`.
//! * 4 further occurrences are **path derivations, not spawns**:
//!   `gpu_invocation_acceptance.rs:115`, `vterm_stage3_acceptance.rs:644`
//!   and `:1180`, `bottom_panel_stage2b_gpu_acceptance.rs:513` all take
//!   `CARGO_BIN_EXE_pmacs`'s *parent* to locate the `pmacs-gpu` sibling.
//!
//! **Mixed files are real: 5 files are both in-process and spawned** —
//! `vterm_stage3_acceptance` constructs an editor at `:159` and reaches
//! a daemon at `:665`. A file-level partition cannot represent them,
//! which is why the ratchet below keys on sites.

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::Command;

use pmacs::bootstrap::BootstrapRoots;
use pmacs::editor::EditorState;

#[path = "common/iso.rs"]
mod iso;

// ---------------------------------------------------------------------------
// Isolated construction still finishes initialization (framing §1.8)
// ---------------------------------------------------------------------------

/// **N** — isolated construction skips the ambient *reads* and still
/// flips the init-complete gate.
///
/// Config loading and `set_init_complete()` share one conditional block,
/// so the tempting fix — skip the block when roots are redirected —
/// would leave every isolated test permanently in the init phase.
/// `tests/m8_2_acceptance.rs:75` documents its dependence on
/// integration-test construction being init-complete, so that is not a
/// hypothetical.
///
/// Falsified by wrapping the block in `if roots.is_ambient()`.
#[test]
fn isolated_construction_is_init_complete() {
    let state = EditorState::new_with_roots(&iso::roots());
    assert!(
        state.lua_host.is_init_complete(),
        "isolated construction must still leave the init phase, or every \
         suite that reopens it (m8_2) breaks"
    );
    // The paired half — that the *ambient* constructor is unchanged —
    // deliberately does NOT live here. Asserting it needs an ambient
    // `EditorState::new()`, and an ambient construction in an ordinary
    // parent test reads the developer's real `init.lua` and materializes
    // packages into their real data root: the exact exposure this suite
    // exists to remove, committed by the suite itself. It lives in the
    // re-exec'd positive control below, where the roots are controlled
    // by construction.
}

/// **N** — the init phase is genuinely closed, not merely reported
/// closed.
///
/// A flag read is one bool; this asserts the *behaviour* the flag gates,
/// so a fix that sets the flag without the surrounding block having run
/// cannot pass. `pmacs.attach` is init-only and must now refuse.
#[test]
fn isolated_construction_closes_the_init_only_lua_surface() {
    let state = EditorState::new_with_roots(&iso::roots());
    let err = state
        .lua_host
        .lua()
        .load(r#"pmacs.attach { target = "local:/run/pmacs/x.sock" }"#)
        .exec()
        .expect_err("pmacs.attach must refuse after init");
    let text = err.to_string();
    assert!(
        text.contains("init"),
        "the refusal must name the init phase; got {text}"
    );
}

// ---------------------------------------------------------------------------
// Isolated construction redirects the writes (framing §1.6)
// ---------------------------------------------------------------------------

/// **N** — the bundled-package materialization lands in the redirected
/// data root.
///
/// Asserts content produced, not an invariant preserved: the package
/// tree has to actually exist under the isolated root. A test that only
/// checked "the real root was not modified" would pass on a machine
/// where the real root already held identical bytes, because
/// `write_if_changed` is content-gated.
#[test]
fn isolated_construction_materializes_into_the_redirected_data_root() {
    let base = Path::new(env!("CARGO_TARGET_TMPDIR")).join("materialize-probe");
    let _ = std::fs::remove_dir_all(&base);
    let roots = BootstrapRoots::isolated_under(&base);
    let dir = roots.bundled_runtime_dir().expect("redirected data root");
    assert!(!dir.exists(), "the probe root must start absent");

    let _state = EditorState::new_with_roots(&roots);

    let manifest = dir.join("repl").join("pmacs.toml");
    assert!(
        manifest.is_file(),
        "bundled packages must materialize under the redirected data \
         root; {} is missing",
        manifest.display()
    );
    let text = std::fs::read_to_string(&manifest).expect("read materialized manifest");
    assert!(
        text.contains("repl"),
        "the materialized manifest must be the bundled package's own; got {text:?}"
    );
}

/// **N** — `install_state_dirs` honours the redirected state root.
///
/// It runs *after* the constructor, so a constructor-only parameter
/// would leave it resolving `PMACS_STATE_HOME` / `XDG_STATE_HOME` from
/// the environment and hand an isolated session the developer's real
/// state dir.
#[test]
fn install_state_dirs_honours_the_redirected_state_root() {
    let base = Path::new(env!("CARGO_TARGET_TMPDIR")).join("state-probe");
    let roots = BootstrapRoots::isolated_under(&base);
    let state = EditorState::new_with_roots(&roots);
    state.install_state_dirs();

    let dir = state
        .lua_host
        .lua()
        .app_data_ref::<pmacs::lua_bindings::StateDir>()
        .expect("install_state_dirs must configure a state dir")
        .0
        .clone();
    assert_eq!(
        dir,
        roots.state_dir().unwrap(),
        "state dir must be redirected"
    );
    let history = state.core.borrow().minibuffer.history_dir.clone();
    assert_eq!(
        history,
        roots.history_dir(),
        "minibuffer history must be redirected too"
    );
}

// ---------------------------------------------------------------------------
// Hostile ambient environment (framing Bet 3, acceptance 7)
// ---------------------------------------------------------------------------

/// Names the isolated base for the isolated child; its presence is the
/// signal that this process *is* that child.
const ISOLATED_CHILD_BASE: &str = "PMACS_AMBIENT_ISOLATION_CHILD_BASE";
/// Presence marks the ambient positive-control child.
const AMBIENT_CONTROL_CHILD: &str = "PMACS_AMBIENT_ISOLATION_CONTROL";

/// An `init.lua` that leaves a mark an assertion can see. The real
/// developer `init.lua` that produced this lane broke
/// `compile_mode_acceptance` by redefining a command pmacs already
/// defines; a marker global is the same exposure with a cheaper failure
/// mode, and it discriminates read-vs-not-read directly.
const HOSTILE_INIT_LUA: &str = "_G.HOSTILE_INIT_RAN = true\n";

fn hostile_root(name: &str) -> PathBuf {
    let root = Path::new(env!("CARGO_TARGET_TMPDIR")).join(name);
    let _ = std::fs::remove_dir_all(&root);
    let config = root.join("pmacs");
    std::fs::create_dir_all(&config).expect("create hostile config dir");
    std::fs::write(config.join("init.lua"), HOSTILE_INIT_LUA).expect("write hostile init.lua");
    // Pre-seed the data root the ambient resolver would use, so a write
    // into it is visible as a *change*, not just as a new tree.
    let seeded = root
        .join("pmacs")
        .join("builtin-packages")
        .join(format!("v{}", env!("CARGO_PKG_VERSION")));
    std::fs::create_dir_all(&seeded).expect("seed hostile data root");
    std::fs::write(seeded.join("SEED"), b"pre-seeded\n").expect("write seed marker");
    root
}

/// Flat snapshot of a tree: relative path → contents (directories map to
/// the empty vector). Sorted, so comparison is order-independent.
fn snapshot(root: &Path) -> BTreeMap<PathBuf, Vec<u8>> {
    fn walk(root: &Path, dir: &Path, out: &mut BTreeMap<PathBuf, Vec<u8>>) {
        let Ok(entries) = std::fs::read_dir(dir) else {
            return;
        };
        for entry in entries.flatten() {
            let path = entry.path();
            let rel = path
                .strip_prefix(root)
                .expect("entry is under root")
                .to_path_buf();
            if path.is_dir() {
                out.insert(rel, Vec::new());
                walk(root, &path, out);
            } else {
                out.insert(rel, std::fs::read(&path).unwrap_or_default());
            }
        }
    }
    let mut out = BTreeMap::new();
    walk(root, root, &mut out);
    out
}

/// The five storage variables plus `HOME`, all aimed at `root`. `HOME`
/// is included here — and only here — because this is the adversary: a
/// machine that leaves an XDG variable unset falls back to it, and the
/// point of a hostile environment is to leave no path out.
fn hostile_env(root: &Path) -> Vec<(&'static str, PathBuf)> {
    vec![
        ("HOME", root.to_path_buf()),
        ("XDG_CONFIG_HOME", root.to_path_buf()),
        ("XDG_DATA_HOME", root.to_path_buf()),
        ("XDG_STATE_HOME", root.to_path_buf()),
        ("PMACS_STATE_HOME", root.to_path_buf()),
        ("XDG_CACHE_HOME", root.to_path_buf()),
    ]
}

fn run_child(test_name: &str, env: Vec<(&'static str, PathBuf)>) -> (bool, String) {
    let exe = std::env::current_exe().expect("current test binary");
    let output = Command::new(exe)
        .args(["--exact", test_name, "--nocapture", "--test-threads=1"])
        .envs(env)
        .output()
        .unwrap_or_else(|e| panic!("re-exec `{test_name}`: {e}"));
    let mut log = String::new();
    log.push_str("--- stdout ---\n");
    log.push_str(&String::from_utf8_lossy(&output.stdout));
    log.push_str("--- stderr ---\n");
    log.push_str(&String::from_utf8_lossy(&output.stderr));
    assert!(
        log.contains("1 passed") || !output.status.success(),
        "child `{test_name}` ran no test — the `--exact` filter went stale\n{log}"
    );
    (output.status.success(), log)
}

/// **Positive control.** Under the hostile environment, the *ambient*
/// constructor really is captured by it.
///
/// Without this the isolation assertion below is unfalsifiable: an
/// `init.lua` that never loads under any circumstances would satisfy
/// "the isolated editor did not load it" while proving nothing.
///
/// **This is the suite's only ambient construction, and it runs only as
/// a re-exec'd child** (the marker gates it), where the roots are
/// controlled by construction. An ambient `EditorState::new()` in an
/// ordinary parent test would read the developer's real `init.lua` and
/// write their real data root — so the one place that legitimately needs
/// the ambient constructor is also the one place where the environment
/// has already been redirected. Every ambient claim this suite makes
/// belongs here for that reason.
#[test]
fn ambient_construction_under_a_hostile_environment_is_captured_by_it() {
    if std::env::var_os(AMBIENT_CONTROL_CHILD).is_none() {
        return;
    }
    let state = EditorState::new();
    // Relocated from `isolated_construction_is_init_complete`: the
    // ambient constructor is unchanged by this lane and still finishes
    // initialization. Asserting it needs an ambient construction, which
    // is only safe here.
    assert!(
        state.lua_host.is_init_complete(),
        "the ambient constructor must still leave the init phase — the \
         roots parameter changes which directory is read, never whether \
         the block runs"
    );
    let ran: bool = state
        .lua_host
        .lua()
        .load("return _G.HOSTILE_INIT_RAN == true")
        .eval()
        .expect("read the hostile marker");
    assert!(
        ran,
        "the ambient constructor must load the hostile init.lua — if it \
         does not, the isolation assertion proves nothing"
    );
    // And the write half: the ambient constructor materializes into the
    // hostile data root.
    let dir = pmacs::builtin_packages::bundled_runtime_dir();
    assert!(
        dir.join("repl").join("pmacs.toml").is_file(),
        "the ambient constructor must write into the hostile data root; \
         {} is empty",
        dir.display()
    );
}

/// The isolated child: same hostile environment, redirected roots.
#[test]
fn isolated_construction_under_a_hostile_environment_ignores_it() {
    let Some(base) = std::env::var_os(ISOLATED_CHILD_BASE) else {
        return;
    };
    let base = PathBuf::from(base);
    let roots = BootstrapRoots::isolated_under(&base);
    let state = EditorState::new_with_roots(&roots);
    let ran: bool = state
        .lua_host
        .lua()
        .load("return _G.HOSTILE_INIT_RAN == true")
        .eval()
        .expect("read the hostile marker");
    assert!(!ran, "the hostile init.lua must not have been loaded");
    assert!(
        state.lua_host.is_init_complete(),
        "and initialization must still have finished"
    );
    // Content produced, in the right place.
    let dir = roots.bundled_runtime_dir().expect("redirected data root");
    assert!(
        dir.join("repl").join("pmacs.toml").is_file(),
        "bundled packages must land under the isolated root; {} is empty",
        dir.display()
    );
}

/// **N** — the whole of Bet 3: green under a hostile environment, and
/// the hostile root byte-identical afterwards.
///
/// Two children, because the two halves need opposite environments to be
/// meaningful: the positive control must be *captured* by its hostile
/// root (and so modifies it), while the isolated child must leave its
/// own hostile root untouched.
#[test]
fn a_hostile_ambient_environment_is_neither_read_nor_written() {
    // Half 1 — the control. Its hostile root is expected to change.
    let control = hostile_root("hostile-control");
    let before_control = snapshot(&control);
    let (ok, log) = run_child(
        "ambient_construction_under_a_hostile_environment_is_captured_by_it",
        {
            let mut env = hostile_env(&control);
            env.push((AMBIENT_CONTROL_CHILD, PathBuf::from("1")));
            env
        },
    );
    assert!(ok, "the ambient positive control must be captured\n{log}");
    let after_control = snapshot(&control);
    assert_ne!(
        before_control, after_control,
        "the control's hostile root must have been written into — if it \
         was not, this environment is not hostile and the isolated half \
         below asserts nothing"
    );

    // Half 2 — the isolated child. Its hostile root must be untouched.
    let hostile = hostile_root("hostile-isolated");
    let isolated_base = Path::new(env!("CARGO_TARGET_TMPDIR")).join("hostile-isolated-roots");
    let _ = std::fs::remove_dir_all(&isolated_base);
    let before = snapshot(&hostile);
    assert!(!before.is_empty(), "the hostile root must not be empty");
    let (ok, log) = run_child(
        "isolated_construction_under_a_hostile_environment_ignores_it",
        {
            let mut env = hostile_env(&hostile);
            env.push((ISOLATED_CHILD_BASE, isolated_base.clone()));
            env
        },
    );
    assert!(ok, "the isolated child must stay green\n{log}");
    let after = snapshot(&hostile);
    assert_eq!(
        before, after,
        "the hostile root must be byte-identical afterwards — a green \
         suite that still wrote into it has not demonstrated isolation"
    );
    // And the writes went somewhere: the isolated tree exists.
    assert!(
        isolated_base.join("data").join("pmacs").is_dir(),
        "the isolated data root must have been written instead"
    );
}

// ---------------------------------------------------------------------------
// Adoption ratchet (framing acceptance 12)
// ---------------------------------------------------------------------------

/// Files permitted to construct an editor through the **ambient** entry
/// points, **with the exact number of sites each is permitted**.
///
/// `journey_acceptance` is ambient on purpose: it is the golden-journey
/// ratchet, and its whole claim is that the production entry point
/// `pmacs FILE` calls has a caller. It is isolated by re-execing itself
/// with controlled roots instead (framing §1.10). This file is ambient
/// only inside the positive control above, which never runs except as a
/// deliberately re-exec'd child.
///
/// **The count is the point, not decoration.** A bare file-level
/// exemption is the weakest form of this ratchet: it licenses the named
/// file to grow *new* ambient sites forever. That is not hypothetical —
/// review round 1 of this PR found an ambient `EditorState::new()` in an
/// ordinary parent test of **this very file**, and the file-level
/// exemption is precisely what let it through a green ratchet. Every
/// exemption is now a census entry, so an added site fails even inside
/// an allowlisted file, and a removed one has to be recorded.
const AMBIENT_ALLOWLIST: &[(&str, usize)] = &[
    // 19 `new()` + 7 `open(` — the golden journey, every one of them
    // reached only from inside a re-exec'd child. (29 textual
    // occurrences; the scanner drops 2 assertion-message mentions and
    // the assembled `concat!` needle in the self-source check.)
    ("journey_acceptance.rs", 26),
    // Exactly one: the re-exec'd positive control.
    ("ambient_isolation_acceptance.rs", 1),
];

/// Strip comments and string-literal *contents* from Rust source, so a
/// scan counts calls rather than mentions.
///
/// Both matter here. `m5_6_acceptance.rs:94` names `EditorState::new` in
/// a comment only to say it deliberately does not call it, and
/// `journey_acceptance.rs:2030` carries it inside an assertion message.
/// Raw strings (`r#"..."#`) are pervasive in this suite, so they are
/// handled rather than hoped about.
fn strip_comments_and_strings(src: &str) -> String {
    let b: Vec<char> = src.chars().collect();
    let mut out = String::with_capacity(src.len());
    let mut i = 0;
    while i < b.len() {
        // Line comment.
        if b[i] == '/' && i + 1 < b.len() && b[i + 1] == '/' {
            while i < b.len() && b[i] != '\n' {
                i += 1;
            }
            continue;
        }
        // Block comment (Rust's nest).
        if b[i] == '/' && i + 1 < b.len() && b[i + 1] == '*' {
            let mut depth = 1;
            i += 2;
            while i < b.len() && depth > 0 {
                if b[i] == '/' && i + 1 < b.len() && b[i + 1] == '*' {
                    depth += 1;
                    i += 2;
                } else if b[i] == '*' && i + 1 < b.len() && b[i + 1] == '/' {
                    depth -= 1;
                    i += 2;
                } else {
                    i += 1;
                }
            }
            out.push(' ');
            continue;
        }
        // Raw string: r, then any number of #, then ".
        if b[i] == 'r' {
            let mut j = i + 1;
            let mut hashes = 0;
            while j < b.len() && b[j] == '#' {
                hashes += 1;
                j += 1;
            }
            if j < b.len() && b[j] == '"' {
                j += 1;
                loop {
                    if j >= b.len() {
                        break;
                    }
                    if b[j] == '"' {
                        let mut k = j + 1;
                        let mut seen = 0;
                        while k < b.len() && b[k] == '#' && seen < hashes {
                            seen += 1;
                            k += 1;
                        }
                        if seen == hashes {
                            j = k;
                            break;
                        }
                    }
                    j += 1;
                }
                out.push(' ');
                i = j;
                continue;
            }
        }
        // Ordinary string.
        if b[i] == '"' {
            i += 1;
            while i < b.len() {
                if b[i] == '\\' {
                    i += 2;
                    continue;
                }
                if b[i] == '"' {
                    i += 1;
                    break;
                }
                i += 1;
            }
            out.push(' ');
            continue;
        }
        out.push(b[i]);
        i += 1;
    }
    out
}

fn tests_dir() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("tests")
}

/// Every `.rs` file under `tests/`, including `tests/common/`.
fn test_sources() -> Vec<(String, String)> {
    let mut out = Vec::new();
    let dir = tests_dir();
    let push_dir = |d: &Path, out: &mut Vec<(String, String)>| {
        for entry in std::fs::read_dir(d).expect("read tests dir").flatten() {
            let path = entry.path();
            if path.extension().is_some_and(|e| e == "rs") {
                let name = path
                    .file_name()
                    .expect("file name")
                    .to_string_lossy()
                    .into_owned();
                out.push((name, std::fs::read_to_string(&path).expect("read source")));
            }
        }
    };
    push_dir(&dir, &mut out);
    push_dir(&dir.join("common"), &mut out);
    out
}

/// **N** — a *durable* adoption ratchet, not a one-time census.
///
/// One self-spawning hostile-environment test proves the seam works; it
/// cannot notice a raw `EditorState::new()` added to a different binary
/// next month. This can. Falsified by adding an ambient constructor to
/// any non-allowlisted suite.
#[test]
fn no_test_outside_the_allowlist_constructs_an_ambient_editor() {
    let sources = test_sources();
    // A broken glob must not read as a clean tree.
    assert!(
        sources.len() > 90,
        "expected the whole tests/ corpus; found only {} files",
        sources.len()
    );
    let needles = [
        concat!("EditorState::", "new()"),
        concat!("EditorState::", "open("),
    ];
    let mut offenders: Vec<String> = Vec::new();
    let mut miscounted: Vec<String> = Vec::new();
    let mut seen_allowlisted: Vec<&str> = Vec::new();
    for (name, src) in &sources {
        let code = strip_comments_and_strings(src);
        let hits: usize = needles.iter().map(|n| code.matches(n).count()).sum();
        if hits == 0 {
            continue;
        }
        match AMBIENT_ALLOWLIST.iter().find(|(f, _)| *f == name.as_str()) {
            Some((file, allowed)) => {
                seen_allowlisted.push(file);
                // An allowlisted file is exempted for the sites it was
                // reviewed with, not for any it grows later.
                if hits != *allowed {
                    miscounted.push(format!("{name}: {hits} site(s), allowlist says {allowed}"));
                }
            }
            None => offenders.push(format!("{name} ({hits} site(s))")),
        }
    }
    offenders.sort();
    miscounted.sort();
    assert!(
        offenders.is_empty(),
        "these suites construct an editor through the ambient entry \
         points, so they read the developer's real init.lua and write \
         into their real data root: {offenders:?}\n\
         Use `EditorState::new_with_roots(&crate::iso::roots())` (see \
         tests/common/iso.rs), or add the file to AMBIENT_ALLOWLIST with \
         a reason.",
    );
    assert!(
        miscounted.is_empty(),
        "allowlisted files whose ambient site count moved: {miscounted:?}\n\
         MORE than allowed means a new ambient construction slipped into \
         an exempted file — the failure mode a bare file-level exemption \
         cannot see. FEWER means the census is stale; update the count.",
    );
    // Dead allowlist entries are how a ratchet rots: an entry that no
    // longer needs to be there silently licenses a future regression.
    let mut missing: Vec<&str> = AMBIENT_ALLOWLIST
        .iter()
        .map(|(f, _)| *f)
        .filter(|f| !seen_allowlisted.contains(f))
        .collect();
    missing.sort_unstable();
    assert!(
        missing.is_empty(),
        "allowlisted files that no longer construct an ambient editor — \
         remove them: {missing:?}"
    );
}

/// **N** — the ratchet's scanner is not fooled by prose.
///
/// A grep-shaped answer is what cost this lane a review round; the scan
/// above only ratchets if it distinguishes a call from a mention. Pinned
/// with the exact shapes the corpus actually contains.
#[test]
fn the_ratchet_scanner_counts_calls_not_mentions() {
    let sample = r##"
//! `EditorState::new()` in a doc comment.
// `EditorState::new()` in a line comment.
/* `EditorState::new()` in a block comment. */
fn f() {
    let msg = "EditorState::open(file) must not greet";
    let raw = r#"EditorState::new() inside a raw string"#;
    let _ = EditorState::new();
}
"##;
    let code = strip_comments_and_strings(sample);
    assert_eq!(
        code.matches(concat!("EditorState::", "new()")).count(),
        1,
        "only the call survives; got {code:?}"
    );
    assert_eq!(
        code.matches(concat!("EditorState::", "open(")).count(),
        0,
        "the assertion-message mention must not count; got {code:?}"
    );
}

/// **N** — the seam actually reached the corpus.
///
/// The ratchet above is an absence check, and an absence check passes on
/// a tree where nobody constructs an editor at all. This asserts the
/// positive: the isolated constructor has broad adoption.
#[test]
fn the_isolated_constructor_has_been_adopted_across_the_corpus() {
    let sources = test_sources();
    let adopters: Vec<&String> = sources
        .iter()
        .filter(|(_, src)| {
            let code = strip_comments_and_strings(src);
            code.contains(concat!("EditorState::new", "_with_roots("))
                || code.contains(concat!("EditorState::open", "_with_roots("))
        })
        .map(|(name, _)| name)
        .collect();
    assert!(
        adopters.len() >= 60,
        "expected the whole in-process population to have migrated; only \
         {} files did",
        adopters.len()
    );
}
