test: close the DB-suite hermeticity + vacuous-skip gaps
Three related fixes from the test audit. **The CLI journey fixture gets its own database.** It spawned a real picloud — whose dispatcher/orchestrator claim loops are global by design (one instance owns one database) — against the shared dev DB, so it could claim the manager-core suites' outbox/workflow rows (the same class of bug already fixed for the e2e suites, one binary over). It now clones one dedicated database per journey run from the migrated template. test-support gains `named_test_db_url` (explicit stable name) + a blocking wrapper for the sync `LazyLock` fixture. The one journey that talks to Postgres directly (dead-letter injection) now uses the fixture's DB URL, not the base DATABASE_URL, so it hits the database the server reads. **workflow_orchestrator moves to per-test databases.** Its `claim_ready_step` is global, so the old harness serialized every test behind a process-wide CLAIM_LOCK AND ran `DELETE FROM workflow_runs` (unscoped — it wiped every app's runs) before each one. A private database per test makes the global claim see only that test's rows, so both the lock and the unscoped DELETE are deleted. **DB-backed suites fail loud instead of skipping green.** ~15 manager-core suites `return None` when DATABASE_URL is unset and report PASS — so in any environment that lost its database the entire integration surface reports green while running nothing (why the CI gap went unnoticed for so long). New `picloud_test_support::abort_if_db_required` panics when `PICLOUD_REQUIRE_DB` is set (CI now sets it) but DATABASE_URL is not, injected into each suite's skip path. Local runs without the var still skip cleanly. Mutation-verified: with PICLOUD_REQUIRE_DB=1 and DATABASE_URL unset, a suite panics; without the var it skips. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -113,12 +113,82 @@ fn test_name() -> String {
|
||||
/// template, cannot clone it) — a misconfigured integration environment should
|
||||
/// fail loudly, not silently skip.
|
||||
pub async fn test_db_url(suite: &str) -> Option<String> {
|
||||
let name = db_name(suite, &test_name());
|
||||
clone_template_into(&name).await
|
||||
}
|
||||
|
||||
/// A freshly-cloned database with an EXPLICIT, stable name — for a fixture that
|
||||
/// wants ONE database for a whole test binary rather than one per test (e.g. the
|
||||
/// CLI journey harness, which spawns a single real picloud). Same drop-recreate
|
||||
/// semantics, so a crashed run reclaims the name. `None` when `DATABASE_URL` is
|
||||
/// unset.
|
||||
///
|
||||
/// # Panics
|
||||
/// If `DATABASE_URL` is set but the database can't be created (see [`test_db_url`]).
|
||||
pub async fn named_test_db_url(name: &str) -> Option<String> {
|
||||
clone_template_into(name).await
|
||||
}
|
||||
|
||||
/// Blocking wrapper over [`named_test_db_url`] for sync fixtures (e.g. a
|
||||
/// `LazyLock` init that has no async runtime). Spins a private current-thread
|
||||
/// runtime so callers need no tokio dependency of their own.
|
||||
///
|
||||
/// # Panics
|
||||
/// As [`named_test_db_url`], plus if a runtime cannot be built.
|
||||
pub fn named_test_db_url_blocking(name: &str) -> Option<String> {
|
||||
if std::env::var("DATABASE_URL").is_err() {
|
||||
// Cheap pre-check so we don't spin a runtime just to return None, and so
|
||||
// the require-DB gate below fires without one either.
|
||||
return skip_or_panic("cli-journeys");
|
||||
}
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("build a current-thread runtime for named_test_db_url_blocking")
|
||||
.block_on(named_test_db_url(name))
|
||||
}
|
||||
|
||||
/// Panic if `PICLOUD_REQUIRE_DB` is set while `DATABASE_URL` is unset. CI sets the
|
||||
/// var, so a lost/misconfigured database surfaces as a RED build instead of a
|
||||
/// suite that reports green while executing nothing (the vacuous-skip trap).
|
||||
///
|
||||
/// Call this from a suite that has its own `DATABASE_URL`-unset skip path (the
|
||||
/// many `manager-core/tests/*` suites that connect to the shared dev DB directly)
|
||||
/// so they honor the same CI gate as the test-support-based suites, without
|
||||
/// having to be rewritten onto per-test databases.
|
||||
///
|
||||
/// ```ignore
|
||||
/// let Ok(url) = std::env::var("DATABASE_URL") else {
|
||||
/// picloud_test_support::abort_if_db_required("my_suite");
|
||||
/// return None;
|
||||
/// };
|
||||
/// ```
|
||||
pub fn abort_if_db_required(suite: &str) {
|
||||
assert!(
|
||||
std::env::var("PICLOUD_REQUIRE_DB").is_err(),
|
||||
"{suite}: DATABASE_URL is unset but PICLOUD_REQUIRE_DB is set — refusing to \
|
||||
skip DB-backed tests silently. Point DATABASE_URL at Postgres, or unset \
|
||||
PICLOUD_REQUIRE_DB to allow skipping."
|
||||
);
|
||||
}
|
||||
|
||||
/// Decide what to do when `DATABASE_URL` is unset. Returns `None` (skip) in a
|
||||
/// normal local run, but PANICS via [`abort_if_db_required`] when
|
||||
/// `PICLOUD_REQUIRE_DB` is set.
|
||||
fn skip_or_panic(suite: &str) -> Option<String> {
|
||||
abort_if_db_required(suite);
|
||||
eprintln!("{suite}: DATABASE_URL unset — skipping");
|
||||
None
|
||||
}
|
||||
|
||||
/// Shared clone core: ensure the migrated template exists, then drop+recreate
|
||||
/// `name` as a clone of it. Returns the connection URL, or `None` when
|
||||
/// `DATABASE_URL` is unset (respecting the require-DB gate).
|
||||
async fn clone_template_into(name: &str) -> Option<String> {
|
||||
let Ok(url) = std::env::var("DATABASE_URL") else {
|
||||
eprintln!("{suite}: DATABASE_URL unset — skipping");
|
||||
return None;
|
||||
return skip_or_panic(name);
|
||||
};
|
||||
let (root, query) = split_url(&url).expect("DATABASE_URL must name a database");
|
||||
let name = db_name(suite, &test_name());
|
||||
|
||||
let mut admin = PgConnection::connect(&url)
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user