Skip to content

test(seed): cover seed reconcile against a live manager - #500

Merged
bdchatham merged 1 commit into
mainfrom
test/seed-reconcile-envtest
Jul 27, 2026
Merged

test(seed): cover seed reconcile against a live manager#500
bdchatham merged 1 commit into
mainfrom
test/seed-reconcile-envtest

Conversation

@bdchatham

Copy link
Copy Markdown
Collaborator

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 (required nodeKey, exactly-one, secretName immutability) and nothing else.

Reconcile-level coverage lives in seinetwork/envtest, which despite the name starts a real manager with both SeiNodeReconciler and SeiNetworkReconciler (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:

Test Guards against
ReconcilesToSeedShapedStatefulSet cosmos-exporter blocking forever on a gRPC port a seed never binds; /lag_status readiness that can never answer, so the pod never reaches Ready; missing GOMEMLIMIT on a request==limit footprint
MountsNodeKeyFromSecret an identity left to seid init, which regenerates on the data volume and breaks every client holding the published NodeID. Also pins subPath + read-only, so a Secret edit cannot swap the identity under a running seid
SchedulesOnTheSeedNodepool a seed inheriting the RPC-class default pool, which karpenter.sh/do-not-disrupt then pins. Asserts the fixture's seed pool differs from the default, so the test cannot pass vacuously
TakesGenesisProgressionAndIsNotStateSyncGated validate-node-key and configure-genesis present, snapshot-restore and configure-state-sync absent, and StateSyncReady resolving False/NotApplicable rather than staying absent per the conditions doctrine

The 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 servesSeidRPC to return true — treating a seed as an RPC-serving node — fails TestSeed_ReconcilesToSeedShapedStatefulSet on the cosmos-exporter assertion, with the container's until (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/envtest 70s, seinetwork/envtest 120s, nodetask/envtest 11s. Tests only — no source changes.

🤖 Generated with Claude Code

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>
@cursor

cursor Bot commented Jul 27, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Test-only change with no production code paths modified.

Overview
Adds envtest reconcile tests for seed SeiNode resources under internal/controller/seinetwork/envtest, where a real controller manager runs (unlike seed admission tests in node/envtest, which only exercise CEL against the apiserver).

The new suite creates seed nodes and waits for convergence, then asserts runtime-critical outcomes: a single seid container with no cosmos-exporter, TCP P2P readiness on 26656 instead of RPC HTTP probes, GOMEMLIMIT set, node-key Secret volume mount with read-only subPath, scheduling on the dedicated seed nodepool (affinity/tolerations), and a genesis-only plan (validate-node-key, configure-genesis; no snapshot/state-sync tasks) with StateSyncReady reported False / NotApplicable.

Reviewed by Cursor Bugbot for commit e49816f. Bugbot is set up for automated code reviews on this repo. Configure here.

@bdchatham
bdchatham merged commit 92850d5 into main Jul 27, 2026
5 checks passed
bdchatham added a commit to sei-protocol/seictl that referenced this pull request Jul 27, 2026
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>
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