Summary
cargo test -p buzz-db --lib -- --ignored — with no BUZZ_TEST_DATABASE_URL or DATABASE_URL set — runs DROP SCHEMA IF EXISTS public CASCADE against a hardcoded postgres://buzz:buzz_dev@localhost:5432/buzz. That is the Buzz Desktop development database on a default local setup.
This happened. On 2026-07-25 I ran that command twice while reviewing an unrelated security fix and destroyed the shared dev schema for about three minutes: window 23:59:11.170Z -> 00:02:02.930Z. It self-restored only because the test binary kept running to the rebuild. A Ctrl-C, a tool timeout, or a panic between the DROP SCHEMA and the following CREATE SCHEMA + re-migration leaves the developer's database empty with no automatic recovery.
How that window is established, and a caveat for anyone reproducing it. The back edge comes from a state artifact rather than a log: every one of the 24 rows in _sqlx_migrations on the live database has an installed_on inside a 283ms burst starting 2026-07-25 20:02:02.647 EDT, i.e. the schema now serving traffic was built then. The catalog agrees exhaustively: public is OID 3238914, every table in it falls in 3238915-3240452, and zero relations predate the namespace, so nothing survived the drop.
The caveat is worth more than the number. This server runs log_statement = none with log_min_error_statement = error, so the Postgres log contains only statements that failed. Every DROP SCHEMA IF EXISTS public CASCADE line in it is there because it errored; the successful drop that actually destroyed the schema was never logged, and neither were the successful CREATE SCHEMA or any successful migration. Three attempts to bound this incident from that log produced three different answers before we checked log_statement. If you are timing an incident on a default Homebrew Postgres, reach for state that carries its own clock (_sqlx_migrations.installed_on, pg_class OID survivorship) before reaching for the log.
Mechanism
// crates/buzz-db/src/migration.rs:103
const TEST_DB_URL: &str = "postgres://buzz:buzz_dev@localhost:5432/buzz";
// crates/buzz-db/src/migration.rs:1027-1031
async fn connect_test_pool() -> PgPool {
let database_url = std::env::var("BUZZ_TEST_DATABASE_URL")
.or_else(|_| std::env::var("DATABASE_URL"))
.unwrap_or_else(|_| TEST_DB_URL.to_owned()); // <-- silent fallback to the dev DB
...
}
// crates/buzz-db/src/migration.rs:1037
async fn reset_public_schema(pool: &PgPool) {
sqlx::query("DROP SCHEMA IF EXISTS public CASCADE") ...
Three #[ignore = "requires Postgres"] tests call reset_public_schema:
migration.rs:1061 — pre_0007_ambiguous_nip_rs_data_blocks_without_mutation_and_allows_retry
migration.rs:1131 — populated_upgrade_preserves_search_policy_except_for_push_leases
migration.rs:1191 — run_migrations_applies_consolidated_initial_schema_on_fresh_database
#[ignore] is not a safeguard here: --ignored is exactly the flag you reach for when you want "the Postgres-backed tests," and nothing in that command names a database.
Note the contrast with scripts/start-isolated-test-relay.sh:96, which spells it DROP SCHEMA public CASCADE and pipes it through docker compose exec postgres, so it can only ever reach the container on :5471. The IF EXISTS spelling appears exactly once in the tree, which is how the incident was traced.
Second, independent problem: the suite is already broken in parallel
Because these three tests drop the schema out from under every other test in the same binary, cargo test -p buzz-db --lib -- --ignored is nondeterministic:
- 10 failures on clean
main at the same commit, varying run to run
- passes 127/128 with
--test-threads=1 (the one failure is product_feedback wanting an explicit BUZZ_TEST_DATABASE_URL)
Nobody has noticed because scripts/run-tests.sh:93 only ever runs cargo test -p buzz-db --lib without --ignored, and the Postgres-backed tests are otherwise only run under a filter. So the documented CI path never exercises the sweep, and the sweep is the dangerous command.
Proposed fix
Two changes, both small:
- Delete the fallback.
connect_test_pool should .expect("set BUZZ_TEST_DATABASE_URL to an isolated test database") rather than default to anything. No hardcoded URL constant. A test that needs a database it can destroy should require the developer to name it.
- Make
reset_public_schema unreachable from a suite-wide sweep. #[ignore] does not distinguish "needs Postgres" from "will destroy Postgres". Gate the three destructive tests behind something --ignored alone cannot satisfy — e.g. a distinct required env var (BUZZ_TEST_DESTRUCTIVE_DB=1), a separate test target, or a #[cfg(feature = ...)] — so that the reasonable command stays safe and the destructive one has to be asked for by name.
Optionally (3) have the destructive tests refuse a database whose URL they didn't receive explicitly and which contains unexpected data — but (1) and (2) are the load-bearing ones, and (3) is a heuristic where those two are structural.
Fixing the parallel nondeterminism follows for free from (2): once the schema-dropping tests can't be swept into the same run as everything else, the remaining Postgres tests can run in parallel honestly.
Not urgent as a security issue, urgent as a data-loss issue
There is no remote attacker here. The exposure is: any contributor typing a plausible test command loses their local Buzz Desktop data, with no warning and no confirmation prompt. "Don't type the obvious command" is not a safeguard.
Summary
cargo test -p buzz-db --lib -- --ignored— with noBUZZ_TEST_DATABASE_URLorDATABASE_URLset — runsDROP SCHEMA IF EXISTS public CASCADEagainst a hardcodedpostgres://buzz:buzz_dev@localhost:5432/buzz. That is the Buzz Desktop development database on a default local setup.This happened. On 2026-07-25 I ran that command twice while reviewing an unrelated security fix and destroyed the shared dev schema for about three minutes: window
23:59:11.170Z -> 00:02:02.930Z. It self-restored only because the test binary kept running to the rebuild. ACtrl-C, a tool timeout, or a panic between theDROP SCHEMAand the followingCREATE SCHEMA+ re-migration leaves the developer's database empty with no automatic recovery.How that window is established, and a caveat for anyone reproducing it. The back edge comes from a state artifact rather than a log: every one of the 24 rows in
_sqlx_migrationson the live database has aninstalled_oninside a 283ms burst starting2026-07-25 20:02:02.647 EDT, i.e. the schema now serving traffic was built then. The catalog agrees exhaustively:publicis OID3238914, every table in it falls in3238915-3240452, and zero relations predate the namespace, so nothing survived the drop.The caveat is worth more than the number. This server runs
log_statement = nonewithlog_min_error_statement = error, so the Postgres log contains only statements that failed. EveryDROP SCHEMA IF EXISTS public CASCADEline in it is there because it errored; the successful drop that actually destroyed the schema was never logged, and neither were the successfulCREATE SCHEMAor any successful migration. Three attempts to bound this incident from that log produced three different answers before we checkedlog_statement. If you are timing an incident on a default Homebrew Postgres, reach for state that carries its own clock (_sqlx_migrations.installed_on,pg_classOID survivorship) before reaching for the log.Mechanism
Three
#[ignore = "requires Postgres"]tests callreset_public_schema:migration.rs:1061—pre_0007_ambiguous_nip_rs_data_blocks_without_mutation_and_allows_retrymigration.rs:1131—populated_upgrade_preserves_search_policy_except_for_push_leasesmigration.rs:1191—run_migrations_applies_consolidated_initial_schema_on_fresh_database#[ignore]is not a safeguard here:--ignoredis exactly the flag you reach for when you want "the Postgres-backed tests," and nothing in that command names a database.Note the contrast with
scripts/start-isolated-test-relay.sh:96, which spells itDROP SCHEMA public CASCADEand pipes it throughdocker compose exec postgres, so it can only ever reach the container on:5471. TheIF EXISTSspelling appears exactly once in the tree, which is how the incident was traced.Second, independent problem: the suite is already broken in parallel
Because these three tests drop the schema out from under every other test in the same binary,
cargo test -p buzz-db --lib -- --ignoredis nondeterministic:mainat the same commit, varying run to run--test-threads=1(the one failure isproduct_feedbackwanting an explicitBUZZ_TEST_DATABASE_URL)Nobody has noticed because
scripts/run-tests.sh:93only ever runscargo test -p buzz-db --libwithout--ignored, and the Postgres-backed tests are otherwise only run under a filter. So the documented CI path never exercises the sweep, and the sweep is the dangerous command.Proposed fix
Two changes, both small:
connect_test_poolshould.expect("set BUZZ_TEST_DATABASE_URL to an isolated test database")rather than default to anything. No hardcoded URL constant. A test that needs a database it can destroy should require the developer to name it.reset_public_schemaunreachable from a suite-wide sweep.#[ignore]does not distinguish "needs Postgres" from "will destroy Postgres". Gate the three destructive tests behind something--ignoredalone cannot satisfy — e.g. a distinct required env var (BUZZ_TEST_DESTRUCTIVE_DB=1), a separate test target, or a#[cfg(feature = ...)]— so that the reasonable command stays safe and the destructive one has to be asked for by name.Optionally (3) have the destructive tests refuse a database whose URL they didn't receive explicitly and which contains unexpected data — but (1) and (2) are the load-bearing ones, and (3) is a heuristic where those two are structural.
Fixing the parallel nondeterminism follows for free from (2): once the schema-dropping tests can't be swept into the same run as everything else, the remaining Postgres tests can run in parallel honestly.
Not urgent as a security issue, urgent as a data-loss issue
There is no remote attacker here. The exposure is: any contributor typing a plausible test command loses their local Buzz Desktop data, with no warning and no confirmation prompt. "Don't type the obvious command" is not a safeguard.