noxid-cli 0.2.1

The Noxid compiler command line: check, build, test, adapt, and the agent surface
use noxid_runtime::NODE_TEST_DOM;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};

const APP_COMMIT: &str = "b88e2734ba368cf0562a60fe7c148603716e2f2a";

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

struct TempTree {
    root: PathBuf,
}

impl TempTree {
    fn new(label: &str) -> Self {
        let ordinal = NEXT_TEMP.fetch_add(1, Ordering::Relaxed);
        let root = std::env::temp_dir().join(format!(
            "noxid-t3-3-qa2-{label}-{}-{ordinal}",
            std::process::id()
        ));
        fs::create_dir_all(&root).expect("create T3-3 QA2 scratch directory");
        Self { root }
    }
}

impl Drop for TempTree {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.root);
    }
}

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

fn assert_success(output: &Output, context: &str) {
    assert!(
        output.status.success(),
        "{context}:\nstdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}

fn archived_app(scratch: &TempTree) -> PathBuf {
    let root = repository_root();
    let mut git = Command::new("git")
        .args([
            "archive",
            "--format=tar",
            APP_COMMIT,
            "--",
            "benchmarks/js-framework-benchmark/Nuu.toml",
            "benchmarks/js-framework-benchmark/src/benchmark.client.js",
            "benchmarks/js-framework-benchmark/src/benchmark.client.js.nuu-contract",
            "benchmarks/js-framework-benchmark/src/global.css",
            "benchmarks/js-framework-benchmark/src/routes/index.html/+page.nuu",
        ])
        .current_dir(&root)
        .stdout(Stdio::piped())
        .spawn()
        .expect("archive exact T3-3 app commit");
    let mut tar = Command::new("tar")
        .args(["-xf", "-", "-C"])
        .arg(&scratch.root)
        .stdin(Stdio::piped())
        .spawn()
        .expect("extract exact T3-3 app commit");
    std::io::copy(
        &mut git.stdout.take().expect("Git archive stdout"),
        &mut tar.stdin.take().expect("tar stdin"),
    )
    .expect("pipe exact T3-3 archive");
    assert!(git.wait().expect("wait for Git archive").success());
    assert!(tar.wait().expect("wait for tar extraction").success());
    let app = scratch.root.join("benchmarks/js-framework-benchmark");
    // The archived commit predates the Noxid rename; the current toolchain
    // requires the renamed project layout, so translate the extracted names.
    for (historical, current) in [
        ("Nuu.toml", "Noxid.toml"),
        (
            "src/benchmark.client.js.nuu-contract",
            "src/benchmark.client.js.nox-contract",
        ),
        (
            "src/routes/index.html/+page.nuu",
            "src/routes/index.html/+page.nox",
        ),
    ] {
        fs::rename(app.join(historical), app.join(current))
            .expect("translate archived app file to renamed layout");
    }
    app
}

fn all_text(root: &Path) -> String {
    fn visit(path: &Path, output: &mut String) {
        for entry in fs::read_dir(path).expect("read QA2 output directory") {
            let path = entry.expect("read QA2 output entry").path();
            if path.is_dir() {
                visit(&path, output);
            } else if let Ok(text) = fs::read_to_string(path) {
                output.push_str(&text);
            }
        }
    }
    let mut output = String::new();
    visit(root, &mut output);
    output
}

#[test]
fn qa_t3_3_round2_reconstructs_and_falsifies_replacement_provenance() {
    let root = repository_root();
    let output = Command::new("node")
        .args([
            "--test",
            "benchmarks/js-framework-benchmark/tools/qa2-provenance.test.mjs",
        ])
        .current_dir(&root)
        .output()
        .expect("Node.js is required for T3-3 QA2 provenance tests");
    assert_success(&output, "T3-3 QA2 provenance and corruption suite");
}

#[test]
fn qa_t3_3_round2_executes_official_ids_operations_and_key_identity() {
    let scratch = TempTree::new("keyed-dom");
    let project = archived_app(&scratch);
    let output = scratch.root.join("build");
    let build = Command::new(env!("CARGO_BIN_EXE_noxid"))
        .arg("build")
        .arg(&project)
        .arg("--out-dir")
        .arg(&output)
        .current_dir(repository_root())
        .output()
        .expect("build exact committed benchmark app for QA2");
    assert_success(&build, "T3-3 QA2 exact-commit build");

    let script = output.join("qa2-keyed-dom.mjs");
    fs::write(
        &script,
        format!(
            r#"{NODE_TEST_DOM}
Math.random = () => 0;
const {{ mountBenchmarkPage }} = await import("./assets/BenchmarkPage.js");
const host = document.createElement("div");
const app = mountBenchmarkPage(host);
const byId = (id) => host.querySelector(`[id="${{id}}"]`);
const click = (node) => node.dispatchEvent({{ type: "click", defaultPrevented: false }});
const text = (node) => node.nodeType === 3 ? node.data : node.childNodes.map(text).join("");
const rows = () => byId("tbody").querySelectorAll("tr");
const id = (row) => Number(text(row.querySelectorAll("td")[0]));
for (const expected of ["main", "run", "runlots", "add", "update", "clear", "swaprows", "tbody"]) {{
  if (!byId(expected)) throw new Error(`missing official id ${{expected}}`);
}}
click(byId("run"));
if (rows().length !== 1000 || id(rows()[0]) !== 1 || id(rows()[999]) !== 1000) throw new Error("create 1k boundary");
const first = rows()[0];
const second = rows()[1];
const nineNinetyNine = rows()[998];
click(byId("update"));
if (rows()[0] !== first || !text(rows()[0]).includes(" !!!") || text(rows()[9]).includes(" !!!") || !text(rows()[10]).includes(" !!!")) throw new Error("update/key identity boundary");
click(rows()[0].querySelectorAll("a")[0]);
if (rows()[0].getAttribute("class") !== "danger") throw new Error("select operation");
click(byId("swaprows"));
if (rows()[1] !== nineNinetyNine || rows()[998] !== second || id(rows()[1]) !== 999 || id(rows()[998]) !== 2) throw new Error("swap/key identity boundary");
click(rows()[0].querySelectorAll("a")[1]);
if (rows().length !== 999 || rows().some((row) => id(row) === 1) || rows().some((row) => row.getAttribute("class") === "danger")) throw new Error("remove operation");
click(byId("clear"));
if (rows().length !== 0) throw new Error("clear operation");
click(byId("runlots"));
if (rows().length !== 10000 || id(rows()[0]) !== 1001 || id(rows()[9999]) !== 11000) throw new Error("create 10k boundary");
click(byId("add"));
if (rows().length !== 11000 || id(rows()[10000]) !== 11001 || id(rows()[10999]) !== 12000) throw new Error("append 1k boundary");
app.dispose();
console.log("t3-3-qa2-keyed-dom-ok");
"#
        ),
    )
    .expect("write QA2 generated DOM script");
    let execution = Command::new("node")
        .arg(script.file_name().expect("QA2 DOM script name"))
        .current_dir(&output)
        .output()
        .expect("execute QA2 generated DOM script");
    assert_success(&execution, "T3-3 QA2 generated DOM behavior");
}

#[test]
fn qa_t3_3_round2_production_base_and_same_output_rebuild_are_fresh() {
    let scratch = TempTree::new("production-rebuild");
    let project = archived_app(&scratch);
    let output = scratch.root.join("dist");
    let bundle = || {
        Command::new(env!("CARGO_BIN_EXE_noxid"))
            .arg("bundle")
            .arg(&project)
            .arg("--out-dir")
            .arg(&output)
            .current_dir(repository_root())
            .output()
            .expect("bundle exact committed benchmark app for QA2")
    };
    assert_success(&bundle(), "T3-3 QA2 initial production bundle");
    let index = fs::read_to_string(output.join("index.html")).expect("read QA2 production index");
    assert!(index.contains("setPublicPaths([\"/frameworks/keyed/nuulang/dist/\"])"));
    assert!(index.contains("data-farm-resource=true"));
    let first = all_text(&output);
    assert!(first.contains("NuuLang-keyed"));
    assert!(!first.contains(&repository_root().to_string_lossy().to_string()));

    let page = project.join("src/routes/index.html/+page.nox");
    let edited = fs::read_to_string(&page)
        .expect("read archived QA2 page")
        .replacen("<h1>NuuLang-keyed</h1>", "<h1>QA2-fresh-output</h1>", 1);
    let mut file = fs::File::create(&page).expect("open archived QA2 page for edit");
    file.write_all(edited.as_bytes())
        .expect("edit archived QA2 page");
    assert_success(&bundle(), "T3-3 QA2 same-output rebuild");
    let rebuilt = all_text(&output);
    assert!(rebuilt.contains("QA2-fresh-output"));
    assert!(!rebuilt.contains(">NuuLang-keyed<"));
    let rebuilt_index =
        fs::read_to_string(output.join("index.html")).expect("read rebuilt QA2 index");
    assert!(rebuilt_index.contains("/frameworks/keyed/nuulang/dist/assets/"));
}