From 7f465fac2a99ea30212edb9dc90ab53c244a71cc Mon Sep 17 00:00:00 2001 From: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Date: Fri, 9 Oct 2026 10:47:16 -0400 Subject: [PATCH] ci: keep CARGO_HOME outside the checkout so root Rust caches save Hermit's rustup package sets CARGO_HOME to ${HERMIT_ENV}/.hermit/rust. Swatinem/rust-cache keeps only target/ outputs for packages whose manifest is outside the workspace root, so every registry crate looked like a workspace member and each root-workspace cache saved an empty target/ (1303 B on main for unit-tests, rust-lint, cross, relay-e2e, and mesh lifecycle). The exact-key hit then blocked every later save. Set CARGO_HOME to ${HOME}/.cargo-hermit (and its bin/ on PATH) in bin/hermit.hcl. Hermit applies that env whenever it execs cargo, so a workflow-level CARGO_HOME export cannot do this; the environment config fixes CI and local alike. The rust-cache contract pins it, and bin/hermit.hcl now selects the Rust CI lanes. Consolidate the default-feature just test-unit nextest runs into one invocation with per-package filter clauses, so shared dependencies compile once. Feature-flagged runs and doctests stay separate. Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> --- .github/workflows/ci.yml | 1 + Justfile | 333 +++++++++--------- bin/hermit.hcl | 9 + .../test-rust-cache-contract-regressions.sh | 28 ++ scripts/test-rust-cache-contract.sh | 17 + 5 files changed, 228 insertions(+), 160 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1ff1c47fe..b3e541846d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,7 @@ jobs: - 'Cargo.toml' - 'Cargo.lock' - 'rust-toolchain.toml' + - 'bin/hermit.hcl' - '.config/nextest.toml' - 'scripts/postgres-test-*.sh' - 'scripts/reconcile-schema-after-pgschema.sql' diff --git a/Justfile b/Justfile index c85b620fd8..9990fa606d 100644 --- a/Justfile +++ b/Justfile @@ -398,155 +398,145 @@ test-unit: set -euo pipefail ./scripts/test-ensure-local-relay-key.sh if command -v cargo-nextest &>/dev/null; then - cargo nextest run -p buzz-core -p buzz-auth --lib - cargo nextest run -p buzz-audit --lib - # Cross-pod disconnect payload tests (NIP-FI and community archive - # fence); all infra-free. - cargo nextest run -p buzz-pubsub --lib -E 'test(/^conn_control::tests::/)' - # buzz-auth NIP-FI verifier doctests. The sealed-authority - # `compile_fail` doctests prove the default-feature public API alone - # cannot forge the issuer→JWKS authority; nextest does not run - # doctests, hence this separate step. The verifier's regression suite - # lives in the in-crate `#[cfg(test)] mod tests`, so `--lib` above - # already runs it. - cargo test -p buzz-auth --doc - cargo nextest run -p buzz-voice --lib - cargo nextest run -p buzz-cli - # buzz-sdk builder/validation unit tests: pure event-builder and input - # validation (e.g. the canvas writer-discipline/skew guard and the - # canvas_write_survived predicate), no infra. `--lib` runs all unit - # tests without the rustdoc dependency-resolution flake the full-package - # invocation hits. Enumerated explicitly because nothing in CI runs - # `cargo test --workspace` — membership buys clippy/check, not tests. - cargo nextest run -p buzz-sdk --lib - # buzz-acp owns the relay-to-agent trust boundary. Run its tests here so - # forged relay events cannot regain a path into agent routing unnoticed. - cargo nextest run -p buzz-acp - # Exercise the real MCP server for modern and legacy clients. - cargo nextest run -p buzz-dev-mcp - # buzz-db migrator/lint tests: pure SQL-parsing unit tests (no infra). - # They guard the embedded-migrator invariant (the complete checked-in - # additive migration set; legacy cutover/backfill remains an operator - # script, not startup state) and the tenant-scoping lints. The - # Postgres-backed buzz-db tests are - # #[ignore]d, so --lib runs only the infra-free set. Without this gate a - # stray file in migrations/ or a broken lint ships green. - cargo nextest run -p buzz-db --lib - # buzz-db source-policy tests: infra-free scans of the crate sources - # that enforce the event-write admission chokepoint, metric/provenance - # contracts, and their fixtures. They live in an integration-test - # binary, so `--lib` above does not run them. - cargo nextest run -p buzz-db --test observability_source - # buzz-db `AdmittedTx` doctests. The `compile_fail` cases prove code - # outside the crate can neither construct an admitted transaction nor - # pass a raw `sqlx::Transaction` to an event-write helper; nextest - # does not run doctests, hence this separate step. - cargo test -p buzz-db --doc - # Storage accounting crosses three crates whose focused regression - # suites are otherwise absent from the infra-free unit lane. - cargo nextest run -p buzz-media --lib \ - -E 'test(=bucket_index::tests::bucket_snapshot_json_round_trip_preserves_community_keys)' - # buzz-admin: storage-snapshot worker plus the community archive - # command parser, validation, and propagation-evidence tests. - cargo nextest run -p buzz-admin \ - -E 'test(storage_snapshot) + test(/^communities::tests::/) + test(/^tests::communities_/)' - # Multi-tenant conformance gate (buzz-conformance): the independent - # replay checker + golden fixtures. No infra — pure in-process trace - # replay — so it belongs in the unit job. Run all targets (lib + the - # tests/replay_fixtures.rs integration test), not just --lib. - cargo nextest run -p buzz-conformance - # Gateway unit and black-box HTTP tests are infra-free. Postgres-backed - # contract/race tests run in the dedicated CI job below. - cargo nextest run -p buzz-push-gateway - cargo nextest run -p buzz-push-gateway --features personal-dev-app-attest - # Kubernetes backend provider: the decision layers (state machine, GC - # planner, env precedence, naming, wire) are pure functions with a fake - # substrate, so they belong in the unit job. Enumerated explicitly - # because nothing in CI runs `cargo test --workspace` — workspace - # membership alone buys clippy/check, not a single executed test. - cargo nextest run -p buzz-backend-kubernetes - # Feature-flag crate coverage: run once with LaunchDarkly enabled. - # This includes all default tests plus the cfg(feature="launchdarkly") - # tests, avoiding duplicate default-only execution in the unit lane. - cargo nextest run -p buzz-feature-flags --features launchdarkly - # buzz-agent: two infra-free concerns run together by executing the - # whole crate (lib + integration tests), because nothing in CI runs - # `cargo test --workspace`, so without this stanza neither the crate's - # library tests nor its integration tests execute remotely. - # * model-capabilities corpus (lib): 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; without it a manifest edit that diverges Rust from the - # corpus ships green. - # * OAuth auth coordinator (lib concurrency matrix + databricks - # integration tests): lock single-flight, cooldown, cross-process - # crash recovery — infra-free via a stub OIDC provider and an - # injected browser opener, no network or Postgres. - cargo nextest run -p buzz-agent - # Admin API auth-boundary tests (api::admin in buzz-relay): the NIP-98 - # duplicate-tag rejections, the Host/Origin replay-ordering causal pair, - # the admin.localhost origin/advertisement/canonical-URL pins, and the - # host-oracle/credential-first checks. These are the regression guard for - # the /api/admin/v1 moderation auth surface. Enumerated explicitly because - # nothing in CI runs `cargo test --workspace`, `just test-unit` did not - # enumerate `buzz-relay --lib`, and Backend Integration selects only the - # #[ignore]d Postgres suites — so these non-ignored tests ran in no lane - # and a red one could ship green (exactly how a broken admin test slipped - # past every gate once). Scoped to api::admin, not the whole buzz-relay - # --lib, because api::media has non-ignored tests that require Postgres. - # DB-backed api::admin tests are #[ignore]d and run in the PostgreSQL - # lane; the non-ignored ones reject before touching the database. Any - # new non-ignored test here must stay DB-free: without a database, a - # DB fallthrough only "passes" by waiting out the ~30s sqlx acquire - # timeout. - # The second clause adds the relay's pure authorization-decision tests: - # the NIP-29 channel membership grid (handlers::channel_authz), the - # moderation capability grid (handlers::moderation_authz), and the pure - # helpers in handlers::side_effects. They ran in NO lane before — - # `test(/^api::admin::/)` never matched them, and the PostgreSQL lane - # pairs `--run-ignored ignored-only` with a `postgres_tests::` - # default-filter — so a red one shipped green, exactly the gap the - # api::admin clause above was added to close. - # Deliberately scoped to these three modules instead of all of - # `handlers::`: the wider set is mostly Postgres-backed, and five of its - # non-postgres_tests cases only "pass" without a database by waiting out - # the ~30s sqlx acquire timeout, so they do not belong in the infra-free - # unit job either. The REQ subscription-lifecycle tests are picked by - # exact name for the same reason: the rest of handlers::req needs a DB. - # The third clause adds the NIP-FI HTTP ingress and its router/config - # neighbours: nip_fi_http, nip_fi_config, router, api::parse_query_tests, - # and the Git transport off_mode_precedence_tests. All are infra-free - # and finish in well under a second with no DATABASE_URL, so none wait - # out the sqlx acquire timeout. `--bin buzz-relay` adds main.rs's - # `tests::` module (JWKS refresh cadence) and `composition_tests::` - # (JWKS source + refresh-loop composition), which `--lib` cannot reach - # because they live in the binary target; the nested - # `tests::postgres_tests::` stays in the PostgreSQL lane. - # The fourth clause adds the startup step timers (startup_steps) and the - # env- and log-capture tests in telemetry and config. All three modules - # are infra-free; config parses env into a struct and never connects. - # `^config::` is anchored so it does not also select nip_fi_config. - cargo nextest run -p buzz-relay --lib --bin buzz-relay \ - -E 'test(/^api::admin::/) + test(/^handlers::channel_authz::/) + test(/^handlers::moderation_authz::/) + test(/^handlers::side_effects::tests::/) + test(/^storage_sweep::tests::/) + test(/^nip_fi_core::tests::/) + test(/^nip_fi_http::tests::/) + test(/^nip_fi_config::tests::/) + test(/^readiness::tests::/) + test(/^router::tests::/) + test(/^api::parse_query_tests::/) + test(/^api::git::transport::off_mode_precedence_tests::/) + test(/^audio::join::tests::/) + test(/^audio::handler::tests::/) + test(/^nip_fi_gate::tests::/) + test(/^nip_fi_session::tests::/) + test(/^nip_fi_shadow::tests::/) + test(/^nip_fi_shadow_session::tests::/) + test(/^startup_steps::tests::/) + test(/^telemetry::tests::/) + test(/^config::tests::/) + test(=state::tests::neither_a_confirmed_inactive_community_nor_a_failed_lookup_admits_the_socket) + test(=state::tests::periodic_revalidation_disconnects_inside_the_fenced_callback) + test(=state::tests::bare_community_disconnect_fails_closed_when_the_fence_is_unavailable) + test(=connection::tests::send_loop_sends_policy_close_when_community_is_archived) + test(=state::tests::community_disconnect_then_nip_fi_keeps_community_deleted_reason) + test(=state::tests::disconnect_community_wins_reason_losing_nip_fi_does_not_enqueue_frame) + test(=handlers::req::tests::timed_out_historical_read_deregisters_before_closed) + test(=handlers::req::tests::superseded_timeout_leaves_replacement_intact) + test(=handlers::req::tests::search_claim_retires_live_and_yields_to_replacement) + test(=handlers::req::tests::concurrent_claims_and_stale_teardowns_keep_the_last_owner) + test(=handlers::req::tests::timeout_closed_is_emitted_before_a_replacement_can_claim) + test(=handlers::req::tests::revoke_then_replacement_keeps_replacement_whole) + test(=handlers::req::tests::claims_after_connection_cleanup_are_refused) + test(=handlers::req::tests::dropped_terminal_frame_cancels_connection) + test(=handlers::req::tests::revoke_dropped_terminal_frame_cancels_connection) + (kind(bin) & (test(/^tests::/) + test(/^composition_tests::/) + test(/^env_filter_tests::/)) - test(/^tests::postgres_tests::/))' - # Note on audio::join::tests scope: the full suite is infra-free (no DB, - # no Redis). The infra-free audio/FI regression witnesses — bootstrap - # ordering barrier, CommitConfirmed arm, pending-close invisibility, - # abnormal-stream-close fanout, and never-ready-sink writer witnesses — - # are all selected by audio::join::tests and audio::handler::tests. - # DB-backed audio join tests use #[ignore] and run in the postgres lane. - # NIP-FI (S3/S4) relay witnesses: the wholly-new nip_fi_upgrade and - # api::nip_fi modules, - # the auth metrics contract module, plus the exact NIP-FI tests added, - # or whose assertions changed, in mixed modules (audio::room, - # connection, handlers::*, state). nip_fi_config and router are - # selected whole by the command above. Mixed modules are listed by exact - # name so main's unselected tests (several wait out the ~30s sqlx - # acquire timeout) stay out. NIP-FI tests that need Postgres live in - # postgres_tests and - # run in the PostgreSQL lane. Keep scripts/run-tests.sh in step. - cargo nextest run -p buzz-relay --lib -E ' - test(/^nip_fi_upgrade::/) + # One nextest invocation resolves features once, so shared crates and + # dependencies compile once instead of once per package run. Each + # clause below is scoped to one package and to the targets its old + # per-package command selected (`--lib` -> kind(lib), `--test X` -> + # binary_id(pkg::X)); the -p list is derived from those clauses. + # Feature-flagged runs and doctests stay separate below: enabling a + # feature here would unify it into every crate in this build. + unit_filters=( + 'package(buzz-core) & kind(lib)' + 'package(buzz-auth) & kind(lib)' + 'package(buzz-audit) & kind(lib)' + # Cross-pod disconnect payload tests (NIP-FI and community archive + # fence); all infra-free. + 'package(buzz-pubsub) & kind(lib) & (test(/^conn_control::tests::/))' + 'package(buzz-voice) & kind(lib)' + 'package(buzz-cli)' + # buzz-sdk builder/validation unit tests: pure event-builder and input + # validation (e.g. the canvas writer-discipline/skew guard and the + # canvas_write_survived predicate), no infra. `--lib` runs all unit + # tests without the rustdoc dependency-resolution flake the full-package + # invocation hits. Enumerated explicitly because nothing in CI runs + # `cargo test --workspace` — membership buys clippy/check, not tests. + 'package(buzz-sdk) & kind(lib)' + # buzz-acp owns the relay-to-agent trust boundary. Run its tests here so + # forged relay events cannot regain a path into agent routing unnoticed. + # This includes the infra-free author-gate and queue lib tests; ignored + # lifecycle tests remain excluded and run in their integration lanes. + 'package(buzz-acp)' + # Exercise the real MCP server for modern and legacy clients. + 'package(buzz-dev-mcp)' + # buzz-db migrator/lint tests: pure SQL-parsing unit tests (no infra). + # They guard the embedded-migrator invariant (the complete checked-in + # additive migration set; legacy cutover/backfill remains an operator + # script, not startup state) and the tenant-scoping lints. The + # Postgres-backed buzz-db tests are + # #[ignore]d, so --lib runs only the infra-free set. Without this gate a + # stray file in migrations/ or a broken lint ships green. + 'package(buzz-db) & kind(lib)' + # buzz-db source-policy tests: infra-free scans of the crate sources + # that enforce the event-write admission chokepoint, metric/provenance + # contracts, and their fixtures. They live in an integration-test + # binary, so `--lib` above does not run them. + 'package(buzz-db) & binary_id(buzz-db::observability_source)' + # Storage accounting crosses three crates whose focused regression + # suites are otherwise absent from the infra-free unit lane. + 'package(buzz-media) & kind(lib) & (test(=bucket_index::tests::bucket_snapshot_json_round_trip_preserves_community_keys))' + # buzz-admin: storage-snapshot worker plus the community archive + # command parser, validation, and propagation-evidence tests. + 'package(buzz-admin) & (test(storage_snapshot) + test(/^communities::tests::/) + test(/^tests::communities_/))' + # Multi-tenant conformance gate (buzz-conformance): the independent + # replay checker + golden fixtures. No infra — pure in-process trace + # replay — so it belongs in the unit job. Run all targets (lib + the + # tests/replay_fixtures.rs integration test), not just --lib. + 'package(buzz-conformance)' + # Gateway unit and black-box HTTP tests are infra-free. Postgres-backed + # contract/race tests run in the dedicated CI job below. + 'package(buzz-push-gateway)' + # Kubernetes backend provider: the decision layers (state machine, GC + # planner, env precedence, naming, wire) are pure functions with a fake + # substrate, so they belong in the unit job. Enumerated explicitly + # because nothing in CI runs `cargo test --workspace` — workspace + # membership alone buys clippy/check, not a single executed test. + 'package(buzz-backend-kubernetes)' + # buzz-agent: two infra-free concerns run together by executing the + # whole crate (lib + integration tests), because nothing in CI runs + # `cargo test --workspace`, so without this stanza neither the crate's + # library tests nor its integration tests execute remotely. + # * model-capabilities corpus (lib): 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; without it a manifest edit that diverges Rust from the + # corpus ships green. + # * OAuth auth coordinator (lib concurrency matrix + databricks + # integration tests): lock single-flight, cooldown, cross-process + # crash recovery — infra-free via a stub OIDC provider and an + # injected browser opener, no network or Postgres. + 'package(buzz-agent)' + # Admin API auth-boundary tests (api::admin in buzz-relay): the NIP-98 + # duplicate-tag rejections, the Host/Origin replay-ordering causal pair, + # the admin.localhost origin/advertisement/canonical-URL pins, and the + # host-oracle/credential-first checks. These are the regression guard for + # the /api/admin/v1 moderation auth surface. Enumerated explicitly because + # nothing in CI runs `cargo test --workspace`, `just test-unit` did not + # enumerate `buzz-relay --lib`, and Backend Integration selects only the + # #[ignore]d Postgres suites — so these non-ignored tests ran in no lane + # and a red one could ship green (exactly how a broken admin test slipped + # past every gate once). Scoped to api::admin, not the whole buzz-relay + # --lib, because api::media has non-ignored tests that require Postgres. + # DB-backed api::admin tests are #[ignore]d and run in the PostgreSQL + # lane; the non-ignored ones reject before touching the database. Any + # new non-ignored test here must stay DB-free: without a database, a + # DB fallthrough only "passes" by waiting out the ~30s sqlx acquire + # timeout. + # The second clause adds the relay's pure authorization-decision tests: + # the NIP-29 channel membership grid (handlers::channel_authz), the + # moderation capability grid (handlers::moderation_authz), and the pure + # helpers in handlers::side_effects. They ran in NO lane before — + # `test(/^api::admin::/)` never matched them, and the PostgreSQL lane + # pairs `--run-ignored ignored-only` with a `postgres_tests::` + # default-filter — so a red one shipped green, exactly the gap the + # api::admin clause above was added to close. + # Deliberately scoped to these three modules instead of all of + # `handlers::`: the wider set is mostly Postgres-backed, and five of its + # non-postgres_tests cases only "pass" without a database by waiting out + # the ~30s sqlx acquire timeout, so they do not belong in the infra-free + # unit job either. The REQ subscription-lifecycle tests are picked by + # exact name for the same reason: the rest of handlers::req needs a DB. + # The third clause adds the NIP-FI HTTP ingress and its router/config + # neighbours: nip_fi_http, nip_fi_config, router, api::parse_query_tests, + # and the Git transport off_mode_precedence_tests. All are infra-free + # and finish in well under a second with no DATABASE_URL, so none wait + # out the sqlx acquire timeout. `--bin buzz-relay` adds main.rs's + # `tests::` module (JWKS refresh cadence) and `composition_tests::` + # (JWKS source + refresh-loop composition), which `--lib` cannot reach + # because they live in the binary target; the nested + # `tests::postgres_tests::` stays in the PostgreSQL lane. + # The fourth clause adds the startup step timers (startup_steps) and the + # env- and log-capture tests in telemetry and config. All three modules + # are infra-free; config parses env into a struct and never connects. + # `^config::` is anchored so it does not also select nip_fi_config. + 'package(buzz-relay) & (kind(lib) | binary_id(buzz-relay::bin/buzz-relay)) & (test(/^api::admin::/) + test(/^handlers::channel_authz::/) + test(/^handlers::moderation_authz::/) + test(/^handlers::side_effects::tests::/) + test(/^storage_sweep::tests::/) + test(/^nip_fi_core::tests::/) + test(/^nip_fi_http::tests::/) + test(/^nip_fi_config::tests::/) + test(/^readiness::tests::/) + test(/^router::tests::/) + test(/^api::parse_query_tests::/) + test(/^api::git::transport::off_mode_precedence_tests::/) + test(/^audio::join::tests::/) + test(/^audio::handler::tests::/) + test(/^nip_fi_gate::tests::/) + test(/^nip_fi_session::tests::/) + test(/^nip_fi_shadow::tests::/) + test(/^nip_fi_shadow_session::tests::/) + test(/^startup_steps::tests::/) + test(/^telemetry::tests::/) + test(/^config::tests::/) + test(=state::tests::neither_a_confirmed_inactive_community_nor_a_failed_lookup_admits_the_socket) + test(=state::tests::periodic_revalidation_disconnects_inside_the_fenced_callback) + test(=state::tests::bare_community_disconnect_fails_closed_when_the_fence_is_unavailable) + test(=connection::tests::send_loop_sends_policy_close_when_community_is_archived) + test(=state::tests::community_disconnect_then_nip_fi_keeps_community_deleted_reason) + test(=state::tests::disconnect_community_wins_reason_losing_nip_fi_does_not_enqueue_frame) + test(=handlers::req::tests::timed_out_historical_read_deregisters_before_closed) + test(=handlers::req::tests::superseded_timeout_leaves_replacement_intact) + test(=handlers::req::tests::search_claim_retires_live_and_yields_to_replacement) + test(=handlers::req::tests::concurrent_claims_and_stale_teardowns_keep_the_last_owner) + test(=handlers::req::tests::timeout_closed_is_emitted_before_a_replacement_can_claim) + test(=handlers::req::tests::revoke_then_replacement_keeps_replacement_whole) + test(=handlers::req::tests::claims_after_connection_cleanup_are_refused) + test(=handlers::req::tests::dropped_terminal_frame_cancels_connection) + test(=handlers::req::tests::revoke_dropped_terminal_frame_cancels_connection) + (kind(bin) & (test(/^tests::/) + test(/^composition_tests::/) + test(/^env_filter_tests::/)) - test(/^tests::postgres_tests::/)))' + # Note on audio::join::tests scope: the full suite is infra-free (no DB, + # no Redis). The infra-free audio/FI regression witnesses — bootstrap + # ordering barrier, CommitConfirmed arm, pending-close invisibility, + # abnormal-stream-close fanout, and never-ready-sink writer witnesses — + # are all selected by audio::join::tests and audio::handler::tests. + # DB-backed audio join tests use #[ignore] and run in the postgres lane. + # NIP-FI (S3/S4) relay witnesses: the wholly-new nip_fi_upgrade and + # api::nip_fi modules, + # the auth metrics contract module, plus the exact NIP-FI tests added, + # or whose assertions changed, in mixed modules (audio::room, + # connection, handlers::*, state). nip_fi_config and router are + # selected whole by the clause above. Mixed modules are listed by exact + # name so main's unselected tests (several wait out the ~30s sqlx + # acquire timeout) stay out. NIP-FI tests that need Postgres live in + # postgres_tests and + # run in the PostgreSQL lane. Keep scripts/run-tests.sh in step. + 'package(buzz-relay) & kind(lib) & (test(/^nip_fi_upgrade::/) + test(/^metrics::contract_tests::/) + test(=audio::room::tests::roster_revisions_are_ordered_and_snapshot_is_authoritative) + test(=connection::tests::auth_lifecycle_reconciles_every_terminal_and_never_leaks_gauge) @@ -590,17 +580,40 @@ test-unit: + test(=state::tests::community_disconnect_then_nip_fi_keeps_community_deleted_reason) + test(=state::tests::disconnect_community_wins_reason_losing_nip_fi_does_not_enqueue_frame) + test(=state::tests::manager_disconnect_sets_reason_enqueues_frame_then_cancels) - + test(/^api::nip_fi::/)' - # boot_lifecycle spawns the real relay binary and asserts its startup - # lifecycle, including that buzz_startup_phase_* reaches /metrics. Its - # non-ignored tests need no Postgres or Redis; the Postgres cases are - # #[ignore]d and stay in the PostgreSQL lane. Keep - # scripts/run-tests.sh in step. - cargo nextest run -p buzz-relay --test boot_lifecycle - # ACP author-gate and queue tests protect the trust boundary between - # relay events and agent prompts. They are infra-free; ignored lifecycle - # tests remain excluded and run in their dedicated integration lanes. - cargo nextest run -p buzz-acp --lib + + test(/^api::nip_fi::/))' + # boot_lifecycle spawns the real relay binary and asserts its startup + # lifecycle, including that buzz_startup_phase_* reaches /metrics. Its + # non-ignored tests need no Postgres or Redis; the Postgres cases are + # #[ignore]d and stay in the PostgreSQL lane. Keep + # scripts/run-tests.sh in step. + 'package(buzz-relay) & binary_id(buzz-relay::boot_lifecycle)' + ) + unit_packages=() + for clause in "${unit_filters[@]}"; do + package=${clause#package(} + unit_packages+=(-p "${package%%)*}") + done + unit_expr=$(printf ' | (%s)' "${unit_filters[@]}") + cargo nextest run "${unit_packages[@]}" -E "${unit_expr# | }" + # buzz-auth NIP-FI verifier doctests. The sealed-authority + # `compile_fail` doctests prove the default-feature public API alone + # cannot forge the issuer→JWKS authority; nextest does not run + # doctests, hence this separate step. The verifier's regression suite + # lives in the in-crate `#[cfg(test)] mod tests`, so `--lib` above + # already runs it. + cargo test -p buzz-auth --doc + # buzz-db `AdmittedTx` doctests. The `compile_fail` cases prove code + # outside the crate can neither construct an admitted transaction nor + # pass a raw `sqlx::Transaction` to an event-write helper; nextest + # does not run doctests, hence this separate step. + cargo test -p buzz-db --doc + # The gateway's default-feature tests run above; this adds the + # personal-dev App Attest build. + cargo nextest run -p buzz-push-gateway --features personal-dev-app-attest + # Feature-flag crate coverage: run once with LaunchDarkly enabled. + # This includes all default tests plus the cfg(feature="launchdarkly") + # tests, avoiding duplicate default-only execution in the unit lane. + cargo nextest run -p buzz-feature-flags --features launchdarkly else ./scripts/run-tests.sh unit fi diff --git a/bin/hermit.hcl b/bin/hermit.hcl index b429ec9613..691f6d7d02 100644 --- a/bin/hermit.hcl +++ b/bin/hermit.hcl @@ -1 +1,10 @@ manage-git = true +# Keep Cargo's registry, git checkouts, and installed tools outside the +# checkout. The rustup package defaults CARGO_HOME to ${HERMIT_ENV}/.hermit/rust, +# which made Swatinem/rust-cache treat every registry crate as a workspace +# member and save an empty target/ for the root workspace. A shared home also +# lets worktrees reuse one registry. scripts/test-rust-cache-contract.sh pins this. +env = { + "CARGO_HOME": "${HOME}/.cargo-hermit", + "PATH": "${HOME}/.cargo-hermit/bin:${PATH}", +} diff --git a/scripts/test-rust-cache-contract-regressions.sh b/scripts/test-rust-cache-contract-regressions.sh index 0275b87341..d4253f7746 100755 --- a/scripts/test-rust-cache-contract-regressions.sh +++ b/scripts/test-rust-cache-contract-regressions.sh @@ -12,6 +12,8 @@ for workflow in "$root"/.github/workflows/*.yaml; do cp "$workflow" "$tmp/.github/workflows/" done cp "$root/renovate.json" "$tmp/renovate.json" +mkdir -p "$tmp/bin" +cp "$root/bin/hermit.hcl" "$tmp/bin/hermit.hcl" run_contract() { BUZZ_RUST_CACHE_CONTRACT_ROOT="$tmp" "$root/scripts/test-rust-cache-contract.sh" @@ -139,6 +141,32 @@ YAML run_contract >/dev/null rm -rf "$tmp/.github/actions" "$tmp/.github/workflows/new-cache-user.yaml" +write_hermit_config() { + cat > "$tmp/bin/hermit.hcl" +} + +write_hermit_config <<'HCL' +manage-git = true +HCL +expect_failure 'bin/hermit.hcl must set CARGO_HOME once' + +write_hermit_config <<'HCL' +env = { + "CARGO_HOME": "${HERMIT_ENV}/.cargo", + "PATH": "${HERMIT_ENV}/.cargo/bin:${PATH}", +} +HCL +expect_failure 'bin/hermit.hcl must set CARGO_HOME once' + +write_hermit_config <<'HCL' +env = { + "CARGO_HOME": "${HOME}/.cargo-hermit", +} +HCL +expect_failure 'must put ${HOME}/.cargo-hermit/bin on PATH' +cp "$root/bin/hermit.hcl" "$tmp/bin/hermit.hcl" +run_contract >/dev/null + python3 - "$tmp/.github/workflows/_ci-rust.yml" <<'PY' import pathlib import sys diff --git a/scripts/test-rust-cache-contract.sh b/scripts/test-rust-cache-contract.sh index cf69c8aadc..69ea796cb2 100755 --- a/scripts/test-rust-cache-contract.sh +++ b/scripts/test-rust-cache-contract.sh @@ -91,6 +91,23 @@ cache_uses.each do |path, location, ref| end end +# Hermit's rustup package defaults CARGO_HOME to ${HERMIT_ENV}/.hermit/rust. +# rust-cache keeps only target/ outputs whose manifest is outside the workspace +# root, so a registry under the checkout makes every dependency look like a +# workspace member: the root-workspace cache saves an empty target/ and the +# exact-key hit then blocks every later save. Hermit's environment env wins +# over a CARGO_HOME exported by a workflow step, so the fix has to live here. +hermit_config = (root / "bin" / "hermit.hcl").read.force_encoding("UTF-8") +cargo_home = hermit_config.scan(/^\s*"?CARGO_HOME"?\s*[:=]\s*"([^"]*)"/).flatten +unless cargo_home.length == 1 && cargo_home.first.start_with?("${HOME}/") && + !cargo_home.first.include?("HERMIT_ENV") + abort "bin/hermit.hcl must set CARGO_HOME once to a ${HOME}/ path outside the checkout" +end +cargo_bin = "#{cargo_home.first}/bin" +unless hermit_config.match?(/^\s*"?PATH"?\s*[:=]\s*"#{Regexp.escape(cargo_bin)}:\$\{PATH\}"/) + abort "bin/hermit.hcl must put #{cargo_bin} on PATH" +end + rust_ci_path = root / ".github" / "workflows" / "_ci-rust.yml" rust_ci = workflows.fetch(rust_ci_path) { load_workflow.call(rust_ci_path) } jobs = rust_ci["jobs"]