Skip to content

Upgrade tests#2441

Closed
zancas wants to merge 66 commits into
devfrom
upgrade_tests
Closed

Upgrade tests#2441
zancas wants to merge 66 commits into
devfrom
upgrade_tests

Conversation

@zancas

@zancas zancas commented Jul 8, 2026

Copy link
Copy Markdown
Member

Numerous upgrades to the test harness:

  • add synthetic proposable wallet builder
  • make validator single source of truth for NU height
  • migrate tests from libto-node to unit
  • add makers test doorway

To run invoke:

makers help <-- for help
makers test <-- for all the (containerized with podman) tests
makers hierarchy-test <-- tests from fastest to slowest stopped at the first failing category

zancas and others added 30 commits July 5, 2026 20:39
…ange

Follows the CryptoProvider pattern established in zaino:
zingolabs/zaino@a62dcd3

rustls 0.23 auto-selects a provider only when exactly one of its ring /
aws_lc_rs features is enabled. The workspace pinned rustls with the ring
feature while rustls's default features already enable aws-lc-rs, so
feature unification put both providers in the graph and nothing installed
a process-level default - four zingo-netutils tests panicked with "Could
not automatically determine the process-level CryptoProvider".

Changes:

- zingo-netutils gains crypto::ensure_default_crypto_provider(): a
  guarded, first-install-wins install of aws-lc-rs, called where the
  crate builds rustls configs. An embedder (e.g. zingo-mobile) that
  installs its own provider first keeps it. Re-exported through zingolib.
- The seven scattered ad-hoc ring provider installs (LightClient
  constructors, sync_example_wallet, zingo-cli main, libtonode tests)
  are replaced with that one helper.
- Workspace features: rustls drops ring and gains prefer-post-quantum
  (the X25519MLKEM768 hybrid group leads outbound handshakes; ML-KEM is
  only available on aws-lc-rs); hyper-rustls and tokio-rustls switch
  ring -> aws-lc-rs; tonic gains tls-aws-lc so the provider is asserted
  in the feature graph rather than inherited.
- zingo-price: reqwest switches to rustls-tls-no-provider (never
  auto-selects) and installs the provider before fetching; it sits below
  zingo-netutils in the graph, so it carries a mirrored helper.
- Real crypto use excised: the legacy BIP32 HMAC-SHA512 derivation in
  extended_transparent.rs moves from ring::hmac to the RustCrypto
  hmac + sha2 crates. The stored-wallet regression tests
  (verify_example_wallet_*) confirm derivation is unchanged.
- The ring and rustls dependencies are dropped from zingolib, zingo-cli,
  and libtonode-tests; ring leaves the workspace dependency table.
- zcash_proofs's download-params feature (whose minreq -> rustls 0.21 ->
  ring subtree is the sole ring residue) is now enabled only for the
  build script that fetches sapling params; the runtime dependency graph
  is 100% aws-lc-rs.

Verified: the four previously failing zingo-netutils tests pass
(including the live mainnet-indexer connect); 95/95 unit tests across
zingo-netutils, zingo-price, and zingolib; cargo clippy clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit de11e16)
Flaky tests should surface as failures instead of being masked by
automatic retries.

- ZINGOLIB_NEXTEST_RETRIES defaults to "0" and is now a conditional
  [env] entry (env_not_set), matching ZINGOLIB_NEXTEST_FILTER: cargo-make
  otherwise clobbers inherited environment variables, so this is what
  makes a per-run `ZINGOLIB_NEXTEST_RETRIES=N makers test ...` override
  actually work when investigating a flake.
- The two hardcoded `--retries 2` nextest invocations in
  .github/workflows/test.yaml (which bypass makers) become `--retries 0`.
- `makers help` example lines updated to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ersions

- zcash_local_net + zingo_test_vectors: git-pinned to the infrastructure
  repo's bump_to_NU6.3 branch (0dc4a51), replacing the stale rev pin.
- ZAINO_IMAGE_TAG 0.3.1 -> 0.6.0-rc.1-no-tls (same digest published under
  zingodevops/zaino and zingodevops/zainod, so the docker-ci FROM line
  still resolves).
- ZEBRA_VERSION=6.0.0-rc.0 added; not yet consumed, reserved for the
  zebrad provisioning wiring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 0983747)
- libtonode-tests no longer pins test_lwd_zcashd; it runs the no-feature
  default network combo, the Core stack (zainod + zebrad).
- zingolib_testutils: delete the test_zainod_zcashd and test_lwd_zcashd
  features; test_lwd_zebrad survives (behind the infrastructure repo's
  legacy-stack feature) until the Legacy stack is removed as a unit.
- scenarios.rs: four network_combo cfg blocks reduced to two; stale
  zcashd doc comments rewritten in Validator/Indexer terms.
- CONTEXT.md: seed the glossary (Network combo, Faucet; Core/Legacy
  stack deferred to the infrastructure repo's glossary).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Darkside is lightwalletd's darksidewalletd mode, and the bump_to_NU6.3
infrastructure gates the lightwalletd indexer behind the opt-in
legacy-stack feature (scheduled for removal there). Without it the
darkside suite no longer compiles; this completes the zebrad port for
that suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The zainod+zebrad default network combo needs a zebrad binary, but the
CI image never contained one, so every test that launches a local net
died with the infrastructure's "Test executable must be set in
TEST_BINARIES_DIR" spawn panic.

Wire ZEBRA_VERSION from .env.testing-artifacts through the whole chain,
following zaino's pattern:

- docker-ci/Dockerfile: zebrad-downloader stage from
  docker.io/zfnd/zebra:${ZEBRA_VERSION}; the binary is copied from
  /usr/local/bin/zebrad into /usr/bin/, with a provenance LABEL.
- Makefile.toml: ZEBRA_VERSION joins the build-image --build-args and
  echo output, the container_image_tag inputs (so a zebra bump rebuilds
  the image), and /usr/bin/zebrad joins the container symlink list that
  populates TEST_BINARIES_DIR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`add_subtree_roots` fails only inside the CI container, with the wallet
consistently 32 sapling shards short of the server's count (1096 vs
1128); the same commit passes on the host. pepper-sync's subtree-root
fetch accepts a clean-but-premature stream end as complete, so the open
question is what ends the stream early in the container.

Add an #[ignore]d diagnostic, run manually in the environment under
investigation:

  ZINGOLIB_NEXTEST_FILTER='' makers container-test -p libtonode-tests \
    -E 'test(diagnose_subtree_root_stream)' --run-ignored all --no-capture

For three fresh connections it prints the sapling subtree-root count,
the chain height where the stream ended, and how it ended — then the
decisive experiment: resuming from start_index = count on the same
connection. Zero additional roots means that backend genuinely has only
that many (a lagging backend behind the zec.rocks load balancer);
roughly 32 more means the stream is being cut mid-flight in the
container's network path. Finally it reports where pepper-sync's own
fetch path lands the wallet.

On the host all connections currently report 1128 roots, clean end,
resume 0, wallet 1128.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ing it

get_subtree_roots accepted a clean-but-premature stream end as complete:
its retry logic only fires on DeadlineExceeded / EOF-decode errors, so a
stream cut mid-flight (proxy, flow control) silently truncated the
wallet's shard tree instead of surfacing.

For unbounded requests (max_entries == 0, the only in-tree callers), a
clean end is now trusted only after a resume pass from the current
index comes back empty; a cut stream resumes where it stopped. Costs
one empty round-trip on the happy path. Bounded requests keep
single-pass semantics — resuming would fetch past the caller's cap.

Defense-in-depth arising from the add_subtree_roots container failures
(wallet 1096 vs server 1128 sapling shards); the evidence for that
incident points to a stuck backend behind the zec.rocks load balancer,
which no client-side change can paper over — see issue #2440 and the
diagnose_subtree_root_stream diagnostic added in the previous commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…drop obsolete shield step

Two fixes discovered by the first zebrad-backed scenario runs:

- Scenario defaults passed the all-at-1 ActivationHeights::default(), but
  zebrad rejects every NU6.x activation block whose lockbox/funding-stream
  config doesn't pair with the heights, stalling the chain at genesis.
  default_test_activation_heights() now returns the harness's regtest
  fixture shape (all=1, nu5=2, nu6=2, nu6_1/2=5) — the only shape its
  subsidy fixtures support — with nu6_3 unactivated: this branch's wallet
  builds against released librustzcash (NU6.2-max branch ids), so an
  NU6.3-active chain rejects every wallet transaction. Drop the
  set_nu6_3(None) override when the ironwood wallet branch lands.

- zebrad_shielded_funds no longer shields before offloading: zebrad mines
  to Orchard natively (the "does not support shielded mining" comment was
  stale), so coinbase already lands in the pool and the shield found no
  transparent funds (InsufficientFunds(0)).

fast::basic_scenario passes end-to-end on zainod + zebrad with this
(launch, orchard mining, 100-block maturity, sync, sends). Exact-balance
assertions calibrated for the streamless zcashd stack (e.g. 1_875_000_000
in mine_to_transparent) still need recalibration for the deferred funding
stream (1/100 of subsidy from height 2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The deferred (lockbox) funding stream in the harness fixture skims 1/100
of the block subsidy from height 2 onward, and orchard coinbase accrues
from the block after NU5 activation (height 3) — both confirmed to the
zatoshi by server-run failures. The zcashd-era expectations assumed full
subsidy per block from height 1.

- zingolib_testutils::scenarios gains DEFERRED_STREAM_SKIM,
  POST_STREAM_BLOCK_REWARD, FUND_OFFLOAD_AMOUNT, mined_block_rewards_total,
  FUNDED_FAUCET_SETUP_HEIGHT and funded_faucet_orchard_balance so
  expectations are derived, not magic.
- Recalibrated: mine_to_transparent, mine_to_transparent_coinbase_maturity,
  mine_to_transparent_and_shield, mine_to_transparent_and_propose_shielding,
  value_transfers, filter_empty_messages, mine_to_orchard (un-ignored:
  its "zebrad cannot mine shielded" reason is disproven),
  send_orchard_back_and_forth (faucet-as-miner collects coinbase + fees).
- Sapling mining is the one true zebrad gap:
  send_mined_sapling_to_orchard becomes send_mined_orchard_to_orchard
  (the spend-mined-funds state transition it exercises is pool-agnostic);
  unfunded_mobileclient mines transparent (no client holds the mining
  key); mine_to_sapling (pre-existing ignore, reason corrected) and
  send_heartwood_sapling_funds (also needs Canopy above 1, which the
  config writer refuses) stay ignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tions

The fast-set run showed two failure mechanisms beyond the funding-stream
arithmetic (which mine_to_transparent_and_shield/propose confirmed):

1. Sync race: bare sync_and_await reaches whatever the Indexer has
   ingested, which lags the Validator right after generate_blocks —
   mine_to_transparent observed one block's coinbase at a height-3 tip.
   New scenarios::sync_client_to_validator_tip loops sync until the
   client fully scans the Validator's tip; every scenario-setup sync and
   the failing tests' post-generate syncs now use it.

2. Receiver selection: with an Orchard miner, block 1 (pre-NU5) pays the
   miner's SAPLING receiver the full pre-stream subsidy (observed
   s_balance: 625000000). Orchard-scenario expectations now include
   BLOCK_ONE_SAPLING_COINBASE.

Model constants are hypothesis-encoded so the next run adjudicates each
with a distinct delta: ORCHARD_COINBASE_START_HEIGHT=2 (off-by-one
POST_STREAM_BLOCK_REWARD means flip to 3), fee-returns-to-miner (10_000
deltas), and a new fast::orchard_miner_coinbase_distribution probe
asserts the full o/s/t distribution at a known tip.

Also: sync_all_epochs_from_heartwood -> sync_all_expressible_epochs
(canopy=3 and staggered NU heights are inexpressible: the config writer
requires Canopy at 1 and the subsidy fixtures pair only with the fixture
shape); unified_address_discovery syncs to tip before asserting
discovery (same race).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shielded coinbase has no 100-block maturity rule (transparent-only);
the server run proved it: value_transfers spends orchard coinbase notes
1-3 blocks old. The 100-block generation in zebrad_shielded_funds was a
relic of the old shield-transparent-coinbase flow, costing every funded
scenario minutes of mining plus a 100-block wallet sync.

Funded setup now ends at height 4 (launch + 2 setup + offload confirm).
Every derived expectation recomputes through FUNDED_FAUCET_SETUP_HEIGHT.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All ten funded scenarios failed at the offload with zebra's "rejected
from the mempool until the next chain tip block ... could not validate
orchard proof" once the maturity dance was removed: at a height-3 tip
the offload had to spend the tip block's own note. Ordinary sends of
blocks-old notes pass (value_transfers), so the rule being tripped is
specifically about the tip block.

zebrad_shielded_funds now syncs first and mines one block before the
spend, so the wallet's selectable notes and anchor sit at least one
block behind zebra's tip. Funded setup ends at height 5; expectations
recompute through FUNDED_FAUCET_SETUP_HEIGHT.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…adder

The tip-separation attempt refuted itself informatively: holding the
wallet one block behind made zebra reject the offload with "incorrect
consensus branch id" — the wallet signs for its-tip+1, and the fixture
ladder activates NU6.1/NU6.2 at height 5, exactly where the streamlined
setup operates. Tests that sync to the real tip before sending
(value_transfers) never hit this.

zebrad_shielded_funds now mines two extra blocks first (tip 5, clearing
the ladder so tip+1 and the inclusion block share the NU6.2 branch id),
syncs to the real tip, then offloads — with four pre-tip notes so
oldest-first selection never touches the tip block's note (the original
"could not validate orchard proof" rejection). Funded setup ends at
height 6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…IGHT

The height-6 funded setup shifted every absolute block height in tests
built on faucet_funded_recipient_default by +3 (funding tx confirms at
setup+2, observed as "Confirmed at 8"). Rewrite the literals in
received_tx_status_pending_to_confirmed_with_mempool_monitor and
send_to_transparent_and_sapling_maintain_balance as offsets from
scenarios::FUNDED_FAUCET_SETUP_HEIGHT so future setup-height changes
recompute instead of breaking.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sends in one transaction, then mines one block at a time — waiting for
the sender's wallet to reach each new height — until the transaction is
confirmed. Owns both send hazards discovered on the zainod+zebrad
stack, so callers need no choreography:

- indexer tip-lag (generate_blocks returns on the validator's height;
  the indexer's poll-based ingestion lags 100-500ms; a sync begun in
  that window silently misses the newest block);
- zebra's tip-block spend rejection ("rejected from the mempool until
  the next chain tip block"): on exactly that error, one separation
  block is mined and the send retried once, mirroring the
  zebrad_shielded_funds fix.

Confirmation is asserted (up to five blocks of tolerance), so a
transaction that misses its block for any new reason fails at the send
site with its txids, not downstream at an unrelated balance assert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause of the historical unified_address_discovery failure,
established by direct measurement: after generate_blocks returns, the
validator has the new block but the indexer's poll-based ingestion
reports the old tip for 100-500ms (10/10 rounds, max ~490ms, zainod
0.4.3 and 0.6.0 alike). A wallet sync begun inside that window
completes against the stale tip and silently misses the newest block.

Exonerated with evidence along the way: discovery bookkeeping (works
when the block is visible), zebra's block templates (the send IS in
the next block), zainod 0.6.0 specifically, and generate_blocks height
accounting.

Deliberately RED until the harness (or its callers) wait for indexer
convergence after block generation; the assertion message carries the
mechanism. Until then, tests must use the wallet-height-aware helpers
(increase_height_and_wait_for_client, sync_client_to_validator_tip,
send_and_bump).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ten sites used bare generate_blocks + sync_and_await (or quick_send +
generate_blocks(1)), each a latent indexer tip-lag or tip-block-spend
flake with retries at 0:

- unified_address_discovery: setup mining via
  increase_height_and_wait_for_client; the four-output send via
  send_and_bump (confirmation now asserted at the send site).
- Three LibtonodeEnvironment sites (messages_*/value_transfers):
  bare recipient sync after increase_chain_height (which waits only on
  the validator) becomes sync_client_to_validator_tip.
- by_address_finsight: three quick_send + mine + sync triplets become
  send_and_bump.
- send_and_sync_with_multiple_notes_no_panic: three
  generate_n_blocks + dual-sync groups become
  increase_height_and_wait_for_client for the first client and
  sync_client_to_validator_tip for the second.

Left alone, deliberately: second-client syncs that follow a
wait-for-client on another client (ordering makes them safe — the
indexer demonstrably has the block once any wallet synced to it), pure
mempool-pickup syncs after unmined sends, and propose-time
generate-without-sync sites whose semantics depend on the wallet NOT
seeing the new blocks.

Verified: unified_address_discovery green through send_and_bump on the
host stack that previously failed deterministically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gfr8)

Clears the runtime copy flagged by the high-severity Dependabot alert
(panic on malformed CRL BIT STRING). Defense-in-depth only: no zls code
configures CRLs or revocation checking, so the vulnerable parse is
unreachable — there is no input channel that delivers a CRL to webpki.

The second locked copy (0.101.7) cannot be bumped: it belongs to the
build-time-only zcash_proofs -> minreq -> rustls 0.21 subtree (pinned
to webpki ^0.101, no patched release in that line) and goes away only
when upstream drops minreq or we drop download-params.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ensure_taddrs_from_old_seeds_work and
address_generation_deterministic_and_coherent assert key derivations
against fixed vectors — nothing they check touches a chain, yet each
spent ~12s launching zebrad+zainod for scaffolding it never used.

They are now zingolib unit tests against LightWallet directly (21ms
total). The one behavioral subtlety is made explicit: the original
"next address index is 2" came from the scenario ClientBuilder
generating an extra sapling-only address after construction, not from
LightClient itself — the unit test reproduces that step with a comment
so the vector-pinned indices and their diversified derivations are
unchanged.

Two fewer LocalNet launches per fast-suite run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
15 chain_generics tests failed at fund_client_orchard's bare
sync_and_await: the Indexer lags the Validator after generate_blocks
(the new diagnostic measures up to ~515ms), so the faucet signs from a
stale tip and zebra rejects the send near the NU6.1/NU6.2 activation
boundary.

ConductChain gains sync_client_to_tip with a bare-sync default (darkside
unchanged); the generic funding flow calls it, and LibtonodeEnvironment
overrides it with scenarios::sync_client_to_validator_tip — the one
shared deterministic-sync implementation.

Also: verify_old_wallet_uses_server_height_in_send's stale height
literal (8, pre-dating the funded-setup change) is now derived from
FUNDED_FAUCET_SETUP_HEIGHT.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
filter_empty_messages and message_thread assert message semantics —
empty-memo filtering, address filtering, and message/value-transfer
ordering — which are pure summary derivation over wallet transaction
records. As integration tests they cost 49s and 132s of LocalNet
mining and syncing (message_thread alone set the fast suite's
wall-clock); as zingolib unit tests they run in 54ms combined.

pepper-sync gains three test-features constructors (WalletNote,
OutgoingNote, and WalletTransaction::new_for_test_with_orchard_notes)
so tests can fabricate received/sent transaction records — including
the recipient_full_unified_address that sent-side filtering matches
against — without a chain. The unit tests reproduce the integration
tests' exact shapes: two empty memos then Hello+empty for filtering;
the five-transaction Alice/Bob/Charlie thread with "Reply to" memos
for counting (bob 3, charlie 2) and bidirectional height ordering.

Two fewer LocalNet launches and ~180s less node time per fast-suite
run; the fast set drops from 18 to 16 tests with coverage relocated,
not removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The test asserts the absence of a lag that is known to exist (measured
10/10 rounds on zainod 0.4.3 and 0.6.0 alike), so in a normal run it
only restates a documented harness limitation. Keep it as an on-demand
probe (cargo nextest run --run-ignored) to re-measure after a
zcash_local_net indexer-convergence barrier lands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The test was marked "temporary for infrastructure integration": it ran
faucet_funded_recipient_default and asserted nothing. During the zebrad
port it earned its keep as the canonical probe — it was the first test
to surface the crypto-provider panic, the heights/subsidy mismatch, and
the funding-flow rework. All of that is now permanently covered:
every funded test exercises the same setup path and then asserts on it,
and orchard_miner_coinbase_distribution asserts the coinbase model
explicitly. What remained was ~70s of assertion-free LocalNet runtime
per run and one extra exposure to the launch port flake.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…et suite

create_send_to_self_with_zfz_active asserts value-transfer KIND
classification: a self-send yields SendToSelf(Basic), and the
Zennies-for-Zingo output yields Sent(Send) addressed to the ZFZ
address. Both are pure summary derivation over a transaction record —
one fabricated SendToSelf transaction whose outgoing orchard notes are
(a) a note to the wallet's own orchard receiver and (b) a note carrying
the decoded ZFZ regtest unified address. 72s of LocalNet becomes 14ms.

Deliberately NOT claimed by this port: the propose_send_all ->
send_stored_proposal execution the integration test drove incidentally;
that pipeline remains covered by the chain-bound send tests
(send_to_tex and the funded-send family).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
simpool_insufficient_1_sapling_to_orchard flaked with "status regression
to Transmitted" in the with_assertions confirmation poll. It cannot be a
regression: pepper-sync's update_status is monotonic. An observed
Transmitted means the wallet has not yet seen the tx anywhere — under
instant regtest mining a tx is often mined before the mempool monitor
observes it (status path Transmitted -> Confirmed, skipping Mempool),
and the check can land in the measured ~500ms Indexer-lag window before
the confirming block is scanned.

Treat Transmitted like Mempool: keep polling. The existing patience
bound converts a persistent Transmitted (a genuine broadcast failure)
into a test failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Net suite

spendable_balance_includes_notes_in_incomplete_shards asserts that a
confirmed, positioned note whose block has no completed orchard shard
still counts as spendable — spendable-balance composition over wallet
state, no chain required. 70s of LocalNet becomes 17ms, and the port is
stricter than the original: the integration test only obtained an
incomplete shard incidentally from regtest's tiny tree, while the unit
test constructs the condition explicitly (empty orchard shard ranges,
fully-scanned birthday..tip, sapling tip checkpoint for the anchor).

Rig additions: pepper-sync's WalletNote::new_for_test gains a position
parameter, and SyncState::new_for_test (test-features) builds sync
state from explicit scan ranges — unlocking future spendability and
witness-gating tests without a chain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zancas and others added 27 commits July 6, 2026 23:13
The two chain-generics proptests ran a single random-valued case each
(Config::with_cases(1)), and their parameter ranges reduced to a
sapling-only source with a transparent-or-sapling receiver. Coverage
therefore differed between runs, failures were not reproducible without
the seed, and the orchard rows that the "any source, any receiver" name
promised never ran at all.

The pool_matrix module enumerates every source and receiver pair with
fixed values, once with change and once without, plus minimum-value and
boundary-value entries on the sapling-to-transparent pair: fourteen
named tests over the unchanged fixture, parallelized by nextest where
the proptests serialized their work inside two tests.

The new coverage found a real defect on its first run. Both
orchard-source rows are rejected by zebra's mempool with "could not
validate orchard proof ... until the next chain tip block" during the
FUNDING send, while the equivalent sapling rows pass. Two fixes were
attempted and rejected: separating the spend from the tip inside the
fixture is undone by the assured-send helper's own pre-propose sync,
and mining a separation block inside the helper without re-syncing
makes the wallet build for the wrong target height whenever the next
block crosses an activation boundary ("transaction uses an incorrect
consensus branch id" at the height-five co-activation these tests
straddle). The orchard rows are left red deliberately: they document
the defect the matrix exists to catch, and the candidate fixes (a
minimum-confirmations wallet policy for tests, or a zebra-side answer
to why orchard proofs anchored at the tip are rejected where sapling
spends are not) deserve their own change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Since the deterministic pool matrix replaced the proptest module,
nothing in libtonode-tests references the proptest crate. The optional
dependency, its feature mapping, and the cargo-shear ignore entry are
removed. The proptests feature itself survives as a pure cfg gate for
the pool_matrix module, so no CI or makers filter that names it breaks.
The workspace-level proptest dependency remains for darkside-tests,
which still runs a live proptest block in its chain-generics module.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
t_incoming_t_outgoing_disallowed, sapling_incoming_sapling_outgoing,
and sapling_to_sapling_scan_together assert wallet-record and policy
logic: summary contents, balance composition, spend-status views, and
the shield-first refusal of transparent spends. Each now runs in
milliseconds against fabricated records. The transparent policy test
lives in zingolib/src/lightclient/send.rs on the builder's new
transparent_coin extension; the two sapling tests live in
zingolib/src/wallet/summary.rs on the record rig, walking a note
through received, pending-spent, and confirmed-spent states by
mutating fabricated records between assertion blocks.

One deliberate reduction: the scan_together port does not mark the
funding note spent, because summary derivation validates spend links
against the spending transaction's actual bundle nullifiers, which
fabricated empty-bundle transactions cannot satisfy — and the original
never asserted spend status. The companion test covers spend-status
views through interfaces that read the records directly.

Rig additions: TransparentCoin::new_for_test plus outgoing-sapling and
transparent-coin attachments on WalletTransaction in pepper-sync, and
SyntheticWalletBuilder::transparent_coin, which completes the builder's
coverage of all three pools.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The slow module's OutgoingNoteInterface import lost its last user when
sapling_incoming_sapling_outgoing moved to zingolib. cargo fix missed it
because the warning only surfaces in the test profile the container
builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tip_spend_rejection suite formats the root-cause search for the
pool_matrix orchard-row failures as two rounds of single-predicate
experimental cells, each varying one factor with the hypothesis table
and observed verdicts recorded in the module documentation.

Round one (tall funded-faucet chain, single non-coinbase orchard note,
note depth and output pool varied independently, anchor held at the
tip): all four cells ACCEPTED, falsifying the tip-note, tip-anchor, and
orchard-output hypotheses in that environment. Round two (the exact
chain-generics environment: short chain near the height-5 NU6.1/6.2
co-activation, miner faucet holding only young orchard coinbase notes):
an orchard-output send is REJECTED ("could not validate orchard proof
... until the next chain tip block"), the identical sapling-output send
is ACCEPTED — eliminating note selection for good, since sender,
amounts, and funds are identical — and ten extra blocks cure the
orchard rejection, eliminating coinbase youth.

Conclusion: zebra rejects orchard-OUTPUT transactions built adjacent to
the activation boundary and accepts sapling equivalents; distance from
the boundary is the operative variable. Accordingly, the simpool
fixture now mines five blocks past the boundary before funding, and
every pool_matrix row passes, including the two formerly-red orchard
rows. The zebra-side mechanism of the differential remains open for an
upstream issue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t rule

The pool_matrix orchard-source cases failed at broadcast with "could
not validate orchard proof": fund_client_orchard confirms the sender's
note at the chain tip, and the send fixtures proposed immediately, so
the spend consumed a tip-block note — the same zebra mempool constraint
the scenario offload hit earlier. The shared assertion helpers
(assure_propose_send_bump_sync_all_recipients and
assure_propose_shield_bump_sync) now mine one separation block and sync
the sender to the real tip through the ConductChain seam before
proposing, so every fixture inherits the fix, including chained sends
where each round's change note would otherwise sit in the tip block.

sapling_sends_to_transparent_minimum_value failed with "transaction is
dust": zcashd's regtest relayed non-standard transactions, so the case
could send one zatoshi; zebra enforces its mempool dust rule everywhere.
The minimum transparent receiver value is now the canonical 546-zatoshi
dust threshold, with the divergence documented at the case definition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
send_to_transparent_and_sapling_maintain_balance is kept deliberately as
the one end-to-end summary-shape test, so its fixtures must track what
the real send pipeline produces. The second-wave funding receipt's fee
is paid by the faucet, whose note pool is fragmented by the earlier
waves under the Core-stack funding model; with smallest-first note
selection the send is a four-logical-action transaction, so the
observed zip317 fee is 20_000 where the monolithic zcashd-era funding
produced 10_000. The summary-equality helper already ignores txids,
datetimes, and note output indices, so the fee was the only real
mismatch in the observed failure.

The remaining wave fixtures carry fees shaped by the same flow; the
next server run adjudicates whether any of them drifted as well.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ress

mine_to_sapling asserted balances from mining into the sapling pool.
The zcash_local_net zebrad validator supports only Orchard and
Transparent miner pools and panics on a Sapling request, and zebra has
no plans to grow sapling mining; the test had been ignored on exactly
that ground and could never run again. Orchard mining balance coverage
lives on in mine_to_orchard.

send_heartwood_sapling_funds was doubly inexpressible: it also mined to
sapling, and its custom activation ladder placed Canopy at height 3,
which the harness's zebrad config writer refuses because the emitted
regtest config hardcodes Canopy at height 1.

sync_all_epochs (already ignored as hanging when syncing pre-sapling)
staggered every upgrade from Sapling at height 3 through NU6.2 at
height 17 — a shape the config writer cannot express for the same
Canopy constraint, and one the harness's subsidy fixtures could not
fund. Its purpose survives in sync_all_expressible_epochs, which
crosses every epoch boundary the Core stack can represent.

All three premises are Legacy-stack-shaped: they describe chains only
zcashd could produce. Deleting them records that judgment rather than
leaving ignores that imply future work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both the fast and slow modules imported ActivationHeights solely for
the tests deleted in the previous commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nothing in this repository consumes the zcashd or zcash-cli binaries
anymore. The zebrad port removed every zcashd-backed network combo from
the test suites, and the last tests whose premises required a
zcashd-shaped chain were deleted in the preceding commit. Unlike
lightwalletd, which the darkside suite still launches, zcashd's
provisioning was pure dead weight: an image-build stage pulling the
zodlinc/zcash image, two binary copies, a version label, a build
argument, two symlinks, and a version pin that participated in the
container image tag.

This removes the zcashd downloader stage, ARGs, LABEL, and COPY lines
from docker-ci/Dockerfile, the ZCASH_VERSION tag-string component, echo,
build argument, and the zcashd/zcash-cli symlinks from Makefile.toml,
and the ZCASH_VERSION pin from .env.testing-artifacts. The image tag
therefore changes, forcing a one-time CI image rebuild that also picks
up the zainod 0.6.0-rc.1-no-tls bump. lightwalletd remains provisioned
until the darkside suite has a zaino-based replacement or is retired —
the tracked precondition for removing the remainder of the Legacy
stack.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Until now no test separated the indexer's mempool view from the
validator's; the nearest discriminator was the block-height tip-lag
probe. This suite uses zainod's send_transaction as the validator-side
event marker (it relays zebra's own acceptance verdict, so quick_send
resolving Ok means the transaction is in zebra's mempool) and measures
the two remaining legs with one cell each.

Verdicts, recorded in the module documentation: zainod's GetMempoolTx
shows the transaction about fifty MILLISECONDS after zebra accepts it,
falsifying the earlier attribution of multi-second waits to the
indexer's polling cadence (the claim in the poll-not-sleep commit
message is corrected here). The sender's wallet record reaches Mempool
status at the exact microsecond sync_and_await returns, and never
reaches it without a sync session, because pepper-sync's mempool
monitor exists only inside a sync session's lifecycle. The seconds
always belonged to the sync session itself, and the assured-send
helper's old fixed sleep was pure waste.

The rejection-side attribution cells for the boundary-adjacent
orchard-output phenomenon are designed in the module documentation and
blocked on two named seams: an rpc_listen_port accessor on the running
Zebrad (infrastructure side) for direct-submission verdict parity and
same-bytes resubmission, and a build-without-broadcast test-features
seam in zingolib's send path for offline orchard-proof verification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The GitHub workflows still symlinked zcashd and zcash-cli out of the CI
image and omitted zainod and zebrad, which the container-test flow has
provided via Makefile.toml since the zebrad port. Once the image
rebuilds without zcashd those lines would create dangling symlinks. The
workflows now link exactly the surviving binary set: lightwalletd (for
darkside), zainod, and zebrad. The docker-ci README's version-bump note
follows suit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ra rev

Advances the zcash_local_net and zingo_test_vectors pins to
infrastructure 34fce91, which allocates harness listener ports from a
fixed band below every default ephemeral range (partitioned per process
and bind-checked) and adds LocalNet::await_indexer_convergence plus
generate_blocks_converged. The port change removes the launch-time
RpcReadinessTimeout flake at its root: ports picked from the kernel's
ephemeral range could be reused as outgoing source ports in the window
between picking and the child's bind. The convergence barrier gives
callers a way to wait for the Indexer to serve the Validator's tip
instead of compensating wallet-side.

The scenario helpers' adoption of the barrier is in flight in a
parallel session; this pin unblocks it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The infrastructure rev at 34fce91 (pinned in the previous commit)
brings LocalNet::await_indexer_convergence and
generate_blocks_converged (infra commit fa2da0b) — the harness-level
retirement of the indexer tip-lag race this repo measured at 100-500ms
in ten of ten rounds and has been closing wallet-side ever since.

The barrier is defined only for zainod, whose "Syncing block" log line
is the observation channel, so the scenario layer gains a small
IndexerConvergence dispatch trait: the zainod implementation calls the
harness barrier, and the lightwalletd implementation is a no-op behind
the test_lwd_zebrad feature, where the callers' wallet-side height
polling remains the functional guarantee. The two wait helpers
(increase_height_and_wait_for_client, sync_client_to_validator_tip)
converge the indexer before syncing, so their height polls complete on
the first pass instead of spinning against a lagging indexer; the
bound propagates to send_and_bump, zebrad_shielded_funds, and
send_value_between_clients_and_sync. The tip_spend_rejection module
doc also restructures two footnotes that tripped clippy's
doc-list-indentation lint.

Verified on both feature combos plus clippy, and live on the host:
unified_address_discovery drops from about 75 to 38 seconds, and the
mempool attribution cell passes unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ignored diagnostic indexer_tip_lags_validator_after_block_generation
existed to document that raw generate_blocks returns before the Indexer
serves the new tip (measured 10/10 rounds, max 475ms). With
zcash_local_net's await_indexer_convergence landed and the scenario
helpers routing through it, the meaningful assertion inverts: the test
is renamed indexer_converges_with_validator_after_block_generation,
loses its ignore, and asserts that after the barrier the Indexer's
served tip matches the Validator's in every one of ten rounds. If the
barrier regresses, this is the test that goes red.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`makers test all` previously gated its three phases, stopping at the
first failure — fast feedback, but a red run reported only the phase
that broke, and three separate nextest summaries scrolled past with no
combined accounting. Zaino solved the same problem with a small Rust
runner (tools/test-runner's live-summary), and this adopts that design:
a workbench binary, test-summary, runs the packages, zingo-cli, and
libtonode phases in order — all of them, even when an earlier phase
fails, so the summary reflects the whole suite — streams each phase's
output live while capturing it, parses each nextest Summary line with
ANSI stripped, and prints a per-phase table with totals:

  ====================== test summary ==========================
    packages:     107 run,  107 passed,    0 failed,    0 skipped
    zingo-cli:     77 run,   77 passed,    0 failed,    0 skipped
    libtonode:     65 run,   64 passed,    1 failed,    0 skipped
    TOTAL:        249 run,  248 passed,    1 failed,    0 skipped
  ==============================================================

A phase that exits non-zero without producing a summary line is called
out as a probable build failure so its zero row is not read as clean,
and the front door exits non-zero if any phase failed. The runner sets
the empty ZINGOLIB_NEXTEST_FILTER itself, preserving `all`'s
slow-tests-included semantics. The parser carries zaino's unit tests
forward; the set-validation error path was exercised directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The running Zebrad already exposes rpc_listen_port via the copy_getters
macro; the module doc claimed an infrastructure accessor was missing
because a grep for literal pub fn missed the macro-generated getter.
The only remaining blocker for the rejection-side attribution cells is
the zingolib build-without-broadcast seam.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restore the stop-on-failure behavior the front door had before the
summary runner: a failing packages or zingo-cli phase stops the later,
more expensive phases from launching, so a broken unit test never costs
a libtonode run. The combined summary table still prints, with the
phases a failure prevented from running marked "not run (an earlier
phase failed)" so the accounting stays honest about what executed. This
is a deliberate divergence from zaino's runner, which executes all
partitions unconditionally; the doc comment records it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tracing_error_from_pepper_sync_goes_to_log_file spent a fixed twelve
seconds per run, and the diagnosis found the comment blaming the wrong
mechanism. The test spawns its mock gRPC server on the test's own tokio
runtime, whose default flavor is current-thread; the fixed
std::thread::sleep then blocked that single thread, so the server was
never polled, the child's get_latest_block hung unanswered, and the
logged ERROR was the child's ten-second client-side transport timeout —
not the mock's configured DEADLINE_EXCEEDED, which never got to be
served. The multi_thread runtime flavor lets the mock actually answer,
the error lands sub-second, and an event-driven poll on the log file
(with a twenty-second ceiling) replaces the fixed sleep. The assertions
are unchanged; the guarantee — a pepper_sync fetch ERROR reaches the
log file and not stderr — is what it always was.

interactive_mode_redirects_tracing_to_log_file had the same
fixed-interval shape without the runtime bug; it now polls for the
startup INFO lines the assertion needs. The pair drops from about
fifteen seconds to about two.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round three of the discrimination suite runs the decisive same-bytes
experiments. No new wallet seam was needed: transmission failure leaves
the built Transaction in the wallet's Failed record, so the exact bytes
zebra judged are recoverable, and the running Zebrad already exposes
its JSON-RPC port. zingolib_testutils gains a validator_rpc module
(sendrawtransaction and getrawmempool probes that bypass the indexer,
where a mempool rejection is data rather than an error), and the
mempool-lag cell upgrades its acceptance-marker inference to a direct
getrawmempool observation.

The verdicts, pinned by boundary_rejection_attribution: direct
submission at the boundary reproduces the rejection, so zainod relays
zebra's verdict faithfully; and the IDENTICAL bytes resubmitted after
five blocks of distance are rejected as "transaction uses an incorrect
consensus branch id". The wallet builds boundary-adjacent transactions
under the pre-activation branch id — zebra judged correctly at both
heights, and its transient "until the next chain tip block" framing
(plus the orchard-proof surface of a wrong-branch-id context) was the
only misdirection. The culprit is the wallet-side builder's
target-height and branch-id selection when the next block is an
activation block: a zingolib/librustzcash concern, not a zebra or
zainod one. The residual open question, recorded in the module doc, is
why the sapling-output twin built at the same height carries a valid
branch id.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The portability sweep's A and B verdicts, executed. Three summary-logic
tests move to zingolib/src/wallet/summary.rs on the record rig:
send_funds_to_all_pools (per-pool balance aggregation over one
confirmed note per pool), by_address_finsight (memo-bytes-per-address
over outgoing notes), and value_transfers (four-memo aggregation into
one transfer plus idempotence and sort stability, renamed
value_transfers_aggregation_and_ordering). Four proposal-shape tests
move to a new proposal_shape module in zingolib/src/lightclient/propose.rs:
shield_transparent (formerly ignored in libtonode and asserting nothing;
the offline proposal now asserts the step, input, and change shape),
four_coin_shield_proposal_shape (from
mine_to_transparent_and_propose_shielding — all five original
assertions were proposal-shape; fabricated coins propose identically to
mined ones since pepper-sync carries no coinbase marker), and the
load-bearing halves of the chain_generics ignore_dust_inputs and
note_selection_order fixtures (dust exclusion and
minimal-change covering selection, asserted directly on the proposal's
selected inputs instead of on confirmed records). The two fixtures'
remaining lifecycle wrappers restated what the pool_matrix family
already covers, so the fixtures and their instantiations are deleted
along with the four concrete.rs originals and shield_transparent.rs.

The shield ports exposed a builder bug worth having found: fabricated
transparent coins carried an empty script, and
WalletTransparentOutput::from_parts silently drops coins whose script
does not parse back to an address, so shield proposals saw no funds.
TransparentCoin::new_for_test now demands a script and the builder
derives the real P2PKH script from the wallet's own address.

One analysis correction is recorded rather than executed:
verify_old_wallet_uses_server_height_in_send was assessed as portable
via a mock indexer, but the send path never fetches server height —
transaction creation reads the wallet's own sync state through
WalletRead. Its remaining protection is that zebra accepts a broadcast
from a wallet two blocks behind the tip, which is inherently e2e, so it
stays in libtonode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The attribution triangle measured a ~2.5 second floor on sync_and_await
against a ten-block regtest chain, reproducible to the millisecond —
deterministic structure, not workload. Instrumenting the session found
the composition: the engine's own work ends at ~1.4s, after which the
caller paid a full second for the mempool monitor to notice the
shutdown flag on its one-second tick (sync() joins that task), plus up
to half a second of completion-detection quantization in await_sync's
500ms poll.

Three changes, none touching what is checked — only how often: the
scanner-shutdown wait polls the drain condition at 25ms with the old
one-second ceiling as worst case instead of sleeping the second
unconditionally; the mempool monitor's shutdown tick drops from one
second to 50ms (it only bounds flag-notice latency, and it sits on
every session's join path); and await_sync polls at 50ms instead of
500ms.

Measured: sync_and_await on the attribution cell drops from 2.502s to
1.302s, and unified_address_discovery from 38 to 31 seconds (75s two
days ago). Tests average five to seven syncs each, so the saving
compounds across the whole live suite. pepper-sync and zingolib unit
suites pass (114/114).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every libtonode and zingo-cli test launches a private zebrad and zainod
pair, and unbounded parallelism oversubscribes the container: measured
stretch on individual LocalNet tests is five to seven times their solo
wall-clock once roughly forty node pairs contend for cores. A nextest
test group now caps those two packages at eight concurrent tests, on
both the default and ci profiles; unit tests keep full parallelism.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cargo silently ignores --package when --workspace is also present, so
`makers test packages -p FOO` ran the entire packages set while
appearing to scope to one package. Rather than patch the merge logic,
this rebuilds the front door on cargo's own semantics.

`makers test "$@"` is now a containerized `cargo nextest run "$@"`:
package selections, positional test-name filters, and arbitrary flags
all pass through untouched. Two reserved first words remain as canned
invocations: `packages` (the workspace minus the live packages) and
`live` (only libtonode-tests and zingo-cli). Explicit package-selection
flags replace a reserved word's scope entirely, because merging them is
exactly the trap the silent-ignore behavior sets. The `libtonode`,
`zingo-cli`, and `all` words are removed; the first two are spelled
`-p`, and the three-phase gated suite is invoked directly as
`cargo run -q --manifest-path tools/workbench/Cargo.toml --bin
test-summary`, whose phases now select their scope with `-p` instead of
the removed reserved words.

The default `not test(slow)` filter is removed, along with the whole
ZINGOLIB_NEXTEST_FILTER override mechanism it existed to serve;
filtering is spelled `-E '...'` per nextest idiom, so local runs now
include the slow:: tests by default. PR CI is unaffected because
ci-pr.yaml already passes its filter explicitly. The README testing
section is updated to match, including its stale claim of two default
retries (the default is zero).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pend observations

A note's spending_transaction field is the wallet's only durable record of an
on-chain spend. Spend detection consumes the corresponding nullifier-map entry,
cleanup prunes the map behind the fully-scanned frontier, and scanned ranges are
never revisited, so once a spending transaction's block has been scanned the
field cannot be reconstructed by any forward sync. set_transactions_failed
nevertheless permitted the Confirmed -> Failed transition and unconditionally
reset those fields, converting spent notes into permanent phantom unspent notes:
every subsequent proposal selected them and was rejected by the validator as an
orchard double-spend. This failure mode was observed in the wild as a duplicate
nullifier rejection reporting "in finalized state: true".

set_transactions_failed now refuses txids whose record is Confirmed and logs a
warning naming the refused txid. A mined transaction cannot fail; the only
protocol event that un-mines it is a reorg, and the reorg path retains the old
behavior through the new pub(crate) set_transactions_failed_unchecked, which is
sound there because truncation simultaneously reopens the affected scan ranges
and thereby guarantees re-detection.

The damage was permanent exactly when sync had raced ahead of the spending
transaction, which is exactly when its record is Confirmed, so the guard is
coextensive with the harm. Three new tests in sync::test::spend_reset_lifecycle
pin the lifecycle: failing_a_confirmed_transaction_must_not_destroy_the_spend_observation
asserts the enforced invariant, reset_before_scan_heals_on_spend_detection pins
the self-healing pre-scan sequence, and scanned_transaction_overwrites_failed_record
pins the record replacement that completes the healing. In support, the
test-features constructor gates in wallet.rs widen to any(test, feature) so
in-crate unit tests can use them, and the mock wallet implements the previously
todo!() SyncNullifiers::get_nullifiers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the term for a note the wallet offers as spendable although its nullifier
is already on chain, and distinguish it from a pending-spent note, whose spend
the wallet knows about and correctly excludes from selection. The term names
the failure mode fixed in pepper-sync's set_transactions_failed guard and keeps
the discussion from overloading "stale" or "stuck".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`makers test` became a flat nextest passthrough, so a failing unit test
no longer prevents the expensive live suites from running; that gating
lives only in the workbench test-summary runner, which lost its makers
front door when the `all` reserved word was removed. This restores a
front door: `makers hierarchy-test` is a pure delegation to
`cargo run -q --manifest-path tools/workbench/Cargo.toml --bin
test-summary`, forwarding extra args with cargo-make's `${@}` splat.
The help text now names the task instead of spelling out the cargo
invocation, so the incantation is written in exactly one place.

The runner itself now rejects cargo package-selection args (exit 2)
before launching any phase. Forwarded args reach every phase's nextest
invocation, where a package selection would silently replace all three
phase scopes with the same one — the same class of silent mis-scoping
the passthrough rework removed from `makers test`. The new
is_package_selection_arg mirrors the has_package_selection_args case
list in Makefile.toml's base-script, and unit tests pin both the
rejected flag forms and the ordinary nextest args that must pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@zancas zancas marked this pull request as draft July 8, 2026 02:27
@zancas zancas closed this Jul 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant