noxid-cli 0.2.0

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() -> Self {
        let root = std::env::temp_dir().join(format!(
            "noxid-wo38-queue-sqlite-{}-{}",
            std::process::id(),
            NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed)
        ));
        fs::create_dir_all(root.join("server/queues")).expect("create queue fixture");
        fs::create_dir_all(root.join("data")).expect("create SQLite directory");
        fs::write(
            root.join("Noxid.toml"),
            "[app]\ntitle = \"Queue SQLite refusal\"\n",
        )
        .expect("write manifest");
        fs::write(root.join("package.json"), "{\"type\":\"module\"}\n")
            .expect("write package marker");
        fs::write(
            root.join("server/queues/Work.nox"),
            "queue Work { payload {} retry: 0 backoff: 1s }\n",
        )
        .expect("write queue");
        fs::write(
            root.join("server/host.js"),
            "export const queues = Object.freeze({ \"queue:Work\": async () => null });\n",
        )
        .expect("write queue host");
        Self { root }
    }

    fn run(&self, operation: &str, output: &str, database_url: Option<&str>) -> Output {
        let mut command = Command::new(env!("CARGO_BIN_EXE_noxid"));
        command.args([operation, ".", "--out-dir", output]);
        if operation == "adapt" {
            command.args(["--adapter", "node"]);
        }
        command.current_dir(&self.root);
        match database_url {
            Some(url) => command.env("DATABASE_URL", url),
            None => command.env_remove("DATABASE_URL"),
        };
        command.output().expect("run queue build operation")
    }
}

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

#[test]
fn deferred_queue_on_sqlite_build_and_adapt_refuse_before_publishing_output() {
    let fixture = Fixture::new();
    let primed = fixture.run("build", "cached", None);
    assert!(
        primed.status.success(),
        "URL-less queue build failed: {}",
        String::from_utf8_lossy(&primed.stderr)
    );

    for (operation, output, database_url) in [
        ("build", "sqlite-file", "sqlite://data/app.db"),
        ("adapt", "sqlite-memory", "sqlite::memory:"),
    ] {
        let refused = fixture.run(operation, output, Some(database_url));
        let transcript = format!(
            "stdout:\n{}\nstderr:\n{}",
            String::from_utf8_lossy(&refused.stdout),
            String::from_utf8_lossy(&refused.stderr)
        );
        assert!(!refused.status.success(), "{transcript}");
        assert!(
            transcript.contains("error[QUEUE_REQUIRES_SHARED_DATABASE]"),
            "{transcript}"
        );
        assert!(transcript.contains("shared PostgreSQL"), "{transcript}");
        assert!(
            !fixture.root.join(output).exists(),
            "refused {operation} published {output}"
        );
    }
}