@appthrust/kest 0.1.1 → 0.2.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
@@ -1,10 +1,12 @@
1
1
  # Kest
2
2
 
3
+ ![Kest – Kubernetes E2E testing designed for humans and AI alike](social-preview.svg)
4
+
3
5
  > **Preview Release** -- Kest is currently in `0.x` preview. The API may change based on feedback. Breaking changes can occur in any `0.x` release. A stable `1.0.0` will be released once the API solidifies. Feel free to [open an issue](https://github.com/appthrust/kest/issues/new) if you have any feedback.
4
6
 
5
- **TypeScript-first E2E testing framework for Kubernetes**
7
+ **Kubernetes E2E testing designed for humans and AI alike**
6
8
 
7
- Kest makes it easy to write reliable end-to-end tests for Kubernetes controllers, operators, and admission webhooks. You write test scenarios in TypeScript with full type safety, autocompletion, and the familiar `expect()` API.
9
+ Kest makes it easy to write reliable end-to-end tests for Kubernetes controllers, operators, and admission webhooks. You write test scenarios in TypeScript with full type safety, autocompletion, and the familiar `expect()` API. When a test fails, Kest generates structured Markdown reports that are easy for humans to scan and for AI assistants to parse -- making troubleshooting straightforward regardless of who (or what) is debugging.
8
10
 
9
11
  ```ts
10
12
  import { expect } from "bun:test";
@@ -335,6 +337,7 @@ The top-level API surface available in every test callback.
335
337
  | ----------------------------------------------------------------------- | ------------------------------------------------ |
336
338
  | `apply(manifest, options?)` | Apply a Kubernetes manifest and register cleanup |
337
339
  | `applyStatus(manifest, options?)` | Apply a status subresource (server-side apply) |
340
+ | `delete(resource, options?)` | Delete a resource by API version, kind, and name |
338
341
  | `get(resource, options?)` | Fetch a resource by API version, kind, and name |
339
342
  | `assert(resource, options?)` | Fetch a resource and run assertions with retries |
340
343
  | `assertList(resource, options?)` | Fetch a list of resources and run assertions |
@@ -345,7 +348,7 @@ The top-level API surface available in every test callback.
345
348
 
346
349
  ### Namespace / Cluster
347
350
 
348
- Returned by `newNamespace()` and `useCluster()` respectively. They expose the same core methods (`apply`, `applyStatus`, `get`, `assert`, `assertList`, `newNamespace`) scoped to their namespace or cluster context.
351
+ Returned by `newNamespace()` and `useCluster()` respectively. They expose the same core methods (`apply`, `applyStatus`, `delete`, `get`, `assert`, `assertList`) scoped to their namespace or cluster context. `Cluster` additionally supports `newNamespace`.
349
352
 
350
353
  ### Action Options
351
354
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@appthrust/kest",
3
- "version": "0.1.1",
4
- "description": "TypeScript-first E2E testing framework for Kubernetes controllers, operators, and webhooks",
3
+ "version": "0.2.0",
4
+ "description": "Kubernetes E2E testing framework designed for humans and AI alike",
5
5
  "type": "module",
6
6
  "module": "ts/index.ts",
7
7
  "exports": {
@@ -0,0 +1,25 @@
1
+ import type { K8sResource, K8sResourceReference } from "../apis";
2
+ import type { OneWayMutateDef } from "./types";
3
+
4
+ export const deleteResource = {
5
+ type: "oneWayMutate",
6
+ name: "Delete",
7
+ mutate:
8
+ ({ kubectl }) =>
9
+ async <T extends K8sResource>(resource: K8sResourceReference<T>) => {
10
+ await kubectl.delete(toKubectlType(resource), resource.name);
11
+ return undefined;
12
+ },
13
+ } satisfies OneWayMutateDef<K8sResourceReference, void>;
14
+
15
+ function toKubectlType<T extends K8sResource>(
16
+ resource: K8sResourceReference<T>
17
+ ): string {
18
+ const { kind, apiVersion } = resource;
19
+ const [group, version] = apiVersion.split("/");
20
+ if (version === undefined) {
21
+ // core group cannot include version in the type
22
+ return kind;
23
+ }
24
+ return [kind, version, group].filter(Boolean).join(".");
25
+ }
package/ts/apis/index.ts CHANGED
@@ -13,7 +13,8 @@ import type { $ as BunDollar } from "bun";
13
13
  * Some actions also register cleanup ("revert") handlers which run during
14
14
  * scenario cleanup. For example, {@link Scenario.apply} registers a revert that
15
15
  * deletes the applied resource. One-way mutations such as
16
- * {@link Scenario.applyStatus} do not register a revert.
16
+ * {@link Scenario.applyStatus} and {@link Scenario.delete} do not register a
17
+ * revert.
17
18
  */
18
19
 
19
20
  /**
@@ -88,6 +89,31 @@ export interface Scenario {
88
89
  options?: undefined | ActionOptions
89
90
  ): Promise<void>;
90
91
 
92
+ /**
93
+ * Deletes a Kubernetes resource using `kubectl delete`.
94
+ *
95
+ * This action is retried when it throws.
96
+ *
97
+ * Note: this is a one-way mutation and does not register a cleanup handler.
98
+ *
99
+ * @template T - The expected Kubernetes resource shape.
100
+ * @param resource - Group/version/kind and name of the resource to delete.
101
+ * @param options - Retry options such as timeout and polling interval.
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * await s.delete({
106
+ * apiVersion: "v1",
107
+ * kind: "ConfigMap",
108
+ * name: "my-config",
109
+ * });
110
+ * ```
111
+ */
112
+ delete<T extends K8sResource>(
113
+ resource: K8sResourceReference<T>,
114
+ options?: undefined | ActionOptions
115
+ ): Promise<void>;
116
+
91
117
  /**
92
118
  * Fetches a Kubernetes resource and returns it as a typed object.
93
119
  *
@@ -370,6 +396,22 @@ export interface Cluster {
370
396
  options?: undefined | ActionOptions
371
397
  ): Promise<void>;
372
398
 
399
+ /**
400
+ * Deletes a Kubernetes resource in this cluster using `kubectl delete`.
401
+ *
402
+ * This action is retried when it throws.
403
+ *
404
+ * Note: this is a one-way mutation and does not register a cleanup handler.
405
+ *
406
+ * @template T - The expected Kubernetes resource shape.
407
+ * @param resource - Group/version/kind and name of the resource to delete.
408
+ * @param options - Retry options such as timeout and polling interval.
409
+ */
410
+ delete<T extends K8sResource>(
411
+ resource: K8sResourceReference<T>,
412
+ options?: undefined | ActionOptions
413
+ ): Promise<void>;
414
+
373
415
  /**
374
416
  * Fetches a Kubernetes resource by GVK and name.
375
417
  *
@@ -534,6 +576,22 @@ export interface Namespace {
534
576
  options?: undefined | ActionOptions
535
577
  ): Promise<void>;
536
578
 
579
+ /**
580
+ * Deletes a Kubernetes resource in this namespace using `kubectl delete`.
581
+ *
582
+ * This action is retried when it throws.
583
+ *
584
+ * Note: this is a one-way mutation and does not register a cleanup handler.
585
+ *
586
+ * @template T - The expected Kubernetes resource shape.
587
+ * @param resource - Group/version/kind and name of the resource to delete.
588
+ * @param options - Retry options such as timeout and polling interval.
589
+ */
590
+ delete<T extends K8sResource>(
591
+ resource: K8sResourceReference<T>,
592
+ options?: undefined | ActionOptions
593
+ ): Promise<void>;
594
+
537
595
  /**
538
596
  * Fetches a namespaced Kubernetes resource by GVK and name.
539
597
  *
@@ -3,6 +3,7 @@ import { applyNamespace } from "../actions/apply-namespace";
3
3
  import { applyStatus } from "../actions/apply-status";
4
4
  import { assert } from "../actions/assert";
5
5
  import { assertList } from "../actions/assert-list";
6
+ import { deleteResource } from "../actions/delete";
6
7
  import { exec } from "../actions/exec";
7
8
  import { get } from "../actions/get";
8
9
  import type { MutateDef, OneWayMutateDef, QueryDef } from "../actions/types";
@@ -31,6 +32,7 @@ export function createScenario(deps: CreateScenarioOptions): InternalScenario {
31
32
  return {
32
33
  apply: createMutateFn(deps, apply),
33
34
  applyStatus: createOneWayMutateFn(deps, applyStatus),
35
+ delete: createOneWayMutateFn(deps, deleteResource),
34
36
  exec: createMutateFn(deps, exec),
35
37
  get: createQueryFn(deps, get),
36
38
  assert: createQueryFn(deps, assert),
@@ -193,6 +195,7 @@ const createNewNamespaceFn =
193
195
  return {
194
196
  apply: createMutateFn(namespacedDeps, apply),
195
197
  applyStatus: createOneWayMutateFn(namespacedDeps, applyStatus),
198
+ delete: createOneWayMutateFn(namespacedDeps, deleteResource),
196
199
  get: createQueryFn(namespacedDeps, get),
197
200
  assert: createQueryFn(namespacedDeps, assert),
198
201
  assertList: createQueryFn(namespacedDeps, assertList),
@@ -212,6 +215,7 @@ const createUseClusterFn =
212
215
  return {
213
216
  apply: createMutateFn(clusterDeps, apply),
214
217
  applyStatus: createOneWayMutateFn(clusterDeps, applyStatus),
218
+ delete: createOneWayMutateFn(clusterDeps, deleteResource),
215
219
  get: createQueryFn(clusterDeps, get),
216
220
  assert: createQueryFn(clusterDeps, assert),
217
221
  assertList: createQueryFn(clusterDeps, assertList),