@appthrust/kest 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 appthrust
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,412 @@
1
+ # Kest
2
+
3
+ > **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
+
5
+ **TypeScript-first E2E testing framework for Kubernetes**
6
+
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.
8
+
9
+ ```ts
10
+ import { expect } from "bun:test";
11
+ import { test } from "@appthrust/kest";
12
+
13
+ test("Deployment creates expected ReplicaSet", async (s) => {
14
+ s.given("a namespace exists");
15
+ const ns = await s.newNamespace();
16
+
17
+ s.when("I apply a Deployment");
18
+ await ns.apply({
19
+ apiVersion: "apps/v1",
20
+ kind: "Deployment",
21
+ metadata: { name: "my-app" },
22
+ spec: {
23
+ replicas: 2,
24
+ selector: { matchLabels: { app: "my-app" } },
25
+ template: {
26
+ metadata: { labels: { app: "my-app" } },
27
+ spec: { containers: [{ name: "app", image: "nginx" }] },
28
+ },
29
+ },
30
+ });
31
+
32
+ s.then("the Deployment should be available");
33
+ await ns.assert({
34
+ apiVersion: "apps/v1",
35
+ kind: "Deployment",
36
+ name: "my-app",
37
+ test() {
38
+ expect(this.status?.availableReplicas).toBe(2);
39
+ },
40
+ });
41
+ // Cleanup is automatic: resources are deleted in reverse order,
42
+ // then the namespace is removed.
43
+ });
44
+ ```
45
+
46
+ ## Why TypeScript?
47
+
48
+ YAML and Go are the norm in the Kubernetes ecosystem, so why TypeScript?
49
+
50
+ **Why not YAML?** E2E tests are inherently procedural -- apply resources, wait for reconciliation, assert state, clean up. YAML is a data format, not a programming language, and becomes clunky when you try to express these sequential workflows directly.
51
+
52
+ **Why not Go?** Go has an excellent Kubernetes client ecosystem, but TypeScript object literals are far more concise than Go structs for expressing Kubernetes manifests inline. Tests read closer to the YAML you already know, without the boilerplate of typed struct initialization and pointer helpers.
53
+
54
+ **What TypeScript brings:**
55
+
56
+ - **Editor support** -- autocompletion, inline type checking, go-to-definition
57
+ - **Readability** -- object literals map naturally to Kubernetes manifests
58
+ - **Flexibility** -- loops, conditionals, helper functions, and shared fixtures are just code
59
+ - **Ecosystem** -- use any npm package for setup, assertions, or data generation
60
+
61
+ ## Features
62
+
63
+ ### Ephemeral Namespaces
64
+
65
+ Each test gets an isolated, auto-generated namespace (e.g. `kest-a1b2c`). Resources are confined to this namespace, eliminating interference between tests and enabling safe parallel execution. The namespace is deleted when the test ends.
66
+
67
+ ```ts
68
+ const ns = await s.newNamespace();
69
+ // All resources applied through `ns` are scoped to this namespace.
70
+ ```
71
+
72
+ ### Automatic Cleanup (Reverse-Order, Blocking)
73
+
74
+ 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.
75
+
76
+ ```
77
+ Created: Namespace → ConfigMap → Deployment → Service
78
+ Cleaned: Service → Deployment → ConfigMap → Namespace
79
+ ```
80
+
81
+ ### Retry-Based Assertions
82
+
83
+ Kubernetes is eventually consistent. Kest retries assertions automatically until they pass or a timeout expires, so you don't need fragile `sleep()` calls.
84
+
85
+ ```ts
86
+ await ns.assert({
87
+ apiVersion: "v1",
88
+ kind: "ConfigMap",
89
+ name: "my-config",
90
+ test() {
91
+ // Retried until this passes (default: 5s timeout, 200ms interval)
92
+ expect(this.data?.mode).toBe("production");
93
+ },
94
+ });
95
+ ```
96
+
97
+ Custom timeouts are supported per action:
98
+
99
+ ```ts
100
+ await ns.assert(
101
+ {
102
+ apiVersion: "apps/v1",
103
+ kind: "Deployment",
104
+ name: "my-app",
105
+ test() {
106
+ expect(this.status?.availableReplicas).toBe(3);
107
+ },
108
+ },
109
+ { timeout: "30s", interval: "1s" },
110
+ );
111
+ ```
112
+
113
+ ### Multiple Manifest Formats
114
+
115
+ Apply resources using whichever format is most convenient:
116
+
117
+ ```ts
118
+ // Inline YAML string
119
+ await ns.apply(`
120
+ apiVersion: v1
121
+ kind: ConfigMap
122
+ metadata:
123
+ name: my-config
124
+ data:
125
+ mode: demo
126
+ `);
127
+
128
+ // TypeScript object literal (with type checking)
129
+ await ns.apply<ConfigMap>({
130
+ apiVersion: "v1",
131
+ kind: "ConfigMap",
132
+ metadata: { name: "my-config" },
133
+ data: { mode: "demo" },
134
+ });
135
+
136
+ // Imported YAML file
137
+ await ns.apply(import("./manifests/config-map.yaml"));
138
+ ```
139
+
140
+ ### Multi-Cluster Support
141
+
142
+ Test scenarios that span multiple clusters:
143
+
144
+ ```ts
145
+ test("resources sync across clusters", async (s) => {
146
+ const primary = await s.useCluster({ context: "kind-primary" });
147
+ const secondary = await s.useCluster({
148
+ context: "kind-secondary",
149
+ kubeconfig: ".kubeconfig.yaml",
150
+ });
151
+
152
+ const ns1 = await primary.newNamespace();
153
+ const ns2 = await secondary.newNamespace();
154
+
155
+ await ns1.apply(/* ... */);
156
+ await ns2.assert(/* ... */);
157
+ });
158
+ ```
159
+
160
+ ### Status Subresource Support
161
+
162
+ Simulate controller behavior by applying status subresources via server-side apply:
163
+
164
+ ```ts
165
+ await ns.applyStatus({
166
+ apiVersion: "example.com/v1",
167
+ kind: "MyResource",
168
+ metadata: { name: "my-resource" },
169
+ status: {
170
+ conditions: [
171
+ {
172
+ type: "Ready",
173
+ status: "True",
174
+ lastTransitionTime: "2026-01-01T00:00:00Z",
175
+ reason: "Reconciled",
176
+ message: "Resource is ready.",
177
+ },
178
+ ],
179
+ },
180
+ });
181
+ ```
182
+
183
+ ### List Assertions
184
+
185
+ Assert against a collection of resources:
186
+
187
+ ```ts
188
+ await ns.assertList<ConfigMap>({
189
+ apiVersion: "v1",
190
+ kind: "ConfigMap",
191
+ test() {
192
+ expect(this.some((c) => c.metadata.name === "my-config")).toBe(true);
193
+ expect(this.some((c) => c.metadata.name === "deleted-config")).toBe(false);
194
+ },
195
+ });
196
+ ```
197
+
198
+ ### Shell Command Execution
199
+
200
+ Run arbitrary shell commands with optional revert handlers for cleanup:
201
+
202
+ ```ts
203
+ const name = await s.exec({
204
+ do: async ({ $ }) => {
205
+ const name = "my-secret";
206
+ await $`kubectl create secret generic ${name} --from-literal=password=s3cr3t`.quiet();
207
+ return name;
208
+ },
209
+ revert: async ({ $ }) => {
210
+ await $`kubectl delete secret my-secret`.quiet();
211
+ },
212
+ });
213
+ ```
214
+
215
+ ### BDD-Style Reporting
216
+
217
+ Structure tests with Given/When/Then annotations for readable output:
218
+
219
+ ```ts
220
+ test("ConfigMap lifecycle", async (s) => {
221
+ s.given("a namespace exists");
222
+ const ns = await s.newNamespace();
223
+
224
+ s.when("I apply a ConfigMap");
225
+ await ns.apply(/* ... */);
226
+
227
+ s.then("the ConfigMap should have the expected data");
228
+ await ns.assert(/* ... */);
229
+ });
230
+ ```
231
+
232
+ ### Markdown Test Reports
233
+
234
+ When a test fails (or when `KEST_SHOW_REPORT=1` is set), Kest generates a detailed Markdown report showing every action, the exact `kubectl` commands executed, stdout/stderr output, and cleanup results. This provides full transparency into what happened during the test, making troubleshooting straightforward -- for both humans and AI assistants.
235
+
236
+ ```markdown
237
+ # ConfigMap lifecycle
238
+
239
+ ## Scenario Overview
240
+
241
+ | # | Action | Resource | Status |
242
+ | --- | ---------------- | ------------------- | ------ |
243
+ | 1 | Create namespace | kest-9hdhj | ✅ |
244
+ | 2 | Apply | ConfigMap/my-config | ✅ |
245
+ | 3 | Assert | ConfigMap/my-config | ✅ |
246
+
247
+ ## Scenario Details
248
+
249
+ ### Given: a namespace exists
250
+
251
+ ✅ Create Namespace "kest-9hdhj"
252
+ ...
253
+
254
+ ### Cleanup
255
+
256
+ | # | Action | Resource | Status |
257
+ | --- | ---------------- | ------------------- | ------ |
258
+ | 1 | Delete | ConfigMap/my-config | ✅ |
259
+ | 2 | Delete namespace | kest-9hdhj | ✅ |
260
+ ```
261
+
262
+ ## Getting Started
263
+
264
+ ### Prerequisites
265
+
266
+ - [Bun](https://bun.sh/) v1.3.8 or later
267
+ - `kubectl` configured with access to a Kubernetes cluster
268
+ - A running Kubernetes cluster (e.g. [kind](https://kind.sigs.k8s.io/), [minikube](https://minikube.sigs.k8s.io/), or a remote cluster)
269
+
270
+ ### Installation
271
+
272
+ ```sh
273
+ bun add -d @appthrust/kest
274
+ ```
275
+
276
+ ### Write Your First Test
277
+
278
+ Create a test file, e.g. `my-operator.test.ts`:
279
+
280
+ ```ts
281
+ import { expect } from "bun:test";
282
+ import { test } from "@appthrust/kest";
283
+
284
+ test("ConfigMap is created with correct data", async (s) => {
285
+ s.given("a new namespace exists");
286
+ const ns = await s.newNamespace();
287
+
288
+ s.when("I apply a ConfigMap");
289
+ await ns.apply({
290
+ apiVersion: "v1",
291
+ kind: "ConfigMap",
292
+ metadata: { name: "app-config" },
293
+ data: { environment: "test" },
294
+ });
295
+
296
+ s.then("the ConfigMap should contain the expected data");
297
+ await ns.assert({
298
+ apiVersion: "v1",
299
+ kind: "ConfigMap",
300
+ name: "app-config",
301
+ test() {
302
+ expect(this.data?.environment).toBe("test");
303
+ },
304
+ });
305
+ });
306
+ ```
307
+
308
+ ### Run Tests
309
+
310
+ ```sh
311
+ bun test
312
+ ```
313
+
314
+ To always show the Markdown test report (not just on failure):
315
+
316
+ ```sh
317
+ KEST_SHOW_REPORT=1 bun test
318
+ ```
319
+
320
+ ## API Reference
321
+
322
+ ### `test(label, callback, options?)`
323
+
324
+ Entry point for defining a test scenario. The callback receives a `Scenario` object.
325
+
326
+ | Option | Type | Default | Description |
327
+ | --------- | -------- | ------- | ------------------------------------ |
328
+ | `timeout` | `string` | `"60s"` | Maximum duration for the entire test |
329
+
330
+ ### Scenario
331
+
332
+ The top-level API surface available in every test callback.
333
+
334
+ | Method | Description |
335
+ | ----------------------------------------------------------------------- | ------------------------------------------------ |
336
+ | `apply(manifest, options?)` | Apply a Kubernetes manifest and register cleanup |
337
+ | `applyStatus(manifest, options?)` | Apply a status subresource (server-side apply) |
338
+ | `get(resource, options?)` | Fetch a resource by API version, kind, and name |
339
+ | `assert(resource, options?)` | Fetch a resource and run assertions with retries |
340
+ | `assertList(resource, options?)` | Fetch a list of resources and run assertions |
341
+ | `newNamespace(name?, options?)` | Create an ephemeral namespace |
342
+ | `exec(input, options?)` | Execute shell commands with optional revert |
343
+ | `useCluster(ref)` | Create a cluster-bound API surface |
344
+ | `given(desc)` / `when(desc)` / `then(desc)` / `and(desc)` / `but(desc)` | BDD annotations for reporting |
345
+
346
+ ### Namespace / Cluster
347
+
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.
349
+
350
+ ### Action Options
351
+
352
+ All actions accept an optional options object for retry configuration.
353
+
354
+ | Option | Type | Default | Description |
355
+ | ---------- | -------- | --------- | ---------------------------- |
356
+ | `timeout` | `string` | `"5s"` | Maximum retry duration |
357
+ | `interval` | `string` | `"200ms"` | Delay between retry attempts |
358
+
359
+ Duration strings support units like `"200ms"`, `"5s"`, `"1m"`.
360
+
361
+ ## Type Safety
362
+
363
+ Define TypeScript interfaces for your Kubernetes resources to get full type checking in manifests and assertions:
364
+
365
+ ```ts
366
+ import type { K8sResource } from "@appthrust/kest";
367
+
368
+ interface MyCustomResource extends K8sResource {
369
+ apiVersion: "example.com/v1";
370
+ kind: "MyResource";
371
+ metadata: { name: string };
372
+ spec: {
373
+ replicas: number;
374
+ image: string;
375
+ };
376
+ status?: {
377
+ conditions: Array<{
378
+ type: string;
379
+ status: "True" | "False" | "Unknown";
380
+ }>;
381
+ };
382
+ }
383
+
384
+ // Full autocompletion and type checking:
385
+ await ns.apply<MyCustomResource>({
386
+ apiVersion: "example.com/v1",
387
+ kind: "MyResource",
388
+ metadata: { name: "my-instance" },
389
+ spec: { replicas: 3, image: "my-app:latest" },
390
+ });
391
+
392
+ await ns.assert<MyCustomResource>({
393
+ apiVersion: "example.com/v1",
394
+ kind: "MyResource",
395
+ name: "my-instance",
396
+ test() {
397
+ // `this` is typed as MyCustomResource
398
+ expect(this.spec.replicas).toBe(3);
399
+ },
400
+ });
401
+ ```
402
+
403
+ ## Environment Variables
404
+
405
+ | Variable | Description |
406
+ | ------------------ | ----------------------------------------------------------------------- |
407
+ | `KEST_SHOW_REPORT` | Set to `"1"` to show Markdown reports for all tests (not just failures) |
408
+ | `KEST_SHOW_EVENTS` | Set to `"1"` to dump raw recorder events for debugging |
409
+
410
+ ## License
411
+
412
+ [MIT](LICENSE)
@@ -0,0 +1,6 @@
1
+ apiVersion: v1
2
+ kind: ConfigMap
3
+ metadata:
4
+ name: my-config-2
5
+ data:
6
+ mode: demo-2