@appthrust/kest 0.5.0 → 0.7.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 +57 -20
- package/example/example.test.ts +28 -0
- package/package.json +1 -1
- package/ts/actions/assert-apply-error.ts +43 -0
- package/ts/actions/assert-create-error.ts +43 -0
- package/ts/actions/create-namespace.ts +1 -1
- package/ts/apis/index.ts +167 -0
- package/ts/naming/index.ts +2 -2
- package/ts/recording/index.ts +1 -0
- package/ts/reporter/markdown/model.ts +1 -1
- package/ts/reporter/markdown/parser/index.ts +27 -7
- package/ts/reporter/markdown/renderer/index.ts +3 -12
- package/ts/reporter/markdown/strip-ansi.ts +8 -0
- package/ts/retry.ts +1 -0
- package/ts/scenario/index.ts +8 -0
package/README.md
CHANGED
|
@@ -113,17 +113,14 @@ await ns.assert({
|
|
|
113
113
|
Custom timeouts are supported per action:
|
|
114
114
|
|
|
115
115
|
```ts
|
|
116
|
-
await ns.assert(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
expect(this.status?.availableReplicas).toBe(3);
|
|
123
|
-
},
|
|
116
|
+
await ns.assert({
|
|
117
|
+
apiVersion: "apps/v1",
|
|
118
|
+
kind: "Deployment",
|
|
119
|
+
name: "my-app",
|
|
120
|
+
test() {
|
|
121
|
+
expect(this.status?.availableReplicas).toBe(3);
|
|
124
122
|
},
|
|
125
|
-
|
|
126
|
-
);
|
|
123
|
+
});
|
|
127
124
|
```
|
|
128
125
|
|
|
129
126
|
### Create Resources
|
|
@@ -252,7 +249,7 @@ await ns.assertOne<ConfigMap>({
|
|
|
252
249
|
test() {
|
|
253
250
|
expect(this.data?.mode).toBe("auto");
|
|
254
251
|
},
|
|
255
|
-
}
|
|
252
|
+
});
|
|
256
253
|
```
|
|
257
254
|
|
|
258
255
|
`assertOne` throws if zero or more than one resource matches, and retries until exactly one is found or the timeout expires.
|
|
@@ -269,19 +266,57 @@ await ns.assertAbsence({
|
|
|
269
266
|
});
|
|
270
267
|
```
|
|
271
268
|
|
|
272
|
-
|
|
269
|
+
### Error Assertions
|
|
270
|
+
|
|
271
|
+
Assert that applying or creating a resource produces an error (e.g. an admission webhook rejects the request, or a validation rule fails). The `test` callback inspects the error -- `this` is bound to the `Error`:
|
|
273
272
|
|
|
274
273
|
```ts
|
|
275
|
-
await ns.
|
|
276
|
-
{
|
|
277
|
-
apiVersion: "
|
|
278
|
-
kind: "
|
|
279
|
-
name: "my-
|
|
274
|
+
await ns.assertApplyError({
|
|
275
|
+
apply: {
|
|
276
|
+
apiVersion: "example.com/v1",
|
|
277
|
+
kind: "MyResource",
|
|
278
|
+
metadata: { name: "my-resource" },
|
|
279
|
+
spec: { immutableField: "changed" },
|
|
280
|
+
},
|
|
281
|
+
test() {
|
|
282
|
+
expect(this.message).toContain("field is immutable");
|
|
280
283
|
},
|
|
281
|
-
|
|
282
|
-
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
The `test` callback participates in retry -- if it throws, the action is retried until the callback passes or the timeout expires. This is useful when a webhook is being set up asynchronously:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
await ns.assertApplyError({
|
|
291
|
+
apply: {
|
|
292
|
+
apiVersion: "example.com/v1",
|
|
293
|
+
kind: "MyResource",
|
|
294
|
+
metadata: { name: "my-resource" },
|
|
295
|
+
spec: { immutableField: "changed" },
|
|
296
|
+
},
|
|
297
|
+
test(error) {
|
|
298
|
+
expect(error.message).toContain("field is immutable");
|
|
299
|
+
},
|
|
300
|
+
});
|
|
283
301
|
```
|
|
284
302
|
|
|
303
|
+
`assertCreateError` works identically for `kubectl create`:
|
|
304
|
+
|
|
305
|
+
```ts
|
|
306
|
+
await ns.assertCreateError({
|
|
307
|
+
create: {
|
|
308
|
+
apiVersion: "v1",
|
|
309
|
+
kind: "ConfigMap",
|
|
310
|
+
metadata: { name: "already-exists" },
|
|
311
|
+
},
|
|
312
|
+
test(error) {
|
|
313
|
+
expect(error.message).toContain("already exists");
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
If the apply/create unexpectedly succeeds (e.g. the webhook is not yet active), the resource is immediately reverted and the action retries until the expected error occurs.
|
|
319
|
+
|
|
285
320
|
### Label Resources
|
|
286
321
|
|
|
287
322
|
Add, update, or remove labels on Kubernetes resources using `kubectl label`:
|
|
@@ -470,6 +505,8 @@ The top-level API surface available in every test callback.
|
|
|
470
505
|
| ----------------------------------------------------------------------- | ----------------------------------------------------------- |
|
|
471
506
|
| `apply(manifest, options?)` | Apply a Kubernetes manifest and register cleanup |
|
|
472
507
|
| `create(manifest, options?)` | Create a Kubernetes resource and register cleanup |
|
|
508
|
+
| `assertApplyError(input, options?)` | Assert that `kubectl apply` produces an error |
|
|
509
|
+
| `assertCreateError(input, options?)` | Assert that `kubectl create` produces an error |
|
|
473
510
|
| `applyStatus(manifest, options?)` | Apply a status subresource (server-side apply) |
|
|
474
511
|
| `delete(resource, options?)` | Delete a resource by API version, kind, and name |
|
|
475
512
|
| `label(input, options?)` | Add, update, or remove labels on a resource |
|
|
@@ -486,7 +523,7 @@ The top-level API surface available in every test callback.
|
|
|
486
523
|
|
|
487
524
|
### Namespace / Cluster
|
|
488
525
|
|
|
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`.
|
|
526
|
+
Returned by `newNamespace()` and `useCluster()` respectively. They expose the same core methods (`apply`, `create`, `assertApplyError`, `assertCreateError`, `applyStatus`, `delete`, `label`, `get`, `assert`, `assertAbsence`, `assertList`, `assertOne`) scoped to their namespace or cluster context. `Cluster` additionally supports `newNamespace`.
|
|
490
527
|
|
|
491
528
|
`Namespace` also exposes a `name` property:
|
|
492
529
|
|
package/example/example.test.ts
CHANGED
|
@@ -60,6 +60,34 @@ test("Example: applies ConfigMap using YAML, file import, and object literal", a
|
|
|
60
60
|
// 4. Namespace
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
test("Example: diff demo - ConfigMap data mismatch (expected to fail)", async (s) => {
|
|
64
|
+
s.given("a new namespace exists");
|
|
65
|
+
const ns = await s.newNamespace();
|
|
66
|
+
|
|
67
|
+
s.when("I apply a ConfigMap with actual data");
|
|
68
|
+
await ns.apply<ConfigMap>({
|
|
69
|
+
apiVersion: "v1",
|
|
70
|
+
kind: "ConfigMap",
|
|
71
|
+
metadata: { name: "diff-demo" },
|
|
72
|
+
data: { mode: "actual-value", env: "production" },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
s.then("asserting with different expected data should produce a diff");
|
|
76
|
+
await ns.assert<ConfigMap>({
|
|
77
|
+
apiVersion: "v1",
|
|
78
|
+
kind: "ConfigMap",
|
|
79
|
+
name: "diff-demo",
|
|
80
|
+
test() {
|
|
81
|
+
expect(this).toMatchObject({
|
|
82
|
+
data: {
|
|
83
|
+
mode: "expected-value",
|
|
84
|
+
env: "staging",
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
63
91
|
test("Example: asserts a non-existent ConfigMap (expected to fail)", async (s) => {
|
|
64
92
|
s.given("a new namespace exists");
|
|
65
93
|
const ns = await s.newNamespace();
|
package/package.json
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AssertApplyErrorInput } from "../apis";
|
|
2
|
+
import { apply } from "./apply";
|
|
3
|
+
import type { MutateDef } from "./types";
|
|
4
|
+
|
|
5
|
+
export const assertApplyError = {
|
|
6
|
+
type: "mutate",
|
|
7
|
+
name: "AssertApplyError",
|
|
8
|
+
mutate:
|
|
9
|
+
({ kubectl }) =>
|
|
10
|
+
async (input) => {
|
|
11
|
+
const applyFn = apply.mutate({ kubectl });
|
|
12
|
+
let result: Awaited<ReturnType<typeof applyFn>> | undefined;
|
|
13
|
+
let rejection: Error | undefined;
|
|
14
|
+
try {
|
|
15
|
+
result = await applyFn(input.apply);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
rejection = err as Error;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Apply succeeded unexpectedly -- revert immediately and throw so that
|
|
21
|
+
// the scenario wrapper retries.
|
|
22
|
+
if (result !== undefined) {
|
|
23
|
+
await result.revert();
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Expected ${apply.describe(input.apply)} to err, but it succeeded`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Apply erred as expected -- run test callback.
|
|
30
|
+
// biome-ignore lint/style/noNonNullAssertion: rejection is guaranteed non-undefined when result is undefined
|
|
31
|
+
await input.test.call(rejection!, rejection!);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
async revert() {
|
|
35
|
+
// Nothing to clean up -- the resource was never created.
|
|
36
|
+
},
|
|
37
|
+
output: undefined,
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
describe: (input) => {
|
|
41
|
+
return `${apply.describe(input.apply)} (expected error)`;
|
|
42
|
+
},
|
|
43
|
+
} satisfies MutateDef<AssertApplyErrorInput, void>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AssertCreateErrorInput } from "../apis";
|
|
2
|
+
import { create } from "./create";
|
|
3
|
+
import type { MutateDef } from "./types";
|
|
4
|
+
|
|
5
|
+
export const assertCreateError = {
|
|
6
|
+
type: "mutate",
|
|
7
|
+
name: "AssertCreateError",
|
|
8
|
+
mutate:
|
|
9
|
+
({ kubectl }) =>
|
|
10
|
+
async (input) => {
|
|
11
|
+
const createFn = create.mutate({ kubectl });
|
|
12
|
+
let result: Awaited<ReturnType<typeof createFn>> | undefined;
|
|
13
|
+
let rejection: Error | undefined;
|
|
14
|
+
try {
|
|
15
|
+
result = await createFn(input.create);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
rejection = err as Error;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Create succeeded unexpectedly -- revert immediately and throw so that
|
|
21
|
+
// the scenario wrapper retries.
|
|
22
|
+
if (result !== undefined) {
|
|
23
|
+
await result.revert();
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Expected ${create.describe(input.create)} to err, but it succeeded`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Create erred as expected -- run test callback.
|
|
30
|
+
// biome-ignore lint/style/noNonNullAssertion: rejection is guaranteed non-undefined when result is undefined
|
|
31
|
+
await input.test.call(rejection!, rejection!);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
async revert() {
|
|
35
|
+
// Nothing to clean up -- the resource was never created.
|
|
36
|
+
},
|
|
37
|
+
output: undefined,
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
describe: (input) => {
|
|
41
|
+
return `${create.describe(input.create)} (expected error)`;
|
|
42
|
+
},
|
|
43
|
+
} satisfies MutateDef<AssertCreateErrorInput, void>;
|
package/ts/apis/index.ts
CHANGED
|
@@ -87,6 +87,75 @@ export interface Scenario {
|
|
|
87
87
|
options?: undefined | ActionOptions
|
|
88
88
|
): Promise<void>;
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Asserts that `kubectl apply` produces an error.
|
|
92
|
+
*
|
|
93
|
+
* The manifest is applied, and the action succeeds when the API server
|
|
94
|
+
* returns an error (e.g. an admission webhook rejects the request). The
|
|
95
|
+
* `test` callback must also pass for the action to succeed.
|
|
96
|
+
*
|
|
97
|
+
* If the apply unexpectedly succeeds, the created resource is immediately
|
|
98
|
+
* reverted and the action is retried until the expected error occurs or the
|
|
99
|
+
* timeout expires.
|
|
100
|
+
*
|
|
101
|
+
* @template T - The expected Kubernetes resource shape.
|
|
102
|
+
* @param input - Manifest to apply and error assertion callback.
|
|
103
|
+
* @param options - Retry options such as timeout and polling interval.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* await s.assertApplyError({
|
|
108
|
+
* apply: {
|
|
109
|
+
* apiVersion: "example.com/v1",
|
|
110
|
+
* kind: "MyResource",
|
|
111
|
+
* metadata: { name: "my-resource" },
|
|
112
|
+
* spec: { immutableField: "changed" },
|
|
113
|
+
* },
|
|
114
|
+
* test() {
|
|
115
|
+
* expect(this.message).toContain("field is immutable");
|
|
116
|
+
* },
|
|
117
|
+
* });
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
assertApplyError<T extends K8sResource>(
|
|
121
|
+
input: AssertApplyErrorInput<T>,
|
|
122
|
+
options?: undefined | ActionOptions
|
|
123
|
+
): Promise<void>;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Asserts that `kubectl create` produces an error.
|
|
127
|
+
*
|
|
128
|
+
* The manifest is created, and the action succeeds when the API server
|
|
129
|
+
* returns an error. The `test` callback must also pass for the action to
|
|
130
|
+
* succeed.
|
|
131
|
+
*
|
|
132
|
+
* If the create unexpectedly succeeds, the created resource is immediately
|
|
133
|
+
* reverted and the action is retried until the expected error occurs or the
|
|
134
|
+
* timeout expires.
|
|
135
|
+
*
|
|
136
|
+
* @template T - The expected Kubernetes resource shape.
|
|
137
|
+
* @param input - Manifest to create and error assertion callback.
|
|
138
|
+
* @param options - Retry options such as timeout and polling interval.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* await s.assertCreateError({
|
|
143
|
+
* create: {
|
|
144
|
+
* apiVersion: "v1",
|
|
145
|
+
* kind: "ConfigMap",
|
|
146
|
+
* metadata: { name: "already-exists" },
|
|
147
|
+
* },
|
|
148
|
+
* test(error) {
|
|
149
|
+
* expect(error.message).toContain("already exists");
|
|
150
|
+
* },
|
|
151
|
+
* });
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
assertCreateError<T extends K8sResource>(
|
|
155
|
+
input: AssertCreateErrorInput<T>,
|
|
156
|
+
options?: undefined | ActionOptions
|
|
157
|
+
): Promise<void>;
|
|
158
|
+
|
|
90
159
|
/**
|
|
91
160
|
* Applies the `status` subresource using server-side apply.
|
|
92
161
|
*
|
|
@@ -573,6 +642,30 @@ export interface Cluster {
|
|
|
573
642
|
options?: undefined | ActionOptions
|
|
574
643
|
): Promise<void>;
|
|
575
644
|
|
|
645
|
+
/**
|
|
646
|
+
* Asserts that `kubectl apply` produces an error.
|
|
647
|
+
*
|
|
648
|
+
* @template T - The expected Kubernetes resource shape.
|
|
649
|
+
* @param input - Manifest to apply and error assertion callback.
|
|
650
|
+
* @param options - Retry options such as timeout and polling interval.
|
|
651
|
+
*/
|
|
652
|
+
assertApplyError<T extends K8sResource>(
|
|
653
|
+
input: AssertApplyErrorInput<T>,
|
|
654
|
+
options?: undefined | ActionOptions
|
|
655
|
+
): Promise<void>;
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Asserts that `kubectl create` produces an error.
|
|
659
|
+
*
|
|
660
|
+
* @template T - The expected Kubernetes resource shape.
|
|
661
|
+
* @param input - Manifest to create and error assertion callback.
|
|
662
|
+
* @param options - Retry options such as timeout and polling interval.
|
|
663
|
+
*/
|
|
664
|
+
assertCreateError<T extends K8sResource>(
|
|
665
|
+
input: AssertCreateErrorInput<T>,
|
|
666
|
+
options?: undefined | ActionOptions
|
|
667
|
+
): Promise<void>;
|
|
668
|
+
|
|
576
669
|
/**
|
|
577
670
|
* Applies the `status` subresource using server-side apply.
|
|
578
671
|
*
|
|
@@ -871,6 +964,30 @@ export interface Namespace {
|
|
|
871
964
|
options?: undefined | ActionOptions
|
|
872
965
|
): Promise<void>;
|
|
873
966
|
|
|
967
|
+
/**
|
|
968
|
+
* Asserts that `kubectl apply` produces an error in this namespace.
|
|
969
|
+
*
|
|
970
|
+
* @template T - The expected Kubernetes resource shape.
|
|
971
|
+
* @param input - Manifest to apply and error assertion callback.
|
|
972
|
+
* @param options - Retry options such as timeout and polling interval.
|
|
973
|
+
*/
|
|
974
|
+
assertApplyError<T extends K8sResource>(
|
|
975
|
+
input: AssertApplyErrorInput<T>,
|
|
976
|
+
options?: undefined | ActionOptions
|
|
977
|
+
): Promise<void>;
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Asserts that `kubectl create` produces an error in this namespace.
|
|
981
|
+
*
|
|
982
|
+
* @template T - The expected Kubernetes resource shape.
|
|
983
|
+
* @param input - Manifest to create and error assertion callback.
|
|
984
|
+
* @param options - Retry options such as timeout and polling interval.
|
|
985
|
+
*/
|
|
986
|
+
assertCreateError<T extends K8sResource>(
|
|
987
|
+
input: AssertCreateErrorInput<T>,
|
|
988
|
+
options?: undefined | ActionOptions
|
|
989
|
+
): Promise<void>;
|
|
990
|
+
|
|
874
991
|
/**
|
|
875
992
|
* Applies the `status` subresource in this namespace using server-side apply.
|
|
876
993
|
*
|
|
@@ -1279,6 +1396,56 @@ export interface ResourceOneTest<T extends K8sResource = K8sResource> {
|
|
|
1279
1396
|
readonly test: (this: T, resource: T) => unknown | Promise<unknown>;
|
|
1280
1397
|
}
|
|
1281
1398
|
|
|
1399
|
+
/**
|
|
1400
|
+
* A test definition for {@link Scenario.assertApplyError},
|
|
1401
|
+
* {@link Cluster.assertApplyError}, and {@link Namespace.assertApplyError}.
|
|
1402
|
+
*
|
|
1403
|
+
* Attempts `kubectl apply` and asserts that the API server returns an error.
|
|
1404
|
+
* When the operation errors as expected, the `test` callback is invoked with
|
|
1405
|
+
* `this` bound to the {@link Error}.
|
|
1406
|
+
*/
|
|
1407
|
+
export interface AssertApplyErrorInput<T extends K8sResource = K8sResource> {
|
|
1408
|
+
/**
|
|
1409
|
+
* The manifest to apply. Accepts the same formats as {@link Scenario.apply}:
|
|
1410
|
+
* an object literal, a YAML string, or an imported YAML module.
|
|
1411
|
+
*/
|
|
1412
|
+
readonly apply: ApplyingManifest<T>;
|
|
1413
|
+
|
|
1414
|
+
/**
|
|
1415
|
+
* Assertion callback invoked when the apply errors as expected.
|
|
1416
|
+
*
|
|
1417
|
+
* `this` is bound to the {@link Error} returned by the API server.
|
|
1418
|
+
* Throwing (or rejecting) signals that the error did not match expectations
|
|
1419
|
+
* and triggers a retry (if timeout allows).
|
|
1420
|
+
*/
|
|
1421
|
+
readonly test: (this: Error, error: Error) => unknown | Promise<unknown>;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/**
|
|
1425
|
+
* A test definition for {@link Scenario.assertCreateError},
|
|
1426
|
+
* {@link Cluster.assertCreateError}, and {@link Namespace.assertCreateError}.
|
|
1427
|
+
*
|
|
1428
|
+
* Attempts `kubectl create` and asserts that the API server returns an error.
|
|
1429
|
+
* When the operation errors as expected, the `test` callback is invoked with
|
|
1430
|
+
* `this` bound to the {@link Error}.
|
|
1431
|
+
*/
|
|
1432
|
+
export interface AssertCreateErrorInput<T extends K8sResource = K8sResource> {
|
|
1433
|
+
/**
|
|
1434
|
+
* The manifest to create. Accepts the same formats as {@link Scenario.create}:
|
|
1435
|
+
* an object literal, a YAML string, or an imported YAML module.
|
|
1436
|
+
*/
|
|
1437
|
+
readonly create: ApplyingManifest<T>;
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* Assertion callback invoked when the create errors as expected.
|
|
1441
|
+
*
|
|
1442
|
+
* `this` is bound to the {@link Error} returned by the API server.
|
|
1443
|
+
* Throwing (or rejecting) signals that the error did not match expectations
|
|
1444
|
+
* and triggers a retry (if timeout allows).
|
|
1445
|
+
*/
|
|
1446
|
+
readonly test: (this: Error, error: Error) => unknown | Promise<unknown>;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1282
1449
|
/**
|
|
1283
1450
|
* Kubernetes cluster selector for {@link Scenario.useCluster}.
|
|
1284
1451
|
*/
|
package/ts/naming/index.ts
CHANGED
|
@@ -8,7 +8,8 @@ const consonantDigits = "bcdfghjklmnpqrstvwxyz0123456789";
|
|
|
8
8
|
export function randomConsonantDigits(length = 8): string {
|
|
9
9
|
let result = "";
|
|
10
10
|
for (let i = 0; i < length; i++) {
|
|
11
|
-
result +=
|
|
11
|
+
result +=
|
|
12
|
+
consonantDigits[Math.floor(Math.random() * consonantDigits.length)];
|
|
12
13
|
}
|
|
13
14
|
return result;
|
|
14
15
|
}
|
|
@@ -24,4 +25,3 @@ export function randomConsonantDigits(length = 8): string {
|
|
|
24
25
|
export function generateName(prefix: string, suffixLength = 5): string {
|
|
25
26
|
return `${prefix}${randomConsonantDigits(suffixLength)}`;
|
|
26
27
|
}
|
|
27
|
-
|
package/ts/recording/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
Report,
|
|
8
8
|
Scenario,
|
|
9
9
|
} from "../model";
|
|
10
|
+
import { stripAnsi } from "../strip-ansi";
|
|
10
11
|
|
|
11
12
|
const bddKeywordByKind = {
|
|
12
13
|
BDDGiven: "given",
|
|
@@ -89,6 +90,9 @@ function handleNonBDDEvent(state: ParseState, event: Event): void {
|
|
|
89
90
|
case "RetryEnd":
|
|
90
91
|
handleRetryEnd(state, event);
|
|
91
92
|
return;
|
|
93
|
+
case "RetryAttempt":
|
|
94
|
+
handleRetryAttempt(state);
|
|
95
|
+
return;
|
|
92
96
|
case "RetryStart":
|
|
93
97
|
return;
|
|
94
98
|
default:
|
|
@@ -114,7 +118,7 @@ function handleActionStart(
|
|
|
114
118
|
return;
|
|
115
119
|
}
|
|
116
120
|
|
|
117
|
-
const action: Action = { name: event.data.description };
|
|
121
|
+
const action: Action = { name: event.data.description, commands: [] };
|
|
118
122
|
scenario.overview.push({
|
|
119
123
|
name: event.data.description,
|
|
120
124
|
status: "pending",
|
|
@@ -158,7 +162,9 @@ function applyRegularActionEnd(
|
|
|
158
162
|
currentAction.error = {
|
|
159
163
|
message: {
|
|
160
164
|
text: event.data.error.message,
|
|
161
|
-
language: isDiffLike(event.data.error.message)
|
|
165
|
+
language: isDiffLike(stripAnsi(event.data.error.message))
|
|
166
|
+
? "diff"
|
|
167
|
+
: "text",
|
|
162
168
|
},
|
|
163
169
|
};
|
|
164
170
|
}
|
|
@@ -180,17 +186,21 @@ function handleCommandResult(
|
|
|
180
186
|
}
|
|
181
187
|
|
|
182
188
|
const { currentAction } = state;
|
|
183
|
-
if (!currentAction
|
|
189
|
+
if (!currentAction || currentAction.commands.length === 0) {
|
|
184
190
|
return;
|
|
185
191
|
}
|
|
186
192
|
|
|
187
|
-
|
|
193
|
+
const command = currentAction.commands.at(-1);
|
|
194
|
+
if (!command) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
command.stdout = {
|
|
188
198
|
text: event.data.stdout,
|
|
189
199
|
...(event.data.stdoutLanguage
|
|
190
200
|
? { language: event.data.stdoutLanguage }
|
|
191
201
|
: {}),
|
|
192
202
|
};
|
|
193
|
-
|
|
203
|
+
command.stderr = {
|
|
194
204
|
text: event.data.stderr,
|
|
195
205
|
...(event.data.stderrLanguage
|
|
196
206
|
? { language: event.data.stderrLanguage }
|
|
@@ -216,7 +226,7 @@ function handleCommandRun(
|
|
|
216
226
|
if (!state.currentAction) {
|
|
217
227
|
return;
|
|
218
228
|
}
|
|
219
|
-
state.currentAction.
|
|
229
|
+
state.currentAction.commands.push(createCommandFromRun(event));
|
|
220
230
|
}
|
|
221
231
|
|
|
222
232
|
function handleScenarioStart(
|
|
@@ -259,6 +269,16 @@ function handleCleanupActionEnd(
|
|
|
259
269
|
return true;
|
|
260
270
|
}
|
|
261
271
|
|
|
272
|
+
function handleRetryAttempt(state: ParseState): void {
|
|
273
|
+
if (state.inCleanup) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (!state.currentAction) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
state.currentAction.commands = [];
|
|
280
|
+
}
|
|
281
|
+
|
|
262
282
|
function handleRetryEnd(
|
|
263
283
|
state: ParseState,
|
|
264
284
|
event: Extract<Event, { kind: "RetryEnd" }>
|
|
@@ -338,7 +358,7 @@ function bddFromEvent(event: Event): BDDSection | undefined {
|
|
|
338
358
|
return { keyword, description: event.data.description, actions: [] };
|
|
339
359
|
}
|
|
340
360
|
|
|
341
|
-
function isDiffLike(message: string): boolean {
|
|
361
|
+
export function isDiffLike(message: string): boolean {
|
|
342
362
|
const lines = message.split(/\r?\n/);
|
|
343
363
|
let sawPlus = false;
|
|
344
364
|
let sawMinus = false;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { codeToANSIForcedColors } from "../../shiki";
|
|
2
2
|
import type { MarkdownReporterOptions } from "../index";
|
|
3
3
|
import type { Action, Report } from "../model";
|
|
4
|
+
import { stripAnsi } from "../strip-ansi";
|
|
4
5
|
|
|
5
6
|
const markdownLang = "markdown";
|
|
6
7
|
const markdownTheme = "catppuccin-mocha";
|
|
@@ -83,15 +84,6 @@ async function highlightMarkdown(
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
function stripAnsi(input: string): string {
|
|
87
|
-
// Prefer Bun's built-in ANSI stripper when available.
|
|
88
|
-
if (typeof Bun !== "undefined" && typeof Bun.stripANSI === "function") {
|
|
89
|
-
return Bun.stripANSI(input);
|
|
90
|
-
}
|
|
91
|
-
// biome-ignore lint/suspicious/noControlCharactersInRegex: intended
|
|
92
|
-
return input.replace(/\u001b\[[0-9;]*m/g, "");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
87
|
function trimFinalNewline(input: string): string {
|
|
96
88
|
return input.replace(/\n$/, "");
|
|
97
89
|
}
|
|
@@ -165,7 +157,7 @@ export function renderReport(
|
|
|
165
157
|
if (!status) {
|
|
166
158
|
if (action.error) {
|
|
167
159
|
status = "failure";
|
|
168
|
-
} else if (action.
|
|
160
|
+
} else if (action.commands.length > 0) {
|
|
169
161
|
status = "success";
|
|
170
162
|
} else {
|
|
171
163
|
status = "pending";
|
|
@@ -180,8 +172,7 @@ export function renderReport(
|
|
|
180
172
|
lines.push(`**${emoji} ${stripAnsi(action.name)}**${attemptsSuffix}`);
|
|
181
173
|
lines.push("");
|
|
182
174
|
|
|
183
|
-
const cmd
|
|
184
|
-
if (cmd) {
|
|
175
|
+
for (const cmd of action.commands) {
|
|
185
176
|
const base = [cmd.cmd, ...cmd.args].join(" ").trim();
|
|
186
177
|
const stdin = cmd.stdin?.text;
|
|
187
178
|
const stdinLanguage = cmd.stdin?.language ?? "text";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function stripAnsi(input: string): string {
|
|
2
|
+
// Prefer Bun's built-in ANSI stripper when available.
|
|
3
|
+
if (typeof Bun !== "undefined" && typeof Bun.stripANSI === "function") {
|
|
4
|
+
return Bun.stripANSI(input);
|
|
5
|
+
}
|
|
6
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: intended
|
|
7
|
+
return input.replace(/\u001b\[[0-9;]*m/g, "");
|
|
8
|
+
}
|
package/ts/retry.ts
CHANGED
package/ts/scenario/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { apply } from "../actions/apply";
|
|
|
2
2
|
import { applyStatus } from "../actions/apply-status";
|
|
3
3
|
import { assert } from "../actions/assert";
|
|
4
4
|
import { assertAbsence } from "../actions/assert-absence";
|
|
5
|
+
import { assertApplyError } from "../actions/assert-apply-error";
|
|
6
|
+
import { assertCreateError } from "../actions/assert-create-error";
|
|
5
7
|
import { assertList } from "../actions/assert-list";
|
|
6
8
|
import { assertOne } from "../actions/assert-one";
|
|
7
9
|
import { create } from "../actions/create";
|
|
@@ -39,6 +41,8 @@ export function createScenario(deps: CreateScenarioOptions): InternalScenario {
|
|
|
39
41
|
return {
|
|
40
42
|
apply: createMutateFn(deps, apply),
|
|
41
43
|
create: createMutateFn(deps, create),
|
|
44
|
+
assertApplyError: createMutateFn(deps, assertApplyError),
|
|
45
|
+
assertCreateError: createMutateFn(deps, assertCreateError),
|
|
42
46
|
applyStatus: createOneWayMutateFn(deps, applyStatus),
|
|
43
47
|
delete: createOneWayMutateFn(deps, deleteResource),
|
|
44
48
|
label: createOneWayMutateFn(deps, label),
|
|
@@ -205,6 +209,8 @@ const createNewNamespaceFn =
|
|
|
205
209
|
name: namespaceName,
|
|
206
210
|
apply: createMutateFn(namespacedDeps, apply),
|
|
207
211
|
create: createMutateFn(namespacedDeps, create),
|
|
212
|
+
assertApplyError: createMutateFn(namespacedDeps, assertApplyError),
|
|
213
|
+
assertCreateError: createMutateFn(namespacedDeps, assertCreateError),
|
|
208
214
|
applyStatus: createOneWayMutateFn(namespacedDeps, applyStatus),
|
|
209
215
|
delete: createOneWayMutateFn(namespacedDeps, deleteResource),
|
|
210
216
|
label: createOneWayMutateFn(namespacedDeps, label),
|
|
@@ -229,6 +235,8 @@ const createUseClusterFn =
|
|
|
229
235
|
return {
|
|
230
236
|
apply: createMutateFn(clusterDeps, apply),
|
|
231
237
|
create: createMutateFn(clusterDeps, create),
|
|
238
|
+
assertApplyError: createMutateFn(clusterDeps, assertApplyError),
|
|
239
|
+
assertCreateError: createMutateFn(clusterDeps, assertCreateError),
|
|
232
240
|
applyStatus: createOneWayMutateFn(clusterDeps, applyStatus),
|
|
233
241
|
delete: createOneWayMutateFn(clusterDeps, deleteResource),
|
|
234
242
|
label: createOneWayMutateFn(clusterDeps, label),
|