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:
MechaCat02
2026-07-15 19:39:13 +02:00
parent 2445074c87
commit 345db4a076
22 changed files with 131 additions and 59 deletions

View File

@@ -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