use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::{SystemTime, UNIX_EPOCH};
struct QaFixture {
root: PathBuf,
}
impl QaFixture {
fn new(label: &str) -> Self {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock after Unix epoch")
.as_nanos();
let root = std::env::temp_dir().join(format!(
"noxid-wo38-qa-round1-{label}-{}-{nonce}",
std::process::id()
));
fs::create_dir_all(root.join("server/db/migrations"))
.expect("create QA migration directory");
fs::create_dir(root.join("data")).expect("create QA database directory");
fs::write(root.join("Noxid.toml"), "[project]\nname = \"wo38-qa\"\n")
.expect("write QA project manifest");
Self { root }
}
fn migration(&self, name: &str, sql: &str) {
fs::write(self.root.join("server/db/migrations").join(name), sql)
.expect("write QA migration");
}
fn remove_migration(&self, name: &str) {
fs::remove_file(self.root.join("server/db/migrations").join(name))
.expect("remove QA migration");
}
fn noxid(&self, arguments: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_noxid"))
.current_dir(&self.root)
.args(arguments)
.env("DATABASE_URL", "sqlite://data/qa.db")
.output()
.expect("execute noxid against SQLite QA fixture")
}
}
impl Drop for QaFixture {
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}: {}",
output_text(output)
);
}
#[test]
fn qa_round1_docker_free_sqlite_contract_lane_executes_every_node_probe() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let output = Command::new("node")
.args(["--test", "plugins/drizzle-orm/sqlite.qa_round1.test.mjs"])
.current_dir(root)
.env("DOCKER_HOST", "unix:///definitely-not-a-docker-socket")
.output()
.expect("Node.js is required by the Docker-free SQLite gate");
assert_success(&output, "execute permanent SQLite QA Node lane");
let text = output_text(&output);
for proof in [
"qa_round1 SQLite URL grammar",
"qa_round1 node:sqlite shim",
"qa_round1 Drizzle transactions",
"qa_round1 validatedRows",
"qa_round1 disabled node:sqlite",
] {
assert!(
text.contains(proof),
"missing execution proof {proof}: {text}"
);
}
assert!(
text.contains("pass 5"),
"all five probes must execute: {text}"
);
assert!(
!text.contains("skipped 1"),
"SQLite QA lane skipped: {text}"
);
}
#[test]
fn qa_round1_sqlite_migration_ddl_and_history_record_roll_back_together() {
let fixture = QaFixture::new("ddl-rollback");
fixture.migration(
"0001-transactional-ddl.sql",
"CREATE TABLE rollback_probe (id INTEGER PRIMARY KEY);\nINSERT INTO missing_table VALUES (1);\n",
);
let pending = fixture.noxid(&["db", "status"]);
assert_success(&pending, "read pending migration before failure");
assert!(
output_text(&pending).contains("pending 0001-transactional-ddl.sql"),
"{}",
output_text(&pending)
);
let failed = fixture.noxid(&["db", "migrate"]);
assert!(
!failed.status.success(),
"failing migration committed: {}",
output_text(&failed)
);
assert!(
!output_text(&failed).contains("warning[MYSQL_DDL_AUTOCOMMIT]"),
"SQLite must not inherit MySQL's DDL-autocommit warning: {}",
output_text(&failed)
);
let still_pending = fixture.noxid(&["db", "status"]);
assert_success(
&still_pending,
"read migration status after transactional failure",
);
assert!(
output_text(&still_pending).contains("pending 0001-transactional-ddl.sql"),
"history row survived failed migration: {}",
output_text(&still_pending)
);
fixture.migration(
"0001-transactional-ddl.sql",
"CREATE TABLE rollback_probe (id INTEGER PRIMARY KEY);\nINSERT INTO rollback_probe VALUES (1);\n",
);
let recovered = fixture.noxid(&["db", "migrate"]);
assert_success(
&recovered,
"apply corrected migration after SQLite DDL rollback",
);
let recovered_text = output_text(&recovered);
assert!(
recovered_text.contains("applied 0001-transactional-ddl.sql"),
"{recovered_text}"
);
assert!(
!recovered_text.contains("warning[MYSQL_DDL_AUTOCOMMIT]"),
"SQLite transaction was reported with MySQL semantics: {recovered_text}"
);
let applied = fixture.noxid(&["db", "status"]);
assert_success(&applied, "read applied status after recovery");
assert!(
output_text(&applied).contains("applied 0001-transactional-ddl.sql"),
"{}",
output_text(&applied)
);
}
#[test]
fn qa_round1_sqlite_history_refuses_reordering_and_checksum_drift() {
let fixture = QaFixture::new("history");
fixture.migration(
"0002-original.sql",
"CREATE TABLE history_probe (id INTEGER PRIMARY KEY);\n",
);
let migrated = fixture.noxid(&["db", "migrate"]);
assert_success(&migrated, "apply initial migration history");
fixture.migration("0001-inserted-before.sql", "SELECT 1;\n");
let reordered = fixture.noxid(&["db", "status"]);
assert!(
!reordered.status.success(),
"reordered history was accepted: {}",
output_text(&reordered)
);
assert!(
output_text(&reordered)
.contains("applied migration history is not a prefix of the ordered local files"),
"{}",
output_text(&reordered)
);
fixture.remove_migration("0001-inserted-before.sql");
fixture.migration("0002-original.sql", "SELECT 'edited after apply';\n");
let checksum = fixture.noxid(&["db", "status"]);
assert!(
!checksum.status.success(),
"checksum drift was accepted: {}",
output_text(&checksum)
);
assert!(
output_text(&checksum)
.contains("checksum mismatch for applied migration 0002-original.sql"),
"{}",
output_text(&checksum)
);
}
#[test]
fn qa_round1_sqlite_missing_parent_refuses_without_creating_a_directory_or_file() {
let fixture = QaFixture::new("missing-parent");
let output = Command::new(env!("CARGO_BIN_EXE_noxid"))
.current_dir(&fixture.root)
.args(["db", "status"])
.env("DATABASE_URL", "sqlite://missing/nested/qa.db")
.output()
.expect("execute SQLite missing-parent probe");
assert!(!output.status.success(), "missing parent was admitted");
assert!(
output_text(&output).contains("error[SQLITE_DATABASE_PARENT_MISSING]"),
"{}",
output_text(&output)
);
assert!(
!fixture.root.join("missing").exists(),
"runner created a directory for a typo'd SQLite URL"
);
}