use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::thread;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
struct ComposePostgres {
file: PathBuf,
project: String,
}
impl ComposePostgres {
fn start() -> Option<Self> {
if !Command::new("docker")
.arg("info")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|status| status.success())
{
eprintln!(
"WO-24 QA round 1 Postgres: SKIP (Docker daemon unavailable; concurrent claim, scheduled delivery, injection, and claim drift were not exercised)"
);
return None;
}
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock after epoch")
.as_nanos();
let postgres = Self {
file: Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/wo19-postgres/compose.yaml"),
project: format!("noxidwo24qa1{}{}", std::process::id(), nonce),
};
let output = postgres
.command()
.args(["up", "--detach"])
.output()
.expect("start QA Postgres");
assert!(
output.status.success(),
"Docker was available but QA Postgres failed to start: {}",
String::from_utf8_lossy(&output.stderr)
);
for _ in 0..480 {
if postgres
.command()
.args([
"exec",
"--no-TTY",
"postgres",
"pg_isready",
"-U",
"noxid_test",
"-d",
"noxid_test",
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("probe QA Postgres")
.success()
{
return Some(postgres);
}
thread::sleep(Duration::from_millis(250));
}
panic!("QA Postgres did not become ready within 120 seconds");
}
fn command(&self) -> Command {
let mut command = Command::new("docker");
command
.args(["compose", "-f"])
.arg(&self.file)
.args(["--project-name", &self.project]);
command
}
fn database_url(&self) -> String {
let output = self
.command()
.args(["port", "postgres", "5432"])
.output()
.expect("resolve QA Postgres port");
assert!(output.status.success());
let mapping = String::from_utf8(output.stdout).expect("UTF-8 port mapping");
let port = mapping
.trim()
.rsplit_once(':')
.map(|(_, port)| port)
.expect("mapped port");
format!("postgres://noxid_test:noxid_test@127.0.0.1:{port}/noxid_test")
}
}
impl Drop for ComposePostgres {
fn drop(&mut self) {
let _ = self
.command()
.args(["down", "--volumes", "--remove-orphans"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
}
struct Scratch(PathBuf);
impl Drop for Scratch {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
#[test]
fn qa_round1_postgres_claims_once_preserves_hostile_text_and_refuses_drift() {
let Some(postgres) = ComposePostgres::start() else {
return;
};
let root = std::env::temp_dir().join(format!("noxid-wo24-postgres-qa1-{}", std::process::id()));
let _ = fs::remove_dir_all(&root);
let _scratch = Scratch(root.clone());
fs::create_dir_all(root.join("server/queues")).expect("create QA queue project");
fs::write(
root.join("Noxid.toml"),
"[app]\ntitle = \"Queue Postgres QA\"\n",
)
.expect("write config");
fs::write(root.join("package.json"), "{\"type\":\"module\"}\n").expect("write package");
fs::write(
root.join("server/queues/Deliver.nox"),
"queue Deliver { payload { label: String } retry: 0 backoff: 1s }\n",
)
.expect("write queue");
fs::write(
root.join("server/host.js"),
r#"export const queues = Object.freeze({
"queue:Deliver": async (payload) => payload.label,
});
"#,
)
.expect("write queue host");
let build = Command::new(env!("CARGO_BIN_EXE_noxid"))
.args(["build", ".", "--out-dir", "dist"])
.current_dir(&root)
.output()
.expect("build QA queue project");
assert!(
build.status.success(),
"{}",
String::from_utf8_lossy(&build.stderr)
);
#[cfg(unix)]
{
let repository_modules = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../node_modules");
if !repository_modules.join("postgres").exists() {
eprintln!(
"WO-24 QA round 1 Postgres: SKIP (workspace postgres Node driver unavailable)"
);
return;
}
std::os::unix::fs::symlink(&repository_modules, root.join("node_modules"))
.expect("link admitted postgres driver");
}
fs::write(
root.join("dist/qa.mjs"),
r#"import postgres from "postgres";
import { enqueue, workQueueOnce, queueStatus, closeQueueDatabase } from "./server/handler.js";
const hostile = "x'); DROP TABLE _noxid_jobs; -- </script>
✓";
const first = await enqueue("Deliver", { label: hostile }, { runAt: "2026-08-29T12:00:00Z" });
const second = await enqueue("Deliver", { label: "second" }, { runAt: "2026-08-29T12:00:00Z" });
const claims = await Promise.all([
workQueueOnce({ queue: "Deliver", worker: "qa-left", now: "2026-08-29T12:00:00Z" }),
workQueueOnce({ queue: "Deliver", worker: "qa-right", now: "2026-08-29T12:00:00Z" }),
]);
if (new Set(claims.map((job) => job?.id)).size !== 2) throw new Error(`duplicate concurrent claim: ${JSON.stringify(claims)}`);
if (!claims.every((job) => job?.state === "completed")) throw new Error(`claim did not complete: ${JSON.stringify(claims)}`);
const values = new Map(claims.map((job) => [job.id, job.value]));
if (values.get(first.id) !== hostile || values.get(second.id) !== "second") throw new Error(`payload text changed: ${JSON.stringify(claims)}`);
await enqueue("Deliver", { label: "future" }, { runAt: "2026-08-29T13:00:00Z" });
if (await workQueueOnce({ queue: "Deliver", now: "2026-08-29T12:59:59.999Z" }) !== null) throw new Error("future job ran early");
const future = await workQueueOnce({ queue: "Deliver", now: "2026-08-29T13:00:00Z" });
if (future?.state !== "completed" || future.value !== "future") throw new Error(`scheduled job changed: ${JSON.stringify(future)}`);
const sql = postgres(process.env.DATABASE_URL, { max: 1 });
await sql`INSERT INTO _noxid_jobs (id, queue, payload, state, attempts, run_at) VALUES ('qa-drift', 'Deliver', ${{ wrong: true }}, 'pending', 0, ${new Date("2026-08-29T14:00:00Z")})`;
let drift = null;
try { await workQueueOnce({ queue: "Deliver", now: "2026-08-29T14:00:00Z" }); } catch (error) { drift = error?.code; }
if (drift !== "QUEUE_PAYLOAD_DRIFT") throw new Error(`claim drift escaped: ${drift}`);
const driftRows = await sql`SELECT state FROM _noxid_jobs WHERE id = 'qa-drift'`;
if (driftRows[0]?.state !== "dead-letter") throw new Error(`drift was not dead-lettered: ${JSON.stringify(driftRows)}`);
const status = await queueStatus("Deliver");
if (!status.some((entry) => entry.state === "completed" && entry.count === 3)) throw new Error(`completed status changed: ${JSON.stringify(status)}`);
if (!status.some((entry) => entry.state === "dead-letter" && entry.count === 1)) throw new Error(`dead-letter status changed: ${JSON.stringify(status)}`);
await sql.end({ timeout: 1 });
await closeQueueDatabase();
"#,
)
.expect("write QA Postgres exercise");
let node = Command::new("node")
.arg("qa.mjs")
.current_dir(root.join("dist"))
.env("DATABASE_URL", postgres.database_url())
.output()
.expect("run QA Postgres contract");
assert!(
node.status.success(),
"stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&node.stdout),
String::from_utf8_lossy(&node.stderr)
);
}