mirror of
https://github.com/block/buzz.git
synced 2026-11-13 21:01:53 +00:00
## Summary This PR is stacked on #9064. It does two things to the Git-on-object-storage code in `buzz-relay`. It replaces the TLA+ model of the push protocol with one that states what the protocol is supposed to implement and checks that it does. It also changes the rule by which the relay decides whether to publish a push, because the new model shows that the rule #9064 introduces still publishes requests in which git accepted nothing. ### Relationship to #9064 The branch is `main`, then the two commits of #9064, then four commits of this PR: the specification work, the publish-rule change, a review fix merged from #9429, and a fix to `check.sh`. A merge of `main` follows them; it was needed because `main` changed `Justfile`, which #9429 also edits, after the branch point. The first two commits are #9064's, replayed onto current `main` without conflicts and otherwise unchanged. The base of this PR stays `main` because #9064's branch is about 200 commits behind it. Once #9064 merges, this branch can be rebased and the diff shrinks to the last four commits. Until then, review the commits after the first two. This PR keeps two of #9064's three changes as they are: the `published_at` stamp that orders kind:30618 events by the manifest chain, and `protect_landed`, which lets a landed conditional write stand when the serving lease lapses after it. It replaces the third, the publish rule. ### The publish rule #9064 publishes when git exits 0 and the status report has an `ok` line or no `ng` line. That handles a push in which one ref is updated and another refused. The report, however, is not evidence of what git did. - git exits 0 for a request that carries no commands, and for a push the pre-receive hook declined. A decline is reported only to a client that negotiated `report-status`. For a request with no commands, and for a decline when the client did not negotiate a report, there is no `ng` line. The relay then writes the pointer and emits a relay-signed kind:30618 event that names the caller as pusher, although the push policy approved nothing. - The report is read from a stream whose framing the client's capabilities determine, so the decision depends on parsing a client-shaped stream. The decision is now taken from the workspace. `push_disposition` compares the refs after receive-pack with the refs the workspace was hydrated from. Unchanged refs are answered with git's own output and nothing is published. Changed refs are published if git exited normally. Changed refs after an abnormal exit fail the request. The snapshot that is compared is the one `cas_publish` publishes, so git's per-ref report is true of the published state. The status-report scanner and its tests are removed, and the ref-name conflict case they covered moves into the table-driven test described below. Three smaller changes follow. The ref-snapshot parser now fails closed: output that was not UTF-8 used to read as an empty ref set and a malformed line was skipped, and either would publish a manifest that lacks refs. `manifest_changed` in `finalize_push` could not be false and is removed. Taking the snapshot before the publish decision moved its resource-limit error (snapshot larger than 4 MiB, or more refs than a manifest may hold) onto a path that answered 500; #9429 restores the 413 the publish path gave before and adds `snapshot_resource_limit_answers_413`. ### What the specification work adds - `docs/spec/GitRefDB.tla` is the specification: a ref database in which a push takes effect in one step, is applied only if authorized and only against the old values it names, and is reported truthfully. - `docs/spec/GitOnObjectStore.tla` is the protocol: several refs, an object graph with reachability, packs, compaction, the pre-receive hook, the publish decision, the default-branch setter, repository creation, and a reader. `git receive-pack` is represented by a stated contract rather than modeled internally. - The correctness statement is `Refines`: every behavior of the protocol is a behavior of the specification. TLC checks it together with the store invariants (fence, reader completeness, closure of published manifests). - `docs/spec/check.sh` pins TLC 1.7.4 by SHA-256 and runs 28 checks. Eight instances must pass. Nineteen seeded design errors must each be detected with the violation named for them. One coverage check confirms that a behavior the properties depend on (git reporting `ok` for a command that changes nothing) is reachable. A name passed to `check.sh` that is not a check is an error, so a misspelled selection cannot report success. - Both earlier publish rules are among the seeded errors: the original rule (exit 0 and no `ng`) and #9064's status-report rule. For each, TLC reports a publication the policy did not approve (`PublishIsAuthorizedChange`) and a violation of `Refines`. - `docs/spec/GitPushPath.qnt`, the Quint model from #9064 that covers the serving lease and the event timestamps, is extended. The publish rule becomes a parameter with three values, a request may carry no commands or come from a client without a status report, and a new invariant, `publicationChangesRefs`, states that a published manifest differs in its refs from the one it replaces. The implemented rule satisfies all four invariants; the status-report rule violates the new one with a zero-command request. - `docs/spec/lean/GitPointer.lean` is a Lean 4 proof, with no dependencies, of the pointer discipline for any number of clients, refs and objects: the fence, closure, that every pointer change is a legal step from the value it replaces, that the length of the pointer's predecessor chain equals the number of pointer writes, and that a successful conditional write implies no intervening write. - `.github/workflows/spec.yml` runs TLC and Lean when a file under `docs/spec/` changes. On this PR's first run the TLC job took 10 minutes 19 seconds and the Lean job 22 seconds. Quint is not run in CI. - `docs/git-on-object-storage.md` is revised to match. The push listing follows the implemented order. The verification section separates definitions, assumptions, results checked by TLC, results proved in Lean, results checked with Quint, evidence about the implementation, and limitations. The invariant names cited by code comments and by migration 0002 are kept as aliases in the TLA+ module. ### Assumptions and limitations The TLC results hold for the bounded instances in `check.sh`, not for arbitrary sizes. The Lean proof is unbounded but covers the pointer discipline only, not the refinement. The Quint results are from random simulation (2,000,000 runs of at most 14 steps) and a bounded symbolic check (8 steps, Apalache); neither is a proof for unbounded runs. All of them rest on the assumptions named in the document: the object store's conditional-write semantics, ETag equality implying body equality, no SHA-256 collision among the values that occur, the stated contract for `git receive-pack`, and the policy endpoint deciding authorization. The correspondence between the models and the Rust code is established by reading the code and by the tests below; it is not mechanized. The document states one assumption that #9064's ordering fix depends on and that no model covers (Assumption C1). A manifest whose parent carries no `published_at` (the empty manifest written when a repository is announced, or a manifest written before the field existed) is stamped with the writing instance's current time plus one second. Its event is selected over the parent's only if that value exceeds the parent event's `created_at`, which was taken from the clock of the instance that emitted it. This holds when one instance handles both, and otherwise when the writing instance's clock is not behind the other's by more than the time elapsed between the two events. The assumption concerns one write per repository. The document lists its removal as a proposed extension, for example by having the first push after an unstamped manifest read the stored event's `created_at` and exceed it. ### Related issue Related to #2621 (CI for the formal models). This adds the TLC job for the git model and a Lean job; it does not cover the Tamarin models or the NIP-PL scripts, so it should not close that issue. ### Testing Run locally on the restacked branch: - `docs/spec/check.sh`: 28 checks, each with the expected result (8 pass, 19 seeded errors violate the named property, 1 coverage witness reached). About three minutes on 16 cores; the largest instance has 6,773,286 distinct states. - `lake build` in `docs/spec/lean` (Lean 4.33.1): succeeds. The safety theorems depend only on `propext`, `Classical.choice` and `Quot.sound`. - Quint 0.33.0, `GitPushPath.qnt`: `quint run --main fixed --invariant safety --max-steps 14 --max-samples 2000000 --seed 0x1` reports no violation; `quint verify --main fixed --invariant safety --max-steps 8` (Apalache 0.62.1) reports no violation; each single-defect module produces the counterexample the document lists. - `cargo fmt --all -- --check` and `cargo clippy -p buzz-relay --all-targets -- -D warnings`: clean. - `cargo nextest run -p buzz-relay --lib -E 'test(/^api::/) + test(/side_effects/)'`: 158 passed. - The storage-backed `finalize_push` tests (`api:
:transport::track_c_tests::external_infra_minio_tests`, with `--run-ignored ignored-only`) against local Postgres and MinIO: 5 passed. - `e2e_git` against a local relay built from this branch, with Postgres, Redis and MinIO: 3 passed (clone/push/fetch/force-push round trip, concurrent push, partially refused push). The storage probe in that test needs `BUZZ_E2E_GIT_COMMUNITY_ID` set to the local community's id. - `just file-size-check` and `just security-review-check`: pass. Two regression tests bind the changed decision to production code, and both were confirmed to fail when the decision is forced back to the exit status: - `push_is_published_exactly_when_receive_pack_changed_refs` runs real git through the production `run_git_at` and `observe_push` for seven ways a push can end: a request with no commands, a push the hook declines with and without a status report, an accepted push, an accepted command that asks for the value the ref already has, a push in which one ref is updated and another refused for its old value, and a push that creates two refs whose names conflict, of which git creates one and refuses the other. It needs no services. Which of the two conflicting refs git creates depends on the git version (2.50 creates the first command's ref, 2.56 on the flake-check runner creates the other), so the test accepts either outcome and requires that each ref is in the published snapshot exactly when git reported `ok` for it. The first version of this case assumed one outcome and failed the Rust flake check for that reason. Before #9429, `just test-unit` selected neither `api:
:cas_publish::tests` nor `api:
:transport::track_c_tests`, so this test ran only in the flake check; both modules are now in the unit-test lane (45 tests, none needing services). It is Unix-only because its hook fixture sets file modes; the first push of this PR failed Windows clippy for that reason, and the test and its fixture are now gated with `#[cfg(unix)]`. That gating has not been compiled for Windows locally, so the Windows CI job is the check. - `finalize_push_publishes_nothing_when_receive_pack_changed_no_ref` calls `finalize_push` with an unchanged workspace and checks that the pointer is not written and no kind:30618 event is stored. It needs Postgres and an object store. Not done, or not covered: - No CI job selects the storage-backed `finalize_push` tests or `e2e_git`, so the second regression test and the end-to-end tests are evidence only when someone runs them. Adding them to CI needs a change to the test fixture, which applies migrations itself, and is left as a follow-up. - `just ci` was not run in full. - No human has tested the change. The human-testing step of the readiness checklist in `AGENTS.md` is not met, and `buzz-review-completed` is not claimed. The local secret-scanning hook (`sadscan`) was skipped for the commits and pushes that touch `crates/buzz-relay/src/api/git/settings_tests.rs`. It scans whole files and flags the P-256 test key fixtures that file already contains on `main`; this branch adds no key material. --------- Signed-off-by: Jordan Mecom <jm@squareup.com> Signed-off-by: Tom Brow <tomb@squareup.com> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@localhost>
646 lines
32 KiB
Bash
Executable File
646 lines
32 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
|
|
|
|
# A retain past a full subscriber command queue (no Redis dialed;
|
|
# infra-free). Mirrors `just test-unit`.
|
|
run_test_step "buzz-pubsub full-queue retain test" \
|
|
cargo test -p buzz-pubsub --lib -- --exact --nocapture \
|
|
tests::queue_full_retain_does_not_wait_and_schedules_reconciliation
|
|
|
|
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
|
|
|
|
run_test_step "buzz-ws-client tests" \
|
|
cargo test -p buzz-ws-client -- --nocapture
|
|
|
|
run_test_step "buzz-pairing-cli tests" \
|
|
cargo test -p buzz-pairing-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 NIP-FI policy command tests" \
|
|
cargo test -p buzz-admin nip_fi_policy::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::revoke_of_a_replaced_holder_spares_the_pending_replacement \
|
|
handlers::req::tests::superseded_or_closed_requests_cannot_claim \
|
|
connection::tests::close_fences_a_req_whose_handler_has_not_run \
|
|
connection::tests::only_the_newest_same_id_req_answers \
|
|
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::missed_pongs_close_with_their_reason_and_count \
|
|
connection::tests::client_reply_to_a_jittered_restart_closes_as_shutdown \
|
|
connection::tests::client_close_racing_a_policy_disconnect_keeps_the_policy_reason \
|
|
connection::tests::server_close_reason_ranks_shutdown_policy_then_cancellation \
|
|
connection::tests::auth_timeout_after_a_rejected_auth_closes_as_auth_failed \
|
|
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 Git push publish-decision tests" \
|
|
cargo test -p buzz-relay --lib api::git::cas_publish::tests:: -- --nocapture
|
|
|
|
run_test_step "buzz-relay Git push track C tests" \
|
|
cargo test -p buzz-relay --lib api::git::transport::track_c_tests:: -- --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 public invite previews" \
|
|
cargo test -p buzz-relay --lib invite_preview -- --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 runtime concurrency caps" \
|
|
cargo test -p buzz-relay --lib concurrency_limits::tests:: -- --nocapture
|
|
run_test_step "buzz-relay feature-flag provider selection" \
|
|
cargo test -p buzz-relay --lib feature_flags::tests:: -- --nocapture
|
|
run_test_step "buzz-relay AUTH concurrency shed" \
|
|
cargo test -p buzz-relay --lib handlers::auth::tests::auth_cap_sheds_a_verified_login_with_a_bare_close -- --exact --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
|
