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-qa2-{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 2\"\n",
        )
        .expect("write project manifest");
        fs::write(root.join("package.json"), "{\"type\":\"module\"}\n")
            .expect("write ESM package marker");
        fs::write(
            root.join("server/host.js"),
            "export const queues = Object.freeze({});\n",
        )
        .expect("write inert 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 file");
    }

    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, name: &str, source: &str) -> Output {
        self.write(&format!("dist/{name}.mjs"), source);
        Command::new("node")
            .arg(format!("{name}.mjs"))
            .current_dir(self.root.join("dist"))
            .env_remove("DATABASE_URL")
            .output()
            .expect("execute generated queue runtime")
    }
}

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

fn output_text(output: &Output) -> String {
    format!(
        "stdout:\n{}\nstderr:\n{}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    )
}

fn assert_success(output: &Output, context: &str) {
    assert!(
        output.status.success(),
        "{context}:\n{}",
        output_text(output)
    );
}

#[test]
fn qa_round2_imported_named_result_branches_validate_through_deep_wrappers() {
    let fixture = Fixture::new("deep-imports");
    fixture.write(
        "server/types.nox",
        "type Receipt { id: Int }\ntype Failure { message: String }\n",
    );
    fixture.write(
        "server/queues/Outcomes.nox",
        r#"import { Receipt, Failure } from "../types.nox"
queue Outcomes {
    payload { batches: Array<Map<String, Optional<Result<Receipt, Failure>>>> }
    retry: 2
    backoff: 2s
}
"#,
    );
    assert_success(&fixture.build(), "build deeply wrapped imported payload");

    let node = fixture.run_node(
        "deep-imports",
        r#"import { enqueue } from "./server/handler.js";
async function code(operation) {
  try { await operation(); } catch (error) { return error?.code; }
  return null;
}
const valid = { batches: [{
  accepted: { tag: "Ok", value: { id: 7 } },
  refused: { tag: "Err", value: { message: "closed" } },
  absent: null,
}] };
if (await code(() => enqueue("Outcomes", valid)) !== "QUEUE_DATABASE_URL_REQUIRED") throw new Error("valid named Result branches did not reach storage");
if (await code(() => enqueue("Outcomes", { batches: [{ bad: { tag: "Ok", value: { id: "7" } } }] })) !== "QUEUE_PAYLOAD_TYPE") throw new Error("invalid Receipt branch escaped");
if (await code(() => enqueue("Outcomes", { batches: [{ bad: { tag: "Err", value: { message: 7 } } }] })) !== "QUEUE_PAYLOAD_TYPE") throw new Error("invalid Failure branch escaped");
"#,
    );
    assert_success(&node, "execute deeply wrapped imported validation");
}

#[test]
fn qa_round2_imported_payload_edits_invalidate_but_content_touches_hit_cache() {
    let fixture = Fixture::new("import-cache");
    fixture.write("server/types.nox", "type Receipt { id: Int }\n");
    fixture.write(
        "server/queues/Outcomes.nox",
        r#"import { Receipt } from "../types.nox"
queue Outcomes { payload { result: Result<Receipt, String> } retry: 1 backoff: 1s }
"#,
    );
    assert_success(&fixture.build(), "build initial imported queue payload");

    fixture.write("server/types.nox", "type Receipt { id: String }\n");
    let edited = fixture.build();
    assert_success(&edited, "rebuild edited imported queue payload");
    assert!(
        String::from_utf8_lossy(&edited.stdout).contains("persistent cache: miss"),
        "imported type edit reused stale queue cache:\n{}",
        output_text(&edited)
    );
    let node = fixture.run_node(
        "edited-validator",
        r#"import { enqueue } from "./server/handler.js";
async function code(payload) {
  try { await enqueue("Outcomes", payload); } catch (error) { return error?.code; }
  return null;
}
if (await code({ result: { tag: "Ok", value: { id: 7 } } }) !== "QUEUE_PAYLOAD_TYPE") throw new Error("stale Int validator survived imported edit");
if (await code({ result: { tag: "Ok", value: { id: "seven" } } }) !== "QUEUE_DATABASE_URL_REQUIRED") throw new Error("fresh String validator was not emitted");
"#,
    );
    assert_success(&node, "execute refreshed queue validator");

    let identical = fs::read_to_string(fixture.root.join("server/types.nox"))
        .expect("read imported type for touch");
    fixture.write("server/types.nox", &identical);
    let touched = fixture.build();
    assert_success(&touched, "rebuild content-identical imported queue payload");
    assert!(
        String::from_utf8_lossy(&touched.stdout).contains("persistent cache: hit"),
        "content-identical touch missed queue cache:\n{}",
        output_text(&touched)
    );
}

#[test]
fn qa_round2_enqueue_options_are_ordinary_data_and_run_at_is_date_or_iso_only() {
    let fixture = Fixture::new("hostile-options");
    fixture.write(
        "server/queues/Safe.nox",
        "queue Safe { payload { label: String } retry: 0 backoff: 1s }\n",
    );
    assert_success(&fixture.build(), "build hostile options fixture");
    let node = fixture.run_node(
        "hostile-options",
        r#"import { enqueue } from "./server/handler.js";
async function code(options) {
  try { await enqueue("Safe", { label: "ok" }, options); } catch (error) { return error?.code; }
  return null;
}
let reads = 0;
const accessor = {};
Object.defineProperty(accessor, "runAt", { enumerable: true, get() { reads += 1; return "2026-08-30T12:00:00Z"; } });
const inherited = Object.create({ runAt: "2026-08-30T12:00:00Z" });
const cases = [
  ["accessor", accessor, "QUEUE_OPTIONS_INVALID"],
  ["inherited", inherited, "QUEUE_OPTIONS_INVALID"],
  ["numeric", { runAt: 0 }, "QUEUE_RUN_AT_INVALID"],
  ["boolean", { runAt: true }, "QUEUE_RUN_AT_INVALID"],
];
const failures = [];
for (const [label, options, expected] of cases) {
  const observed = await code(options);
  if (observed !== expected) failures.push(`${label}: expected ${expected}, saw ${observed}`);
}
if (reads !== 0) failures.push(`runAt getter executed ${reads} time(s)`);
if (failures.length > 0) throw new Error(failures.join("; "));
"#,
    );
    assert_success(&node, "reject effectful or non-temporal enqueue options");
}

#[test]
fn qa_round2_stopping_worker_clears_scheduled_poll_after_boundary_failure() {
    let fixture = Fixture::new("worker-disposal");
    fixture.write(
        "server/queues/Safe.nox",
        "queue Safe { payload {} retry: 0 backoff: 1s }\n",
    );
    assert_success(&fixture.build(), "build worker disposal fixture");
    let node = fixture.run_node(
        "worker-disposal",
        r#"import { startQueueWorker } from "./server/handler.js";
const timers = new Map();
const errors = [];
let nextTimer = 0;
const worker = startQueueWorker({
  pollIntervalMs: 17,
  setTimeout(callback, delay) { const id = ++nextTimer; timers.set(id, { callback, delay }); return id; },
  clearTimeout(id) { timers.delete(id); },
  onError(error) { errors.push(error?.code); },
});
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
if (errors.length !== 1 || errors[0] !== "QUEUE_DATABASE_URL_REQUIRED") throw new Error(`unexpected worker boundary errors: ${JSON.stringify(errors)}`);
if (timers.size !== 1 || [...timers.values()][0].delay !== 17) throw new Error(`poll timer was not scheduled exactly once: ${JSON.stringify([...timers.values()].map(({ delay }) => delay))}`);
await worker.stop();
if (timers.size !== 0) throw new Error(`worker stop leaked ${timers.size} timer(s)`);
"#,
    );
    assert_success(&node, "dispose queue worker polling owner");
}