@appthrust/kest 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -78,6 +78,13 @@ const ns = await s.newNamespace({ generateName: "foo-" });
78
78
  // Namespace name will be like "foo-d7kpn"
79
79
  ```
80
80
 
81
+ The returned object has a `name` property that holds the generated namespace name:
82
+
83
+ ```ts
84
+ const ns = await s.newNamespace({ generateName: "foo-" });
85
+ console.log(ns.name); //=> "foo-d7kpn"
86
+ ```
87
+
81
88
  ### Automatic Cleanup (Reverse-Order, Blocking)
82
89
 
83
90
  Resources are deleted in the reverse order they were created (LIFO). Kest waits until each resource is fully removed before proceeding, preventing flaky failures caused by lingering resources or `Terminating` namespaces.
@@ -219,6 +226,37 @@ await ns.assertList<ConfigMap>({
219
226
  });
220
227
  ```
221
228
 
229
+ ### Single-Resource List Assertions
230
+
231
+ Assert that exactly one resource of a kind exists (or matches a predicate) and test it:
232
+
233
+ ```ts
234
+ // Assert exactly one ConfigMap exists and check its data
235
+ await ns.assertOne<ConfigMap>({
236
+ apiVersion: "v1",
237
+ kind: "ConfigMap",
238
+ test() {
239
+ expect(this.data?.mode).toBe("demo");
240
+ },
241
+ });
242
+ ```
243
+
244
+ Use the optional `where` predicate to narrow candidates when multiple resources exist:
245
+
246
+ ```ts
247
+ // Find the one ConfigMap whose name starts with "generated-"
248
+ await ns.assertOne<ConfigMap>({
249
+ apiVersion: "v1",
250
+ kind: "ConfigMap",
251
+ where: (cm) => cm.metadata.name.startsWith("generated-"),
252
+ test() {
253
+ expect(this.data?.mode).toBe("auto");
254
+ },
255
+ }, { timeout: "30s", interval: "1s" });
256
+ ```
257
+
258
+ `assertOne` throws if zero or more than one resource matches, and retries until exactly one is found or the timeout expires.
259
+
222
260
  ### Absence Assertions
223
261
 
224
262
  Assert that a resource does not exist (e.g. after deletion or to verify a controller hasn't created something):
@@ -439,6 +477,7 @@ The top-level API surface available in every test callback.
439
477
  | `assert(resource, options?)` | Fetch a resource and run assertions with retries |
440
478
  | `assertAbsence(resource, options?)` | Assert that a resource does not exist |
441
479
  | `assertList(resource, options?)` | Fetch a list of resources and run assertions |
480
+ | `assertOne(resource, options?)` | Assert exactly one resource matches, then run assertions |
442
481
  | `newNamespace(name?, options?)` | Create an ephemeral namespace (supports `{ generateName }`) |
443
482
  | `generateName(prefix)` | Generate a random-suffix name (statistical uniqueness) |
444
483
  | `exec(input, options?)` | Execute shell commands with optional revert |
@@ -447,7 +486,13 @@ The top-level API surface available in every test callback.
447
486
 
448
487
  ### Namespace / Cluster
449
488
 
450
- Returned by `newNamespace()` and `useCluster()` respectively. They expose the same core methods (`apply`, `create`, `applyStatus`, `delete`, `label`, `get`, `assert`, `assertAbsence`, `assertList`) scoped to their namespace or cluster context. `Cluster` additionally supports `newNamespace`.
489
+ Returned by `newNamespace()` and `useCluster()` respectively. They expose the same core methods (`apply`, `create`, `applyStatus`, `delete`, `label`, `get`, `assert`, `assertAbsence`, `assertList`, `assertOne`) scoped to their namespace or cluster context. `Cluster` additionally supports `newNamespace`.
490
+
491
+ `Namespace` also exposes a `name` property:
492
+
493
+ | Property | Type | Description |
494
+ | -------- | -------- | -------------------------------------------------- |
495
+ | `name` | `string` | The generated namespace name (e.g. `"kest-abc12"`) |
451
496
 
452
497
  ### Action Options
453
498
 
@@ -617,11 +662,11 @@ Starting with one file per scenario reserves room to grow. Each file has built-i
617
662
 
618
663
  **Name files after the behavior they verify.** A reader should know what a test checks without opening the file:
619
664
 
620
- | ✅ Good | ❌ Bad |
621
- | --- | --- |
622
- | `creates-namespaces-with-labels.test.ts` | `tenant-test-1.test.ts` |
623
- | `rejects-reserved-selector-labels.test.ts` | `validation.test.ts` |
624
- | `rolls-out-when-image-changes.test.ts` | `deployment-tests.test.ts` |
665
+ | ✅ Good | ❌ Bad |
666
+ | ------------------------------------------ | -------------------------- |
667
+ | `creates-namespaces-with-labels.test.ts` | `tenant-test-1.test.ts` |
668
+ | `rejects-reserved-selector-labels.test.ts` | `validation.test.ts` |
669
+ | `rolls-out-when-image-changes.test.ts` | `deployment-tests.test.ts` |
625
670
 
626
671
  **Exceptions.** Tiny negative-case variations of the same API -- where each case is only a few lines and they are always read together (e.g. boundary-condition lists) -- may share a file. When you do this, leave a comment explaining why, because the exception easily becomes the norm.
627
672
 
@@ -714,39 +759,114 @@ await ns.apply(import("./fixtures/deployment-v2.yaml"));
714
759
  await ns.apply(import("./fixtures/deployment-v1.ts"));
715
760
  ```
716
761
 
717
- If you do use helpers, limit them to **name generation** -- never to assembling `spec`:
762
+ **Manifest-visibility checklist:**
763
+
764
+ - [ ] Can you understand the full input by reading just the test file?
765
+ - [ ] Is the diff between test steps visible as a spec-level change?
766
+ - [ ] Can you reconstruct the applied manifest from the failure report alone?
767
+
768
+ ### Avoiding naming collisions between tests
769
+
770
+ When tests run in parallel, hard-coded resource names can collide. In most cases, `newNamespace()` is all you need -- each test gets its own namespace, so names like `"my-config"` or `"my-app"` are already isolated:
718
771
 
719
772
  ```ts
720
- // Good helper generates a name; the manifest stays in the test
721
- const name = s.generateName("deploy-");
773
+ const ns = await s.newNamespace();
774
+
775
+ await ns.apply({
776
+ apiVersion: "v1",
777
+ kind: "ConfigMap",
778
+ metadata: { name: "app-config" }, // safe — no other test shares this namespace
779
+ data: { mode: "test" },
780
+ });
781
+ ```
782
+
783
+ Because the namespace itself is unique, the resource names inside it don't need to be randomized. **You only need `s.generateName` when `newNamespace` alone is not enough.**
784
+
785
+ A common mistake is to reach for `s.generateName` on _every_ resource "just to be safe." This adds no real protection when the resources are already in an isolated namespace, and it makes tests harder to read and debug:
786
+
787
+ ```ts
788
+ // ❌ Bad — generateName on namespaced resources inside an isolated namespace.
789
+ // The random suffixes add noise without preventing any actual collisions.
790
+ const ns = await s.newNamespace();
791
+
792
+ const configName = s.generateName("cfg-");
793
+ const deployName = s.generateName("deploy-");
794
+
795
+ await ns.apply({
796
+ apiVersion: "v1",
797
+ kind: "ConfigMap",
798
+ metadata: { name: configName }, // "cfg-x7k2m" — hard to find in logs
799
+ data: { mode: "test" },
800
+ });
722
801
  await ns.apply({
723
802
  apiVersion: "apps/v1",
724
803
  kind: "Deployment",
725
- metadata: { name },
726
- spec: {
727
- /* full spec here, not hidden in a function */
728
- },
804
+ metadata: { name: deployName }, // "deploy-p3n8r" — what was this test about?
805
+ // ...
729
806
  });
730
807
  ```
731
808
 
732
- **Manifest-visibility checklist:**
809
+ ```ts
810
+ // ✅ Good — fixed, descriptive names inside an isolated namespace.
811
+ // The namespace already guarantees no collisions. Fixed names are
812
+ // easy to read, easy to grep in logs, and match the failure report.
813
+ const ns = await s.newNamespace();
733
814
 
734
- - [ ] Can you understand the full input by reading just the test file?
735
- - [ ] Is the diff between test steps visible as a spec-level change?
736
- - [ ] Can you reconstruct the applied manifest from the failure report alone?
737
- - [ ] Are helper functions limited to name generation (no `spec` assembly)?
815
+ await ns.apply({
816
+ apiVersion: "v1",
817
+ kind: "ConfigMap",
818
+ metadata: { name: "app-config" },
819
+ data: { mode: "test" },
820
+ });
821
+ await ns.apply({
822
+ apiVersion: "apps/v1",
823
+ kind: "Deployment",
824
+ metadata: { name: "my-app" },
825
+ // ...
826
+ });
827
+ ```
738
828
 
739
- ### Avoiding naming collisions between tests
829
+ **When to use `s.generateName`:**
740
830
 
741
- When tests run in parallel, hard-coded resource names can collide (especially when you create cluster-scoped resources).
831
+ | Situation | Approach |
832
+ | ------------------------------------------------------ | ----------------------------------------- |
833
+ | Namespaced resource inside `newNamespace()` | Use a fixed name (default) |
834
+ | Same-kind resources created multiple times in one test | `s.generateName` or numbered fixed names |
835
+ | Cluster-scoped resource (e.g. `ClusterRole`, `CRD`) | `s.generateName` (no namespace isolation) |
836
+ | Fixed name causes unintended upsert / side effects | `s.generateName` |
742
837
 
743
- Kest offers a few ways to avoid these collisions:
838
+ Choose **necessary and sufficient** over "safe side" -- every random suffix is a readability trade-off.
744
839
 
745
- - Use `s.newNamespace()` to isolate namespaced resources per test (recommended default).
746
- - Use `s.newNamespace({ generateName: "prefix-" })` to keep isolation while making the namespace name easier to recognize in logs/reports.
747
- - Use `s.generateName("prefix-")` to generate a random-suffix name when you need additional names outside of `newNamespace` (e.g. cluster-scoped resources).
840
+ **How the two helpers work:**
748
841
 
749
- `s.newNamespace(...)` actually creates the `Namespace` via `kubectl create` and retries on name collisions (regenerating a new name each attempt), so once it succeeds the namespace name is unique in the cluster. `s.generateName(...)` is a pure string helper and provides **statistical uniqueness** only (collisions are extremely unlikely, but not impossible).
842
+ - `newNamespace(...)` creates a `Namespace` via `kubectl create` and retries on name collisions (regenerating a new name each attempt), so once it succeeds the namespace name is guaranteed unique in the cluster.
843
+ - `s.generateName(...)` is a pure string helper that provides **statistical uniqueness** only (collisions are extremely unlikely, but not impossible).
844
+
845
+ **Using `newNamespace` with `.name` (cross-namespace references):**
846
+
847
+ Every `Namespace` object returned by `newNamespace` has a `.name` property. When resources in one namespace need to reference another namespace by name -- for example in cross-namespace `spec.*Ref.namespace` fields -- use `.name`:
848
+
849
+ ```ts
850
+ const nsA = await s.newNamespace({ generateName: "infra-" });
851
+ const nsB = await s.newNamespace({ generateName: "app-" });
852
+
853
+ s.when("I apply a resource in nsB that references nsA");
854
+ await nsB.apply({
855
+ apiVersion: "example.com/v1",
856
+ kind: "AppConfig",
857
+ metadata: { name: "my-app" },
858
+ spec: {
859
+ secretStoreRef: {
860
+ namespace: nsA.name, // e.g. "infra-k7rtn"
861
+ name: "vault",
862
+ },
863
+ },
864
+ });
865
+ ```
866
+
867
+ **Using `s.generateName` (for cluster-scoped resources):**
868
+
869
+ For cluster-scoped resources (where `newNamespace` is not applicable), use `s.generateName`:
750
870
 
751
871
  ```ts
752
872
  s.given("a cluster-scoped resource name should not collide with other tests");
@@ -766,6 +886,12 @@ await s.create({
766
886
  });
767
887
  ```
768
888
 
889
+ **Naming-collision checklist:**
890
+
891
+ - [ ] Are namespaced resources inside `newNamespace` using fixed names (not `generateName`)?
892
+ - [ ] Is `generateName` reserved for cluster-scoped resources or multi-instance cases?
893
+ - [ ] Can you identify every resource in the failure report without decoding random suffixes?
894
+
769
895
  ## Type Safety
770
896
 
771
897
  Define TypeScript interfaces for your Kubernetes resources to get full type checking in manifests and assertions:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appthrust/kest",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Kubernetes E2E testing framework designed for humans and AI alike",
5
5
  "type": "module",
6
6
  "module": "ts/index.ts",
@@ -0,0 +1,93 @@
1
+ import { YAML } from "bun";
2
+ import type { K8sResource, ResourceOneTest } from "../apis";
3
+ import { parseK8sResourceListYaml } from "../k8s-resource";
4
+ import type { QueryDef } from "./types";
5
+
6
+ export const assertOne = {
7
+ type: "query",
8
+ name: "AssertOne",
9
+ query:
10
+ ({ kubectl }) =>
11
+ async <T extends K8sResource>(
12
+ condition: ResourceOneTest<T>
13
+ ): Promise<T> => {
14
+ const yaml = await kubectl.list(toKubectlType(condition));
15
+ const result = parseK8sResourceListYaml(yaml);
16
+ if (!result.ok) {
17
+ throw new Error(
18
+ `Invalid Kubernetes resource list: ${result.violations.join(", ")}`
19
+ );
20
+ }
21
+
22
+ const fetched = result.value;
23
+ for (const item of fetched) {
24
+ assertSameGVK(condition, item);
25
+ }
26
+
27
+ const typed = fetched as Array<T>;
28
+ const found = findExactlyOne(typed, condition);
29
+ await condition.test.call(found, found);
30
+ return found;
31
+ },
32
+ describe: <T extends K8sResource>(condition: ResourceOneTest<T>): string => {
33
+ return `Assert one \`${condition.kind}\` resource`;
34
+ },
35
+ } satisfies QueryDef<ResourceOneTest, K8sResource>;
36
+
37
+ function findExactlyOne<T extends K8sResource>(
38
+ items: Array<T>,
39
+ condition: ResourceOneTest<T>
40
+ ): T {
41
+ const candidates = condition.where ? items.filter(condition.where) : items;
42
+ const hasWhere = condition.where !== undefined;
43
+
44
+ if (candidates.length === 0) {
45
+ throw new Error(
46
+ hasWhere
47
+ ? `No ${condition.kind} resource found matching the "where" predicate`
48
+ : `No ${condition.kind} resource found`
49
+ );
50
+ }
51
+
52
+ if (candidates.length > 1) {
53
+ throw new Error(
54
+ hasWhere
55
+ ? `Expected exactly one ${condition.kind} matching the "where" predicate, but found ${candidates.length}`
56
+ : `Expected exactly one ${condition.kind}, but found ${candidates.length}`
57
+ );
58
+ }
59
+
60
+ return candidates[0] as T;
61
+ }
62
+
63
+ function isSameGVK<T extends K8sResource>(
64
+ finding: Pick<ResourceOneTest<T>, "apiVersion" | "kind">,
65
+ fetched: K8sResource
66
+ ): fetched is T {
67
+ return (
68
+ finding.apiVersion === fetched.apiVersion && finding.kind === fetched.kind
69
+ );
70
+ }
71
+
72
+ function assertSameGVK<T extends K8sResource>(
73
+ finding: Pick<ResourceOneTest<T>, "apiVersion" | "kind">,
74
+ fetched: K8sResource
75
+ ): void {
76
+ if (!isSameGVK(finding, fetched)) {
77
+ throw new Error(
78
+ `Fetched Kubernetes resource: ${YAML.stringify(fetched)} is not expected: ${YAML.stringify(finding)}`
79
+ );
80
+ }
81
+ }
82
+
83
+ function toKubectlType<T extends K8sResource>(
84
+ condition: Pick<ResourceOneTest<T>, "apiVersion" | "kind">
85
+ ): string {
86
+ const { kind, apiVersion } = condition;
87
+ const [group, version] = apiVersion.split("/");
88
+ if (version === undefined) {
89
+ // core group cannot include version in the type
90
+ return kind;
91
+ }
92
+ return [kind, version, group].filter(Boolean).join(".");
93
+ }
package/ts/apis/index.ts CHANGED
@@ -296,6 +296,52 @@ export interface Scenario {
296
296
  options?: undefined | ActionOptions
297
297
  ): Promise<Array<T>>;
298
298
 
299
+ /**
300
+ * Fetches a list of Kubernetes resources, asserts that exactly one matches
301
+ * the optional `where` predicate, and runs a test function on it.
302
+ *
303
+ * When `where` is omitted, all resources of the given kind are candidates;
304
+ * the method then asserts that exactly one resource of that kind exists.
305
+ *
306
+ * The `test` callback is invoked with `this` bound to the matching resource.
307
+ * If the callback throws (or rejects), the assertion fails and the whole
308
+ * action is retried until it succeeds or times out.
309
+ *
310
+ * @template T - The expected Kubernetes resource shape.
311
+ * @param resource - Group/version/kind selector, optional `where` predicate,
312
+ * and test callback.
313
+ * @param options - Retry options such as timeout and polling interval.
314
+ *
315
+ * @example
316
+ * ```ts
317
+ * // Assert exactly one ConfigMap exists and check its data
318
+ * await s.assertOne({
319
+ * apiVersion: "v1",
320
+ * kind: "ConfigMap",
321
+ * test() {
322
+ * expect(this.data?.mode).toBe("demo");
323
+ * },
324
+ * });
325
+ * ```
326
+ *
327
+ * @example
328
+ * ```ts
329
+ * // Find the one ConfigMap whose name starts with "generated-"
330
+ * await s.assertOne({
331
+ * apiVersion: "v1",
332
+ * kind: "ConfigMap",
333
+ * where: (cm) => cm.metadata.name.startsWith("generated-"),
334
+ * test() {
335
+ * expect(this.data?.mode).toBe("auto");
336
+ * },
337
+ * });
338
+ * ```
339
+ */
340
+ assertOne<T extends K8sResource>(
341
+ resource: ResourceOneTest<T>,
342
+ options?: undefined | ActionOptions
343
+ ): Promise<T>;
344
+
299
345
  /**
300
346
  * Creates a new namespace and returns a namespaced API surface.
301
347
  *
@@ -686,6 +732,32 @@ export interface Cluster {
686
732
  options?: undefined | ActionOptions
687
733
  ): Promise<Array<T>>;
688
734
 
735
+ /**
736
+ * Fetches a list of Kubernetes resources, asserts that exactly one matches
737
+ * the optional `where` predicate, and runs a test function on it.
738
+ *
739
+ * @template T - The expected Kubernetes resource shape.
740
+ * @param resource - Group/version/kind selector, optional `where` predicate,
741
+ * and test callback.
742
+ * @param options - Retry options such as timeout and polling interval.
743
+ *
744
+ * @example
745
+ * ```ts
746
+ * await cluster.assertOne({
747
+ * apiVersion: "v1",
748
+ * kind: "Namespace",
749
+ * where: (ns) => ns.metadata.name === "my-namespace",
750
+ * test() {
751
+ * expect(this.metadata.labels?.env).toBe("production");
752
+ * },
753
+ * });
754
+ * ```
755
+ */
756
+ assertOne<T extends K8sResource>(
757
+ resource: ResourceOneTest<T>,
758
+ options?: undefined | ActionOptions
759
+ ): Promise<T>;
760
+
689
761
  /**
690
762
  * Creates a new namespace in this cluster and returns a namespaced API.
691
763
  *
@@ -963,6 +1035,32 @@ export interface Namespace {
963
1035
  resource: ResourceListTest<T>,
964
1036
  options?: undefined | ActionOptions
965
1037
  ): Promise<Array<T>>;
1038
+
1039
+ /**
1040
+ * Fetches a list of namespaced Kubernetes resources, asserts that exactly one
1041
+ * matches the optional `where` predicate, and runs a test function on it.
1042
+ *
1043
+ * @template T - The expected Kubernetes resource shape.
1044
+ * @param resource - Group/version/kind selector, optional `where` predicate,
1045
+ * and test callback.
1046
+ * @param options - Retry options such as timeout and polling interval.
1047
+ *
1048
+ * @example
1049
+ * ```ts
1050
+ * await ns.assertOne({
1051
+ * apiVersion: "v1",
1052
+ * kind: "ConfigMap",
1053
+ * where: (cm) => cm.metadata.name.startsWith("generated-"),
1054
+ * test() {
1055
+ * expect(this.data?.mode).toBe("auto");
1056
+ * },
1057
+ * });
1058
+ * ```
1059
+ */
1060
+ assertOne<T extends K8sResource>(
1061
+ resource: ResourceOneTest<T>,
1062
+ options?: undefined | ActionOptions
1063
+ ): Promise<T>;
966
1064
  }
967
1065
 
968
1066
  /**
@@ -1146,6 +1244,41 @@ export interface ResourceListTest<T extends K8sResource = K8sResource> {
1146
1244
  ) => unknown | Promise<unknown>;
1147
1245
  }
1148
1246
 
1247
+ /**
1248
+ * A test definition for {@link Scenario.assertOne}.
1249
+ *
1250
+ * Fetches a list of resources, filters by an optional `where` predicate, asserts
1251
+ * that exactly one resource matches, then runs the `test` callback on it.
1252
+ */
1253
+ export interface ResourceOneTest<T extends K8sResource = K8sResource> {
1254
+ /**
1255
+ * Kubernetes API version (e.g. `"v1"`, `"apps/v1"`).
1256
+ */
1257
+ readonly apiVersion: T["apiVersion"];
1258
+
1259
+ /**
1260
+ * Kubernetes kind (e.g. `"ConfigMap"`, `"Deployment"`).
1261
+ */
1262
+ readonly kind: T["kind"];
1263
+
1264
+ /**
1265
+ * Optional predicate to narrow which resources are candidates.
1266
+ *
1267
+ * When omitted, all resources of the given kind are candidates.
1268
+ * Combined with the strict uniqueness check this means "assert there is
1269
+ * exactly one resource of this kind."
1270
+ */
1271
+ readonly where?: undefined | ((resource: T) => boolean);
1272
+
1273
+ /**
1274
+ * Assertion callback.
1275
+ *
1276
+ * The callback is invoked with `this` bound to the single matching resource.
1277
+ * Throwing (or rejecting) signals a failed assertion.
1278
+ */
1279
+ readonly test: (this: T, resource: T) => unknown | Promise<unknown>;
1280
+ }
1281
+
1149
1282
  /**
1150
1283
  * Kubernetes cluster selector for {@link Scenario.useCluster}.
1151
1284
  */
@@ -3,6 +3,7 @@ import { applyStatus } from "../actions/apply-status";
3
3
  import { assert } from "../actions/assert";
4
4
  import { assertAbsence } from "../actions/assert-absence";
5
5
  import { assertList } from "../actions/assert-list";
6
+ import { assertOne } from "../actions/assert-one";
6
7
  import { create } from "../actions/create";
7
8
  import {
8
9
  type CreateNamespaceInput,
@@ -46,6 +47,7 @@ export function createScenario(deps: CreateScenarioOptions): InternalScenario {
46
47
  assert: createQueryFn(deps, assert),
47
48
  assertAbsence: createQueryFn(deps, assertAbsence),
48
49
  assertList: createQueryFn(deps, assertList),
50
+ assertOne: createQueryFn(deps, assertOne),
49
51
  given: bdd.given(deps),
50
52
  when: bdd.when(deps),
51
53
  // biome-ignore lint/suspicious/noThenProperty: BDD DSL uses `then()` method name
@@ -210,6 +212,7 @@ const createNewNamespaceFn =
210
212
  assert: createQueryFn(namespacedDeps, assert),
211
213
  assertAbsence: createQueryFn(namespacedDeps, assertAbsence),
212
214
  assertList: createQueryFn(namespacedDeps, assertList),
215
+ assertOne: createQueryFn(namespacedDeps, assertOne),
213
216
  };
214
217
  };
215
218
 
@@ -233,6 +236,7 @@ const createUseClusterFn =
233
236
  assert: createQueryFn(clusterDeps, assert),
234
237
  assertAbsence: createQueryFn(clusterDeps, assertAbsence),
235
238
  assertList: createQueryFn(clusterDeps, assertList),
239
+ assertOne: createQueryFn(clusterDeps, assertOne),
236
240
  newNamespace: createNewNamespaceFn(clusterDeps),
237
241
  };
238
242
  };