noxid-cli 0.2.0

The Noxid compiler command line: check, build, test, adapt, and the agent surface
use std::path::Path;
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};

static NEXT_TRACKER_BUILD: AtomicU64 = AtomicU64::new(0);

// Every example in the repository must compile with the current compiler.
// Aspirational syntax belongs in docs/proposals, not examples.
#[test]
fn every_example_compiles() {
    let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
    let examples = root.join("examples");
    let mut checked = 0usize;
    let mut failures = Vec::new();
    // `examples/templates/<name>` holds the `noxid new --template <name>`
    // sources. They are ordinary example projects and compile like every
    // other one, so a template can never ship syntax the compiler rejects.
    let mut roots = std::fs::read_dir(&examples)
        .expect("examples directory")
        .map(|entry| entry.expect("example entry").path())
        .collect::<Vec<_>>();
    let templates = examples.join("templates");
    assert!(
        templates.is_dir(),
        "examples/templates must hold the `noxid new --template` sources"
    );
    roots.extend(
        std::fs::read_dir(&templates)
            .expect("templates directory")
            .map(|entry| entry.expect("template entry").path()),
    );
    roots.sort();
    for path in roots {
        if !path.is_dir() {
            continue;
        }
        if path == templates {
            continue;
        }
        if path.join("Noxid.toml").is_file() {
            let output = Command::new(env!("CARGO_BIN_EXE_noxid"))
                .arg("graph")
                .arg(&path)
                .output()
                .expect("run noxid graph");
            checked += 1;
            if !output.status.success() {
                failures.push(format!(
                    "{}: {}",
                    path.display(),
                    String::from_utf8_lossy(&output.stderr)
                ));
            }
            continue;
        }
        for file in std::fs::read_dir(&path).expect("example files") {
            let file = file.expect("example file").path();
            if file.extension().is_some_and(|ext| ext == "nox") {
                let output = Command::new(env!("CARGO_BIN_EXE_noxid"))
                    .arg("check")
                    .arg(&file)
                    .output()
                    .expect("run noxid check");
                checked += 1;
                if !output.status.success() {
                    failures.push(format!(
                        "{}: {}",
                        file.display(),
                        String::from_utf8_lossy(&output.stderr)
                    ));
                }
            }
        }
    }
    assert!(
        checked > 20,
        "expected to check every example, saw {checked}"
    );
    assert!(
        failures.is_empty(),
        "examples failed:\n{}",
        failures.join("\n")
    );
}

#[test]
fn tracker_project_builds_and_its_executable_scenario_gate_passes() {
    let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
    let tracker = root.join("examples/tracker");
    let out_dir = std::env::temp_dir().join(format!(
        "noxid-tracker-build-{}-{}",
        std::process::id(),
        NEXT_TRACKER_BUILD.fetch_add(1, Ordering::Relaxed)
    ));

    let build = Command::new(env!("CARGO_BIN_EXE_noxid"))
        .arg("build")
        .arg(&tracker)
        .arg("--out-dir")
        .arg(&out_dir)
        .output()
        .expect("build tracker project");
    assert!(
        build.status.success(),
        "tracker build failed: {}",
        String::from_utf8_lossy(&build.stderr)
    );
    assert!(out_dir.join("app.manifest.json").is_file());

    let accessibility = Command::new(env!("CARGO_BIN_EXE_noxid"))
        .arg("a11y")
        .arg(tracker.join("src/routes/+page.nox"))
        .output()
        .expect("audit tracker accessibility");
    assert!(
        accessibility.status.success(),
        "tracker accessibility audit failed: {}",
        String::from_utf8_lossy(&accessibility.stderr)
    );

    let scenarios = Command::new(env!("CARGO_BIN_EXE_noxid"))
        .arg("test")
        .arg(&tracker)
        .arg("--gate")
        .arg("--json")
        .output()
        .expect("run tracker scenarios");
    let report = String::from_utf8(scenarios.stdout).unwrap();
    assert!(
        scenarios.status.success(),
        "tracker scenario gate failed: {}\n{}",
        report,
        String::from_utf8_lossy(&scenarios.stderr)
    );
    assert!(report.contains("\"total\":5,\"passed\":5"), "{report}");

    let _ = std::fs::remove_dir_all(out_dir);
}