diff --git a/tests/bdd/PLAN.md b/tests/bdd/PLAN.md index 2f90cf257..be79b61bb 100644 --- a/tests/bdd/PLAN.md +++ b/tests/bdd/PLAN.md @@ -137,6 +137,7 @@ refactor in every consumer; that is a feature. | `Then the rendered manifests in {string} should contain:` (table) | Requires a `text` header and one or more fixed strings. Recursively inspects regular files under the repo-relative directory and fails if any listed string is absent. `${VAR}` expansion applies to the path and table values. | | `Then the rendered manifests in {string} under directories matching {string} should contain:` (table) | Positive rendered-manifest assertion scoped to files below a directory whose name matches the supplied shell pattern, such as `*-nats`. The render directory, directory-name pattern, and table values support `${VAR}` expansion. | | `Then the rendered manifests in {string} should not contain:` (table) | Requires a `text` header and one or more fixed strings. Recursively inspects regular files under the repo-relative directory and fails if any listed string appears. `${VAR}` expansion applies to the path and table values. | +| `Then these Helm releases should be deployed using context {string}:` (table) | Requires `name` and `namespace` headers, with an optional `revision` header. Runs one explicit-context, all-namespaces `helm list` and asserts that every listed release has status `deployed`; non-empty revision cells are also matched. | | `Then these ServiceMonitors should exist in namespace {string} using context {string}:` (table) | Requires a `name` header and one or more names. Runs one `kubectl get` with every named ServiceMonitor; exit code 0 proves every listed resource exists. | #### YAML comparison semantics @@ -256,8 +257,9 @@ Going away in `tests/bdd`: `Template`, `HelmfileTemplate`, `RegisterCluster`, `InstallNvcaOperator`). Replaced by `When I run command "make ..."`. - The Helm release readback methods (`HelmReleaseDeployed`, - `NVCAOperatorReady`, `NVCAAgentReady`). Replaced by `helm list -o json` - + `kubectl rollout status` / `kubectl wait` directly in Gherkin. + `NVCAOperatorReady`, `NVCAAgentReady`). Release deployment checks use the + table-driven Helm assertion; readiness remains explicit through + `kubectl rollout status` / `kubectl wait` in Gherkin. - The `harness.CLIHarness` interface and its five domain methods (`SelfHostedUp`, `SelfHostedInstallControlPlane`, `SelfHostedComputePlaneRegister`, `SelfHostedComputePlaneInstall`, @@ -460,6 +462,21 @@ func FilesDoNotContain(root string, needles []string) error // to files below a directory whose name matches the shell pattern. func FilesContain(root, directoryNamePattern string, needles []string) error +// HelmListCommand builds an explicit-context, all-namespaces Helm list command. +func HelmListCommand(kubeContext string) (string, error) + +// HelmReleaseExpectation identifies a deployed Helm release and optionally +// pins the expected revision. +type HelmReleaseExpectation struct { + Name string + Namespace string + Revision string +} + +// HelmReleasesDeployed asserts that every expected release exists in Helm's +// JSON output with status deployed and, when provided, the expected revision. +func HelmReleasesDeployed(raw string, expected []HelmReleaseExpectation) error + // ServiceMonitorExistenceCommand builds one kubectl get command whose // successful exit proves every named ServiceMonitor exists. func ServiceMonitorExistenceCommand(namespace, kubeContext string, names []string) (string, error) diff --git a/tests/bdd/dsl/helm.go b/tests/bdd/dsl/helm.go new file mode 100644 index 000000000..55385ff59 --- /dev/null +++ b/tests/bdd/dsl/helm.go @@ -0,0 +1,104 @@ +/* +SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dsl + +import ( + "encoding/json" + "fmt" + "strings" +) + +// HelmReleaseExpectation identifies a deployed Helm release and optionally +// pins the expected revision. +type HelmReleaseExpectation struct { + Name string + Namespace string + Revision string +} + +type helmRelease struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Revision any `json:"revision"` + Status string `json:"status"` +} + +// HelmListCommand builds an explicit-context, all-namespaces Helm list command. +func HelmListCommand(kubeContext string) (string, error) { + kubeContext = strings.TrimSpace(Interpolate(kubeContext)) + if kubeContext == "" { + return "", fmt.Errorf("kube context is empty") + } + return "helm list --all-namespaces --kube-context " + quoteCommandArg(kubeContext) + " -o json", nil +} + +// HelmReleasesDeployed asserts that every expected release exists in Helm's +// JSON output with status deployed and, when provided, the expected revision. +func HelmReleasesDeployed(raw string, expected []HelmReleaseExpectation) error { + var actual []helmRelease + if err := json.Unmarshal([]byte(raw), &actual); err != nil { + return fmt.Errorf("parse helm list json: %w", err) + } + if len(expected) == 0 { + return fmt.Errorf("expected Helm releases are empty") + } + + for _, rawExpectation := range expected { + expectation := HelmReleaseExpectation{ + Name: strings.TrimSpace(Interpolate(rawExpectation.Name)), + Namespace: strings.TrimSpace(Interpolate(rawExpectation.Namespace)), + Revision: strings.TrimSpace(Interpolate(rawExpectation.Revision)), + } + if expectation.Name == "" { + return fmt.Errorf("helm release name is empty") + } + if expectation.Namespace == "" { + return fmt.Errorf("helm release %q namespace is empty", expectation.Name) + } + + release, found := findHelmRelease(actual, expectation.Name, expectation.Namespace) + if !found { + return describeMissingHelmRelease(actual, expectation) + } + if release.Status != "deployed" { + return fmt.Errorf("helm release %q in namespace %q status = %q, want %q", expectation.Name, expectation.Namespace, release.Status, "deployed") + } + if expectation.Revision != "" && fmt.Sprint(release.Revision) != expectation.Revision { + return fmt.Errorf("helm release %q in namespace %q revision = %q, want %q", expectation.Name, expectation.Namespace, fmt.Sprint(release.Revision), expectation.Revision) + } + } + return nil +} + +func findHelmRelease(releases []helmRelease, name, namespace string) (helmRelease, bool) { + for _, release := range releases { + if release.Name == name && release.Namespace == namespace { + return release, true + } + } + return helmRelease{}, false +} + +func describeMissingHelmRelease(releases []helmRelease, expected HelmReleaseExpectation) error { + for _, release := range releases { + if release.Name == expected.Name { + return fmt.Errorf("helm release %q namespace = %q, want %q", expected.Name, release.Namespace, expected.Namespace) + } + } + return fmt.Errorf("helm release %q in namespace %q is missing", expected.Name, expected.Namespace) +} diff --git a/tests/bdd/dsl/helm_test.go b/tests/bdd/dsl/helm_test.go new file mode 100644 index 000000000..5407f5e53 --- /dev/null +++ b/tests/bdd/dsl/helm_test.go @@ -0,0 +1,92 @@ +/* +SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dsl + +import ( + "strings" + "testing" +) + +const deployedHelmReleases = `[ + {"name":"nats","namespace":"nats-system","revision":"2","status":"deployed"}, + {"name":"api","namespace":"nvcf","revision":3,"status":"failed"} +]` + +func TestHelmListCommandUsesExplicitContext(t *testing.T) { + t.Setenv("BDD_TMP_CONTEXT", "k3d-ncp-local") + got, err := HelmListCommand("${BDD_TMP_CONTEXT}") + if err != nil { + t.Fatalf("build command: %v", err) + } + want := "helm list --all-namespaces --kube-context k3d-ncp-local -o json" + if got != want { + t.Fatalf("command = %q, want %q", got, want) + } +} + +func TestHelmListCommandRejectsEmptyContext(t *testing.T) { + if _, err := HelmListCommand(""); err == nil { + t.Fatal("expected empty context error") + } +} + +func TestHelmReleasesDeployedMatchesOptionalRevision(t *testing.T) { + expected := []HelmReleaseExpectation{{Name: "nats", Namespace: "nats-system", Revision: "2"}} + if err := HelmReleasesDeployed(deployedHelmReleases, expected); err != nil { + t.Fatalf("assert releases: %v", err) + } + + expected[0].Revision = "" + if err := HelmReleasesDeployed(deployedHelmReleases, expected); err != nil { + t.Fatalf("assert release without revision: %v", err) + } +} + +func TestHelmReleasesDeployedReportsMissingRelease(t *testing.T) { + err := HelmReleasesDeployed(deployedHelmReleases, []HelmReleaseExpectation{{Name: "missing", Namespace: "nvcf"}}) + if err == nil || !strings.Contains(err.Error(), `helm release "missing" in namespace "nvcf" is missing`) { + t.Fatalf("error = %v", err) + } +} + +func TestHelmReleasesDeployedReportsNamespaceMismatch(t *testing.T) { + err := HelmReleasesDeployed(deployedHelmReleases, []HelmReleaseExpectation{{Name: "nats", Namespace: "wrong"}}) + if err == nil || !strings.Contains(err.Error(), `namespace = "nats-system", want "wrong"`) { + t.Fatalf("error = %v", err) + } +} + +func TestHelmReleasesDeployedReportsStatusMismatch(t *testing.T) { + err := HelmReleasesDeployed(deployedHelmReleases, []HelmReleaseExpectation{{Name: "api", Namespace: "nvcf"}}) + if err == nil || !strings.Contains(err.Error(), `status = "failed", want "deployed"`) { + t.Fatalf("error = %v", err) + } +} + +func TestHelmReleasesDeployedReportsRevisionMismatch(t *testing.T) { + err := HelmReleasesDeployed(deployedHelmReleases, []HelmReleaseExpectation{{Name: "nats", Namespace: "nats-system", Revision: "1"}}) + if err == nil || !strings.Contains(err.Error(), `revision = "2", want "1"`) { + t.Fatalf("error = %v", err) + } +} + +func TestHelmReleasesDeployedRejectsMalformedJSON(t *testing.T) { + if err := HelmReleasesDeployed("not json", []HelmReleaseExpectation{{Name: "nats", Namespace: "nats-system"}}); err == nil { + t.Fatal("expected parse error") + } +} diff --git a/tests/bdd/features/multi-cluster-eks-helmfile.feature b/tests/bdd/features/multi-cluster-eks-helmfile.feature index 26e2f17f6..8a8624772 100644 --- a/tests/bdd/features/multi-cluster-eks-helmfile.feature +++ b/tests/bdd/features/multi-cluster-eks-helmfile.feature @@ -199,25 +199,24 @@ Feature: Install a multi-cluster NVCF stack across two pre-provisioned EKS clust When I run command "make -C deploy/stacks/self-managed install HELMFILE_ENV=eks-bdd-multi" Then the command exit code should be 0 - When I run command "helm list --all-namespaces --kube-context ${EKS_CONTEXT} -o json" - Then the json output should contain rows: - | name | namespace | status | - | nats | nats-system | deployed | - | cert-manager | cert-manager | deployed | - | openbao-server | vault-system | deployed | - | cassandra | cassandra-system | deployed | - | api-keys | api-keys | deployed | - | sis | sis | deployed | - | api | nvcf | deployed | - | nvct-api | nvcf | deployed | - | invocation-service | nvcf | deployed | - | grpc-proxy | nvcf | deployed | - | ess-api | ess | deployed | - | notary-service | nvcf | deployed | - | admin-issuer-proxy | api-keys | deployed | - | reval | nvcf | deployed | - | nats-auth-callout-service | nats-system | deployed | - | ingress | envoy-gateway-system | deployed | + Then these Helm releases should be deployed using context "${EKS_CONTEXT}": + | name | namespace | + | nats | nats-system | + | cert-manager | cert-manager | + | openbao-server | vault-system | + | cassandra | cassandra-system | + | api-keys | api-keys | + | sis | sis | + | api | nvcf | + | nvct-api | nvcf | + | invocation-service | nvcf | + | grpc-proxy | nvcf | + | ess-api | ess | + | notary-service | nvcf | + | admin-issuer-proxy | api-keys | + | reval | nvcf | + | nats-auth-callout-service | nats-system | + | ingress | envoy-gateway-system | # Confirm gateway-routes templated global.domain into the api # HTTPRoute hostname on the control-plane cluster. @@ -339,10 +338,9 @@ Feature: Install a multi-cluster NVCF stack across two pre-provisioned EKS clust """ Then the command exit code should be 0 - When I run command "helm list -n nvca-operator --kube-context ${EKS_COMPUTE_CONTEXT} -o json" - Then the json output should contain rows: - | name | namespace | status | - | nvca-operator | nvca-operator | deployed | + Then these Helm releases should be deployed using context "${EKS_COMPUTE_CONTEXT}": + | name | namespace | + | nvca-operator | nvca-operator | When I run command "kubectl rollout status deployment/nvca-operator -n nvca-operator --context ${EKS_COMPUTE_CONTEXT} --timeout=10m" Then the command exit code should be 0 diff --git a/tests/bdd/features/multi-cluster-helmfile.feature b/tests/bdd/features/multi-cluster-helmfile.feature index 9f08c4809..98e1206d6 100644 --- a/tests/bdd/features/multi-cluster-helmfile.feature +++ b/tests/bdd/features/multi-cluster-helmfile.feature @@ -43,11 +43,13 @@ Feature: Install a local multi-cluster NVCF stack with Helmfile | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | api.env.NVCF_SIDECARS_LLM_ROUTER_CLIENT_IMAGE | nvcr.io/${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM}/stargate-client:0.2.0 | + | observability.profile | disabled | And I copy the file "tests/bdd/fixtures/nvcf-compute-plane-local-bdd-multi.yaml" to "deploy/stacks/nvcf-compute-plane/environments/local-bdd.yaml" And I update yaml file "deploy/stacks/nvcf-compute-plane/environments/local-bdd.yaml" with keys: - | global.imagePullSecrets[0].name | nvcr-pull-secret | - | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | - | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | + | global.imagePullSecrets[0].name | nvcr-pull-secret | + | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | + | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | + | observability.profile | disabled | And I copy the file "deploy/stacks/self-managed/secrets/secrets.yaml.template" to "deploy/stacks/self-managed/secrets/local-bdd-secrets.yaml" And I substitute "REPLACE_WITH_BASE64_DOCKER_CREDENTIAL" in file "deploy/stacks/self-managed/secrets/local-bdd-secrets.yaml" with base64 of "$oauthtoken:${NGC_API_KEY}" # Conflict precheck: single-cluster ncp-local's k3d serverlb @@ -84,27 +86,26 @@ Feature: Install a local multi-cluster NVCF stack with Helmfile When I run command "make -C deploy/stacks/self-managed install HELMFILE_ENV=local-bdd" Then the command exit code should be 0 - When I run command "helm list --all-namespaces --kube-context k3d-ncp-local-cp -o json" - Then the json output should contain rows: - | name | namespace | status | - | nats | nats-system | deployed | - | cert-manager | cert-manager | deployed | - | openbao-server | vault-system | deployed | - | cassandra | cassandra-system | deployed | - | api-keys | api-keys | deployed | - | sis | sis | deployed | - | api | nvcf | deployed | - | nvct-api | nvcf | deployed | - | invocation-service | nvcf | deployed | - | grpc-proxy | nvcf | deployed | - | ess-api | ess | deployed | - | notary-service | nvcf | deployed | - | admin-issuer-proxy | api-keys | deployed | - | reval | nvcf | deployed | - | nats-auth-callout-service | nats-system | deployed | - | ingress | envoy-gateway-system | deployed | - | llm-request-router | nvcf | deployed | - | llm-api-gateway | nvcf | deployed | + Then these Helm releases should be deployed using context "k3d-ncp-local-cp": + | name | namespace | + | nats | nats-system | + | cert-manager | cert-manager | + | openbao-server | vault-system | + | cassandra | cassandra-system | + | api-keys | api-keys | + | sis | sis | + | api | nvcf | + | nvct-api | nvcf | + | invocation-service | nvcf | + | grpc-proxy | nvcf | + | ess-api | ess | + | notary-service | nvcf | + | admin-issuer-proxy | api-keys | + | reval | nvcf | + | nats-auth-callout-service | nats-system | + | ingress | envoy-gateway-system | + | llm-request-router | nvcf | + | llm-api-gateway | nvcf | # These routes are installed by ncp-local before the Helmfile # stack, then become fully resolved once the control-plane @@ -213,10 +214,9 @@ Feature: Install a local multi-cluster NVCF stack with Helmfile """ Then the command exit code should be 0 - When I run command "helm list -n nvca-operator --kube-context k3d-ncp-local-compute-1 -o json" - Then the json output should contain rows: - | name | namespace | status | - | nvca-operator | nvca-operator | deployed | + Then these Helm releases should be deployed using context "k3d-ncp-local-compute-1": + | name | namespace | + | nvca-operator | nvca-operator | When I run command "kubectl rollout status deployment/nvca-operator -n nvca-operator --context k3d-ncp-local-compute-1 --timeout=10m" Then the command exit code should be 0 diff --git a/tests/bdd/features/observability-all.feature b/tests/bdd/features/observability-all.feature index 84050469a..68ddfa932 100644 --- a/tests/bdd/features/observability-all.feature +++ b/tests/bdd/features/observability-all.feature @@ -104,15 +104,14 @@ Feature: Install local Helmfile observability for both planes # Revision 1 proves the compute install did not reinstall or upgrade the # shared observability releases created by the control-plane install. - When I run command "helm list --all-namespaces --kube-context k3d-ncp-local -o json" - Then the json output should contain rows: - | name | namespace | revision | status | - | prometheus-operator-crds | monitoring | 1 | deployed | - | opentelemetry-operator | monitoring | 1 | deployed | - | victoria-metrics | monitoring | 1 | deployed | - | otel-collector | monitoring | 1 | deployed | - | default-monitors | monitoring | 1 | deployed | - | nvca-operator | nvca-operator | 1 | deployed | + Then these Helm releases should be deployed using context "k3d-ncp-local": + | name | namespace | revision | + | prometheus-operator-crds | monitoring | 1 | + | opentelemetry-operator | monitoring | 1 | + | victoria-metrics | monitoring | 1 | + | otel-collector | monitoring | 1 | + | default-monitors | monitoring | 1 | + | nvca-operator | nvca-operator | 1 | When I run command "kubectl rollout status deployment/nvca-operator -n nvca-operator --context k3d-ncp-local --timeout=10m" Then the command exit code should be 0 diff --git a/tests/bdd/features/observability-compute.feature b/tests/bdd/features/observability-compute.feature index 84a66e3ba..7e2cf102d 100644 --- a/tests/bdd/features/observability-compute.feature +++ b/tests/bdd/features/observability-compute.feature @@ -118,15 +118,14 @@ Feature: Install local Helmfile observability with the compute profile kubectl --context k3d-ncp-local-compute-1 delete pod --namespace nvca-system --selector app.kubernetes.io/name=nvca --wait=false """ - When I run command "helm list --all-namespaces --kube-context k3d-ncp-local-compute-1 -o json" - Then the json output should contain rows: - | name | namespace | status | - | prometheus-operator-crds | monitoring | deployed | - | opentelemetry-operator | monitoring | deployed | - | victoria-metrics | monitoring | deployed | - | otel-collector | monitoring | deployed | - | default-monitors | monitoring | deployed | - | nvca-operator | nvca-operator | deployed | + Then these Helm releases should be deployed using context "k3d-ncp-local-compute-1": + | name | namespace | + | prometheus-operator-crds | monitoring | + | opentelemetry-operator | monitoring | + | victoria-metrics | monitoring | + | otel-collector | monitoring | + | default-monitors | monitoring | + | nvca-operator | nvca-operator | When I run command "kubectl rollout status deployment/nvca-operator -n nvca-operator --context k3d-ncp-local-compute-1 --timeout=10m" Then the command exit code should be 0 diff --git a/tests/bdd/features/observability-control.feature b/tests/bdd/features/observability-control.feature index 78ec6baa8..8bc861e90 100644 --- a/tests/bdd/features/observability-control.feature +++ b/tests/bdd/features/observability-control.feature @@ -54,14 +54,13 @@ Feature: Install local Helmfile observability with the control profile Scenario: Control profile installs shared infrastructure and control monitors When I successfully run command "make -C deploy/stacks/self-managed install HELMFILE_ENV=local-bdd-observability-control" - When I run command "helm list --all-namespaces --kube-context k3d-ncp-local -o json" - Then the json output should contain rows: - | name | namespace | status | - | prometheus-operator-crds | monitoring | deployed | - | opentelemetry-operator | monitoring | deployed | - | victoria-metrics | monitoring | deployed | - | otel-collector | monitoring | deployed | - | default-monitors | monitoring | deployed | + Then these Helm releases should be deployed using context "k3d-ncp-local": + | name | namespace | + | prometheus-operator-crds | monitoring | + | opentelemetry-operator | monitoring | + | victoria-metrics | monitoring | + | otel-collector | monitoring | + | default-monitors | monitoring | When I successfully run command "kubectl get opentelemetrycollector nvcf-observability -n monitoring --context k3d-ncp-local -o jsonpath='{.spec.targetAllocator.enabled}'" And the command output should contain "true" diff --git a/tests/bdd/features/single-cluster-eks-helmfile.feature b/tests/bdd/features/single-cluster-eks-helmfile.feature index 3f1b14a12..e5fab01ea 100644 --- a/tests/bdd/features/single-cluster-eks-helmfile.feature +++ b/tests/bdd/features/single-cluster-eks-helmfile.feature @@ -184,25 +184,24 @@ Feature: Install a single-cluster NVCF stack on a pre-provisioned EKS cluster wi When I run command "make -C deploy/stacks/self-managed install HELMFILE_ENV=eks-bdd" Then the command exit code should be 0 - When I run command "helm list --all-namespaces --kube-context ${EKS_CONTEXT} -o json" - Then the json output should contain rows: - | name | namespace | status | - | nats | nats-system | deployed | - | cert-manager | cert-manager | deployed | - | openbao-server | vault-system | deployed | - | cassandra | cassandra-system | deployed | - | api-keys | api-keys | deployed | - | sis | sis | deployed | - | api | nvcf | deployed | - | nvct-api | nvcf | deployed | - | invocation-service | nvcf | deployed | - | grpc-proxy | nvcf | deployed | - | ess-api | ess | deployed | - | notary-service | nvcf | deployed | - | admin-issuer-proxy | api-keys | deployed | - | reval | nvcf | deployed | - | nats-auth-callout-service | nats-system | deployed | - | ingress | envoy-gateway-system | deployed | + Then these Helm releases should be deployed using context "${EKS_CONTEXT}": + | name | namespace | + | nats | nats-system | + | cert-manager | cert-manager | + | openbao-server | vault-system | + | cassandra | cassandra-system | + | api-keys | api-keys | + | sis | sis | + | api | nvcf | + | nvct-api | nvcf | + | invocation-service | nvcf | + | grpc-proxy | nvcf | + | ess-api | ess | + | notary-service | nvcf | + | admin-issuer-proxy | api-keys | + | reval | nvcf | + | nats-auth-callout-service | nats-system | + | ingress | envoy-gateway-system | # Verify gateway-routes templated global.domain into the api # HTTPRoute hostname. Confirms the env-file global.domain value @@ -268,10 +267,9 @@ Feature: Install a single-cluster NVCF stack on a pre-provisioned EKS cluster wi """ Then the command exit code should be 0 - When I run command "helm list -n nvca-operator --kube-context ${EKS_CONTEXT} -o json" - Then the json output should contain rows: - | name | namespace | status | - | nvca-operator | nvca-operator | deployed | + Then these Helm releases should be deployed using context "${EKS_CONTEXT}": + | name | namespace | + | nvca-operator | nvca-operator | When I run command "kubectl rollout status deployment/nvca-operator -n nvca-operator --context ${EKS_CONTEXT} --timeout=10m" Then the command exit code should be 0 diff --git a/tests/bdd/features/single-cluster-helmfile-upstream-images.feature b/tests/bdd/features/single-cluster-helmfile-upstream-images.feature index a951622fc..849ece593 100644 --- a/tests/bdd/features/single-cluster-helmfile-upstream-images.feature +++ b/tests/bdd/features/single-cluster-helmfile-upstream-images.feature @@ -110,14 +110,13 @@ Feature: Install a local single-cluster stack with upstream supporting images When I run command "env HELM_REGISTRY_CONFIG=${REPO_ROOT}/tools/ncp-local-cluster/secrets/docker-config.json make -C deploy/stacks/self-managed install HELMFILE_ENV=local-bdd HELMFILE_SELECTOR=name=api" Then the command exit code should be 0 - When I run command "helm list --all-namespaces -o json" - Then the json output should contain rows: - | name | namespace | status | - | nats | nats-system | deployed | - | cassandra | cassandra-system | deployed | - | ess-api | ess | deployed | - | nats-auth-callout-service | nats-system | deployed | - | api | nvcf | deployed | + Then these Helm releases should be deployed using context "k3d-ncp-local": + | name | namespace | + | nats | nats-system | + | cassandra | cassandra-system | + | ess-api | ess | + | nats-auth-callout-service | nats-system | + | api | nvcf | When I run command: """ diff --git a/tests/bdd/features/single-cluster-helmfile.feature b/tests/bdd/features/single-cluster-helmfile.feature index a38ea42f7..7efeccd26 100644 --- a/tests/bdd/features/single-cluster-helmfile.feature +++ b/tests/bdd/features/single-cluster-helmfile.feature @@ -23,11 +23,13 @@ Feature: Install a local single-cluster NVCF stack with Helmfile | global.imagePullSecrets[0].name | nvcr-pull-secret | | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | + | observability.profile | disabled | And I copy the file "tests/bdd/fixtures/nvcf-compute-plane-local-bdd.yaml" to "deploy/stacks/nvcf-compute-plane/environments/local-bdd.yaml" And I update yaml file "deploy/stacks/nvcf-compute-plane/environments/local-bdd.yaml" with keys: - | global.imagePullSecrets[0].name | nvcr-pull-secret | - | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | - | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | + | global.imagePullSecrets[0].name | nvcr-pull-secret | + | global.helm.sources.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | + | global.image.repository | ${SAMPLE_NGC_ORG}/${SAMPLE_NGC_TEAM} | + | observability.profile | disabled | And I copy the file "deploy/stacks/self-managed/secrets/secrets.yaml.template" to "deploy/stacks/self-managed/secrets/local-bdd-secrets.yaml" # Only ${VAR} is interpolated; bare $oauthtoken stays literal. And I substitute "REPLACE_WITH_BASE64_DOCKER_CREDENTIAL" in file "deploy/stacks/self-managed/secrets/local-bdd-secrets.yaml" with base64 of "$oauthtoken:${NGC_API_KEY}" @@ -72,27 +74,26 @@ Feature: Install a local single-cluster NVCF stack with Helmfile Then the command exit code should be 0 - When I run command "helm list --all-namespaces -o json" - Then the json output should contain rows: - | name | namespace | status | - | nats | nats-system | deployed | - | cert-manager | cert-manager | deployed | - | openbao-server | vault-system | deployed | - | cassandra | cassandra-system | deployed | - | api-keys | api-keys | deployed | - | sis | sis | deployed | - | api | nvcf | deployed | - | nvct-api | nvcf | deployed | - | invocation-service | nvcf | deployed | - | grpc-proxy | nvcf | deployed | - | ess-api | ess | deployed | - | notary-service | nvcf | deployed | - | admin-issuer-proxy | api-keys | deployed | - | reval | nvcf | deployed | - | nats-auth-callout-service | nats-system | deployed | - | ingress | envoy-gateway-system | deployed | - | llm-request-router | nvcf | deployed | - | llm-api-gateway | nvcf | deployed | + Then these Helm releases should be deployed using context "k3d-ncp-local": + | name | namespace | + | nats | nats-system | + | cert-manager | cert-manager | + | openbao-server | vault-system | + | cassandra | cassandra-system | + | api-keys | api-keys | + | sis | sis | + | api | nvcf | + | nvct-api | nvcf | + | invocation-service | nvcf | + | grpc-proxy | nvcf | + | ess-api | ess | + | notary-service | nvcf | + | admin-issuer-proxy | api-keys | + | reval | nvcf | + | nats-auth-callout-service | nats-system | + | ingress | envoy-gateway-system | + | llm-request-router | nvcf | + | llm-api-gateway | nvcf | Rule: Helmfile installs NVCA on the same local cluster after registration via the stack Makefile @@ -137,10 +138,9 @@ Feature: Install a local single-cluster NVCF stack with Helmfile """ Then the command exit code should be 0 - When I run command "helm list -n nvca-operator -o json" - Then the json output should contain rows: - | name | namespace | status | - | nvca-operator | nvca-operator | deployed | + Then these Helm releases should be deployed using context "k3d-ncp-local": + | name | namespace | + | nvca-operator | nvca-operator | When I run command "kubectl rollout status deployment/nvca-operator -n nvca-operator --timeout=10m" Then the command exit code should be 0 diff --git a/tests/bdd/features/single-cluster-up-oneclick.feature b/tests/bdd/features/single-cluster-up-oneclick.feature index 55e47c34b..b70ee9c90 100644 --- a/tests/bdd/features/single-cluster-up-oneclick.feature +++ b/tests/bdd/features/single-cluster-up-oneclick.feature @@ -81,21 +81,19 @@ Feature: Bring up a local single-cluster NVCF stack with the self-hosted up one- Then the command exit code should be 0 # Control plane releases are deployed on the single k3d cluster. - When I run command "helm list --all-namespaces --kube-context k3d-ncp-local -o json" - Then the json output should contain rows: - | name | namespace | status | - | nats | nats-system | deployed | - | cassandra | cassandra-system | deployed | - | openbao-server | vault-system | deployed | - | api-keys | api-keys | deployed | - | sis | sis | deployed | - | api | nvcf | deployed | + Then these Helm releases should be deployed using context "k3d-ncp-local": + | name | namespace | + | nats | nats-system | + | cassandra | cassandra-system | + | openbao-server | vault-system | + | api-keys | api-keys | + | sis | sis | + | api | nvcf | # The compute plane (NVCA operator) is deployed on the same cluster. - When I run command "helm list -n nvca-operator --kube-context k3d-ncp-local -o json" - Then the json output should contain rows: - | name | namespace | status | - | nvca-operator | nvca-operator | deployed | + Then these Helm releases should be deployed using context "k3d-ncp-local": + | name | namespace | + | nvca-operator | nvca-operator | # The agent registered by up reports healthy. When I run command "kubectl wait nvcfbackend ncp-local -n nvca-operator --context k3d-ncp-local --for=jsonpath={.status.agentStatus}=healthy --timeout=10m" diff --git a/tests/bdd/fixtures/nvcf-compute-plane-local-bdd-multi.yaml b/tests/bdd/fixtures/nvcf-compute-plane-local-bdd-multi.yaml index a1d897af6..165fa38dd 100644 --- a/tests/bdd/fixtures/nvcf-compute-plane-local-bdd-multi.yaml +++ b/tests/bdd/fixtures/nvcf-compute-plane-local-bdd-multi.yaml @@ -32,6 +32,14 @@ global: # in the multi-cluster topology. nvcaOperator: selfManaged: + # The simulated GPU density exceeds laptop CPU and memory ratios. Keep + # dynamic discovery's 1x type so NVCA can report local GPU capacity. + featureGateValues: + - -InfraResourceOverhead + - -EnforceHelmFunctionResourceLimits + - -EnforceContainerFunctionResourceLimits + - -EnforceHelmTaskResourceLimits + - -EnforceContainerTaskResourceLimits icmsServiceURL: http://api.sis.svc.cluster.local:8080 revalServiceURL: http://reval.nvcf.svc.cluster.local:8080 natsURL: nats://nats.nats-system.svc.cluster.local:4222 diff --git a/tests/bdd/fixtures/nvcf-compute-plane-local-bdd.yaml b/tests/bdd/fixtures/nvcf-compute-plane-local-bdd.yaml index 5ab7f37ac..8ecaf2607 100644 --- a/tests/bdd/fixtures/nvcf-compute-plane-local-bdd.yaml +++ b/tests/bdd/fixtures/nvcf-compute-plane-local-bdd.yaml @@ -26,6 +26,14 @@ global: nvcaOperator: selfManaged: + # The simulated GPU density exceeds laptop CPU and memory ratios. Keep + # dynamic discovery's 1x type so NVCA can report local GPU capacity. + featureGateValues: + - -InfraResourceOverhead + - -EnforceHelmFunctionResourceLimits + - -EnforceContainerFunctionResourceLimits + - -EnforceHelmTaskResourceLimits + - -EnforceContainerTaskResourceLimits icmsServiceURL: http://api.sis.svc.cluster.local:8080 revalServiceURL: http://reval.nvcf.svc.cluster.local:8080 natsURL: nats://nats.nats-system.svc.cluster.local:4222 diff --git a/tests/bdd/fixtures_test.go b/tests/bdd/fixtures_test.go index 3a5dd51ca..56b9c31ed 100644 --- a/tests/bdd/fixtures_test.go +++ b/tests/bdd/fixtures_test.go @@ -24,6 +24,7 @@ import ( "os/exec" "path/filepath" "regexp" + "slices" "strings" "testing" @@ -149,6 +150,45 @@ func TestNVCFCLILocalFixtureTargetsLocalGRPCGateway(t *testing.T) { } } +func TestComputePlaneLocalBDDFixturesDisableResourceSizingFeatureGates(t *testing.T) { + want := []string{ + "-InfraResourceOverhead", + "-EnforceHelmFunctionResourceLimits", + "-EnforceContainerFunctionResourceLimits", + "-EnforceHelmTaskResourceLimits", + "-EnforceContainerTaskResourceLimits", + } + + for _, fixturePath := range []string{ + "fixtures/nvcf-compute-plane-local-bdd.yaml", + "fixtures/nvcf-compute-plane-local-bdd-multi.yaml", + } { + t.Run(filepath.Base(fixturePath), func(t *testing.T) { + fixtureBytes, err := os.ReadFile(fixturePath) + if err != nil { + t.Fatalf("read compute-plane fixture %s: %v", fixturePath, err) + } + var fixture struct { + Global struct { + NVCAOperator struct { + SelfManaged struct { + FeatureGateValues []string `yaml:"featureGateValues"` + } `yaml:"selfManaged"` + } `yaml:"nvcaOperator"` + } `yaml:"global"` + } + if err := yaml.Unmarshal(fixtureBytes, &fixture); err != nil { + t.Fatalf("parse compute-plane fixture %s: %v", fixturePath, err) + } + + got := fixture.Global.NVCAOperator.SelfManaged.FeatureGateValues + if !slices.Equal(got, want) { + t.Fatalf("featureGateValues = %q, want %q", got, want) + } + }) + } +} + func TestSelfManagedLocalBDDMultiFixtureWiresGRPCWorkerCallback(t *testing.T) { fixtureBytes, err := os.ReadFile("fixtures/self-managed-local-bdd-multi.yaml") if err != nil { diff --git a/tests/bdd/godog_test.go b/tests/bdd/godog_test.go index 56da25ad9..85c7b9e35 100644 --- a/tests/bdd/godog_test.go +++ b/tests/bdd/godog_test.go @@ -305,9 +305,9 @@ func TestSingleClusterUpFeatureFileWiresToSteps(t *testing.T) { // TestSingleClusterUpOneClickFeatureFileWiresToSteps runs the // self-hosted up one-click feature against a fake CommandRunner. The -// helm-list canned outputs carry --kube-context k3d-ncp-local so the -// control-plane and nvca-operator json-rows assertions have something to -// parse; the conflict-precheck k3d-get returns exit 1. +// helm-list canned output carries --kube-context k3d-ncp-local so the +// control-plane and nvca-operator assertions have something to parse; +// the conflict-precheck k3d-get returns exit 1. func TestSingleClusterUpOneClickFeatureFileWiresToSteps(t *testing.T) { t.Setenv("NVCF_CLI", "/usr/bin/nvcf-cli") t.Setenv("NGC_API_KEY", "test-key") @@ -315,7 +315,6 @@ func TestSingleClusterUpOneClickFeatureFileWiresToSteps(t *testing.T) { t.Setenv("SAMPLE_NGC_TEAM", "test-team") suite := newWiringSuite(t, newFakeRunner(map[string]harness.Result{ "helm list --all-namespaces --kube-context k3d-ncp-local -o json": {ExitCode: 0, Stdout: helmListAllNamespacesJSON()}, - "helm list -n nvca-operator --kube-context k3d-ncp-local -o json": {ExitCode: 0, Stdout: helmListNVCAJSON()}, // Conflict precheck: feature asserts the multi-cluster // control-plane is absent. "k3d cluster get ncp-local-cp": {ExitCode: 1}, @@ -389,8 +388,7 @@ func TestMultiClusterUpFeatureFileWiresToSteps(t *testing.T) { // single-cluster-helmfile.feature against a fake runner. The fixture // the feature copies from is seeded into the wiring suite's RepoRoot // so the I copy / I update yaml chain has a real source file. The -// fake runner is pre-loaded with canned JSON for the `helm list` step -// so the json-rows assertion has something to parse. +// fake runner is pre-loaded with canned JSON for the Helm release assertion. func TestSingleClusterHelmfileFeatureFileWiresToSteps(t *testing.T) { t.Setenv("NGC_API_KEY", "test-key") t.Setenv("SAMPLE_NGC_ORG", "test-org") @@ -398,8 +396,7 @@ func TestSingleClusterHelmfileFeatureFileWiresToSteps(t *testing.T) { t.Setenv("NVCF_CLI", "/usr/bin/nvcf-cli") t.Setenv("REPO_ROOT", "/repo-root-placeholder") suite := newWiringSuite(t, newFakeRunner(map[string]harness.Result{ - "helm list --all-namespaces -o json": {ExitCode: 0, Stdout: helmListAllNamespacesJSON()}, - "helm list -n nvca-operator -o json": {ExitCode: 0, Stdout: helmListNVCAJSON()}, + "helm list --all-namespaces --kube-context k3d-ncp-local -o json": {ExitCode: 0, Stdout: helmListAllNamespacesJSON()}, "/usr/bin/nvcf-cli --config /repo-root-placeholder/tests/bdd/fixtures/nvcf-cli-local.yaml function invoke --request-body '{\"message\":\"bdd-echo\",\"repeats\":1}' --timeout 120 --poll-duration 5": { ExitCode: 0, Stdout: "Function invocation completed!\n\nResponse:\n{\"rawResponse\":\"bdd-echo\"}\n", @@ -835,7 +832,7 @@ func TestMultiClusterHelmfileFeatureFileWiresToSteps(t *testing.T) { t.Setenv("REPO_ROOT", "/repo-root-placeholder") suite := newWiringSuite(t, newFakeRunner(map[string]harness.Result{ "helm list --all-namespaces --kube-context k3d-ncp-local-cp -o json": {ExitCode: 0, Stdout: helmListAllNamespacesJSON()}, - "helm list -n nvca-operator --kube-context k3d-ncp-local-compute-1 -o json": {ExitCode: 0, Stdout: helmListNVCAJSON()}, + "helm list --all-namespaces --kube-context k3d-ncp-local-compute-1 -o json": {ExitCode: 0, Stdout: helmListNVCAJSON()}, "/usr/bin/nvcf-cli --config /repo-root-placeholder/tests/bdd/fixtures/nvcf-cli-local.yaml function invoke --request-body '{\"message\":\"bdd-echo\",\"repeats\":1}' --timeout 120 --poll-duration 5": { ExitCode: 0, Stdout: "Function invocation completed!\n\nResponse:\n{\"rawResponse\":\"bdd-echo\"}\n", @@ -929,7 +926,7 @@ func TestSingleClusterHelmfileUpstreamImagesFeatureFileWiresToSteps(t *testing.T upstreamReloader := "docker.io/natsio/nats-server-config-reloader:0.23.0" suite := newWiringSuite(t, newFakeRunner(map[string]harness.Result{ "k3d cluster get ncp-local-cp": {ExitCode: 1}, - "helm list --all-namespaces -o json": { + "helm list --all-namespaces --kube-context k3d-ncp-local -o json": { ExitCode: 0, Stdout: helmListAllNamespacesJSON(), }, @@ -1070,7 +1067,8 @@ func helmListAllNamespacesJSON() string { {"name":"nats-auth-callout-service","namespace":"nats-system","status":"deployed"}, {"name":"ingress","namespace":"envoy-gateway-system","status":"deployed"}, {"name":"llm-request-router","namespace":"nvcf","status":"deployed"}, -{"name":"llm-api-gateway","namespace":"nvcf","status":"deployed"} +{"name":"llm-api-gateway","namespace":"nvcf","status":"deployed"}, +{"name":"nvca-operator","namespace":"nvca-operator","status":"deployed"} ]` } @@ -1349,8 +1347,6 @@ func TestSingleClusterEKSHelmfileFeatureFileWiresToSteps(t *testing.T) { "helm list --all-namespaces --kube-context " + eksContext + " -o json": {ExitCode: 0, Stdout: helmListAllNamespacesJSON()}, // @control-plane: httproute jsonpath assertion expects api.. "kubectl --context " + eksContext + " get httproute nvcf-api -n envoy-gateway -o jsonpath={.spec.hostnames[0]}": {ExitCode: 0, Stdout: "api." + wiringGatewayLB}, - // @nvca-registration: helm list confirms nvca-operator deployed. - "helm list -n nvca-operator --kube-context " + eksContext + " -o json": {ExitCode: 0, Stdout: helmListNVCAJSON()}, })) seedStackBaseYaml(t, suite.Config.RepoRoot) seedComputePlaneBaseYaml(t, suite.Config.RepoRoot) @@ -1447,7 +1443,7 @@ func TestMultiClusterEKSHelmfileFeatureFileWiresToSteps(t *testing.T) { NVCT_GLOBAL_FQDN_GRPC: http://worker-tasks.` + wiringGatewayDomain + ` `}, // compute nvca-operator helm list assertion. - "helm list -n nvca-operator --kube-context " + computeContext + " -o json": {ExitCode: 0, Stdout: helmListNVCAJSON()}, + "helm list --all-namespaces --kube-context " + computeContext + " -o json": {ExitCode: 0, Stdout: helmListNVCAJSON()}, // @function-lifecycle: function invoke returns the echo payload. "/usr/bin/nvcf-cli --config /repo-root-placeholder/tests/bdd/out/nvcf-cli-eks-bdd-multi.yaml function invoke --request-body '{\"message\":\"bdd-echo\",\"repeats\":1}' --timeout 120 --poll-duration 5": { ExitCode: 0, diff --git a/tests/bdd/steps/assertion_steps.go b/tests/bdd/steps/assertion_steps.go index 1a3ed8fd2..daba7c55d 100644 --- a/tests/bdd/steps/assertion_steps.go +++ b/tests/bdd/steps/assertion_steps.go @@ -45,6 +45,7 @@ func registerAssertionSteps(ctx *godog.ScenarioContext, sc *ScenarioContext) { ctx.Step(`^the rendered manifests in "([^"]*)" should contain:$`, sc.renderedManifestsShouldContain) ctx.Step(`^the rendered manifests in "([^"]*)" under directories matching "([^"]*)" should contain:$`, sc.renderedManifestsUnderMatchingDirectoriesShouldContain) ctx.Step(`^the rendered manifests in "([^"]*)" should not contain:$`, sc.renderedManifestsShouldNotContain) + ctx.Step(`^these Helm releases should be deployed using context "([^"]*)":$`, sc.helmReleasesShouldBeDeployed) ctx.Step(`^these ServiceMonitors should exist in namespace "([^"]*)" using context "([^"]*)":$`, sc.serviceMonitorsShouldExist) } @@ -196,6 +197,54 @@ func tableToSingleColumn(table *godog.Table, header string) ([]string, error) { return values, nil } +func (sc *ScenarioContext) helmReleasesShouldBeDeployed(ctx context.Context, kubeContext string, table *godog.Table) error { + expected, err := tableToHelmReleaseExpectations(table) + if err != nil { + return err + } + command, err := dsl.HelmListCommand(kubeContext) + if err != nil { + return err + } + if err := sc.runAndRecord(ctx, command); err != nil { + return err + } + if err := sc.commandExitCodeShouldBe(0); err != nil { + return err + } + return dsl.HelmReleasesDeployed(sc.LastResult.Stdout, expected) +} + +func tableToHelmReleaseExpectations(table *godog.Table) ([]dsl.HelmReleaseExpectation, error) { + if table == nil || len(table.Rows) < 2 { + return nil, fmt.Errorf("table must have name and namespace headers and at least one data row") + } + headers := table.Rows[0].Cells + withRevision := len(headers) == 3 + if len(headers) != 2 && !withRevision { + return nil, fmt.Errorf("table headers must be name, namespace, and optional revision") + } + if strings.TrimSpace(headers[0].Value) != "name" || strings.TrimSpace(headers[1].Value) != "namespace" || withRevision && strings.TrimSpace(headers[2].Value) != "revision" { + return nil, fmt.Errorf("table headers must be name, namespace, and optional revision") + } + + expected := make([]dsl.HelmReleaseExpectation, 0, len(table.Rows)-1) + for index, row := range table.Rows[1:] { + if len(row.Cells) != len(headers) { + return nil, fmt.Errorf("row %d has %d cells, expected %d", index+1, len(row.Cells), len(headers)) + } + release := dsl.HelmReleaseExpectation{ + Name: row.Cells[0].Value, + Namespace: row.Cells[1].Value, + } + if withRevision { + release.Revision = row.Cells[2].Value + } + expected = append(expected, release) + } + return expected, nil +} + func (sc *ScenarioContext) serviceMonitorsShouldExist(ctx context.Context, namespace, kubeContext string, table *godog.Table) error { names, err := tableToSingleColumn(table, "name") if err != nil { diff --git a/tests/bdd/steps/steps_test.go b/tests/bdd/steps/steps_test.go index e77604512..608fa195a 100644 --- a/tests/bdd/steps/steps_test.go +++ b/tests/bdd/steps/steps_test.go @@ -567,6 +567,41 @@ func TestServiceMonitorsShouldExistRunsSingleExplicitGet(t *testing.T) { } } +func TestHelmReleasesShouldBeDeployedRunsSingleExplicitList(t *testing.T) { + sc, fake := newScenarioContext(t) + fake.result = harness.Result{ExitCode: 0, Stdout: `[{"name":"nats","namespace":"nats-system","revision":"1","status":"deployed"}]`} + table := docTable(t, [][]string{ + {"name", "namespace", "revision"}, + {"nats", "nats-system", "1"}, + }) + + if err := sc.helmReleasesShouldBeDeployed(context.Background(), "k3d-ncp-local", table); err != nil { + t.Fatalf("assert Helm releases: %v", err) + } + if len(fake.runs) != 1 { + t.Fatalf("runs = %d, want 1", len(fake.runs)) + } + want := "helm list --all-namespaces --kube-context k3d-ncp-local -o json" + if fake.runs[0].command != want { + t.Fatalf("command = %q, want %q", fake.runs[0].command, want) + } +} + +func TestHelmReleaseTableAcceptsNameAndNamespace(t *testing.T) { + table := docTable(t, [][]string{ + {"name", "namespace"}, + {"nats", "nats-system"}, + }) + + got, err := tableToHelmReleaseExpectations(table) + if err != nil { + t.Fatalf("parse table: %v", err) + } + if len(got) != 1 || got[0].Name != "nats" || got[0].Namespace != "nats-system" || got[0].Revision != "" { + t.Fatalf("expectations = %#v", got) + } +} + func TestRegisterAllRunsAFeatureFile(t *testing.T) { // End-to-end smoke check that RegisterAll wires every category. A // minimal in-memory feature is driven through a Godog TestSuite so