Skip to content

test(discovery): kind-based real-cluster e2e for multi-port discovery - #2063

Merged
slin1237 merged 1 commit into
mainfrom
test/discovery-kind-e2e
Aug 6, 2026
Merged

test(discovery): kind-based real-cluster e2e for multi-port discovery#2063
slin1237 merged 1 commit into
mainfrom
test/discovery-kind-e2e

Conversation

@slin1237

@slin1237 slin1237 commented Aug 5, 2026

Copy link
Copy Markdown
Member

Stacked on #2062 (which stacks on #2061).

Description

Real-cluster coverage for the layers the fake-API-server tests can't reach: RBAC/kubeconfig auth, real watch bookmarks and relists, kubelet readiness transitions, and actual graceful/force deletion semantics.

  • e2e_test/kind_discovery/manifests.yaml — two hostNetwork pods on a kind node: multi-engine-0 runs four HTTP engine servers from one container (the exact multiple-servers-per-pod deployment shape) listed in smg.ai/worker-ports: "28080,28081,28082,28083"; single-engine-0 has no annotation and exercises the --service-discovery-port fallback. The mock engine is a stdlib-only python script shipped via ConfigMap — no image builds.
  • e2e_test/kind_discovery/run.sh — creates the cluster, waits for readiness, starts the smg binary on the runner host against the kind kubeconfig, then asserts via GET /workers: 5 workers (4 + 1 fallback), all carrying smg.ai/pod-uid, exactly 4 labeled smg.ai/pod-name=multi-engine-0; graceful kubectl delete → 4 workers; --grace-period=0 --force → 0. Dumps the smg log tail and pod state on failure; always tears the cluster down.
  • .github/workflows/e2e-kind-discovery.ymlworkflow_dispatch only with a runner input defaulting to ubuntu-latest (GitHub-hosted has Docker, so this runs today); flip the input to the self-hosted pool once dind availability there is confirmed. Not a merge gate.

hostNetwork is what makes the host-side smg able to probe worker ports at the pod IP (= node container IP, routable on Linux runners) — and it also mirrors the real multi-engine GPU-pod deployment pattern.

Validation

Runtime validation needs a Linux Docker host, so this is dispatch-validated on CI rather than locally (local Docker daemon unavailable, and macOS cannot route to kind node IPs regardless). Locally verified: bash -n on the script, workflow + manifest YAML parse, and the embedded multi-port engine server executed standalone — all four ports served /health and /get_model_info correctly. The full discovery pipeline itself is already covered end-to-end by the deterministic fake-API-server tests in #2062; this PR adds the real-cluster plumbing on top.

Suggested first run: gh workflow run e2e-kind-discovery.yml once this merges (or dispatch from this branch).

@github-actions github-actions Bot added ci CI/CD configuration changes tests Test changes labels Aug 5, 2026
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Tests

    • Added opt-in end-to-end testing for Kubernetes service discovery using local kind clusters.
    • Coverage includes engine registration, readiness changes, scaling, pod lifecycle events, namespace scoping, request routing, gateway recovery, and graceful deletion.
    • Added in-cluster journey tests covering deployment, scaling, rollouts, and configuration diagnostics.
    • Added mock engine workloads supporting health checks, metadata, and chat completion scenarios.
  • Chores

    • Added automated workflow support for running the kind-based test suite.
    • Added container and Kubernetes test configurations for gateway and mock-worker scenarios.

Walkthrough

Adds opt-in Kind-based end-to-end tests for Kubernetes service discovery. The change defines mock workloads, local and in-cluster gateway fixtures, lifecycle coverage, fleet-mode coverage, and a manual GitHub Actions workflow.

Changes

Kind discovery E2E

Layer / File(s) Summary
Mock workloads
e2e_test/kind_discovery/Dockerfile.*, e2e_test/kind_discovery/manifests.yaml, e2e_test/kind_discovery/burst.yaml, e2e_test/kind_discovery/in_cluster.yaml
Adds mock engine images, HTTP server manifests, burst Pods, and in-cluster gateway and engine deployments.
Discovery test harness
e2e_test/kind_discovery/conftest.py, e2e_test/kind_discovery/test_in_cluster_journey.py, e2e_test/pyproject.toml
Creates Kind fixtures, starts gateways, provides worker and convergence helpers, and registers the opt-in kind marker.
Discovery lifecycle validation
e2e_test/kind_discovery/test_kind_discovery.py
Tests registration, namespace filtering, port reconciliation, readiness, restart recovery, Pod replacement, routing, and deletion.
In-cluster journey validation
e2e_test/kind_discovery/test_in_cluster_journey.py
Tests initial deployment, scaling, rollout replacement, stale-worker removal, and invalid annotation fallback.
Mock-worker fleet modes
e2e_test/kind_discovery/test_kind_discovery.py
Tests 40-port HTTP scaling, gRPC registration, and PD disaggregation selectors and bootstrap ports.
Workflow execution
.github/workflows/e2e-kind-discovery.yml
Adds a manual workflow that installs Kind and kubectl, builds binaries and images, and runs the Kind discovery suite with timeouts.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitHub Actions
  participant Kind
  participant Kubernetes
  participant smg
  participant MockEngine
  GitHub Actions->>Kind: Create cluster and apply manifests
  Kind->>Kubernetes: Start gateway and engine workloads
  GitHub Actions->>smg: Start discovery gateway
  smg->>Kubernetes: Watch annotated engine Pods
  smg->>MockEngine: Query worker endpoints
  smg-->>GitHub Actions: Route requests to discovered workers
Loading

Possibly related PRs

Suggested labels: model-gateway

Suggested reviewers: catherinesue, key4ng

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the kind-based real-cluster end-to-end testing for multi-port service discovery.
Description check ✅ Passed The description directly explains the kind-based real-cluster coverage, manifests, workflow, validation, and tested discovery behaviors.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/discovery-kind-e2e

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@claude

claude Bot commented Aug 5, 2026

Copy link
Copy Markdown

👋 The PR description doesn't fully follow
PULL_REQUEST_TEMPLATE.md:

  • Missing header: ### Problem
  • Missing header: ### Solution
  • Missing header: ## Changes
  • Missing header: ## Test Plan

Please update the PR description so reviewers have the context they need.

Comment on lines +27 to +30
curl -Lo kind "https://kind.sigs.k8s.io/dl/v0.29.0/kind-linux-amd64"
chmod +x kind && sudo mv kind /usr/local/bin/
curl -Lo kubectl "https://dl.k8s.io/release/v1.33.0/bin/linux/amd64/kubectl"
chmod +x kubectl && sudo mv kubectl /usr/local/bin/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Nit: Other workflows in this repo (e.g. nightly-triage.yml, engine-version-watch.yml) pin downloaded binaries with SHA256 checksums. These curl -Lo calls for kind and kubectl skip verification, which is inconsistent and leaves a supply-chain gap — a compromised CDN or DNS hijack could inject a tampered binary.

Consider adding checksum verification to match the existing repo pattern:

Suggested change
curl -Lo kind "https://kind.sigs.k8s.io/dl/v0.29.0/kind-linux-amd64"
chmod +x kind && sudo mv kind /usr/local/bin/
curl -Lo kubectl "https://dl.k8s.io/release/v1.33.0/bin/linux/amd64/kubectl"
chmod +x kubectl && sudo mv kubectl /usr/local/bin/
curl -Lo kind "https://kind.sigs.k8s.io/dl/v0.29.0/kind-linux-amd64"
echo "<sha256> kind" | sha256sum --check --quiet
chmod +x kind && sudo mv kind /usr/local/bin/
curl -Lo kubectl "https://dl.k8s.io/release/v1.33.0/bin/linux/amd64/kubectl"
echo "<sha256> kubectl" | sha256sum --check --quiet
chmod +x kubectl && sudo mv kubectl /usr/local/bin/

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean, well-structured e2e test. The kind cluster setup, mock engine server, hostNetwork approach, and assertion logic all look solid. Good cleanup trap with diagnostics on failure.

0 🔴 Important · 1 🟡 Nit · 0 🟣 Pre-existing

The one nit is about adding SHA256 checksum verification for the kind/kubectl downloads to match the pattern used in other workflows.

@slin1237
slin1237 force-pushed the test/discovery-kind-e2e branch from f58fe15 to 3096662 Compare August 5, 2026 21:32
@slin1237

slin1237 commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Validated end-to-end on a real cluster: temporarily added a branch push trigger (since workflow_dispatch only registers from the default branch), ran on GitHub-hosted ubuntu-latest, then removed the trigger and force-pushed the branch back to its single clean commit.

Run: https://github.com/smg-project/smg/actions/runs/31047416285 — success. Timeline from the log: kind cluster up in ~45s → both pods Ready → smg discovered 5 workers in ~4s (4 annotated ports + fallback) → ownership-label assertions passed → graceful delete converged in ~6s → force delete converged in ~2s → PASS. Total e2e step ~1 minute; whole job ~16 minutes (dominated by the Rust build).

@slin1237
slin1237 force-pushed the test/discovery-fake-apiserver-it branch from 9747100 to 1a898a3 Compare August 6, 2026 03:10
@slin1237
slin1237 requested a review from gongwei-130 as a code owner August 6, 2026 03:10
@slin1237
slin1237 force-pushed the test/discovery-kind-e2e branch from 3096662 to a353d62 Compare August 6, 2026 03:10
@slin1237
slin1237 force-pushed the test/discovery-fake-apiserver-it branch from 1a898a3 to 763101b Compare August 6, 2026 04:12
@slin1237
slin1237 force-pushed the test/discovery-kind-e2e branch from a353d62 to 3e9e386 Compare August 6, 2026 04:12
@slin1237
slin1237 force-pushed the test/discovery-fake-apiserver-it branch from 763101b to 596fde7 Compare August 6, 2026 04:19
@slin1237
slin1237 force-pushed the test/discovery-kind-e2e branch from 3e9e386 to fb746af Compare August 6, 2026 04:19
Base automatically changed from test/discovery-fake-apiserver-it to main August 6, 2026 04:32
@slin1237
slin1237 force-pushed the test/discovery-kind-e2e branch from fb746af to fc26a6c Compare August 6, 2026 04:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/e2e-kind-discovery.yml:
- Around line 6-9: Remove the push trigger and its test/discovery-kind-e2e
branch filter from the workflow, leaving only the workflow_dispatch trigger for
manual execution.

In `@e2e_test/kind_discovery/run.sh`:
- Around line 28-29: Update workers() to propagate curl failures instead of
converting them to an empty JSON object. In wait_for_count, capture the
worker_count result, detect a failed command, and reject immediately before
comparing the count; preserve normal count-based waiting when the query
succeeds.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ccbb9469-5ea5-4123-a531-792a9e884949

📥 Commits

Reviewing files that changed from the base of the PR and between 711ee70 and d76e40e.

📒 Files selected for processing (4)
  • .github/workflows/e2e-kind-discovery.yml
  • e2e_test/kind_discovery/burst.yaml
  • e2e_test/kind_discovery/manifests.yaml
  • e2e_test/kind_discovery/run.sh

Comment thread .github/workflows/e2e-kind-discovery.yml Outdated
Comment thread e2e_test/kind_discovery/run.sh Outdated
Comment on lines +28 to +29
workers() { curl -sf "http://127.0.0.1:${SMG_PORT}/workers" || echo '{}'; }
worker_count() { workers | jq '[(.workers // .)[]?] | length'; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🔴 Important Propagate /workers query failures.

Line 28 converts every HTTP failure into an empty worker set. A live smg process can still have no reachable /workers endpoint. In that case, wait_for_count 0 succeeds and the forced-deletion check reports a false pass.

Make workers fail on a curl error. In wait_for_count, capture and reject a failed worker_count before comparing the count.

Proposed fix
-workers() { curl -sf "http://127.0.0.1:${SMG_PORT}/workers" || echo '{}'; }
+workers() { curl -fsS "http://127.0.0.1:${SMG_PORT}/workers"; }

 wait_for_count() {
   local expect=$1 what=$2 deadline=$((SECONDS + 120))
   while ((SECONDS < deadline)); do
+    local actual
     # A dead gateway must never satisfy an expected-zero count.
     if [[ -n ${SMG_PID} ]] && ! kill -0 "${SMG_PID}" 2>/dev/null; then
       echo "smg process died while waiting for ${what}"
       return 1
     fi
-    if [[ "$(worker_count)" == "${expect}" ]]; then return 0; fi
+    if ! actual="$(worker_count)"; then
+      echo "could not query workers while waiting for ${what}"
+      return 1
+    fi
+    if [[ "${actual}" == "${expect}" ]]; then return 0; fi

As per coding guidelines, run the silent-failure-hunter agent to detect “swallowed errors, inappropriate fallbacks, and missing error propagation.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e_test/kind_discovery/run.sh` around lines 28 - 29, Update workers() to
propagate curl failures instead of converting them to an empty JSON object. In
wait_for_count, capture the worker_count result, detect a failed command, and
reject immediately before comparing the count; preserve normal count-based
waiting when the query succeeds.

Source: Coding guidelines

@github-actions github-actions Bot added the dependencies Dependency updates label Aug 6, 2026
def __init__(self, binary: Path):
self.binary = binary
self.base_url = f"http://127.0.0.1:{SMG_PORT}"
self.log_path = Path(tempfile.mkstemp(prefix="smg-kind-e2e-", suffix=".log")[1])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Nit: mkstemp returns (fd, name) — discarding [0] leaks the open file descriptor. Either close it explicitly or use NamedTemporaryFile(delete=False):

Suggested change
self.log_path = Path(tempfile.mkstemp(prefix="smg-kind-e2e-", suffix=".log")[1])
fd, path = tempfile.mkstemp(prefix="smg-kind-e2e-", suffix=".log")
os.close(fd)
self.log_path = Path(path)

self.proc: subprocess.Popen | None = None

def start(self) -> None:
log = open(self.log_path, "a")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Nit: The file handle opened here is never closed. After restart() (called by test_gateway_restart_rebuilds_registry), a second handle is opened to the same file while the first remains alive. Consider storing it on self and closing it in stop():

def start(self) -> None:
    self._log_fh = open(self.log_path, "a")
    self.proc = subprocess.Popen(..., stdout=self._log_fh, ...)

def stop(self) -> None:
    ...
    if hasattr(self, "_log_fh") and self._log_fh:
        self._log_fh.close()
        self._log_fh = None

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.github/workflows/e2e-kind-discovery.yml (2)

37-42: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Fail immediately on HTTP download errors.

curl -Lo saves the response body and exits successfully for server errors unless --fail is set. Add --fail to both download commands so this step fails immediately instead of using a malformed binary or error page.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/e2e-kind-discovery.yml around lines 37 - 42, Add the curl
HTTP failure flag to both download commands in the “Install kind and kubectl”
step, ensuring kind and kubectl downloads fail immediately on server-error
responses while preserving the existing destinations and permissions.

Source: Coding guidelines


37-42: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Security Misconfiguration (CWE-494): Download of Code Without Integrity Check

Reachability: External

Verify the downloaded Kind and kubectl binaries before installation.

Lines 39 and 41 install executables from external releases without a checksum or signature check. A compromised release asset can run on the workflow runner when the Kind E2E tests invoke kind or kubectl. Add the official SHA-256 or signature validation for kind-linux-amd64 and kubectl before chmod/sudo mv, and fail before installation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/e2e-kind-discovery.yml around lines 37 - 42, Update the
“Install kind and kubectl” workflow step to verify both downloaded binaries
against their official SHA-256 checksums or signatures before making them
executable or moving them into /usr/local/bin. Fetch the expected verification
data for the pinned releases, validate each file, and ensure any mismatch fails
the step before installation.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e_test/kind_discovery/conftest.py`:
- Around line 77-82: Update workers() and the wait_for_count flow so request
failures and non-2xx /workers responses are not converted into an empty worker
list. Preserve retries for transient failures, but only evaluate the expected
count—including zero—after a successful response whose payload has the valid
worker-list schema; otherwise continue waiting or propagate the failure so
forced deletion cannot pass without confirming worker removal.

---

Outside diff comments:
In @.github/workflows/e2e-kind-discovery.yml:
- Around line 37-42: Add the curl HTTP failure flag to both download commands in
the “Install kind and kubectl” step, ensuring kind and kubectl downloads fail
immediately on server-error responses while preserving the existing destinations
and permissions.
- Around line 37-42: Update the “Install kind and kubectl” workflow step to
verify both downloaded binaries against their official SHA-256 checksums or
signatures before making them executable or moving them into /usr/local/bin.
Fetch the expected verification data for the pinned releases, validate each
file, and ensure any mismatch fails the step before installation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 982658b2-0ab6-47d2-9137-b75a9141e8cf

📥 Commits

Reviewing files that changed from the base of the PR and between d76e40e and f257856.

📒 Files selected for processing (6)
  • .github/workflows/e2e-kind-discovery.yml
  • e2e_test/kind_discovery/__init__.py
  • e2e_test/kind_discovery/conftest.py
  • e2e_test/kind_discovery/manifests.yaml
  • e2e_test/kind_discovery/test_kind_discovery.py
  • e2e_test/pyproject.toml

Comment on lines +77 to +82
def workers(self) -> list[dict]:
try:
payload = requests.get(f"{self.base_url}/workers", timeout=5).json()
except requests.RequestException:
return []
return payload.get("workers", payload if isinstance(payload, list) else [])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🔴 Important Do not treat an unavailable /workers endpoint as an empty worker set.

workers() returns [] for request failures and does not reject non-2xx responses. wait_for_count(0, ...) can then pass while the smg process is alive but /workers is unavailable or failing. This allows the forced-deletion test to pass without confirming worker removal.

Keep retrying transient request failures in wait_for_count, but require a successful response with a valid worker-list schema before accepting an expected count of zero. As per coding guidelines, prioritize missing error handling and inappropriate fallbacks.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 78-78: Request-controlled URL passed to requests; validate against an allowlist to prevent SSRF.
Context: requests.get(f"{self.base_url}/workers", timeout=5)
Note: [CWE-918] Server-Side Request Forgery (SSRF).

(ssrf-requests)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e_test/kind_discovery/conftest.py` around lines 77 - 82, Update workers()
and the wait_for_count flow so request failures and non-2xx /workers responses
are not converted into an empty worker list. Preserve retries for transient
failures, but only evaluate the expected count—including zero—after a successful
response whose payload has the valid worker-list schema; otherwise continue
waiting or propagate the failure so forced deletion cannot pass without
confirming worker removal.

Source: Coding guidelines

Comment thread e2e_test/kind_discovery/test_kind_discovery.py Outdated
Comment thread e2e_test/kind_discovery/test_in_cluster_journey.py Outdated
docker build -t smg-gateway:e2e \
-f e2e_test/kind_discovery/Dockerfile.smg target/debug
# Fail here (not as CrashLoopBackOff later) on any linkage problem.
docker run --rm smg-mock-worker:e2e --help >/dev/null 2>&1 || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Nit: || true silently swallows failures, contradicting the comment on the line above ("Fail here … on any linkage problem"). A missing shared library would exit non-zero and be masked. The gateway check on the next line does fail properly — if mock-worker also supports --help, drop the || true; if it doesn't, the exit code from an unsupported flag is still distinct from a linkage crash (signal vs exit code), so consider checking the exit code explicitly instead.

Three pytest suites against a real kind cluster (no shell assertion
logic; --confcutdir isolates them from the GPU e2e conftest so only
pytest+requests are needed).

Host-gateway suites: multi-port registration with ownership labels;
batch pod bursts; namespace scoping via a decoy pod; kubectl-annotate
port-set edits; kubelet readiness flips keeping workers; gateway
restart rebuild; pod recreation rotating the ownership uid; completions
fanning out across discovered engines; graceful vs force deletion.
mock-worker fleet suites: one pod running forty engines from a single
worker-ports annotation; gRPC engines registering with grpc connection
mode; PD prefill/decode mix with per-engine aligned bootstrap ports.

In-cluster operator journey: smg deployed as a Deployment with
in-cluster ServiceAccount auth and the minimal RBAC from
in_cluster.yaml (reference manifests), discovering a plain multi-port
engine Deployment over the pod network, driven with kubectl scale and
rollout restart; a misconfigured annotation falls back to one worker
and is diagnosable from gateway logs. Each step records convergence
timings into a printed cluster report (rollout capacity floor stayed
at 12/12).

Mock engines return 404 for unknown paths: the gateway's metadata
fetch relies on it to fall back between endpoint variants.

The workflow is workflow_dispatch-only with a runner input defaulting
to ubuntu-latest until Docker availability on the self-hosted runner
pool is confirmed; smg and mock-worker images are built in-job
(pr-test-rust publishes no server binary artifact and its runner image
is not glibc-matched).

Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
@slin1237
slin1237 force-pushed the test/discovery-kind-e2e branch from 6f3b2ec to 88ec5e9 Compare August 6, 2026 06:17
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@slin1237

slin1237 commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

Expanded per review discussion and validated end-to-end on real clusters (temporary branch trigger, since workflow_dispatch only registers from the default branch; trigger removed and the branch squashed back to one commit).

Final validated run: https://github.com/smg-project/smg/actions/runs/31075739665 — 16/16 passed in 3m42s of test time (~20 min wall incl. Rust + image builds). Earlier iterations also caught and fixed a mock-fidelity bug worth noting: the ConfigMap engine answered 200 {} on unknown paths, which silently defeated the gateway's /model_info/get_model_info 404-fallback and registered workers under the unknown model — mocks now 404 unknown paths.

What this suite now covers

Host-gateway lifecycle (9): multi-port registration + ownership labels, batch bursts, namespace scoping (decoy pod in a foreign namespace), kubectl annotate port-set edits, kubelet readiness flips (probe-driven NotReady removes nothing), gateway restart rebuild, pod recreation rotating the ownership uid, data plane (completions fan out across ≥2 discovered engines), graceful vs force deletion.

mock-worker fleets (3): one pod running forty engines from a single smg.ai/worker-ports annotation (URL set verified port-for-port, then unwound), gRPC engines registering with connection_mode: grpc via the dual-probe, and a PD prefill/decode mix asserting per-engine aligned bootstrap ports (29600→29700, 29601→29701).

In-cluster operator journey (4): smg deployed as a Deployment with in-cluster ServiceAccount auth and the minimal RBAC in in_cluster.yaml (doubles as reference manifests — first coverage of the Client::try_default in-cluster branch), discovering a plain no-hostNetwork multi-port engine Deployment over the pod network, driven with kubectl scale and rollout restart; a typo'd annotation falls back to exactly one worker and is diagnosable from gateway logs.

Cluster report from the validated run

0.0s  initial deploy (2 pods x 4 engines)   [converged to 8]
1.0s  scale 2 -> 4 replicas                 [converged to 16]
6.0s  scale 4 -> 1 replicas                 [converged to 4]
6.0s  post-rollout convergence              [converged to 12]
12.0  rollout restart capacity floor        [never dipped below 12/12]
1.0s  typo deployment falls back to one worker

Registration converges in ~1s; removals in ~6s (5s drain settle + one pass); a rolling restart of the engine fleet held full capacity throughout (surge pods registered before old ones drained).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
e2e_test/kind_discovery/conftest.py (1)

78-104: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🟣 Pre-existing Reject failed or invalid /workers responses.

Both helpers convert request failures, non-success JSON responses, and missing workers fields into an empty worker set. This lets KindGateway.wait_for_count(0, ...) pass without confirming forced deletion.

  • e2e_test/kind_discovery/conftest.py#L78-L104: retry transient failures, but only compare counts after a successful response with a valid worker-list schema.
  • e2e_test/kind_discovery/test_in_cluster_journey.py#L46-L66: apply the same response-status and schema validation so both harnesses have the same contract.

As per coding guidelines, “Do not silently fall back to None or a default when configuration validation should fail loudly.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e_test/kind_discovery/conftest.py` around lines 78 - 104, Update
KindGateway.workers and wait_for_count in e2e_test/kind_discovery/conftest.py
(lines 78-104) to retry transient request failures, require a successful
response, and validate that the response contains a worker list before comparing
counts; invalid or failed responses must not become an empty set. Apply the same
response-status and worker-schema validation in
e2e_test/kind_discovery/test_in_cluster_journey.py (lines 46-66) so both
harnesses share this contract.

Source: Coding guidelines

🧹 Nitpick comments (4)
e2e_test/kind_discovery/test_kind_discovery.py (4)

304-305: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

🟡 Nit: urls holds ports, not URLs.

The set comprehension extracts the port suffix. Rename the variable to ports so the assertion reads correctly.

♻️ Proposed change
-        urls = {w["url"].rsplit(":", 1)[-1] for w in gateway.workers()}
-        assert urls == {str(p) for p in self.SCALE_PORTS}
+        ports = {w["url"].rsplit(":", 1)[-1] for w in gateway.workers()}
+        assert ports == {str(p) for p in self.SCALE_PORTS}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e_test/kind_discovery/test_kind_discovery.py` around lines 304 - 305,
Rename the set comprehension variable from urls to ports in the gateway worker
assertion, keeping its extracted port values and comparison unchanged.

283-307: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

🔴 Important: The fleet tests depend on TestKindDiscovery having emptied the cluster.

test_forty_engine_pod_registers_and_unwinds opens with wait_for_count(0, "clean slate before fleet scenarios"). That precondition holds only when test_graceful_then_force_delete ran first in the same session. If a user selects the fleet tests alone (for example with -k Fleet or -k PD), the baseline is 5 workers and this line times out after 120 seconds with a misleading message.

Make the clean slate explicit instead of inherited. Delete the baseline pods in a class-scoped autouse fixture, then wait for zero.

♻️ Proposed change
 class TestKindMockWorkerFleet:
     """Scale and protocol-mix scenarios on the real mock-worker binary."""
 
     SCALE_PORTS = [29000 + i for i in range(40)]
 
+    `@pytest.fixture`(autouse=True, scope="class")
+    def _clean_slate(self, gateway):
+        subprocess.run(
+            ["kubectl", "delete", "-f", str(HERE / "manifests.yaml"),
+             "--ignore-not-found", "--grace-period=0", "--force"],
+            check=True, capture_output=True, text=True,
+        )
+        gateway.wait_for_count(0, "clean slate before fleet scenarios")
+
     def test_forty_engine_pod_registers_and_unwinds(self, gateway):
-        gateway.wait_for_count(0, "clean slate before fleet scenarios")
         apply_manifest(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e_test/kind_discovery/test_kind_discovery.py` around lines 283 - 307,
Update TestKindDiscovery with a class-scoped autouse fixture that deletes the
baseline pods before fleet scenarios run, then waits for the gateway worker
count to reach zero. Ensure test_forty_engine_pod_registers_and_unwinds and
other fleet tests no longer rely on test_graceful_then_force_delete ordering,
while preserving the existing zero-count verification.

83-104: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

🟡 Nit: kubectl create namespace decoy is not idempotent.

The test never deletes the decoy namespace. A second run of this test against the same cluster fails with AlreadyExists because kubectl runs with check=True. Use kubectl apply for the namespace, or delete the namespace at the end of the test.

♻️ Proposed change
-        kubectl("create", "namespace", "decoy")
+        apply_manifest("apiVersion: v1\nkind: Namespace\nmetadata:\n  name: decoy\n")

Note: apply_manifest is defined later in the module. Move it above TestKindDiscovery before using it here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e_test/kind_discovery/test_kind_discovery.py` around lines 83 - 104, Make
namespace setup in test_namespace_scoping_ignores_foreign_pod idempotent by
replacing the failing kubectl create operation with the module’s apply-based
namespace setup, moving apply_manifest above TestKindDiscovery if needed before
use. Preserve the existing decoy pod application and discovery assertions.

309-333: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

🔴 Important: Each fleet test leaks its pod on failure, so later tests cascade.

test_grpc_workers_register_with_grpc_mode assumes the worker count is 0 on entry, and test_pd_mix_with_aligned_bootstrap_ports assumes the gRPC pod was removed. Each test deletes its pod only on the success path. If wait_for_count raises, or if an assertion in the loop at Line 330 fails, the pod stays in the cluster. Every following test then observes an unexpected worker count and fails for an unrelated reason. Add a fixture or try/finally that force-deletes the pod for each test.

♻️ Proposed change
+@pytest.fixture
+def owned_pods():
+    names: list[str] = []
+    yield names
+    for name in names:
+        subprocess.run(
+            ["kubectl", "delete", "pod", name, "--ignore-not-found",
+             "--grace-period=0", "--force"],
+            check=False, capture_output=True, text=True,
+        )

Then register each pod name (fleet-0, grpc-0, prefill-0, decode-0) in owned_pods right after apply_manifest, and drop the trailing kubectl delete calls.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e_test/kind_discovery/test_kind_discovery.py` around lines 309 - 333,
Ensure every fleet test, including test_grpc_workers_register_with_grpc_mode and
the tests creating fleet-0, prefill-0, and decode-0, cleans up its pod on both
success and failure. Register each pod name in the existing owned_pods mechanism
immediately after apply_manifest, then remove the trailing success-only kubectl
delete calls so fixture teardown force-deletes all owned pods.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/e2e-kind-discovery.yml:
- Around line 52-53: Remove the `|| true` fallback from the `docker run --rm
smg-mock-worker:e2e --help` smoke test so non-zero exits propagate immediately
as workflow failures. Preserve the existing silent output redirection and run
the silent-failure-hunter agent on the changed workflow file.
- Around line 22-27: Declare workflow-level GITHUB_TOKEN permissions for the
kind-discovery workflow by adding contents: read before jobs. Keep the existing
checkout and job configuration unchanged while ensuring repository-controlled
steps receive no write permissions.
- Around line 13-24: Restrict the workflow_dispatch runner input to approved
labels by changing the runner input definition to a choice containing only
trusted values such as ubuntu-latest and k8s-runner-cpu. Update the
kind-discovery job’s runs-on expression to use that validated input while
preserving ubuntu-latest as the default.

In `@e2e_test/kind_discovery/test_in_cluster_journey.py`:
- Around line 159-181: Update the rollout test around the floor tracking and
reporting to assert that the recorded minimum worker count is at least 12 before
post-rollout convergence. Keep the worker-count floor separate from elapsed-time
tracking, and report the floor only after enforcing this assertion; use the
existing rollout flow and symbols such as floor, start, and incluster.report.

In `@e2e_test/kind_discovery/test_kind_discovery.py`:
- Around line 166-202: Update test_completions_route_to_discovered_workers so
non-200 responses during fan-out are retried until successful, using a bounded
deadline and short delay rather than asserting on the first attempt. Continue
collecting served-by values only from successful responses, preserve the content
validation, and retain the final distinct-engine assertion.

---

Duplicate comments:
In `@e2e_test/kind_discovery/conftest.py`:
- Around line 78-104: Update KindGateway.workers and wait_for_count in
e2e_test/kind_discovery/conftest.py (lines 78-104) to retry transient request
failures, require a successful response, and validate that the response contains
a worker list before comparing counts; invalid or failed responses must not
become an empty set. Apply the same response-status and worker-schema validation
in e2e_test/kind_discovery/test_in_cluster_journey.py (lines 46-66) so both
harnesses share this contract.

---

Nitpick comments:
In `@e2e_test/kind_discovery/test_kind_discovery.py`:
- Around line 304-305: Rename the set comprehension variable from urls to ports
in the gateway worker assertion, keeping its extracted port values and
comparison unchanged.
- Around line 283-307: Update TestKindDiscovery with a class-scoped autouse
fixture that deletes the baseline pods before fleet scenarios run, then waits
for the gateway worker count to reach zero. Ensure
test_forty_engine_pod_registers_and_unwinds and other fleet tests no longer rely
on test_graceful_then_force_delete ordering, while preserving the existing
zero-count verification.
- Around line 83-104: Make namespace setup in
test_namespace_scoping_ignores_foreign_pod idempotent by replacing the failing
kubectl create operation with the module’s apply-based namespace setup, moving
apply_manifest above TestKindDiscovery if needed before use. Preserve the
existing decoy pod application and discovery assertions.
- Around line 309-333: Ensure every fleet test, including
test_grpc_workers_register_with_grpc_mode and the tests creating fleet-0,
prefill-0, and decode-0, cleans up its pod on both success and failure. Register
each pod name in the existing owned_pods mechanism immediately after
apply_manifest, then remove the trailing success-only kubectl delete calls so
fixture teardown force-deletes all owned pods.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d341ce11-e3df-4fdd-9764-af1eddff7c07

📥 Commits

Reviewing files that changed from the base of the PR and between a198c1d and 88ec5e9.

📒 Files selected for processing (11)
  • .github/workflows/e2e-kind-discovery.yml
  • e2e_test/kind_discovery/Dockerfile.mock-worker
  • e2e_test/kind_discovery/Dockerfile.smg
  • e2e_test/kind_discovery/__init__.py
  • e2e_test/kind_discovery/burst.yaml
  • e2e_test/kind_discovery/conftest.py
  • e2e_test/kind_discovery/in_cluster.yaml
  • e2e_test/kind_discovery/manifests.yaml
  • e2e_test/kind_discovery/test_in_cluster_journey.py
  • e2e_test/kind_discovery/test_kind_discovery.py
  • e2e_test/pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • e2e_test/pyproject.toml

Comment on lines +13 to +24
runner:
description: Runner label (needs Docker for the kind node container)
required: false
default: ubuntu-latest

env:
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"

jobs:
kind-discovery:
runs-on: ${{ inputs.runner || 'ubuntu-latest' }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow file =="
if [ -f .github/workflows/e2e-kind-discovery.yml ]; then
  cat -n .github/workflows/e2e-kind-discovery.yml
else
  echo "missing .github/workflows/e2e-kind-discovery.yml"
fi

echo
echo "== workflow files mentioning inputs.runner, dispatch, or this workflow =="
rg -n "e2e-kind-discovery|inputs\.runner|workflow_dispatch|workflow_call|runs-on|permissions:|persist-credentials|actions/checkout" .github/workflows || true

echo
echo "== repo-level permissions/default branch refs (if available) =="
git ls-files .github/workflows | sed -n '1,120p'

Repository: smg-project/smg

Length of output: 20830


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== setup-rust action =="
if [ -f .github/actions/setup-rust/action.yml ]; then
  cat -n .github/actions/setup-rust/action.yml
else
  echo "missing .github/actions/setup-rust/action.yml"
  # find and print matching action files
  fd -a 'action\.(ya?ml|yaml)$' .github/actions | sed -n '1,80p'
fi

echo
echo "== Python context files =="
ls -la e2e_test/kind_discovery 2>/dev/null || true
for f in e2e_test/kind_discovery/conftest.py e2e_test/kind_discovery/test_in_cluster_journey.py e2e_test/kind_discovery/Dockerfile.mock-worker e2e_test/kind_discovery/Dockerfile.smg; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    sed -n '1,180p' "$f"
  fi
done

Repository: smg-project/smg

Length of output: 15543


Security Misconfiguration (CWE-269): Improper Privilege Management

Reachability: External

Restrict inputs.runner to approved labels.

inputs.runner is a free-form manual workflow input and is passed directly into runs-on, so an actor who can dispatch this workflow can choose any available runner label if an ubuntu-latest runner is unavailable. Use a choice input that only includes trusted labels such as ubuntu-latest and k8s-runner-cpu, or keep runs-on fixed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/e2e-kind-discovery.yml around lines 13 - 24, Restrict the
workflow_dispatch runner input to approved labels by changing the runner input
definition to a choice containing only trusted values such as ubuntu-latest and
k8s-runner-cpu. Update the kind-discovery job’s runs-on expression to use that
validated input while preserving ubuntu-latest as the default.

Source: Coding guidelines

Comment on lines +22 to +27
jobs:
kind-discovery:
runs-on: ${{ inputs.runner || 'ubuntu-latest' }}
timeout-minutes: 45
steps:
- uses: actions/checkout@v7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow file =="
cat -n .github/workflows/e2e-kind-discovery.yml | head -n 220

echo
echo "== workflow permission references in related files =="
rg -n "permissions:|GITHUB_TOKEN|actions/checkout|contents:" .github/workflows .github 2>/dev/null || true

echo
echo "== diff stat/name-only =="
git diff --stat HEAD~1..HEAD 2>/dev/null || git diff --stat 2>/dev/null || true
git diff --name-only HEAD~1..HEAD 2>/dev/null || git diff --name-only 2>/dev/null || true

echo
echo "== parse workflow top-level/job permissions with small python parser =="
python3 - <<'PY'
from pathlib import Path
p = Path('.github/workflows/e2e-kind-discovery.yml')
if not p.exists():
    print("missing .github/workflows/e2e-kind-discovery.yml")
    raise SystemExit
text = p.read_text()
print("has_top_level_permissions=", any(line.startswith("permissions:") for line in text.splitlines()))
for i,line in enumerate(text.splitlines(), 1):
    if line.startswith("permissions:"):
        print(f"permission_block_line={i}: {line!r}")
PY

Repository: smg-project/smg

Length of output: 30124


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect any referenced workflow step(s) without executing repository code.
echo "== workflow step context =="
sed -n '22,160p' .github/workflows/e2e-kind-discovery.yml

Repository: smg-project/smg

Length of output: 1799


Security Misconfiguration (CWE-732): Incorrect Permission Assignment for Critical Resource

Declare explicit least-privilege GITHUB_TOKEN permissions.

This manual workflow_dispatch workflow runs repository-controlled steps after checkout, including a local GitHub Action and e2e tests. Set permissions: contents: read at the workflow scope so the GITHUB_TOKEN cannot be reused for repository mutations.

🧰 Tools
🪛 zizmor (1.29.0)

[warning] 27-27: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/e2e-kind-discovery.yml around lines 22 - 27, Declare
workflow-level GITHUB_TOKEN permissions for the kind-discovery workflow by
adding contents: read before jobs. Keep the existing checkout and job
configuration unchanged while ensuring repository-controlled steps receive no
write permissions.

Source: Coding guidelines

Comment on lines +52 to +53
# Fail here (not as CrashLoopBackOff later) on any linkage problem.
docker run --rm smg-mock-worker:e2e --help >/dev/null 2>&1 || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🟡 Nit Propagate the mock-worker smoke-test failure.

The comment at Line 52 says this command must fail on linkage problems, but Line 53 ends with || true. This converts every non-zero exit into success and delays the error until the cluster tests. Remove || true.

Proposed failure propagation fix
-          docker run --rm smg-mock-worker:e2e --help >/dev/null 2>&1 || true
+          docker run --rm smg-mock-worker:e2e --help >/dev/null 2>&1

As per coding guidelines, run the silent-failure-hunter agent on changed files to detect swallowed errors, inappropriate fallbacks, and missing error propagation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Fail here (not as CrashLoopBackOff later) on any linkage problem.
docker run --rm smg-mock-worker:e2e --help >/dev/null 2>&1 || true
# Fail here (not as CrashLoopBackOff later) on any linkage problem.
docker run --rm smg-mock-worker:e2e --help >/dev/null 2>&1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/e2e-kind-discovery.yml around lines 52 - 53, Remove the
`|| true` fallback from the `docker run --rm smg-mock-worker:e2e --help` smoke
test so non-zero exits propagate immediately as workflow failures. Preserve the
existing silent output redirection and run the silent-failure-hunter agent on
the changed workflow file.

Source: Coding guidelines

Comment on lines +159 to +181
start = time.monotonic()
floor = 12
kubectl("rollout", "restart", "deployment/engines")
while True:
floor = min(floor, len(incluster.workers()))
status = subprocess.run(
["kubectl", "rollout", "status", "deployment/engines", "--timeout=1s"],
capture_output=True,
text=True,
)
if status.returncode == 0:
break
assert time.monotonic() - start < 300, "rollout never completed"

incluster.wait_for_count(12, "post-rollout convergence")
new_ips = engine_pod_ips()
assert incluster.worker_urls() == expected_urls(new_ips)
assert not (expected_urls(old_ips - new_ips) & incluster.worker_urls()), (
"stale workers survived the rollout"
)
incluster.report.append(
("rollout restart capacity floor", float(floor), "min workers during roll (of 12)")
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🔴 Important Assert the rollout capacity floor.

Line 163 records a worker count, but the test only prints floor. A rollout can drop below 12 workers and still pass after final convergence.

Assert floor >= 12. Record elapsed time separately from the worker count.

Proposed fix
         incluster.wait_for_count(12, "post-rollout convergence")
         new_ips = engine_pod_ips()
         assert incluster.worker_urls() == expected_urls(new_ips)
+        assert floor >= 12, f"rollout dropped below required capacity: {floor}/12 workers"
         assert not (expected_urls(old_ips - new_ips) & incluster.worker_urls()), (
             "stale workers survived the rollout"
         )
         incluster.report.append(
-            ("rollout restart capacity floor", float(floor), "min workers during roll (of 12)")
+            (
+                "rollout restart capacity floor",
+                time.monotonic() - start,
+                f"minimum workers during roll: {floor}/12",
+            )
         )

As per coding guidelines, “Run the pr-test-analyzer agent to verify that tests adequately cover new or changed functionality.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
start = time.monotonic()
floor = 12
kubectl("rollout", "restart", "deployment/engines")
while True:
floor = min(floor, len(incluster.workers()))
status = subprocess.run(
["kubectl", "rollout", "status", "deployment/engines", "--timeout=1s"],
capture_output=True,
text=True,
)
if status.returncode == 0:
break
assert time.monotonic() - start < 300, "rollout never completed"
incluster.wait_for_count(12, "post-rollout convergence")
new_ips = engine_pod_ips()
assert incluster.worker_urls() == expected_urls(new_ips)
assert not (expected_urls(old_ips - new_ips) & incluster.worker_urls()), (
"stale workers survived the rollout"
)
incluster.report.append(
("rollout restart capacity floor", float(floor), "min workers during roll (of 12)")
)
start = time.monotonic()
floor = 12
kubectl("rollout", "restart", "deployment/engines")
while True:
floor = min(floor, len(incluster.workers()))
status = subprocess.run(
["kubectl", "rollout", "status", "deployment/engines", "--timeout=1s"],
capture_output=True,
text=True,
)
if status.returncode == 0:
break
assert time.monotonic() - start < 300, "rollout never completed"
incluster.wait_for_count(12, "post-rollout convergence")
new_ips = engine_pod_ips()
assert incluster.worker_urls() == expected_urls(new_ips)
assert floor >= 12, f"rollout dropped below required capacity: {floor}/12 workers"
assert not (expected_urls(old_ips - new_ips) & incluster.worker_urls()), (
"stale workers survived the rollout"
)
incluster.report.append(
(
"rollout restart capacity floor",
time.monotonic() - start,
f"minimum workers during roll: {floor}/12",
)
)
🧰 Tools
🪛 ast-grep (0.45.0)

[error] 163-167: Command coming from incoming request
Context: subprocess.run(
["kubectl", "rollout", "status", "deployment/engines", "--timeout=1s"],
capture_output=True,
text=True,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e_test/kind_discovery/test_in_cluster_journey.py` around lines 159 - 181,
Update the rollout test around the floor tracking and reporting to assert that
the recorded minimum worker count is at least 12 before post-rollout
convergence. Keep the worker-count floor separate from elapsed-time tracking,
and report the floor only after enforcing this assertion; use the existing
rollout flow and symbols such as floor, start, and incluster.report.

Source: Coding guidelines

Comment on lines +166 to +202
def test_completions_route_to_discovered_workers(self, gateway):
import requests

gateway.wait_for_count(5, "baseline before data-plane check")
# Health promotion lags registration; wait for the first 200 before
# asserting fan-out.
deadline = time.monotonic() + 60
while True:
probe = requests.post(
f"{gateway.base_url}/v1/chat/completions",
json={
"model": "kind-e2e-model",
"messages": [{"role": "user", "content": "warmup"}],
},
timeout=10,
)
if probe.status_code == 200:
break
assert time.monotonic() < deadline, f"no worker became routable: {probe.text}"
time.sleep(1)
served_by: set[str] = set()
for i in range(10):
response = requests.post(
f"{gateway.base_url}/v1/chat/completions",
json={
"model": "kind-e2e-model",
"messages": [{"role": "user", "content": f"probe {i}"}],
},
timeout=10,
)
assert response.status_code == 200, response.text
content = response.json()["choices"][0]["message"]["content"]
assert content.startswith("served-by-"), content
served_by.add(content)
# Round-robin over the discovered fleet: several distinct engines
# (pod ports) must actually serve traffic.
assert len(served_by) >= 2, f"traffic pinned to one engine: {served_by}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🔴 Important: The fan-out loop can flake while remaining engines are still being health-promoted.

The warmup loop only proves that one worker became routable. The following 10 requests then assert status_code == 200 on every attempt. If a second engine is registered but not yet health-promoted, the gateway can route to it and return a non-200 response. The test then fails for a timing reason, not a routing defect. Retry non-200 responses inside the fan-out loop instead of asserting on the first attempt.

🐛 Proposed fix
         served_by: set[str] = set()
-        for i in range(10):
-            response = requests.post(
-                f"{gateway.base_url}/v1/chat/completions",
-                json={
-                    "model": "kind-e2e-model",
-                    "messages": [{"role": "user", "content": f"probe {i}"}],
-                },
-                timeout=10,
-            )
-            assert response.status_code == 200, response.text
-            content = response.json()["choices"][0]["message"]["content"]
-            assert content.startswith("served-by-"), content
-            served_by.add(content)
+        deadline = time.monotonic() + 60
+        attempts = 0
+        while len(served_by) < 2 and time.monotonic() < deadline:
+            response = requests.post(
+                f"{gateway.base_url}/v1/chat/completions",
+                json={
+                    "model": "kind-e2e-model",
+                    "messages": [{"role": "user", "content": f"probe {attempts}"}],
+                },
+                timeout=10,
+            )
+            attempts += 1
+            if response.status_code != 200:
+                time.sleep(1)
+                continue
+            content = response.json()["choices"][0]["message"]["content"]
+            assert content.startswith("served-by-"), content
+            served_by.add(content)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_completions_route_to_discovered_workers(self, gateway):
import requests
gateway.wait_for_count(5, "baseline before data-plane check")
# Health promotion lags registration; wait for the first 200 before
# asserting fan-out.
deadline = time.monotonic() + 60
while True:
probe = requests.post(
f"{gateway.base_url}/v1/chat/completions",
json={
"model": "kind-e2e-model",
"messages": [{"role": "user", "content": "warmup"}],
},
timeout=10,
)
if probe.status_code == 200:
break
assert time.monotonic() < deadline, f"no worker became routable: {probe.text}"
time.sleep(1)
served_by: set[str] = set()
for i in range(10):
response = requests.post(
f"{gateway.base_url}/v1/chat/completions",
json={
"model": "kind-e2e-model",
"messages": [{"role": "user", "content": f"probe {i}"}],
},
timeout=10,
)
assert response.status_code == 200, response.text
content = response.json()["choices"][0]["message"]["content"]
assert content.startswith("served-by-"), content
served_by.add(content)
# Round-robin over the discovered fleet: several distinct engines
# (pod ports) must actually serve traffic.
assert len(served_by) >= 2, f"traffic pinned to one engine: {served_by}"
def test_completions_route_to_discovered_workers(self, gateway):
import requests
gateway.wait_for_count(5, "baseline before data-plane check")
# Health promotion lags registration; wait for the first 200 before
# asserting fan-out.
deadline = time.monotonic() + 60
while True:
probe = requests.post(
f"{gateway.base_url}/v1/chat/completions",
json={
"model": "kind-e2e-model",
"messages": [{"role": "user", "content": "warmup"}],
},
timeout=10,
)
if probe.status_code == 200:
break
assert time.monotonic() < deadline, f"no worker became routable: {probe.text}"
time.sleep(1)
served_by: set[str] = set()
deadline = time.monotonic() + 60
attempts = 0
while len(served_by) < 2 and time.monotonic() < deadline:
response = requests.post(
f"{gateway.base_url}/v1/chat/completions",
json={
"model": "kind-e2e-model",
"messages": [{"role": "user", "content": f"probe {attempts}"}],
},
timeout=10,
)
attempts += 1
if response.status_code != 200:
time.sleep(1)
continue
content = response.json()["choices"][0]["message"]["content"]
assert content.startswith("served-by-"), content
served_by.add(content)
# Round-robin over the discovered fleet: several distinct engines
# (pod ports) must actually serve traffic.
assert len(served_by) >= 2, f"traffic pinned to one engine: {served_by}"
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 173-180: Request-controlled URL passed to requests; validate against an allowlist to prevent SSRF.
Context: requests.post(
f"{gateway.base_url}/v1/chat/completions",
json={
"model": "kind-e2e-model",
"messages": [{"role": "user", "content": "warmup"}],
},
timeout=10,
)
Note: [CWE-918] Server-Side Request Forgery (SSRF).

(ssrf-requests)


[warning] 187-194: Request-controlled URL passed to requests; validate against an allowlist to prevent SSRF.
Context: requests.post(
f"{gateway.base_url}/v1/chat/completions",
json={
"model": "kind-e2e-model",
"messages": [{"role": "user", "content": f"probe {i}"}],
},
timeout=10,
)
Note: [CWE-918] Server-Side Request Forgery (SSRF).

(ssrf-requests)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e_test/kind_discovery/test_kind_discovery.py` around lines 166 - 202,
Update test_completions_route_to_discovered_workers so non-200 responses during
fan-out are retried until successful, using a bounded deadline and short delay
rather than asserting on the first attempt. Continue collecting served-by values
only from successful responses, preserve the content validation, and retain the
final distinct-engine assertion.

@slin1237
slin1237 merged commit 2f46bb6 into main Aug 6, 2026
39 of 41 checks passed
@slin1237
slin1237 deleted the test/discovery-kind-e2e branch August 6, 2026 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci CI/CD configuration changes dependencies Dependency updates tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant