mirror of
https://github.com/block/buzz.git
synced 2026-11-07 01:41:06 +00:00
## Problem
Each relay node runs one consumer for events that other nodes publish to
Redis. It delivered them one at a time and awaited the access gate
(visibility and membership lookups) for each event. Under load it fell
more than 4,096 events behind the pub/sub broadcast. Each `Lagged(n)`
then skipped `n` events for good: nothing replayed them, and plain
subscribers were never told.
In a 15-node load test, about 50% of cross-node events were skipped at
10x load and about 75% at 13x. Throughput at 13x was also *lower* than
at 10x. Loadgen didn't show this, because it only times events that
arrive.
## Change
- **Concurrent delivery.** A dispatcher reads the broadcast and only
routes. It drops local echoes and queues each event on one of 16 workers
by `(community, topic)`. Each worker delivers its queue in order, so a
channel's events keep their order while different channels deliver
concurrently. `handlers/multinode_fanout.rs`
- **Known loss closes the affected connections.** An event is lost when
its worker's bounded queue (1,024 events) is full, or when it isn't
delivered within 2 s of this node reading it from Redis, whether it
waited in the broadcast, behind a backlog, or in a stalled delivery.
`ChannelEvent` now carries `received_at`, stamped by the pub/sub
subscriber. For a lost event, the relay sends `CLOSED "resync: live
delivery lagged"` to every sync the event would have reached (NIP-AR
Sync rule 5). It also closes every connection the event would have
reached, so those clients reconnect and catch up from their cursor.
Matching runs before the access gate, so a recipient that would have
been filtered out may be closed too. That costs a reconnect, never a
leak.
- **A lost ephemeral event is only counted.** Presence, typing and
observer frames are never stored, so no reconnect can replay them.
Losing one fences and closes nothing; it is still never delivered past
its deadline. Without this, one stale global presence event would close
nearly every mobile connection in the community, since every mobile
client subscribes to presence.
- **Unknown loss closes every subscribed connection.** If the dispatcher
itself lags the broadcast, it can't know which events it skipped. It
resyncs every sync (unchanged) and closes every connection that has a
subscription.
- **Immediate fence.** Each affected connection is fenced as soon as the
loss is detected. The live fan-out send path (`send_to_text_bytes`,
local and cross-node) delivers it nothing from then on. The fence is
written under the connection map's write guard, and the send path checks
it and queues its frame under one read guard, so no frame slips in
between the check and the send.
- **Why 2 s.** The mobile client resumes from the newest `created_at` it
received, minus 5 s of slack. The desktop app (block/buzz-app) resumes
960 s before its connection last heard the relay, which already covers
the full 900 s `created_at` drift. While an event waits, local fan-out
and other workers keep moving that cursor. The fence stops the cursor,
and the deadline means no withheld event is more than 2 s older than the
fence, so a reconnect replays it. Without the deadline, an event stuck
behind a long backlog could be withheld past the slack and never
replayed.
- **Paced closes.** Only the close is delayed. One closer task closes at
most 50 connections per 10 ms (5,000/s per node). A drop in a hot
channel therefore can't become a reconnect storm (Review-Proven Rule 9).
- `fan_out_pubsub_event` is now split into `take_local_echo` and
`deliver_pubsub_event`, so the dispatcher can check echoes before
queueing. Its behavior is unchanged.
- **Limits (mobile):** replay works while the 2 s deadline, plus the
writer's clock skew and its publish delay, stays under the client's 5 s
slack. The relay accepts `created_at` up to 15 min off, so a writer
clock more than about 3 s slow can still put an event behind the cursor.
An ordinary reconnect has the same limit today; closing it needs a
client rewind instruction, which is out of scope here. The desktop app's
960 s resume window has no such gap. A dispatcher that falls behind
without overflowing the broadcast still never delivers a late event, but
it detects the loss, and fences, late. Events skipped by dispatcher lag
are never seen. Neither case is bounded by the deadline, which is a
cutoff for attempting delivery, not a guaranteed fence time.
- **What a load-test rerun should watch:** under sustained overload the
deadline turns backlog into steady closes (up to 5,000/s per node), and
reconnecting clients replay from the same database pool. Watch
`buzz_multinode_fanout_dropped_total{reason="stale"}`,
`buzz_multinode_fanout_lag_closed_total` and the reconnect REQ rate next
to pool wait. On dispatcher lag, the dispatcher scans the subscription
registry and fences each subscribed connection synchronously, so also
watch how long it stays behind after a `Lagged`.
Metrics:
- `buzz_multinode_fanout_lag_total` (dispatcher lag) and
`buzz_multinode_fanout_total` (delivered) keep their meaning.
- `buzz_multinode_fanout_dropped_total{reason}` counts lost events:
`queue_full`, or `stale` for one not delivered by its deadline. A lost
ephemeral event counts as `ephemeral` instead and closes nothing.
- `buzz_multinode_fanout_lag_closed_total` counts close attempts for a
loss, one batch at a time.
## Rollout
- Staging first. The load-test rerun at 1x, 10x and 13x happens after
merge, with the staging load tests planned once the current relay
changes land. At 1x, expect zero `dropped_total` for stored kinds and
zero `lag_closed_total`. Report delivered %, `dropped_total{reason}`,
`lag_closed_total`, reconnect REQ rate and pool wait.
- Then one production cluster before the rest.
- **Rollback trigger:** `lag_closed_total` climbing steadily, or
reconnect REQs pushing pool wait up, at normal load. Rollback is a plain
revert and redeploy: there are no schema, config or wire changes.
## Ordering
Order is kept within a `(community, topic)`. Events on different topics,
for example a channel event and a global event, can now reach one
subscriber in either order. Before this change they arrived in Redis
arrival order. Nothing in the relay depends on order across topics: the
access gate re-reads membership and visibility per event. Clients order
by `created_at`.
## What got simpler / known debt
- The fan-out loop in `main.rs` shrinks to one `spawn` call, and the lag
handling lives next to the delivery code.
- No new config knobs. The worker count and queue size are constants
until a load test shows they need tuning.
- **Known debt, not in this PR:** a Redis pub/sub reconnect can still
lose events silently for plain subscribers. Syncs are already resynced
in that case (`artifact_sync::watch_link`), but ordinary subscriptions
are not.
## Tests
`handlers::multinode_fanout::tests`, each checked with a mutant on Blox:
- events reach their subscriber through the workers, in publish order
- a full worker queue closes exactly the dropped event's recipients
- a lost ephemeral event (full queue or stale) is counted as `ephemeral`
and neither fences nor closes, while a stored event lost on the same
queue still closes
- a local echo on a full queue closes nothing
- dispatcher lag closes every subscribed connection
- after a known loss or dispatcher lag, a recipient gets no newer live
event while it waits to be closed
- the fence also withholds relay-local fan-out
- an event that waited out its deadline before dispatch is lost, never
delivered
- an event behind a backlog is delivered inside its deadline, and past
it is lost (its recipient fenced and queued for close), never delivered
late (paused clock)
- a delivery that stalls on a database that never answers is cut off at
the deadline and handled as lost
- the closer cancels in paced batches (paused clock), and counts each
batch as it closes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: Luke Tornquist <tornquist@squareup.com>
Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
606 lines
30 KiB
Bash
Executable File
606 lines
30 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# run-tests.sh — Run Buzz test suite
|
|
# =============================================================================
|
|
# Usage:
|
|
# ./scripts/run-tests.sh # run all tests (default)
|
|
# ./scripts/run-tests.sh unit # unit tests only (no infra needed)
|
|
# ./scripts/run-tests.sh integration # integration tests only
|
|
# ./scripts/run-tests.sh all # explicit all
|
|
# =============================================================================
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
MODE="${1:-all}"
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m'
|
|
|
|
log() { echo -e "${BLUE}[run-tests]${NC} $*"; }
|
|
success(){ echo -e "${GREEN}[run-tests]${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}[run-tests]${NC} $*"; }
|
|
error() { echo -e "${RED}[run-tests]${NC} $*" >&2; }
|
|
section(){ echo -e "\n${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"; }
|
|
|
|
cd "${REPO_ROOT}"
|
|
|
|
# ---- Load .env if present ---------------------------------------------------
|
|
|
|
if [[ -f ".env" ]]; then
|
|
log "Loading .env..."
|
|
set -o allexport
|
|
# shellcheck disable=SC1091
|
|
source .env
|
|
set +o allexport
|
|
else
|
|
# Use defaults matching docker-compose.yml
|
|
export DATABASE_URL="postgres://buzz:buzz_dev@localhost:5432/buzz" # sadscan:disable np.postgres.1
|
|
export PGHOST=localhost
|
|
export PGPORT=5432
|
|
export PGUSER=buzz
|
|
export PGPASSWORD=buzz_dev
|
|
export PGDATABASE=buzz
|
|
export REDIS_URL="redis://localhost:6379"
|
|
fi
|
|
|
|
# ---- Track results ----------------------------------------------------------
|
|
|
|
declare -a PASSED=()
|
|
declare -a FAILED=()
|
|
|
|
run_test_step() {
|
|
local name="$1"
|
|
shift
|
|
log "Running: ${name}"
|
|
if "$@"; then
|
|
success "${name} passed"
|
|
PASSED+=("${name}")
|
|
else
|
|
error "${name} FAILED"
|
|
FAILED+=("${name}")
|
|
fi
|
|
}
|
|
|
|
# ---- Check / start infra (for integration tests) ----------------------------
|
|
|
|
ensure_infra() {
|
|
"${REPO_ROOT}/bin/just" _ensure-migrations
|
|
}
|
|
|
|
# ---- Unit tests (no infra needed) -------------------------------------------
|
|
|
|
run_unit_tests() {
|
|
section "Unit Tests (no infra required)"
|
|
|
|
run_test_step "buzz-core tests" \
|
|
cargo test -p buzz-core --lib -- --nocapture
|
|
|
|
run_test_step "buzz-audit tests" \
|
|
cargo test -p buzz-audit --lib -- --nocapture
|
|
|
|
run_test_step "buzz-auth unit tests" \
|
|
cargo test -p buzz-auth --lib -- --nocapture
|
|
|
|
# Mesh JWKS proxy-bypass regression: infra-free loopback I/O with
|
|
# child-process environment isolation. --lib does not select its binary.
|
|
# Keep this fallback in step with `just test-unit`.
|
|
run_test_step "buzz-auth mesh JWKS proxy regression" \
|
|
cargo test -p buzz-auth --test mesh_jwks_proxy -- --nocapture
|
|
|
|
# Cross-pod disconnect payload tests (NIP-FI and community archive fence;
|
|
# infra-free). Mirrors `just test-unit`.
|
|
run_test_step "buzz-pubsub conn_control tests" \
|
|
cargo test -p buzz-pubsub --lib conn_control::tests:: -- --nocapture
|
|
|
|
# Shared Redis connection timeout bounds (loopback fake peers; infra-free).
|
|
# Mirrors `just test-unit`.
|
|
run_test_step "buzz-pubsub connection tests" \
|
|
cargo test -p buzz-pubsub --lib connection::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-voice tests" \
|
|
cargo test -p buzz-voice --lib -- --nocapture
|
|
|
|
# Inter-relay mesh crate (infra-free). Mirrors `just test-unit`.
|
|
run_test_step "buzz-relay-mesh tests" \
|
|
cargo test -p buzz-relay-mesh -- --nocapture
|
|
|
|
run_test_step "buzz-cli tests" \
|
|
cargo test -p buzz-cli -- --nocapture
|
|
|
|
# buzz-sdk builder/validation unit tests: pure event-builder and input
|
|
# validation, no infra. Mirrors the nextest path in `just test-unit` — the
|
|
# two lists must stay in step. `--lib` matches the nextest invocation and
|
|
# avoids the full-package rustdoc dependency-resolution flake.
|
|
run_test_step "buzz-sdk unit tests" \
|
|
cargo test -p buzz-sdk --lib -- --nocapture
|
|
|
|
# buzz-workflow unit tests (infra-free). Mirrors `just test-unit`.
|
|
run_test_step "buzz-workflow unit tests" \
|
|
cargo test -p buzz-workflow --lib -- --nocapture
|
|
|
|
# Keep the relay-to-agent trust-boundary regressions in the fallback path
|
|
# when cargo-nextest is unavailable.
|
|
run_test_step "buzz-acp tests" \
|
|
cargo test -p buzz-acp -- --nocapture
|
|
|
|
# Keep MCP lifecycle coverage in step with the nextest path.
|
|
run_test_step "buzz-dev-mcp tests" \
|
|
cargo test -p buzz-dev-mcp -- --nocapture
|
|
|
|
|
|
# buzz-db migrator/lint unit tests (no infra): guard the embedded-migrator
|
|
# invariant (exactly the consolidated 0001; cutover/backfill stays an operator
|
|
# script, not startup state) and the tenant-scoping lints. The Postgres-backed
|
|
# buzz-db tests are #[ignore]d; nothing here (or in integration mode below,
|
|
# which runs `cargo test -p buzz-db` without --ignored) runs them — they need a
|
|
# separate isolated-DB gate, so --lib keeps this step infra-free.
|
|
run_test_step "buzz-db unit tests" \
|
|
cargo test -p buzz-db --lib -- --nocapture
|
|
run_test_step "buzz-db source-policy tests" \
|
|
cargo test -p buzz-db --test observability_source -- --nocapture
|
|
|
|
# buzz-search unit tests (no infra); its Postgres suite runs in the
|
|
# PostgreSQL lane.
|
|
run_test_step "buzz-search unit tests" \
|
|
cargo test -p buzz-search --lib -- --nocapture
|
|
|
|
run_test_step "buzz-media storage snapshot serialization test" \
|
|
cargo test -p buzz-media --lib bucket_index::tests::bucket_snapshot_json_round_trip_preserves_community_keys -- --exact --nocapture
|
|
|
|
run_test_step "buzz-admin storage snapshot tests" \
|
|
cargo test -p buzz-admin storage_snapshot -- --nocapture
|
|
|
|
run_test_step "buzz-admin community archive command tests" \
|
|
cargo test -p buzz-admin communities -- --nocapture
|
|
|
|
run_test_step "buzz-admin partition audit outcome contract test" \
|
|
cargo test -p buzz-admin tests::partition_audit_outcomes_keep_their_json_names_and_exit_codes -- --exact --nocapture
|
|
|
|
run_test_step "buzz-admin NIP-FI binding command tests" \
|
|
cargo test -p buzz-admin nip_fi::tests -- --nocapture
|
|
|
|
run_test_step "buzz-admin user-deletion intake command tests" \
|
|
cargo test -p buzz-admin user_deletions::tests -- --nocapture
|
|
|
|
# Multi-tenant conformance gate: independent replay checker + golden
|
|
# fixtures (buzz-conformance). Pure in-process trace replay, no infra.
|
|
run_test_step "buzz-conformance tests" \
|
|
cargo test -p buzz-conformance -- --nocapture
|
|
|
|
run_test_step "buzz-push-gateway tests" \
|
|
cargo test -p buzz-push-gateway -- --nocapture
|
|
run_test_step "buzz-push-gateway personal development tests" \
|
|
cargo test -p buzz-push-gateway --features personal-dev-app-attest -- --nocapture
|
|
|
|
# Kubernetes backend provider: pure decision layers driven by a fake
|
|
# substrate, no cluster. Mirrors the nextest path in `just test-unit` —
|
|
# the two lists must stay in step or the fallback silently covers less.
|
|
run_test_step "buzz-backend-kubernetes tests" \
|
|
cargo test -p buzz-backend-kubernetes -- --nocapture
|
|
|
|
# Keep fallback parity with `just test-unit`: one LaunchDarkly-feature run
|
|
# exercises both default and feature-gated buzz-feature-flags tests.
|
|
run_test_step "buzz-feature-flags tests (launchdarkly)" \
|
|
cargo test -p buzz-feature-flags --features launchdarkly -- --nocapture
|
|
|
|
# buzz-agent model-capabilities corpus: the Rust half of the cross-language
|
|
# drift guard. model_capabilities.rs embeds scripts/model-capabilities.json +
|
|
# scripts/normative-corpus.json via include_str! and replays the full locked
|
|
# corpus as pure in-process tests (no infra). Mirrors the nextest path in
|
|
# `just test-unit` — the two lists must stay in step.
|
|
run_test_step "buzz-agent unit tests" \
|
|
cargo test -p buzz-agent --lib -- --nocapture
|
|
|
|
# ACP author-gate and queue tests are pure unit tests. Keep this fallback in
|
|
# step with `just test-unit`; ignored lifecycle tests run elsewhere.
|
|
run_test_step "buzz-acp unit tests" \
|
|
cargo test -p buzz-acp --lib -- --nocapture
|
|
|
|
# Mirror the relay filters from `just test-unit`: the three handler modules,
|
|
# storage-snapshot helpers, readiness and router unit suites, and the single
|
|
# scoped admission regression in state::tests, plus the REQ lifecycle tests.
|
|
run_test_step "buzz-relay push runtime tests" \
|
|
cargo test -p buzz-relay --lib push_runtime:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay operator listener tests" \
|
|
cargo test -p buzz-relay --lib operator_listener::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay channel authorization tests" \
|
|
cargo test -p buzz-relay --lib handlers::channel_authz:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay moderation authorization tests" \
|
|
cargo test -p buzz-relay --lib handlers::moderation_authz:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay side-effects helper tests" \
|
|
cargo test -p buzz-relay --lib handlers::side_effects::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay coalesced refresh tests" \
|
|
cargo test -p buzz-relay --lib handlers::coalesced_refresh::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay system channel scope tests" \
|
|
cargo test -p buzz-relay --lib handlers::system_channels::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay NIP-11 document tests" \
|
|
cargo test -p buzz-relay --lib nip11::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay storage snapshot tests" \
|
|
cargo test -p buzz-relay --lib storage_sweep::tests:: -- --nocapture
|
|
|
|
# Mirror the four audio/FI suites from `just test-unit`'s nextest expression.
|
|
# These are infra-free (no DB, no Redis); the `#[ignore]`-gated DB witnesses
|
|
# are excluded by cargo test's default filter. Keep in step with the Justfile
|
|
# `test-unit` relay expression.
|
|
run_test_step "buzz-relay audio join tests" \
|
|
cargo test -p buzz-relay --lib audio::join::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay audio handler tests" \
|
|
cargo test -p buzz-relay --lib audio::handler::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay NIP-FI gate tests" \
|
|
cargo test -p buzz-relay --lib nip_fi_gate::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay NIP-FI session tests" \
|
|
cargo test -p buzz-relay --lib nip_fi_session::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay NIP-FI shadow recorder tests" \
|
|
cargo test -p buzz-relay --lib nip_fi_shadow::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay NIP-FI shadow session tests" \
|
|
cargo test -p buzz-relay --lib nip_fi_shadow_session::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay startup step tests" \
|
|
cargo test -p buzz-relay --lib startup_steps::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay mesh boot tests" \
|
|
cargo test -p buzz-relay --lib mesh_boot::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay subscription registry tests" \
|
|
cargo test -p buzz-relay --lib subscription::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay multi-node fan-out tests" \
|
|
cargo test -p buzz-relay --lib handlers::multinode_fanout::tests:: -- --nocapture
|
|
|
|
# telemetry and config tests mutate process env. libtest runs one process
|
|
# with many threads (nextest, used by `just test-unit`, isolates each test),
|
|
# so run them single-threaded here. `--skip` keeps the substring filter
|
|
# `config::tests::` from also selecting nip_fi_config, which runs above.
|
|
run_test_step "buzz-relay telemetry tests" \
|
|
cargo test -p buzz-relay --lib telemetry::tests:: -- --test-threads=1 --nocapture
|
|
|
|
run_test_step "buzz-relay config tests" \
|
|
cargo test -p buzz-relay --lib config::tests:: -- --skip nip_fi_config:: --test-threads=1 --nocapture
|
|
|
|
# Mirror the NIP-FI (S3) stanza from `just test-unit`: module filters, then
|
|
# each exact name. Keep this list in step with that stanza's `test(=...)`s.
|
|
run_test_step "buzz-relay NIP-FI config tests" \
|
|
cargo test -p buzz-relay --lib nip_fi_config:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay NIP-FI resolver tests" \
|
|
cargo test -p buzz-relay --lib nip_fi_resolver:: -- --nocapture
|
|
|
|
# Infra-free REQ subscription-lifecycle tests, by exact name: the rest of
|
|
# handlers::req needs a database.
|
|
run_test_step "buzz-relay REQ subscription lifecycle tests" \
|
|
cargo test -p buzz-relay --lib -- --exact --nocapture \
|
|
handlers::req::tests::timed_out_historical_read_deregisters_before_closed \
|
|
handlers::req::tests::superseded_timeout_leaves_replacement_intact \
|
|
handlers::req::tests::search_claim_retires_live_and_yields_to_replacement \
|
|
handlers::req::tests::failed_search_closes_without_eose_or_trying_later_filters \
|
|
handlers::req::tests::concurrent_claims_and_stale_teardowns_keep_the_last_owner \
|
|
handlers::req::tests::timeout_closed_is_emitted_before_a_replacement_can_claim \
|
|
handlers::req::tests::revoke_then_replacement_keeps_replacement_whole \
|
|
handlers::req::tests::claims_after_connection_cleanup_are_refused \
|
|
handlers::req::tests::dropped_terminal_frame_cancels_connection \
|
|
handlers::req::tests::revoke_dropped_terminal_frame_cancels_connection \
|
|
handlers::req::tests::count_fallback_covers_filters_that_can_match_private_deletions \
|
|
handlers::req::tests::group_state_t_and_creator_tags_reach_sql_only_for_group_state_kinds
|
|
|
|
# Access log line tests, mirroring `just test-unit`'s module filters.
|
|
run_test_step "buzz-relay access log tests" \
|
|
cargo test -p buzz-relay --lib access::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay git access log tests" \
|
|
cargo test -p buzz-relay --lib api::git::transport::access_log_tests:: -- --nocapture
|
|
|
|
# Infra-free tests the access-logging change added or edited, by exact
|
|
# name: their modules otherwise need a database.
|
|
run_test_step "buzz-relay access logging infra-free tests" \
|
|
cargo test -p buzz-relay --lib -- --exact --nocapture \
|
|
handlers::req::tests::unqueued_eose_logs_aborted_not_served \
|
|
api::bridge::postgres_tests::reject_code_keeps_only_a_known_prefix \
|
|
api::bridge::postgres_tests::nip98_replay_check_fails_closed_when_guard_errors \
|
|
api::bridge::postgres_tests::verify_bridge_auth_rejects_nip98_event_signed_for_wrong_communitys_host \
|
|
api::bridge::postgres_tests::verify_bridge_auth_can_require_payload_tag_for_json_body_endpoints \
|
|
api::bridge::postgres_tests::verify_bridge_auth_accepts_nip98_event_signed_for_matching_host \
|
|
api::bridge::postgres_tests::moderation_read_query_bearing_nip98_event_verifies_with_matching_query \
|
|
api::bridge::postgres_tests::moderation_read_query_bearing_nip98_event_rejected_against_bare_path \
|
|
api::bridge::postgres_tests::moderation_read_audit_query_bearing_nip98_event_verifies \
|
|
api::bridge::postgres_tests::moderation_read_restricted_no_query_still_verifies \
|
|
connection::tests::aborting_lifecycle_owner_is_drop_safe \
|
|
connection::tests::shutdown_terminalizes_auth_stalled_on_database_before_late_success \
|
|
connection::terminal_close_delivery_tests::upgrade_would_deny_line_without_auth_logs_absent_at_close \
|
|
handlers::event::tests::observer_owner_cache_is_scoped_to_community \
|
|
state::tests::shared_counter_between_direct_and_fanout \
|
|
api::bridge::postgres_tests::nip98_replay_check_logs_only_its_refusal \
|
|
api::bridge::postgres_tests::warn_only_artifact_read_lines_keep_the_filter_summary \
|
|
connection::tests::close_totals_count_a_batch_flushed_by_the_drain_after_cancellation \
|
|
connection::tests::close_totals_count_frames_the_writer_flushed \
|
|
connection::tests::close_totals_skip_a_frame_the_socket_rejects \
|
|
connection::tests::close_totals_skip_frames_stranded_by_a_failed_flush \
|
|
connection::tests::dispatcher_counts_each_req_and_count_frame_once \
|
|
connection::tests::read_before_login_logs_one_denial_from_the_handler \
|
|
connection::tests::read_refused_by_a_full_handler_semaphore_logs_one_denial \
|
|
connection::tests::saturated_handler_write_line_records_whether_its_ok_was_queued \
|
|
connection::tests::read_refused_by_unavailable_admission_logs_one_denial \
|
|
handlers::auth::tests::repeat_auth_after_a_terminal_logs_one_denial \
|
|
nip_fi_upgrade::tests::deny_protected_logs_one_denied_auth_line \
|
|
api::bridge::moderation_refusal_tests::moderation_lookup_failure_is_unavailable \
|
|
api::admin::auth::tests::admin_role_refusal_logs_denied \
|
|
api::admin::auth::tests::admin_duplicate_authorization_logs_duplicate
|
|
|
|
# Infra-free bridge search extension parser tests, by exact name: the rest of
|
|
# api::bridge::postgres_tests needs a database.
|
|
run_test_step "buzz-relay bridge search extension tests" \
|
|
cargo test -p buzz-relay --lib -- --exact --nocapture \
|
|
api::bridge::postgres_tests::bridge_search_mode_extension_accepts_prefix_snake_or_camel_case \
|
|
api::bridge::postgres_tests::bridge_search_sort_extension_selects_newest_only_when_named
|
|
|
|
# Infra-free guard for the destructive invite fixture's database ownership
|
|
# check, by exact name: the rest of api::invites::postgres_tests needs a database.
|
|
run_test_step "buzz-relay invite fixture guard test" \
|
|
cargo test -p buzz-relay --lib -- --exact --nocapture \
|
|
api::invites::postgres_tests::restriction_tests::postgres_tests::destructive_fixture_rejects_shared_database_urls
|
|
|
|
run_test_step "buzz-relay readiness tests" \
|
|
cargo test -p buzz-relay --lib readiness::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay accessible-channel cache tests" \
|
|
cargo test -p buzz-relay --lib -- --exact --nocapture \
|
|
state::tests::accessible_channel_invalidation_is_scoped_to_community \
|
|
state::tests::accessible_channels_join_member_and_open_halves \
|
|
state::tests::channel_deleted_invalidation_is_scoped_to_community
|
|
|
|
run_test_step "buzz-relay admission regression test" \
|
|
cargo test -p buzz-relay --lib state::tests::neither_a_confirmed_inactive_community_nor_a_failed_lookup_admits_the_socket -- --nocapture
|
|
|
|
run_test_step "buzz-relay archive revalidation fence test" \
|
|
cargo test -p buzz-relay --lib state::tests::periodic_revalidation_disconnects_inside_the_fenced_callback -- --exact --nocapture
|
|
|
|
run_test_step "buzz-relay bare disconnect fail-closed test" \
|
|
cargo test -p buzz-relay --lib state::tests::bare_community_disconnect_fails_closed_when_the_fence_is_unavailable -- --exact --nocapture
|
|
|
|
run_test_step "buzz-relay archived close-reason test" \
|
|
cargo test -p buzz-relay --lib connection::tests::send_loop_sends_policy_close_when_community_is_archived -- --exact --nocapture
|
|
|
|
run_test_step "buzz-relay community close first-writer tests" \
|
|
cargo test -p buzz-relay --lib state::tests::community_disconnect_then_nip_fi_keeps_community_deleted_reason -- --exact --nocapture
|
|
|
|
run_test_step "buzz-relay community close suppresses losing denial frame test" \
|
|
cargo test -p buzz-relay --lib state::tests::disconnect_community_wins_reason_losing_nip_fi_does_not_enqueue_frame -- --exact --nocapture
|
|
|
|
run_test_step "buzz-relay read cache tests" \
|
|
cargo test -p buzz-relay --lib -- --exact --nocapture \
|
|
state::tests::admitted_index_follows_every_key_change \
|
|
shared_reads::tests::one_caller_reads_while_the_rest_wait_for_it \
|
|
shared_reads::tests::a_waiter_stops_waiting_for_a_hung_read \
|
|
community_active::tests::concurrent_misses_share_one_read_and_expire_after_max_age \
|
|
community_active::tests::invalidation_fences_an_in_flight_fill_and_its_waiters \
|
|
community_active::tests::errors_are_not_cached \
|
|
state::tests::concurrent_open_channel_misses_share_the_in_flight_read \
|
|
state::tests::concurrent_restriction_and_host_misses_share_the_in_flight_read \
|
|
tenant::tests::app_state_resolver_serves_fresh_cached_hosts_only \
|
|
tenant::tests::an_overlong_host_fails_closed_before_the_resolver \
|
|
state::tests::disconnect_drops_the_cached_restriction_row
|
|
|
|
run_test_step "buzz-relay membership snapshot tests" \
|
|
cargo test -p buzz-relay --lib membership_cache::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay router tests" \
|
|
cargo test -p buzz-relay --lib router::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay NIP-FI shared core tests" \
|
|
cargo test -p buzz-relay --lib nip_fi_core::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay NIP-FI HTTP ingress tests" \
|
|
cargo test -p buzz-relay --lib nip_fi_http::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay API query parsing tests" \
|
|
cargo test -p buzz-relay --lib api::parse_query_tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay API database error mapping tests" \
|
|
cargo test -p buzz-relay --lib api::db_read_error_tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay profile search request tests" \
|
|
cargo test -p buzz-relay --lib api::profiles:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay per-user search cap tests" \
|
|
cargo test -p buzz-relay --lib api::bridge::search_slots::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay Git transport Off-mode precedence tests" \
|
|
cargo test -p buzz-relay --lib api::git::transport::off_mode_precedence_tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay community FI policy tests" \
|
|
cargo test -p buzz-relay --lib nip_fi_policy::tests:: -- --nocapture
|
|
|
|
# Includes the opt-in live-MinIO ::probe tests, which skip without
|
|
# BUZZ_GIT_S3_PROBE=1.
|
|
run_test_step "buzz-relay Git object-store and conformance-probe tests" \
|
|
cargo test -p buzz-relay --lib api::git::store:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay NIP-FI upgrade tests" \
|
|
cargo test -p buzz-relay --lib nip_fi_upgrade:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay NIP-FI admin API tests" \
|
|
cargo test -p buzz-relay --lib api::nip_fi:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay auth metrics contract tests" \
|
|
cargo test -p buzz-relay --lib metrics::contract_tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay partition alert series tests" \
|
|
cargo test -p buzz-relay --lib metrics::partition_alert_tests:: -- --nocapture
|
|
|
|
local nip_fi_exact_tests=(
|
|
audio::room::tests::roster_revisions_are_ordered_and_snapshot_is_authoritative
|
|
connection::tests::auth_lifecycle_reconciles_every_terminal_and_never_leaks_gauge
|
|
connection::tests::startup_cancellation_writes_one_close_line
|
|
connection::tests::connection_limit_refusal_writes_one_close_line
|
|
connection::tests::dropped_authenticated_handler_writes_one_close_line
|
|
audio::room::tests::b1_pending_peer_removed_before_commit_emits_no_delta
|
|
audio::room::tests::b2_commit_peer_emits_exactly_one_joined_delta_and_marks_visible
|
|
audio::room::tests::b3_commit_peer_revision_is_monotone_between_concurrent_events
|
|
audio::room::tests::f7a_pending_peer_excluded_from_snapshot_until_committed
|
|
connection::tests::b2_cancelled_connection_event_frame_not_dispatched
|
|
connection::tests::b3_expiry_denial_precedes_close_through_send_loop
|
|
connection::tests::b3_root_pairing_denial_precedes_close_through_send_loop
|
|
connection::tests::cancellation_during_select_with_fi_denial_routes_through_bounded_path
|
|
connection::tests::cancelled_never_ready_sink_with_queued_fi_denial_exits_within_timeout
|
|
connection::tests::deadline_exp_is_earliest_selects_exp
|
|
connection::tests::deadline_max_connection_lifetime_is_earliest_selects_partition
|
|
connection::tests::deadline_no_lifetime_returns_upstream_only
|
|
connection::tests::expiry_notice_queued_on_ctrl_before_cancel
|
|
connection::tests::f3_root_outer_wrapper_delivers_denial_on_bootstrap_cancellation
|
|
connection::tests::f3_root_pre_built_expired_gate_terminates_connection
|
|
connection::terminal_close_delivery_tests::key_pairing_mismatch_closes_with_1008_on_real_socket
|
|
connection::terminal_close_delivery_tests::session_expiry_denial_closes_with_1008_on_real_socket
|
|
connection::terminal_close_delivery_tests::server_holds_socket_for_client_close_reply
|
|
connection::terminal_close_delivery_tests::server_finishes_promptly_when_client_answers_close
|
|
connection::terminal_close_delivery_tests::stalled_cleanup_does_not_hold_socket_open
|
|
connection::terminal_close_delivery_tests::bootstrap_drain_holds_socket_for_client_close_reply
|
|
connection::terminal_close_delivery_tests::stalled_expiry_barrier_does_not_hold_socket_open
|
|
connection::terminal_close_delivery_tests::blocked_frame_handler_does_not_hold_socket_open
|
|
connection::terminal_close_delivery_tests::refused_bootstrap_holds_connection_slot_until_socket_ends
|
|
connection::terminal_close_delivery_tests::stalled_bootstrap_holds_connection_slot
|
|
connection::terminal_close_delivery_tests::bootstrap_admits_from_the_cached_community_answer
|
|
connection::terminal_close_delivery_tests::peer_wait_replies_to_client_initiated_close
|
|
handlers::auth::tests::b2_pre_cancelled_connection_never_becomes_authenticated
|
|
handlers::auth::tests::fi_ban_check_error_emits_terminal_authorization_unavailable
|
|
handlers::auth::tests::fi_root_authorization_denied_rows_emit_identical_frames
|
|
handlers::auth::tests::fi_invalid_nip42_proof_emits_terminal_evidence_rejected
|
|
handlers::auth::tests::handle_auth_pairing_mismatch_runs_full_root_denial_path
|
|
handlers::auth::tests::shadow_root_auth_records_pairing_denial
|
|
handlers::auth::tests::shadow_root_auth_records_unavailable_owner_read
|
|
handlers::ingest::postgres_tests::relay_admin_ban_maps_to_blocked_auth_failure
|
|
handlers::ingest::postgres_tests::relay_admin_internal_maps_to_error_not_client_fault
|
|
handlers::auth::tests::shadow_root_invalid_nip42_retires_without_record
|
|
handlers::auth::tests::nip42_denial_class_separates_internal_failure_from_bad_evidence
|
|
handlers::event::tests::fanout_access::owner_only_kinds_keep_only_the_owner
|
|
handlers::event::tests::pubsub_fanout::pubsub_owner_only_kinds_reach_only_the_owner
|
|
handlers::event::tests::pubsub_fanout::dispatch_owner_only_kinds_reach_only_the_owner
|
|
handlers::req::tests::p1a_huddle_liveness_req_barrier_expiry_blocks_query_and_emission
|
|
state::tests::f3_cancellation_during_check_terminates_socket_without_waiting_for_check
|
|
state::tests::on_not_run_runs_once_on_each_deny_arm_and_never_on_admit
|
|
state::tests::conn_manager_disconnect_nip_fi_ignores_unproven_connection
|
|
state::tests::conn_manager_disconnect_nip_fi_is_issuer_scoped
|
|
state::tests::conn_manager_disconnect_nip_fi_sets_authorization_denied_reason
|
|
state::tests::nip_fi_disconnect_audio_is_issuer_scoped
|
|
state::tests::nip_fi_owner_disconnect_closes_owned_agent_sockets_on_root_and_audio
|
|
state::tests::nip_fi_disconnect_closes_proven_audio_socket_and_sends_policy_close_reason
|
|
state::tests::nip_fi_disconnect_closes_target_audio_only_and_preserves_collocated_peer
|
|
state::tests::nip_fi_disconnect_does_not_close_different_pubkey_audio_socket
|
|
state::tests::nip_fi_disconnect_does_not_close_unproven_audio_socket
|
|
state::tests::community_disconnect_then_nip_fi_keeps_community_deleted_reason
|
|
state::tests::enforce_end_is_recorded_only_by_the_winning_terminal
|
|
state::tests::disconnect_community_wins_reason_losing_nip_fi_does_not_enqueue_frame
|
|
state::tests::manager_disconnect_sets_reason_enqueues_frame_then_cancels
|
|
)
|
|
local name
|
|
for name in "${nip_fi_exact_tests[@]}"; do
|
|
run_test_step "buzz-relay ${name}" \
|
|
cargo test -p buzz-relay --lib "$name" -- --exact --nocapture
|
|
done
|
|
|
|
run_test_step "buzz-relay binary tests" \
|
|
cargo test -p buzz-relay --bin buzz-relay -- --nocapture
|
|
|
|
run_test_step "buzz-relay invitation token and cutoff contracts" \
|
|
cargo test -p buzz-relay --lib invite_token:: -- --nocapture
|
|
run_test_step "buzz-relay invitation cutoff configuration" \
|
|
cargo test -p buzz-relay --lib config::invite_cutoff_tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay mesh receive lifetime regression" \
|
|
cargo test -p buzz-relay --lib mesh_boot::tests::demo_echo_retains_pending_receive_across_housekeeping_ticks -- --exact --nocapture
|
|
|
|
run_test_step "buzz-relay boot lifecycle tests" \
|
|
cargo test -p buzz-relay --test boot_lifecycle -- --nocapture
|
|
|
|
# Load generator unit tests are infra-free. Keep in step with `just test-unit`.
|
|
run_test_step "buzz-test-client loadgen unit tests" \
|
|
cargo test -p buzz-test-client --lib loadgen:: -- --nocapture
|
|
}
|
|
|
|
# ---- DB / integration tests (infra required) --------------------------------
|
|
|
|
run_integration_tests() {
|
|
section "Integration Tests (requires running services)"
|
|
|
|
ensure_infra
|
|
|
|
run_test_step "buzz-db tests" \
|
|
cargo test -p buzz-db -- --nocapture
|
|
|
|
if find crates/buzz-auth/tests -maxdepth 1 -name '*.rs' -print -quit 2>/dev/null | grep -q .; then
|
|
run_test_step "buzz-auth integration tests" \
|
|
cargo test -p buzz-auth --test '*' -- --nocapture
|
|
else
|
|
run_test_step "buzz-auth (no integration tests found)" true
|
|
fi
|
|
|
|
run_test_step "workspace integration tests" \
|
|
cargo test --test '*' -- --nocapture 2>/dev/null || \
|
|
run_test_step "workspace integration tests (none found)" true
|
|
}
|
|
|
|
# ---- Main -------------------------------------------------------------------
|
|
|
|
START_TIME=$(date +%s)
|
|
|
|
case "${MODE}" in
|
|
unit)
|
|
run_unit_tests
|
|
;;
|
|
integration)
|
|
run_integration_tests
|
|
;;
|
|
all|*)
|
|
run_unit_tests
|
|
run_integration_tests
|
|
;;
|
|
esac
|
|
|
|
END_TIME=$(date +%s)
|
|
ELAPSED=$((END_TIME - START_TIME))
|
|
|
|
# ---- Summary ----------------------------------------------------------------
|
|
|
|
section "Test Summary"
|
|
echo ""
|
|
echo -e " Duration: ${ELAPSED}s"
|
|
echo ""
|
|
|
|
if [[ ${#PASSED[@]} -gt 0 ]]; then
|
|
echo -e " ${GREEN}Passed (${#PASSED[@]}):${NC}"
|
|
for t in "${PASSED[@]}"; do
|
|
echo -e " ${GREEN}pass${NC} ${t}"
|
|
done
|
|
fi
|
|
|
|
if [[ ${#FAILED[@]} -gt 0 ]]; then
|
|
echo ""
|
|
echo -e " ${RED}Failed (${#FAILED[@]}):${NC}"
|
|
for t in "${FAILED[@]}"; do
|
|
echo -e " ${RED}fail${NC} ${t}"
|
|
done
|
|
echo ""
|
|
exit 1
|
|
fi
|
|
|
|
echo ""
|
|
success "All tests passed!"
|
|
exit 0
|