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-wo27-pool-{label}-{}-{ordinal}",
            std::process::id()
        ));
        fs::create_dir_all(root.join("server/api")).expect("create endpoint directory");
        fs::create_dir_all(root.join("server/queues")).expect("create queue directory");
        fs::write(root.join("package.json"), "{\"type\":\"module\"}\n")
            .expect("write package marker");
        Self { root }
    }

    fn build(&self) -> Output {
        Command::new(env!("CARGO_BIN_EXE_noxid"))
            .args(["build", ".", "--out-dir", "dist"])
            .current_dir(&self.root)
            .output()
            .expect("build pool fixture")
    }
}

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

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

#[test]
fn db_pool_flows_to_host_database_environment_and_queue_connection() {
    let fixture = Fixture::new("threading");
    fs::write(
        fixture.root.join("Noxid.toml"),
        "[app]\ntitle = \"Pool threading\"\n\n[server]\ndb_pool = 3\n",
    )
    .expect("write manifest");
    fs::write(
        fixture.root.join("server/api/pool.get.nox"),
        "endpoint Pool { result: Int }\n",
    )
    .expect("write pool endpoint");
    fs::write(
        fixture.root.join("server/queues/Work.nox"),
        "queue Work { payload { value: Int } retry: 1 backoff: 1s }\n",
    )
    .expect("write queue");
    fs::write(
        fixture.root.join("server/host.js"),
        r#"export const endpoints = Object.freeze({
  "endpoint:Pool@1": async (_args, context) => {
    if (context.environment.self && context.environment !== context.environment.self) throw new Error("environment identity changed");
    if (!context.environment.locked) context.environment.authorized = (context.environment.authorized ?? 0) + 1;
    let overwriteRefused = false;
    let deleteRefused = false;
    try { context.environment.dbPool = 99; } catch { overwriteRefused = true; }
    try { delete context.environment.dbPool; } catch { deleteRefused = true; }
    if (!overwriteRefused || !deleteRefused || context.environment.dbPool !== 3) throw new Error("compiler-owned dbPool was mutable");
    return context.environment.dbPool;
  },
});
export const queues = Object.freeze({ "queue:Work": async ({ value }) => value });
"#,
    )
    .expect("write host");
    let build = fixture.build();
    assert_success(&build, "build configured pool fixture");

    let handler = fs::read_to_string(fixture.root.join("dist/server/handler.js"))
        .expect("read generated handler");
    assert!(handler.contains("const databasePoolSize = 3;"));
    assert!(handler.contains("postgres(url, { max: databasePoolSize })"));
    assert!(!handler.contains("postgres(url, { max: 10 })"));
    assert!(handler.contains("Object.defineProperty(environment, \"dbPool\""));

    fs::write(
        fixture.root.join("dist/exercise-pool.mjs"),
        r#"import { fetch as handle } from "./server/handler.js";
const environment = { authorized: 0 };
environment.self = environment;
const response = await handle(new Request("http://noxid.test/api/pool"), environment);
const body = await response.json();
if (response.status !== 200 || body.value !== 3) throw new Error(`configured pool did not reach host: ${response.status} ${JSON.stringify(body)}`);
if (environment.authorized !== 1) throw new Error(`host writes did not reach the original environment: ${JSON.stringify(environment)}`);
const pool = Object.getOwnPropertyDescriptor(environment, "dbPool");
if (pool?.value !== 3 || pool.enumerable !== false || pool.writable !== false || pool.configurable !== false) throw new Error(`compiler-owned pool property changed: ${JSON.stringify(pool)}`);
delete process.env.dbPool;
delete process.env.authorized;
const processResponse = await handle(new Request("http://noxid.test/api/pool"), process.env);
const processBody = await processResponse.json();
if (processResponse.status !== 200 || processBody.value !== 3 || Object.hasOwn(process.env, "dbPool") || process.env.authorized !== "1") throw new Error(`process.env forwarding changed: ${processResponse.status} ${JSON.stringify(processBody)}`);
delete process.env.authorized;
const frozen = Object.freeze({ locked: true });
const frozenResponse = await handle(new Request("http://noxid.test/api/pool"), frozen);
const frozenBody = await frozenResponse.json();
if (frozenResponse.status !== 200 || frozenBody.value !== 3 || Object.hasOwn(frozen, "dbPool")) throw new Error(`frozen environment was mutated or lost dbPool: ${frozenResponse.status} ${JSON.stringify(frozenBody)}`);
"#,
    )
    .expect("write pool exercise");
    let node = Command::new("node")
        .arg("exercise-pool.mjs")
        .current_dir(fixture.root.join("dist"))
        .output()
        .expect("exercise configured pool environment");
    assert_success(&node, "exercise configured pool environment");

    let adapter = fs::read_to_string(
        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../plugins/drizzle-orm/adapter.js"),
    )
    .expect("read Drizzle adapter");
    assert!(adapter.contains("const poolSize = environment?.dbPool ?? 10;"));
    assert!(adapter.contains("postgres(url, { max: poolSize })"));
    assert!(adapter.contains("connectionLimit: poolSize"));
    assert!(!adapter.contains("postgres(url, { max: 10 })"));

    let adapter_test = Command::new("node")
        .args(["--test", "plugins/drizzle-orm/pool.test.mjs"])
        .current_dir(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."))
        .output()
        .expect("run Drizzle pool contract");
    assert_success(&adapter_test, "exercise Drizzle pool contract");
}