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-wo24-qa1-{label}-{}-{ordinal}",
            std::process::id()
        ));
        fs::create_dir_all(root.join("server/queues")).expect("create queue fixture");
        fs::write(
            root.join("Noxid.toml"),
            "[app]\ntitle = \"WO-24 QA round 1\"\n",
        )
        .expect("write config");
        fs::write(root.join("package.json"), "{\"type\":\"module\"}\n")
            .expect("write package marker");
        fs::write(
            root.join("server/host.js"),
            "export const queues = Object.freeze({});\n",
        )
        .expect("write inert queue host");
        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 noxid(&self, arguments: &[&str]) -> Output {
        Command::new(env!("CARGO_BIN_EXE_noxid"))
            .args(arguments)
            .current_dir(&self.root)
            .output()
            .expect("run noxid")
    }

    fn build(&self) -> Output {
        self.noxid(&["build", ".", "--out-dir", "dist"])
    }

    fn run_node(&self, source: &str) -> Output {
        self.write("dist/qa.mjs", source);
        Command::new("node")
            .arg("qa.mjs")
            .current_dir(self.root.join("dist"))
            .env_remove("DATABASE_URL")
            .output()
            .expect("run generated queue module")
    }
}

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

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)
    );
}

#[test]
fn qa_round1_result_wrapped_named_payload_reaches_database_boundary() {
    let fixture = Fixture::new("result-named");
    fixture.write(
        "server/queues/Outcome.nox",
        r#"type Receipt { id: String }
queue Outcome {
    payload { result: Result<Receipt, String> }
    retry: 1
    backoff: 1s
}
"#,
    );
    assert_success(&fixture.build(), "build recursive payload fixture");

    let node = fixture.run_node(
        r#"import { enqueue } from "./server/handler.js";
async function code(operation) {
  try { await operation(); } catch (error) { return error?.code; }
  return null;
}
const accepted = await code(() => enqueue("Outcome", { result: { tag: "Ok", value: { id: "r-1" } } }));
if (accepted !== "QUEUE_DATABASE_URL_REQUIRED") throw new Error(`valid Result<Receipt, String> did not reach storage: ${accepted}`);
const rejected = await code(() => enqueue("Outcome", { result: { tag: "Ok", value: { id: 1 } } }));
if (rejected !== "QUEUE_PAYLOAD_TYPE") throw new Error(`invalid nested Receipt was not refused: ${rejected}`);
"#,
    );
    assert_success(&node, "validate Result-wrapped named queue payload");
}

#[test]
fn qa_round1_payload_validation_never_invokes_accessors_or_accepts_symbols() {
    let fixture = Fixture::new("ordinary-data");
    fixture.write(
        "server/queues/Safe.nox",
        "queue Safe { payload { label: String } retry: 0 backoff: 1s }\n",
    );
    assert_success(&fixture.build(), "build ordinary-data fixture");

    let node = fixture.run_node(
        r#"import { enqueue } from "./server/handler.js";
let reads = 0;
const accessor = {};
Object.defineProperty(accessor, "label", { enumerable: true, get() { reads += 1; return "stolen"; } });
const symbol = { label: "ok", [Symbol("smuggled")]: true };
const inherited = Object.create({ label: "inherited" });
for (const [label, payload] of [["accessor", accessor], ["symbol", symbol], ["prototype", inherited]]) {
  let code = null;
  try { await enqueue("Safe", payload); } catch (error) { code = error?.code; }
  if (code !== "QUEUE_PAYLOAD_TYPE") throw new Error(`${label} payload escaped: ${code}`);
}
if (reads !== 0) throw new Error(`payload getter executed ${reads} time(s)`);
"#,
    );
    assert_success(&node, "exercise ordinary-data queue payload boundary");
}

#[test]
fn qa_round1_empty_and_optional_payload_boundaries_are_closed_but_usable() {
    let fixture = Fixture::new("empty-optional");
    fixture.write(
        "server/queues/Empty.nox",
        "queue Empty { payload {} retry: 0 backoff: 1s }\n",
    );
    fixture.write(
        "server/queues/Maybe.nox",
        "queue Maybe { payload { note: Optional<String> } retry: 0 backoff: 1s }\n",
    );
    assert_success(&fixture.build(), "build boundary-value fixture");

    let node = fixture.run_node(
        r#"import { enqueue } from "./server/handler.js";
async function code(operation) {
  try { await operation(); } catch (error) { return error?.code; }
  return null;
}
for (const operation of [
  () => enqueue("Empty", Object.create(null)),
  () => enqueue("Maybe", {}),
  () => enqueue("Maybe", { note: null }),
]) {
  const observed = await code(operation);
  if (observed !== "QUEUE_DATABASE_URL_REQUIRED") throw new Error(`valid boundary value was refused before storage: ${observed}`);
}
for (const operation of [
  () => enqueue("Empty", { extra: 1 }),
  () => enqueue("Maybe", { note: "ok", extra: 1 }),
]) {
  const observed = await code(operation);
  if (observed !== "QUEUE_PAYLOAD_TYPE") throw new Error(`undeclared key escaped: ${observed}`);
}
"#,
    );
    assert_success(&node, "exercise empty and optional queue payloads");
}

#[test]
fn qa_round1_queue_scenario_executes_conditional_handler_at_stubbed_clock() {
    let fixture = Fixture::new("scenario-if");
    fixture.write(
        "server/queues/Classify.nox",
        r#"queue Classify {
    payload { count: Int }
    handler {
        if count < 0 { return "negative" }
        else { return "nonnegative" }
    }
    retry: 2
    backoff: 7s
    scenario Negative {
        description: "conditional queue handlers execute without live storage"
        clock: "2026-08-29T12:00:00Z"
        when: enqueue(count: -1)
        expect: value == "negative", attempts == 1, refusal == ""
    }
}
"#,
    );
    let output = fixture.noxid(&["test", ".", "--gate", "--json"]);
    assert_success(&output, "execute conditional queue scenario");
    let stdout = String::from_utf8_lossy(&output.stdout);
    assert!(stdout.contains("\"summary\":{\"total\":1,\"passed\":1,\"failed\":0"));
    assert!(stdout.contains("\"semanticUnit\":\"queue:Classify\""));
}

#[test]
fn qa_round1_queue_layout_and_handler_side_effects_fail_closed() {
    let fixture = Fixture::new("fail-closed");
    fixture.write(
        "server/queues/Unsafe.nox",
        r#"queue Unsafe {
    payload { value: Int }
    handler { value = 2 }
    retry: 1
    backoff: 1s
}
"#,
    );
    let rejected = fixture.build();
    assert!(!rejected.status.success());
    let stderr = String::from_utf8_lossy(&rejected.stderr);
    assert!(
        stderr.contains("QUEUE_HANDLER_INVALID"),
        "mutable queue handler did not fail closed:\n{stderr}"
    );
}