test(seed): cover seed reconcile against a live manager - #500
Conversation
The seed admission tests in node/envtest pin the CEL contract but that suite runs no reconciler, so nothing verified that a seed converges to a StatefulSet it can actually run under. These run in seinetwork/envtest, which starts a real manager with SeiNodeReconciler. Each assertion is a way seid would fail: cosmos-exporter blocks on a gRPC port a seed never binds; /lag_status readiness can never answer, so the pod would never reach Ready; an identity left to seid init regenerates on the data volume and breaks every client holding the published NodeID; and the RPC-class default pool costs an order of magnitude more than a seed, pinned there by do-not-disrupt. The plan test is the first coverage of a seed's progression against a live reconciler rather than a planner call, including StateSyncReady resolving NotApplicable rather than staying absent. Verified by mutation: servesSeidRPC returning true fails TestSeed_ReconcilesToSeedShapedStatefulSet on the cosmos-exporter assertion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR SummaryLow Risk Overview The new suite creates seed nodes and waits for convergence, then asserts runtime-critical outcomes: a single Reviewed by Cursor Bugbot for commit e49816f. Bugbot is set up for automated code reviews on this repo. Configure here. |
Patch bump `version.json` v0.0.68 → v0.0.69 to cut a release. On merge, the Releaser workflow tags and publishes v0.0.69, and Containerize pushes `ghcr.io/sei-protocol/seictl:v0.0.69` for amd64/arm64. ## Includes since v0.0.68 - **#240** — sei-config v0.0.23 → v0.0.24, which fixes seed mode's config defaults. ## Why this cut matters more than a usual dependency bump **The sidecar resolves `ConfigIntent`, so the sidecar's sei-config version — not the controller's — is what a node actually boots on.** Seed support in [sei-k8s-controller#499](sei-protocol/sei-k8s-controller#499) / [#500](sei-protocol/sei-k8s-controller#500) is merged and released, but it stays inert until this image ships. The failure mode if it doesn't is quiet. On sei-config v0.0.23, seed mode inherits the base default of a **loopback** P2P listen address, so a seed binds `127.0.0.1:26656` and is reachable by nobody — while booting clean, passing its TCP readiness probe against localhost, and reporting healthy. A seed that no peer can dial looks identical to a working one from inside the cluster. v0.0.24 also disables the listeners a seed has no business running (REST, gRPC, gRPC-web, EVM HTTP+WS), sets the tx indexer to `null`, turns off StateStore with pruning everything, and adds a validation guard rejecting seed mode with pex disabled — a seed exists to exchange peer addresses, and seid will not start without it. ## Sidecar code changes **None.** The bump carries only `go.mod`/`go.sum` plus two tests (`sidecar/tasks/config_apply_test.go`) asserting a seed resolves to a reachable listen address and that a pex-disabled seed is rejected. That the sidecar needed no changes to support an entirely new node mode is the mode-config layering working as designed: the controller emits `{Mode, Overrides}`, the sidecar calls `ResolveIntent`, and sei-config owns every per-mode default. ## Verification `go build ./...` clean; `go test ./...` green across all 16 packages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to #499, closing a coverage gap I flagged when it merged.
The gap
The seed admission tests added in #499 live in
internal/controller/node/envtest, which runs no reconciler — it's a client against a CRD-loaded apiserver. They pin the CEL contract (requirednodeKey, exactly-one,secretNameimmutability) and nothing else.Reconcile-level coverage lives in
seinetwork/envtest, which despite the name starts a real manager with bothSeiNodeReconcilerandSeiNetworkReconciler(suite_test.go:140,172,196). There was no seed equivalent there, so nothing verified that a seed converges to a StatefulSet it can actually run under.What's covered
Every assertion is a concrete way seid would fail, not a shape restatement:
ReconcilesToSeedShapedStatefulSet/lag_statusreadiness that can never answer, so the pod never reaches Ready; missingGOMEMLIMITon a request==limit footprintMountsNodeKeyFromSecretseid init, which regenerates on the data volume and breaks every client holding the published NodeID. Also pinssubPath+ read-only, so a Secret edit cannot swap the identity under a running seidSchedulesOnTheSeedNodepoolkarpenter.sh/do-not-disruptthen pins. Asserts the fixture's seed pool differs from the default, so the test cannot pass vacuouslyTakesGenesisProgressionAndIsNotStateSyncGatedvalidate-node-keyandconfigure-genesispresent,snapshot-restoreandconfigure-state-syncabsent, andStateSyncReadyresolvingFalse/NotApplicablerather than staying absent per the conditions doctrineThe last is the first coverage of a seed's plan against a live reconciler rather than a direct planner call.
Verified by mutation, not just green
Mutating
servesSeidRPCtoreturn true— treating a seed as an RPC-serving node — failsTestSeed_ReconcilesToSeedShapedStatefulSeton the cosmos-exporter assertion, with the container'suntil (exec 3<>/dev/tcp/localhost/9090)wait loop right there in the diff output. That is the exact crash-loop the predicate prevents.(First mutation attempt was a bad one — I disabled cosmos-exporter for all modes rather than enabling it for seed, and the test correctly still passed. Worth noting because a mutation that doesn't fail isn't automatically evidence the test is weak.)
Test
Full integration suite green:
node/envtest70s,seinetwork/envtest120s,nodetask/envtest11s. Tests only — no source changes.🤖 Generated with Claude Code