@appthrust/kest 0.11.0 → 0.13.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
@@ -1168,12 +1168,45 @@ await ns.assert<MyCustomResource>({
1168
1168
  });
1169
1169
  ```
1170
1170
 
1171
+ ## Tips
1172
+
1173
+ ### Debugging failed tests
1174
+
1175
+ By default, Kest cleans up all resources after every test -- even when the test fails. This keeps the cluster tidy but destroys the state you need for debugging. To preserve resources on failure, set `KEST_PRESERVE_ON_FAILURE=1`:
1176
+
1177
+ ```sh
1178
+ KEST_PRESERVE_ON_FAILURE=1 bun test
1179
+ ```
1180
+
1181
+ When active, Kest skips cleanup for failed scenarios so you can inspect the cluster with `kubectl`. The test report will show a "Cleanup (skipped)" notice instead of the usual cleanup table. Remember to delete the leftover resources manually once you're done investigating.
1182
+
1183
+ ### Biome configuration
1184
+
1185
+ If you use [Biome](https://biomejs.dev/) for linting, the `noDoneCallback` rule may flag kest's `test` callback parameter as a false positive. The parameter is a `Scenario` object, not a done callback.
1186
+
1187
+ To suppress this, extend kest's shareable Biome config in your `biome.json`:
1188
+
1189
+ ```json
1190
+ {
1191
+ "extends": ["@appthrust/kest/biome/recommended.json"]
1192
+ }
1193
+ ```
1194
+
1195
+ This disables `lint/style/noDoneCallback` while leaving all other rules untouched. If you already extend another config, add kest's config after it:
1196
+
1197
+ ```json
1198
+ {
1199
+ "extends": ["@suin/biome.json", "@appthrust/kest/biome/recommended.json"]
1200
+ }
1201
+ ```
1202
+
1171
1203
  ## Environment Variables
1172
1204
 
1173
- | Variable | Description |
1174
- | ------------------ | ----------------------------------------------------------------------- |
1175
- | `KEST_SHOW_REPORT` | Set to `"1"` to show Markdown reports for all tests (not just failures) |
1176
- | `KEST_SHOW_EVENTS` | Set to `"1"` to dump raw recorder events for debugging |
1205
+ | Variable | Description |
1206
+ | --------------------------- | ----------------------------------------------------------------------- |
1207
+ | `KEST_SHOW_REPORT` | Set to `"1"` to show Markdown reports for all tests (not just failures) |
1208
+ | `KEST_SHOW_EVENTS` | Set to `"1"` to dump raw recorder events for debugging |
1209
+ | `KEST_PRESERVE_ON_FAILURE` | Set to `"1"` to skip cleanup when a test fails, preserving cluster state for debugging |
1177
1210
 
1178
1211
  ## License
1179
1212
 
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
3
+ "linter": {
4
+ "rules": {
5
+ "style": {
6
+ "noDoneCallback": "off"
7
+ }
8
+ }
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appthrust/kest",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Kubernetes E2E testing framework designed for humans and AI alike",
5
5
  "type": "module",
6
6
  "module": "ts/index.ts",
@@ -8,11 +8,13 @@
8
8
  ".": {
9
9
  "types": "./ts/index.ts",
10
10
  "default": "./ts/index.ts"
11
- }
11
+ },
12
+ "./biome/recommended.json": "./biome/recommended.json"
12
13
  },
13
14
  "files": [
14
15
  "ts/**/*.ts",
15
16
  "!ts/**/*.test.ts",
17
+ "biome/",
16
18
  "example/",
17
19
  "LICENSE",
18
20
  "README.md"
@@ -71,7 +71,8 @@ type RetryEvent =
71
71
 
72
72
  type RevertingsEvent =
73
73
  | BaseEvent<"RevertingsStart">
74
- | BaseEvent<"RevertingsEnd">;
74
+ | BaseEvent<"RevertingsEnd">
75
+ | BaseEvent<"RevertingsSkipped">;
75
76
 
76
77
  type BDDEvent =
77
78
  | BaseEvent<"BDDGiven", { readonly description: string }>
@@ -7,6 +7,7 @@ export interface Scenario {
7
7
  overview: Array<OverviewItem>;
8
8
  details: Array<Tagged<"BDDSection", BDDSection> | Tagged<"Action", Action>>;
9
9
  cleanup: Array<CleanupItem>;
10
+ cleanupSkipped?: boolean;
10
11
  }
11
12
 
12
13
  type Tagged<Tag extends string, Target extends object> = Target & {
@@ -75,6 +75,11 @@ function handleNonBDDEvent(state: ParseState, event: Event): void {
75
75
  state.inCleanup = false;
76
76
  clearCurrentActionState(state);
77
77
  return;
78
+ case "RevertingsSkipped": {
79
+ const scenario = ensureScenario(state.currentScenario, state.report);
80
+ scenario.cleanupSkipped = true;
81
+ return;
82
+ }
78
83
  case "ActionStart":
79
84
  handleActionStart(state, event);
80
85
  return;
@@ -157,7 +157,8 @@ export async function renderReport(
157
157
  const isEmpty =
158
158
  scenario.overview.length === 0 &&
159
159
  scenario.details.length === 0 &&
160
- scenario.cleanup.length === 0;
160
+ scenario.cleanup.length === 0 &&
161
+ !scenario.cleanupSkipped;
161
162
  if (isEmpty) {
162
163
  continue;
163
164
  }
@@ -294,7 +295,19 @@ export async function renderReport(
294
295
  }
295
296
 
296
297
  // Cleanup
297
- if (scenario.cleanup.length > 0) {
298
+ if (scenario.cleanupSkipped) {
299
+ lines.push("### Cleanup (skipped)");
300
+ lines.push("");
301
+ lines.push(
302
+ "Cleanup was skipped because `KEST_PRESERVE_ON_FAILURE=1` is set."
303
+ );
304
+ lines.push(
305
+ "Resources created during this scenario were **not** deleted."
306
+ );
307
+ lines.push(
308
+ "To clean up manually, run the revert commands from a passing test run."
309
+ );
310
+ } else if (scenario.cleanup.length > 0) {
298
311
  lines.push("### Cleanup");
299
312
  lines.push("");
300
313
  lines.push("| # | Action | Status |");
@@ -26,6 +26,9 @@ export function createReverting(deps: Deps) {
26
26
  recorder.record("RevertingsEnd", {});
27
27
  }
28
28
  },
29
+ skip(): void {
30
+ recorder.record("RevertingsSkipped", {});
31
+ },
29
32
  };
30
33
  }
31
34
 
@@ -32,7 +32,7 @@ import { retryUntil } from "../retry";
32
32
  import type { Reverting } from "../reverting";
33
33
 
34
34
  export interface InternalScenario extends Scenario {
35
- cleanup(): Promise<void>;
35
+ cleanup(options?: { skip?: boolean }): Promise<void>;
36
36
  getReport(): Promise<string>;
37
37
  }
38
38
 
@@ -61,8 +61,12 @@ export function createScenario(deps: CreateScenarioOptions): InternalScenario {
61
61
  generateName: (prefix: string) => generateRandomName(prefix),
62
62
  newNamespace: createNewNamespaceFn(deps),
63
63
  useCluster: createUseClusterFn(deps),
64
- async cleanup() {
65
- await reverting.revert();
64
+ async cleanup(options?: { skip?: boolean }) {
65
+ if (options?.skip) {
66
+ reverting.skip();
67
+ } else {
68
+ await reverting.revert();
69
+ }
66
70
  },
67
71
  async getReport() {
68
72
  return await reporter.report(recorder.getEvents());
package/ts/test.ts CHANGED
@@ -43,6 +43,7 @@ type BunTestRunner = (
43
43
  const workspaceRoot = await getWorkspaceRoot();
44
44
  const showReport = process.env["KEST_SHOW_REPORT"] === "1";
45
45
  const showEvents = process.env["KEST_SHOW_EVENTS"] === "1";
46
+ const preserveOnFailure = process.env["KEST_PRESERVE_ON_FAILURE"] === "1";
46
47
 
47
48
  function makeScenarioTest(runner: BunTestRunner): TestFunction {
48
49
  return (label, fn, options) => {
@@ -66,7 +67,9 @@ function makeScenarioTest(runner: BunTestRunner): TestFunction {
66
67
  } catch (error) {
67
68
  testErr = error as Error;
68
69
  }
69
- await scenario.cleanup();
70
+ await scenario.cleanup({
71
+ skip: preserveOnFailure && testErr !== undefined,
72
+ });
70
73
  recorder.record("ScenarioEnd", {});
71
74
  await report(recorder, scenario, testErr);
72
75
  if (testErr) {