noxid-cli 0.2.1

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

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

struct Fixture {
    root: PathBuf,
}

impl Fixture {
    fn new(label: &str) -> Self {
        let ordinal = NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed);
        let root = std::env::temp_dir().join(format!(
            "noxid-wo21-stream-layout-{label}-{}-{ordinal}",
            std::process::id()
        ));
        fs::create_dir_all(&root).expect("create stream endpoint fixture");
        fs::write(
            root.join("Noxid.toml"),
            "[app]\ntitle = \"WO-21 streams\"\n",
        )
        .expect("write project config");
        Self { root }
    }

    fn write(&self, relative: &str, contents: &str) {
        let path = self.root.join(relative);
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).expect("create fixture parent");
        }
        fs::write(path, contents).expect("write fixture source");
    }

    fn build(&self) -> Output {
        Command::new(env!("CARGO_BIN_EXE_noxid"))
            .args(["build", self.root.to_str().expect("UTF-8 fixture")])
            .arg("--out-dir")
            .arg(self.root.join("dist"))
            .output()
            .expect("run stream endpoint build")
    }
}

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

fn assert_failure(output: &Output, code: &str, teaching: &str) {
    assert!(
        !output.status.success(),
        "expected {code}, stdout:\n{}",
        String::from_utf8_lossy(&output.stdout)
    );
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains(code), "missing {code}:\n{stderr}");
    assert!(
        stderr.contains(teaching),
        "missing teaching text:\n{stderr}"
    );
}

#[test]
fn stream_endpoint_is_restricted_to_server_api() {
    let fixture = Fixture::new("routes");
    fixture.write(
        "server/routes/events.get.nox",
        "stream endpoint Events { result: Stream<String> timeout: 30s }",
    );
    let output = fixture.build();
    assert_failure(&output, "STREAM_ENDPOINT_API_LAYOUT_REQUIRED", "server/api");
    assert!(!fixture.root.join("dist").exists());
}

#[test]
fn stream_endpoint_is_get_only() {
    let fixture = Fixture::new("post");
    fixture.write(
        "server/api/events.post.nox",
        "stream endpoint Events { result: Stream<String> timeout: 30s }",
    );
    let output = fixture.build();
    assert_failure(&output, "STREAM_ENDPOINT_REQUIRES_GET", "events.get.nox");
    assert!(!fixture.root.join("dist").exists());
}

#[test]
fn websocket_endpoint_syntax_is_not_admitted_in_sse_phase_one() {
    let fixture = Fixture::new("websocket");
    fixture.write(
        "server/api/socket.get.nox",
        "websocket endpoint Socket { result: Stream<String> }",
    );
    let output = fixture.build();
    assert_failure(&output, "EXPECTED_DECLARATION", "expected an import");
    assert!(!fixture.root.join("dist").exists());
}