Skip to content

test(e2e): replace fixed timeouts with event-driven backstops - #2554

Merged
myasnikovdaniil merged 3 commits into
mainfrom
daniil/split-event-driven
May 6, 2026
Merged

myasnikovdaniil merged 3 commits into
mainfrom
daniil/split-event-driven

Conversation

@myasnikovdaniil

@myasnikovdaniil myasnikovdaniil commented May 2, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

Replaces fragile sleep N / timeout N until …; do sleep …; done patterns in hack/*.bats and hack/*.sh with kubectl wait --for=condition=… plus a small existence backstop:

until kubectl get <resource> >/dev/null 2>&1; do sleep 2; done
kubectl wait <resource> --for=condition=Ready --timeout=…

Why the existence backstop matters

kubectl wait against a not-yet-created resource sits silently for the full timeout and then fails with error: timed out waiting for the condition — useless for diagnosis. With the backstop, the test fails fast with a clear "resource never appeared" error.

Existence backstop added at 14 sites in this sweep across:

  • redis, kafka, openbao, harbor, mongodb, qdrant, clickhouse, mariadb, postgres, bucket, external-dns, foundationdb bats
  • hack/e2e-install-cozystack.bats
  • hack/e2e-apps/run-kubernetes.sh
  • hack/e2e-test-openapi.bats (port-readiness wait replacing fixed sleep 5 after kubectl proxy)

The vm-disk hunk in hack/e2e-apps/vminstance.bats is included here; the vm-instance hunks land separately via the daniil/split-vminstance PR.

Genuine sleeps annotated

The 3 remaining genuine sleeps (LINSTOR node membership propagation, MetalLB end-to-end HTTP path, HR-count emission heuristic) are annotated with # TODO + rationale so a future reader knows they are intentional.

Same green-path semantics; failures now surface in seconds instead of after a 5-minute timeout. Surfaced from #2500.

Release note

NONE

Summary by CodeRabbit

  • Tests

    • Improved e2e stability by replacing fixed sleeps with timeout-based polling across many integration tests, waiting for operator-created resources to exist before readiness checks to reduce race conditions and flakes.
  • Documentation

    • Added clarifying notes for LoadBalancer retry behavior and improved local proxy readiness checks in e2e scripts.

Sweep across `hack/*.bats` and `hack/*.sh` replacing fragile
`sleep N` / `timeout N until …; do sleep …; done` patterns with
`kubectl wait --for=condition=…` plus a small existence backstop:

  until kubectl get <resource> >/dev/null 2>&1; do sleep 2; done
  kubectl wait <resource> --for=condition=Ready --timeout=…

Existence backstop added to 33 `kubectl wait` sites across 12 bats
files (redis, kafka, openbao, harbor, mongodb, qdrant, vminstance,
clickhouse, mariadb, postgres, bucket, install). Without it,
`kubectl wait` against a not-yet-created resource sits silently for
the full timeout; with it, the test fails fast with a clear
"resource never appeared" error.

OpenAPI tests' fixed `sleep 5` after starting `kubectl proxy` is now
a port-readiness wait. The 3 remaining genuine sleeps (LINSTOR node
membership propagation, MetalLB end-to-end HTTP path, HR-count
emission heuristic) are annotated with TODO + rationale so a future
reader knows they are intentional.

Same green-path semantics; failures now surface in seconds instead
of after a 5-minute timeout.

Signed-off-by: Myasnikov Daniil <[email protected]>
Assisted-By: Claude <[email protected]>
@dosubot dosubot Bot added the size:L label May 2, 2026
@github-actions github-actions Bot added area/testing Issues or PRs related to testing (e2e, bats, unit tests) size/L This PR changes 100-499 lines, ignoring generated files labels May 2, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request improves the robustness and speed of the end-to-end test suite by replacing non-deterministic 'sleep' commands with event-driven wait patterns. By implementing existence backstops, the tests now provide immediate feedback when resources fail to materialize, significantly reducing the time spent waiting for timeouts during test failures. The changes cover a wide range of application tests and infrastructure setup scripts, ensuring more reliable CI/CD pipelines.

Highlights

  • Improved E2E Test Reliability: Replaced fragile fixed-duration sleeps with event-driven backstops using 'kubectl wait' and 'until' loops across multiple test suites.
  • Faster Failure Detection: Added existence checks for Kubernetes resources, ensuring tests fail fast with clear errors if a resource fails to appear, rather than waiting for full timeouts.
  • Intentional Sleep Annotation: Annotated remaining necessary sleeps with TODO comments explaining the rationale, improving maintainability for future readers.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@coderabbitai

coderabbitai Bot commented May 2, 2026 •

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 981d8c95-4f07-4a75-bc9a-104c4a909556

📥 Commits

Reviewing files that changed from the base of the PR and between 12ccc53 and cc7551c.

📒 Files selected for processing (1)
  • hack/e2e-apps/kafka.bats

📝 Walkthrough

Walkthrough

Replace fixed sleeps in many e2e test scripts with explicit polling loops that wait for operator-created Kubernetes objects (HelmReleases, StatefulSets, Deployments, PVCs, Jobs, APIService, namespaces, BucketAccesses, etc.) to exist before running kubectl wait or connectivity checks; add a comment documenting LB retry behavior and make kubectl-proxy readiness TCP-checked.

Changes

E2E readiness & existence polling

Layer / File(s) Summary
Existence polling (observation)
hack/e2e-apps/*.bats, hack/e2e-install-cozystack.bats
Add timeout ... sh -ec "until kubectl get <resource> ..." loops to wait for HelmRelease, StatefulSet, Deployment, PVC, Job, APIService, namespace, BucketClaim/BucketAccess, and custom resources to be created before further checks.
Readiness waits (core assertions)
hack/e2e-apps/*.bats, hack/e2e-install-cozystack.bats
Retain existing kubectl wait --for=condition=... / --for=jsonpath=... but run them only after existence polling; add new specific readiness gates (e.g., viewer BucketAccess accessGranted).
Resource-specific sequencing
hack/e2e-apps/bucket.bats, clickhouse.bats, mariadb.bats, postgres.bats, qdrant.bats, redis.bats, vminstance.bats, openbao.bats, mongodb.bats, harbor.bats, kafka.bats
Insert pre-waits for operator-materialized resources (HelmRelease, StatefulSet, PVC, Deployment, jobs) before existing readiness and port/endpoint checks; preserve existing connectivity/functional assertions.
Connectivity / proxy readiness
hack/e2e-test-openapi.bats, hack/e2e-apps/*.{bats}
Replace proxy/sleep with capturing proxy PID and nc -z TCP readiness loop; keep port/endpoint polling for services (ClickHouse, MariaDB, Redis, etc.).
Documentation / comments
hack/e2e-apps/run-kubernetes.sh
Add inline TODO/explanatory comments describing bounded LoadBalancer HTTP-path retry loop and why it’s not expressible as a single Kubernetes wait condition.
Install flow heuristics
hack/e2e-install-cozystack.bats
Add TODO describing HelmRelease visibility snapshot/wait heuristic and add pre-waits for Cozystack APIService and tenant-related resources before kubectl wait calls.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through scripts at break of day,
Replaced sleepy waits with polls that stay.
"Is it created? Ready? Bound?" I peep —
Timeouts guard the garden while tests sleep.
Hops and timeouts, steady and spry, CI blooms by and by. 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: replacing fixed timeouts with event-driven backstops in e2e tests.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch daniil/split-event-driven

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 and usage tips.

@dosubot dosubot Bot added kind/cleanup Categorizes issue or PR as related to cleanup of code, process, or technical debt quality-of-life QoL improvements labels May 2, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request improves the robustness of E2E tests by replacing fixed sleep durations with dynamic retry loops that wait for Kubernetes resources to materialize. This change ensures that kubectl wait commands do not fail prematurely when resources are not yet present in the API. The review feedback suggests expanding these existence checks to include associated system HelmReleases in several test suites and replacing hardcoded resource names with variables to enhance consistency and maintainability.

sleep 5
# Wait for the operator to materialise the HelmRelease before kubectl wait
# kicks in (kubectl wait errors immediately if the object does not exist yet).
timeout 60 sh -ec "until kubectl -n tenant-test get hr ${name} >/dev/null 2>&1; do sleep 2; done"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

low

Since this test waits for both ${name} and ${name}-system HelmReleases, it is safer to include both in the existence backstop to prevent the second kubectl wait from failing immediately if the system release hasn't appeared yet.

  timeout 60 sh -ec "until kubectl -n tenant-test get hr ${name} ${name}-system >/dev/null 2>&1; do sleep 2; done"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The ${name}-system HelmRelease isn't created by the cozystack-controller — it's rendered as a kind: HelmRelease template inside the outer chart (see packages/extra/external-dns/templates/external-dns.yaml, which defines an HR named {{ .Release.Name }}-system). By the time kubectl wait hr ${name} --for=condition=ready returns, the outer chart has been fully applied, so the ${name}-system object already exists. No race here, leaving the backstop as-is.

sleep 5
# Wait for the operator to materialise the HelmRelease before kubectl wait
# kicks in (kubectl wait errors immediately if the object does not exist yet).
timeout 60 sh -ec "until kubectl -n tenant-test get hr ${name} >/dev/null 2>&1; do sleep 2; done"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

low

Similar to the previous test case, including both HelmReleases in the backstop ensures that the subsequent wait for the system release doesn't fail prematurely.

  timeout 60 sh -ec "until kubectl -n tenant-test get hr ${name} ${name}-system >/dev/null 2>&1; do sleep 2; done"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same architecture — the second test case's ${name}-system HR is rendered by the outer chart at packages/extra/external-dns/templates/external-dns.yaml, so it's guaranteed to exist by the time the ${name} HR is Ready. Existing backstop is sufficient.

Comment thread hack/e2e-apps/harbor.bats
sleep 5
# Wait for the operator to materialise the HelmRelease before kubectl wait
# kicks in (kubectl wait errors immediately if the object does not exist yet).
timeout 60 sh -ec "until kubectl -n tenant-test get hr $release >/dev/null 2>&1; do sleep 2; done"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

low

The Harbor test also waits for a system HelmRelease later in the script. Adding it to the initial existence backstop provides better coverage for all resources being waited on.

  timeout 60 sh -ec "until kubectl -n tenant-test get hr $release $release-system >/dev/null 2>&1; do sleep 2; done"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

harbor-${name}-system is rendered as a kind: HelmRelease template inside the outer chart at packages/apps/harbor/templates/harbor.yaml, so it's applied to the cluster as part of the outer HR's reconciliation. By the time line 44's kubectl wait hr $release --for=condition=ready returns, the -system HR already exists. The line-43 backstop is enough.

Comment thread hack/e2e-apps/kafka.bats Outdated
# kicks in (kubectl wait errors immediately if the object does not exist yet).
timeout 60 sh -ec "until kubectl -n tenant-test get hr kafka-$name >/dev/null 2>&1; do sleep 2; done"
kubectl -n tenant-test wait hr kafka-$name --timeout=30s --for=condition=ready
timeout 60 sh -ec "until kubectl -n tenant-test get kafkas test >/dev/null 2>&1; do sleep 2; done"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

low

It is better to use the $name variable here instead of the hardcoded string test for consistency with other tests and to avoid breakage if the test name is changed at the top of the file.

  timeout 60 sh -ec "until kubectl -n tenant-test get kafkas $name >/dev/null 2>&1; do sleep 2; done"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 12ccc53 — switched the backstop to $name for consistency with every other backstop in this PR.

sleep 5
# Wait for the operator to materialise the HelmRelease before kubectl wait
# kicks in (kubectl wait errors immediately if the object does not exist yet).
timeout 60 sh -ec "until kubectl -n tenant-test get hr openbao-$name >/dev/null 2>&1; do sleep 2; done"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

low

Including the system HelmRelease in the existence backstop ensures that the subsequent wait for openbao-$name-system at line 25 is robust against slow resource materialization.

  timeout 60 sh -ec "until kubectl -n tenant-test get hr openbao-$name openbao-$name-system >/dev/null 2>&1; do sleep 2; done"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

openbao-$name-system is rendered as a kind: HelmRelease template inside the outer chart at packages/apps/openbao/templates/openbao.yaml. Once the outer HR is Ready, the inner -system HR has already been applied — no race between the two waits, so the existing backstop is sufficient.

Comment thread hack/e2e-apps/qdrant.bats
sleep 5
# Wait for the operator to materialise the HelmRelease before kubectl wait
# kicks in (kubectl wait errors immediately if the object does not exist yet).
timeout 60 sh -ec "until kubectl -n tenant-test get hr qdrant-$name >/dev/null 2>&1; do sleep 2; done"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

low

Adding the system HelmRelease to the backstop ensures that the wait at line 24 won't fail if the operator hasn't created the resource yet.

  timeout 60 sh -ec "until kubectl -n tenant-test get hr qdrant-$name qdrant-$name-system >/dev/null 2>&1; do sleep 2; done"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

qdrant-$name-system is rendered as a kind: HelmRelease template inside the outer chart at packages/apps/qdrant/templates/qdrant.yaml, so it's created as part of the outer HR's reconciliation. By the time line 23's --for=condition=ready returns, the -system HR already exists. No additional backstop needed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (8)
hack/e2e-apps/qdrant.bats (1)

29-29: ⚡ Quick win

Move inline cleanup to a framework-level teardown.

kubectl delete at the end of the test body is never reached when the test fails mid-run, leaving the Qdrant resource behind and potentially polluting subsequent runs. Based on learnings, cleanup in bats E2E tests under hack/e2e-apps/ should be moved to a global/framework-provided teardown rather than inlined in the test body.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hack/e2e-apps/qdrant.bats` at line 29, Remove the inline cleanup call
"kubectl -n tenant-test delete qdrant.apps.cozystack.io $name" from the
qdrant.bats test body and instead register the resource deletion in the
framework-level/global teardown hook used by the E2E tests; ensure the teardown
runs regardless of test failures and references the same $name variable (or
capture the created resource name in a shared context) so the framework teardown
performs the equivalent deletion for Qdrant resources created by the test.
hack/e2e-apps/mongodb.bats (1)

41-43: ⚡ Quick win

Consider moving inline cleanup to a framework-level teardown.

The kubectl delete at line 42 runs only if all previous steps succeed; a timeout or assertion failure earlier in the test leaves the MongoDB resource behind. The guard at line 5 (delete --ignore-not-found) covers recovery on the next run, but the leftover resource persists for the remainder of the current suite.

Bats supports a teardown function that runs after each test regardless of result — moving the delete there would make cleanup unconditional.

+teardown() {
+  kubectl -n tenant-test delete mongodbs.apps.cozystack.io test --ignore-not-found --timeout=2m || true
+}
+
 `@test` "Create DB MongoDB" {
   name='test'
   kubectl -n tenant-test delete mongodbs.apps.cozystack.io $name --ignore-not-found --timeout=2m || true
-  # Cleanup
-  kubectl -n tenant-test delete mongodbs.apps.cozystack.io $name
 }

Based on learnings: "replace inline cleanup (e.g., kubectl delete in the test body) with framework-level teardown/cleanup … Apply this pattern to all bats tests under hack/e2e-apps."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hack/e2e-apps/mongodb.bats` around lines 41 - 43, Move the inline cleanup
command "kubectl -n tenant-test delete mongodbs.apps.cozystack.io $name" out of
the test body and into a framework-level teardown function so it runs
unconditionally after each test; ensure the teardown can access the test's
"name" variable (make it a global/exported variable or set it in setup) and call
the same kubectl delete there (or use --ignore-not-found) to guarantee cleanup
on timeouts/failures, and apply this teardown pattern to all other bats tests
under hack/e2e-apps.
hack/e2e-apps/foundationdb.bats (1)

119-123: ⚡ Quick win

Consider moving cleanup to framework-level teardown.

The inline kubectl delete + wait loop at the end of the test body keeps cleanup tightly coupled to the test. If any assertion before line 120 fails, cleanup is silently skipped, leaving stale resources that can pollute subsequent runs.

♻️ Sketch of framework-level teardown
+teardown() {
+  kubectl -n tenant-test delete foundationdb test --ignore-not-found || true
+  timeout 120 sh -ec "while kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-test 2>/dev/null; do sleep 10; done" || true
+}
+
 `@test` "Create DB FoundationDB" {
   name='test'
   ...
-  # Clean up
-  kubectl -n tenant-test delete foundationdb $name
-
-  # Wait for cleanup to complete
-  timeout 120 sh -ec "while kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-$name 2>/dev/null; do sleep 10; done"
 }

Based on learnings: "In E2E bats tests like hack/e2e-apps/harbor.bats, replace inline cleanup with framework-level teardown/cleanup … apply this pattern to all bats tests under hack/e2e-apps."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hack/e2e-apps/foundationdb.bats` around lines 119 - 123, The test currently
does inline cleanup with "kubectl -n tenant-test delete foundationdb $name" and
a wait loop which will be skipped if the test fails; move this logic into the
shared framework-level teardown so cleanup always runs. Remove the two lines
from hack/e2e-apps/foundationdb.bats and instead register the created resource
name (e.g., push $name into a shared array like CLEANUP_RESOURCES or call a
helper register_cleanup in the test), then implement a teardown() in the common
bats helper (or the framework harness) that iterates CLEANUP_RESOURCES and
performs the kubectl -n tenant-test delete foundationdb <name> plus the same
timeout/wait loop to ensure deletion; ensure the registration call is invoked
where the resource is created so teardown has the correct $name even if the test
fails.
hack/e2e-apps/postgres.bats (1)

56-57: ⚡ Quick win

Move inline cleanup to framework-level teardown.

Both delete calls are embedded in the test body without --ignore-not-found. If any earlier step times out, execution never reaches these lines and the resources linger in the namespace. The pre-existing cleanup on line 5 uses --ignore-not-found || true; the post-test deletes should follow the same defensiveness, and ideally be moved to a bats teardown (or equivalent framework hook) so they run regardless of test outcome.

♻️ Minimal defensive fix (without restructuring)
-  kubectl -n tenant-test delete postgreses.apps.cozystack.io $name
-  kubectl -n tenant-test delete job.batch/postgres-$name-init-job
+  kubectl -n tenant-test delete postgreses.apps.cozystack.io $name --ignore-not-found
+  kubectl -n tenant-test delete job.batch/postgres-$name-init-job --ignore-not-found

Longer-term, extract these into a bats teardown() function so they execute even on test failure.

Based on learnings: "replace inline cleanup (e.g., kubectl delete in the test body) with framework-level teardown/cleanup … move cleanup logic to a global or framework-provided teardown so the framework can clean namespaces between runs."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hack/e2e-apps/postgres.bats` around lines 56 - 57, The two inline cleanup
kubectl commands ("kubectl -n tenant-test delete postgreses.apps.cozystack.io
$name" and "kubectl -n tenant-test delete job.batch/postgres-$name-init-job")
should be moved out of the test body into a framework-level teardown (e.g., bats
teardown()) so they always run even on failure, and made defensive by adding
--ignore-not-found (or appending "|| true") to each delete call; update the test
to remove these inline deletes and implement the teardown function that performs
both deletes with --ignore-not-found to guarantee cleanup.
hack/e2e-apps/vminstance.bats (1)

64-68: ⚡ Quick win

Consider reordering: HR readiness check (line 68) should precede the VMI IP poll (line 67).

The current sequence is:

  1. Line 66 — backstop: HR exists
  2. Line 67 — poll: VMI has an IP address (20 s)
  3. Line 68 — kubectl wait hr: HR condition=ready (5 s)

Logically, HR readiness is a prerequisite for the VMI to even be created, so the readiness check at line 68 should come right after the existence backstop (line 66), before polling for the VMI IP. In practice this still passes (a running VMI implies the HR is already reconciled), but the ordering is misleading and the 5 s window at line 68 is backed only by that implication rather than an explicit guarantee.

♻️ Suggested reorder
   timeout 60 sh -ec "until kubectl -n tenant-test get hr vm-instance-$name >/dev/null 2>&1; do sleep 2; done"
+  kubectl -n tenant-test wait hr vm-instance-$name --timeout=5s --for=condition=ready
   timeout 20 sh -ec "until kubectl -n tenant-test get vmi vm-instance-$name -o jsonpath='{.status.interfaces[0].ipAddress}' | grep -q '[0-9]'; do sleep 5; done"
-  kubectl -n tenant-test wait hr vm-instance-$name --timeout=5s --for=condition=ready
   kubectl -n tenant-test wait vm vm-instance-$name --timeout=20s --for=condition=ready
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hack/e2e-apps/vminstance.bats` around lines 64 - 68, The test orders the
checks incorrectly: after ensuring the HelmRelease exists, move the readiness
check (the kubectl wait hr --for=condition=ready invocation for hr
vm-instance-$name) to run immediately after the existence backstop and before
polling the VMI IP address; this guarantees the HR is reconciled before the VMI
IP poll (the kubectl -n tenant-test get vmi ...
jsonpath='{.status.interfaces[0].ipAddress}' | grep -q '[0-9]') runs.
hack/e2e-apps/redis.bats (1)

31-31: ⚡ Quick win

Consider moving inline cleanup to a teardown() function.

clickhouse.bats, openbao.bats, kafka.bats, mariadb.bats, and redis.bats all rely on inline kubectl delete at the end of each test body for cleanup. If a test assertion fails mid-run, the cleanup is skipped, which can leave stale resources that pollute subsequent runs. A teardown() hook runs unconditionally after every test.

♻️ Example for redis.bats
+teardown() {
+  kubectl -n tenant-test delete redis.apps.cozystack.io --all --ignore-not-found 2>/dev/null || true
+}
+
 `@test` "Create Redis" {
   ...
-  kubectl -n tenant-test delete redis.apps.cozystack.io $name
 }

Based on learnings: "In E2E bats tests like hack/e2e-apps/harbor.bats, replace inline cleanup (e.g., kubectl delete in the test body) with framework-level teardown/cleanup… Apply this pattern to all bats tests under hack/e2e-apps."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hack/e2e-apps/redis.bats` at line 31, Replace the inline cleanup command
"kubectl -n tenant-test delete redis.apps.cozystack.io $name" inside the
redis.bats test body with a bats teardown() hook so cleanup always runs; add a
teardown() function in hack/e2e-apps/redis.bats that unconditionally runs the
same kubectl delete (using the same $name variable or a shared fixture variable)
and remove the inline delete from the test body; apply the same pattern to
clickhouse.bats, openbao.bats, kafka.bats, and mariadb.bats to ensure resources
are deleted even when a test assertion fails.
hack/e2e-apps/bucket.bats (1)

22-22: ⚡ Quick win

hr bucket-${name} wait also lacks an existence backstop.

Line 22's kubectl wait hr bucket-${name} --timeout=100s is the first kubectl wait in the test and has no preceding polling loop. For consistency with the backstops added on lines 23–28, consider adding one here too.

✏️ Suggested addition
+  timeout 60 sh -ec "until kubectl -n tenant-test get hr bucket-${name} >/dev/null 2>&1; do sleep 2; done"
   kubectl -n tenant-test wait hr bucket-${name} --timeout=100s --for=condition=ready
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hack/e2e-apps/bucket.bats` at line 22, The `kubectl -n tenant-test wait hr
bucket-${name} --timeout=100s --for=condition=ready` call needs an existence
backstop like the other waits; add a short polling loop that retries `kubectl -n
tenant-test get hr bucket-${name}` (or equivalent) with a small sleep and
limited attempts until it returns success before invoking the `kubectl wait` for
readiness, mirroring the backstop pattern used for the later waits in the test
to avoid racing on first wait.
hack/e2e-apps/harbor.bats (1)

54-70: ⚡ Quick win

$release-system kubectl wait is missing an existence backstop.

Every other kubectl wait in this file now has a preceding polling backstop, but the $release-system wait at line 54 does not. If the operator hasn't materialised the hr $release-system object yet, this will silently block for the full --timeout=600s (10 minutes) before producing a failure — exactly the problem the PR set out to fix.

✏️ Suggested addition
+  timeout 60 sh -ec "until kubectl -n tenant-test get hr $release-system >/dev/null 2>&1; do sleep 2; done"
   kubectl -n tenant-test wait hr $release-system --timeout=600s --for=condition=ready || {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hack/e2e-apps/harbor.bats` around lines 54 - 70, Add a short existence poll
before the kubectl wait for hr $release-system: before the line calling "kubectl
-n tenant-test wait hr $release-system --timeout=600s --for=condition=ready" add
a loop that repeatedly runs "kubectl -n tenant-test get hr $release-system"
(sleeping briefly between attempts) and fails after a short backstop (e.g.,
30–60s) if the HelmRelease object never appears; this mirrors the other waits'
pattern and ensures the script fails fast when the hr resource hasn't been
created before invoking wait.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@hack/e2e-apps/kafka.bats`:
- Around line 46-47: The two commands added in hack/e2e-apps/kafka.bats
hard-code the Kafka resource name as "test"; change both occurrences to use the
existing name variable (use "$name") so they follow the PR convention and will
guard the correct resource; update the timeout loop command (until kubectl -n
tenant-test get kafkas ...) and the kubectl wait invocation (kubectl wait kafkas
-n tenant-test ...) to reference "$name" instead of the literal "test".

In `@hack/e2e-apps/qdrant.bats`:
- Line 24: Add the same existence backstop before calling kubectl wait on the
HelmRelease resource qdrant-$name-system: detect the presence of the resource
(e.g., poll kubectl -n tenant-test get hr qdrant-$name-system until it exists or
a short timeout elapses) and only then run kubectl -n tenant-test wait hr
qdrant-$name-system --for=condition=ready --timeout=120s; this mirrors the guard
used for qdrant-$name and prevents kubectl wait from hanging when the
HelmRelease does not exist.

In `@hack/e2e-install-cozystack.bats`:
- Around line 288-290: Add an existence backstop before the "kubectl wait
hr/tenant-test" call: poll for the HelmRelease resource (hr/tenant-test) to
exist (e.g. with a loop using "kubectl get hr/tenant-test >/dev/null 2>&1" and a
short sleep/retry) after the Tenant resource is applied, and only then run
"kubectl wait hr/tenant-test --for=condition=ready"; this ensures the kubectl
wait on hr/tenant-test doesn't fail immediately due to the resource not yet
being created.

---

Nitpick comments:
In `@hack/e2e-apps/bucket.bats`:
- Line 22: The `kubectl -n tenant-test wait hr bucket-${name} --timeout=100s
--for=condition=ready` call needs an existence backstop like the other waits;
add a short polling loop that retries `kubectl -n tenant-test get hr
bucket-${name}` (or equivalent) with a small sleep and limited attempts until it
returns success before invoking the `kubectl wait` for readiness, mirroring the
backstop pattern used for the later waits in the test to avoid racing on first
wait.

In `@hack/e2e-apps/foundationdb.bats`:
- Around line 119-123: The test currently does inline cleanup with "kubectl -n
tenant-test delete foundationdb $name" and a wait loop which will be skipped if
the test fails; move this logic into the shared framework-level teardown so
cleanup always runs. Remove the two lines from hack/e2e-apps/foundationdb.bats
and instead register the created resource name (e.g., push $name into a shared
array like CLEANUP_RESOURCES or call a helper register_cleanup in the test),
then implement a teardown() in the common bats helper (or the framework harness)
that iterates CLEANUP_RESOURCES and performs the kubectl -n tenant-test delete
foundationdb <name> plus the same timeout/wait loop to ensure deletion; ensure
the registration call is invoked where the resource is created so teardown has
the correct $name even if the test fails.

In `@hack/e2e-apps/harbor.bats`:
- Around line 54-70: Add a short existence poll before the kubectl wait for hr
$release-system: before the line calling "kubectl -n tenant-test wait hr
$release-system --timeout=600s --for=condition=ready" add a loop that repeatedly
runs "kubectl -n tenant-test get hr $release-system" (sleeping briefly between
attempts) and fails after a short backstop (e.g., 30–60s) if the HelmRelease
object never appears; this mirrors the other waits' pattern and ensures the
script fails fast when the hr resource hasn't been created before invoking wait.

In `@hack/e2e-apps/mongodb.bats`:
- Around line 41-43: Move the inline cleanup command "kubectl -n tenant-test
delete mongodbs.apps.cozystack.io $name" out of the test body and into a
framework-level teardown function so it runs unconditionally after each test;
ensure the teardown can access the test's "name" variable (make it a
global/exported variable or set it in setup) and call the same kubectl delete
there (or use --ignore-not-found) to guarantee cleanup on timeouts/failures, and
apply this teardown pattern to all other bats tests under hack/e2e-apps.

In `@hack/e2e-apps/postgres.bats`:
- Around line 56-57: The two inline cleanup kubectl commands ("kubectl -n
tenant-test delete postgreses.apps.cozystack.io $name" and "kubectl -n
tenant-test delete job.batch/postgres-$name-init-job") should be moved out of
the test body into a framework-level teardown (e.g., bats teardown()) so they
always run even on failure, and made defensive by adding --ignore-not-found (or
appending "|| true") to each delete call; update the test to remove these inline
deletes and implement the teardown function that performs both deletes with
--ignore-not-found to guarantee cleanup.

In `@hack/e2e-apps/qdrant.bats`:
- Line 29: Remove the inline cleanup call "kubectl -n tenant-test delete
qdrant.apps.cozystack.io $name" from the qdrant.bats test body and instead
register the resource deletion in the framework-level/global teardown hook used
by the E2E tests; ensure the teardown runs regardless of test failures and
references the same $name variable (or capture the created resource name in a
shared context) so the framework teardown performs the equivalent deletion for
Qdrant resources created by the test.

In `@hack/e2e-apps/redis.bats`:
- Line 31: Replace the inline cleanup command "kubectl -n tenant-test delete
redis.apps.cozystack.io $name" inside the redis.bats test body with a bats
teardown() hook so cleanup always runs; add a teardown() function in
hack/e2e-apps/redis.bats that unconditionally runs the same kubectl delete
(using the same $name variable or a shared fixture variable) and remove the
inline delete from the test body; apply the same pattern to clickhouse.bats,
openbao.bats, kafka.bats, and mariadb.bats to ensure resources are deleted even
when a test assertion fails.

In `@hack/e2e-apps/vminstance.bats`:
- Around line 64-68: The test orders the checks incorrectly: after ensuring the
HelmRelease exists, move the readiness check (the kubectl wait hr
--for=condition=ready invocation for hr vm-instance-$name) to run immediately
after the existence backstop and before polling the VMI IP address; this
guarantees the HR is reconciled before the VMI IP poll (the kubectl -n
tenant-test get vmi ... jsonpath='{.status.interfaces[0].ipAddress}' | grep -q
'[0-9]') runs.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3abdaa9d-ac42-4e55-9faa-168c4dfab528

📥 Commits

Reviewing files that changed from the base of the PR and between 5786afe and 9a71dc1.

📒 Files selected for processing (16)
  • hack/e2e-apps/bucket.bats
  • hack/e2e-apps/clickhouse.bats
  • hack/e2e-apps/external-dns.bats
  • hack/e2e-apps/foundationdb.bats
  • hack/e2e-apps/harbor.bats
  • hack/e2e-apps/kafka.bats
  • hack/e2e-apps/mariadb.bats
  • hack/e2e-apps/mongodb.bats
  • hack/e2e-apps/openbao.bats
  • hack/e2e-apps/postgres.bats
  • hack/e2e-apps/qdrant.bats
  • hack/e2e-apps/redis.bats
  • hack/e2e-apps/run-kubernetes.sh
  • hack/e2e-apps/vminstance.bats
  • hack/e2e-install-cozystack.bats
  • hack/e2e-test-openapi.bats

Comment thread hack/e2e-apps/kafka.bats Outdated
Comment thread hack/e2e-apps/qdrant.bats
# kicks in (kubectl wait errors immediately if the object does not exist yet).
timeout 60 sh -ec "until kubectl -n tenant-test get hr qdrant-$name >/dev/null 2>&1; do sleep 2; done"
kubectl -n tenant-test wait hr qdrant-$name --timeout=60s --for=condition=ready
kubectl -n tenant-test wait hr qdrant-$name-system --timeout=120s --for=condition=ready

@coderabbitai coderabbitai Bot May 2, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing existence backstop for qdrant-$name-system HelmRelease.

Line 22 adds a polling guard before waiting on hr qdrant-$name, but line 24's kubectl wait hr qdrant-$name-system has no equivalent backstop. Per the PR's own stated rationale, kubectl wait against a non-existent resource blocks silently for the full timeout (120 s here) and then fails with an unhelpful error — the exact problem this PR is fixing everywhere else.

🛡️ Proposed fix
  timeout 60 sh -ec "until kubectl -n tenant-test get hr qdrant-$name >/dev/null 2>&1; do sleep 2; done"
  kubectl -n tenant-test wait hr qdrant-$name --timeout=60s --for=condition=ready
+ timeout 60 sh -ec "until kubectl -n tenant-test get hr qdrant-$name-system >/dev/null 2>&1; do sleep 2; done"
  kubectl -n tenant-test wait hr qdrant-$name-system --timeout=120s --for=condition=ready
📝 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
kubectl -n tenant-test wait hr qdrant-$name-system --timeout=120s --for=condition=ready
timeout 60 sh -ec "until kubectl -n tenant-test get hr qdrant-$name-system >/dev/null 2>&1; do sleep 2; done"
kubectl -n tenant-test wait hr qdrant-$name-system --timeout=120s --for=condition=ready
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hack/e2e-apps/qdrant.bats` at line 24, Add the same existence backstop before
calling kubectl wait on the HelmRelease resource qdrant-$name-system: detect the
presence of the resource (e.g., poll kubectl -n tenant-test get hr
qdrant-$name-system until it exists or a short timeout elapses) and only then
run kubectl -n tenant-test wait hr qdrant-$name-system --for=condition=ready
--timeout=120s; this mirrors the guard used for qdrant-$name and prevents
kubectl wait from hanging when the HelmRelease does not exist.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

qdrant-$name-system isn't created by the operator — it's a kind: HelmRelease template rendered inside the outer chart at packages/apps/qdrant/templates/qdrant.yaml. By the time line 23's kubectl wait hr qdrant-$name --for=condition=ready returns, the outer chart has been fully applied, so qdrant-$name-system already exists in the cluster. No race here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@myasnikovdaniil, thanks for the clear explanation — that makes sense. Since qdrant-$name-system is templated as a child HelmRelease inside the outer Qdrant chart (at packages/apps/qdrant/templates/qdrant.yaml), it is guaranteed to exist once the outer HelmRelease reaches ready, so the existence backstop is indeed unnecessary here. I'll retract this comment.


✏️ Learnings added
Learnt from: myasnikovdaniil
Repo: cozystack/cozystack PR: 2554
File: hack/e2e-apps/qdrant.bats:24-24
Timestamp: 2026-05-05T08:35:27.054Z
Learning: In `hack/e2e-apps/qdrant.bats`, `hr qdrant-$name-system` does NOT need an existence backstop before `kubectl wait`. It is a `kind: HelmRelease` template rendered inside the outer Qdrant Helm chart (`packages/apps/qdrant/templates/qdrant.yaml`), not created asynchronously by the operator. By the time `kubectl wait hr qdrant-$name --for=condition=ready` completes, the outer chart is fully applied and `qdrant-$name-system` already exists in the cluster.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: lexfrei
Repo: cozystack/cozystack PR: 2058
File: hack/e2e-apps/harbor.bats:3-45
Timestamp: 2026-02-16T18:50:59.859Z
Learning: In E2E bats tests like hack/e2e-apps/harbor.bats, replace inline cleanup (e.g., kubectl delete in the test body) with framework-level teardown/cleanup. Do not rely on traps or per-test cleanup; instead, move cleanup logic to a global or framework-provided teardown so the framework can clean namespaces between runs. Apply this pattern to all bats tests under hack/e2e-apps (and similar E2E suites) to ensure consistent and reliable cleanup without embedding it in individual tests.

Learnt from: myasnikovdaniil
Repo: cozystack/cozystack PR: 2559
File: hack/select-e2e_test.bats:75-80
Timestamp: 2026-05-02T14:46:54.942Z
Learning: In Bash Automated Testing System (Bats) tests, the `run` helper captures command output and strips the trailing newline from `$output`. Therefore, for single-line commands, exact string assertions like `[ "$output" = "redis" ]` should be safe and should not be tripped up by trailing newline/whitespace. No extra `xargs`-style trimming is needed solely to remove a trailing newline for single-line outputs.

Comment thread hack/e2e-install-cozystack.bats
- kafka.bats: use $name in the kafkas existence backstop, matching the
  variable used in the kubectl wait below it (consistency with the rest
  of this PR's backstops).
- e2e-install-cozystack.bats: the Tenant→HelmRelease materialisation is
  asynchronous (cozystack-controller observes the Tenant CR and creates
  the HR), so guard hr/tenant-test with the same existence backstop
  pattern used elsewhere.

Signed-off-by: Myasnikov Daniil <[email protected]>
@dosubot dosubot Bot added size:L and removed size/L This PR changes 100-499 lines, ignoring generated files labels May 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM. Solid replacement of fragile sleep N patterns with event-driven backstops across 14 sites. The shape — until kubectl get <resource> >/dev/null 2>&1; do sleep 2; done followed by kubectl wait --for=condition=… — is the canonical fix for the "kubectl wait against not-yet-created object sits silent for the full timeout" failure mode, and the inline rationale on each call site documents the why.

What's solid:

  • E2E Tests is green on this HEAD — load-bearing verification that the replacements actually work in a running cluster, not just in static analysis. Replacing 14 sleep sites without breaking E2E is not trivial; the fact that CI passes is the empirical signal that the existence-backstops cover the right resources.
  • Genuine sleeps preserved with # TODO(e2e-replace-fixed-timeouts): genuine sleep … annotations explaining why no API signal would suffice (HR-count emission heuristic, LINSTOR node membership propagation, MetalLB end-to-end HTTP path). Future readers see they are intentional rather than missed during the sweep.
  • kafka.bats fix in 12ccc53d9 parametrizes the existence-backstop — kubectl get kafkas test → kubectl get kafkas $name — so the backstop actually matches the resource name the test case creates.
  • e2e-test-openapi.bats fixes the trap "kill $!" scoping bug while at it: kubectl proxy & proxy_pid=$!; trap "kill $proxy_pid" EXIT evaluates $! at trap-set time so it captures the right PID, plus nc -z localhost 21234 polling replaces the fixed sleep 0.5 with a real readiness signal.
  • Backwards-compatible: tests that pass today on the old sleep timing pass on the new event-driven path; failure mode is now seconds-to-fail with a clear "resource never appeared" error instead of 5-minute silent timeouts.

Three nit-level observations, none blocking:

  1. Pre-existing kafka.bats inconsistency: kubectl wait kafkas -n tenant-test test --timeout=300s --for=condition=ready (line 47) still hardcodes the resource name test instead of $name. The existence-backstop one line above was correctly parametrized in 12ccc53d9, but the wait itself wasn't. Not introduced by this PR — but 12ccc53d9 is touching the surrounding lines, so reasonably in scope to fix here as a follow-up commit.
  2. Comment duplication: the same 2-line shebang ("Wait for the operator to materialise the HelmRelease … kubectl wait errors immediately if the object does not exist yet") appears verbatim in 14 sites. A sourced helper wait_for_resource <ns> <kind> <name> [poll_interval] would DRY this up considerably, but that's a separate refactor.
  3. Inconsistent poll cadence in the backstops: most use sleep 2, kafka PVC uses sleep 10, external-dns uses sleep 5. Cosmetic — sleep 2 would work everywhere; the sleep 10 cases predate this PR. Not worth a fix-up commit.

@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label May 5, 2026
…2554)

Address lexfrei's nit on PR #2554: the pre-existing
`kubectl wait kafkas -n tenant-test test ...` on line 47 hardcoded the
resource name, while the existence backstop one line above was already
parametrized with `$name` in 12ccc53. Switch the wait to `$name` for
consistency so changing the test's resource name in one place keeps the
two lines aligned.

Signed-off-by: Myasnikov Daniil <[email protected]>
@github-actions github-actions Bot added size/L This PR changes 100-499 lines, ignoring generated files and removed size:L labels May 6, 2026
@myasnikovdaniil
myasnikovdaniil merged commit 7585b0c into main May 6, 2026
16 of 18 checks passed
@myasnikovdaniil
myasnikovdaniil deleted the daniil/split-event-driven branch May 6, 2026 13:13

@kvaps Andrei Kvapil (kvaps) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM. Sweep is correctly scoped and the pattern is right.

The until kubectl get … >/dev/null; do sleep 2; done + kubectl wait --for=condition=… shape is the right way to cooperate with informers on resources that the operator materializes asynchronously. Without the existence backstop, kubectl wait sits silently against a NotFound for the full timeout and surfaces a useless "timed out waiting for the condition" — diagnosis-time impact is real.

14 sites covered across the per-app bats files plus the install/openapi/run-kubernetes harness — exactly the surface that flakes on cold runs. Genuine sleeps that survived (LINSTOR node membership propagation, MetalLB end-to-end HTTP, HR-count emission heuristic) are TODO-annotated with rationale, which is the right call for sleeps that aren't race-prone polling.

Vm-disk hunk in vminstance.bats co-landing here, vm-instance hunks in #2555 — clean split.

Same green-path semantics throughout, failure surface drops from 5-minute opaque timeouts to seconds with a clear "resource never appeared" error.

Ship it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/testing Issues or PRs related to testing (e2e, bats, unit tests) kind/cleanup Categorizes issue or PR as related to cleanup of code, process, or technical debt lgtm This PR has been approved by a maintainer quality-of-life QoL improvements size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants