@appthrust/kest 0.3.0 → 0.3.1

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
@@ -119,6 +119,21 @@ await ns.assert(
119
119
  );
120
120
  ```
121
121
 
122
+ ### Create Resources
123
+
124
+ Use `kubectl create` instead of `kubectl apply` when you need to ensure a resource is freshly created (e.g. the resource must not already exist, or you want to use `generateName`):
125
+
126
+ ```ts
127
+ await ns.create({
128
+ apiVersion: "v1",
129
+ kind: "ConfigMap",
130
+ metadata: { name: "my-config" },
131
+ data: { mode: "demo" },
132
+ });
133
+ ```
134
+
135
+ Like `apply`, `create` registers a cleanup handler that deletes the resource when the test ends. The key difference is that `kubectl create` fails if the resource already exists, whereas `kubectl apply` performs an upsert.
136
+
122
137
  ### Multiple Manifest Formats
123
138
 
124
139
  Apply resources using whichever format is most convenient:
@@ -398,6 +413,7 @@ The top-level API surface available in every test callback.
398
413
  | Method | Description |
399
414
  | ----------------------------------------------------------------------- | ------------------------------------------------- |
400
415
  | `apply(manifest, options?)` | Apply a Kubernetes manifest and register cleanup |
416
+ | `create(manifest, options?)` | Create a Kubernetes resource and register cleanup |
401
417
  | `applyStatus(manifest, options?)` | Apply a status subresource (server-side apply) |
402
418
  | `delete(resource, options?)` | Delete a resource by API version, kind, and name |
403
419
  | `label(input, options?)` | Add, update, or remove labels on a resource |
@@ -412,7 +428,7 @@ The top-level API surface available in every test callback.
412
428
 
413
429
  ### Namespace / Cluster
414
430
 
415
- Returned by `newNamespace()` and `useCluster()` respectively. They expose the same core methods (`apply`, `applyStatus`, `delete`, `label`, `get`, `assert`, `assertAbsence`, `assertList`) scoped to their namespace or cluster context. `Cluster` additionally supports `newNamespace`.
431
+ 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`.
416
432
 
417
433
  ### Action Options
418
434
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appthrust/kest",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Kubernetes E2E testing framework designed for humans and AI alike",
5
5
  "type": "module",
6
6
  "module": "ts/index.ts",
@@ -52,6 +52,7 @@
52
52
  "devDependencies": {
53
53
  "@biomejs/biome": "^2.3.13",
54
54
  "@suin/biome.json": "^0.1.0",
55
+ "@taml/encoder": "^1.0.0",
55
56
  "@tsconfig/bun": "^1.0.10",
56
57
  "@tsconfig/strictest": "^2.0.8",
57
58
  "@types/bun": "^1.3.7",
@@ -30,6 +30,15 @@ export const applyNamespace = {
30
30
  });
31
31
  return { revert, output: name };
32
32
  },
33
+ describe: (input) => {
34
+ if (input === undefined) {
35
+ return "Apply `Namespace` with auto-generated name";
36
+ }
37
+ if (typeof input === "string") {
38
+ return `Apply \`Namespace\` "${input}"`;
39
+ }
40
+ return `Apply \`Namespace\` with prefix "${input.generateName}"`;
41
+ },
33
42
  } satisfies MutateDef<ApplyNamespaceInput, string>;
34
43
 
35
44
  function resolveNamespaceName(input: ApplyNamespaceInput): string {
@@ -1,5 +1,5 @@
1
1
  import type { ApplyingManifest } from "../apis";
2
- import { parseK8sResourceAny } from "../k8s-resource";
2
+ import { getResourceMeta, parseK8sResourceAny } from "../k8s-resource";
3
3
  import type { OneWayMutateDef } from "./types";
4
4
 
5
5
  export const applyStatus = {
@@ -20,4 +20,11 @@ export const applyStatus = {
20
20
  await kubectl.applyStatus(result.value);
21
21
  return undefined;
22
22
  },
23
+ describe: (manifest) => {
24
+ const meta = getResourceMeta(manifest);
25
+ if (meta === undefined) {
26
+ return "Apply status of a resource";
27
+ }
28
+ return `Apply status of \`${meta.kind}\` "${meta.name}"`;
29
+ },
23
30
  } satisfies OneWayMutateDef<ApplyingManifest, void>;
@@ -1,5 +1,5 @@
1
1
  import type { ApplyingManifest } from "../apis";
2
- import { parseK8sResourceAny } from "../k8s-resource";
2
+ import { getResourceMeta, parseK8sResourceAny } from "../k8s-resource";
3
3
  import type { MutateDef } from "./types";
4
4
 
5
5
  export const apply = {
@@ -24,4 +24,11 @@ export const apply = {
24
24
  output: undefined,
25
25
  };
26
26
  },
27
+ describe: (manifest) => {
28
+ const meta = getResourceMeta(manifest);
29
+ if (meta === undefined) {
30
+ return "Apply a resource";
31
+ }
32
+ return `Apply \`${meta.kind}\` "${meta.name}"`;
33
+ },
27
34
  } satisfies MutateDef<ApplyingManifest, void>;
@@ -22,6 +22,8 @@ export const assertAbsence = {
22
22
  `Expected ${resource.kind} "${resource.name}" to be absent, but it exists`
23
23
  );
24
24
  },
25
+ describe: (resource) =>
26
+ `Assert that \`${resource.kind}\` "${resource.name}" is absent`,
25
27
  } satisfies QueryDef<K8sResourceReference, void>;
26
28
 
27
29
  /**
@@ -28,6 +28,9 @@ export const assertList = {
28
28
  await condition.test.call(typed, typed);
29
29
  return typed;
30
30
  },
31
+ describe: <T extends K8sResource>(condition: ResourceListTest<T>): string => {
32
+ return `Assert a list of \`${condition.kind}\` resources`;
33
+ },
31
34
  } satisfies QueryDef<ResourceListTest, Array<K8sResource>>;
32
35
 
33
36
  function isSameGVK<T extends K8sResource>(
@@ -19,6 +19,9 @@ export const assert = {
19
19
  await condition.test.call(fetched, fetched);
20
20
  return fetched;
21
21
  },
22
+ describe: <T extends K8sResource>(condition: ResourceTest<T>): string => {
23
+ return `Assert \`${condition.kind}\` "${condition.name}"`;
24
+ },
22
25
  } satisfies QueryDef<ResourceTest, K8sResource>;
23
26
 
24
27
  function toKubectlType<T extends K8sResource>(
@@ -0,0 +1,34 @@
1
+ import type { ApplyingManifest } from "../apis";
2
+ import { getResourceMeta, parseK8sResourceAny } from "../k8s-resource";
3
+ import type { MutateDef } from "./types";
4
+
5
+ export const create = {
6
+ type: "mutate",
7
+ name: "Create",
8
+ mutate:
9
+ ({ kubectl }) =>
10
+ async (manifest) => {
11
+ const result = await parseK8sResourceAny(manifest);
12
+ if (!result.ok) {
13
+ throw new Error(
14
+ `Invalid Kubernetes resource: ${result.violations.join(", ")}`
15
+ );
16
+ }
17
+ await kubectl.create(result.value);
18
+ return {
19
+ async revert() {
20
+ await kubectl.delete(result.value.kind, result.value.metadata.name, {
21
+ ignoreNotFound: true,
22
+ });
23
+ },
24
+ output: undefined,
25
+ };
26
+ },
27
+ describe: (manifest) => {
28
+ const meta = getResourceMeta(manifest);
29
+ if (meta === undefined) {
30
+ return "Create a resource";
31
+ }
32
+ return `Create \`${meta.kind}\` "${meta.name}"`;
33
+ },
34
+ } satisfies MutateDef<ApplyingManifest, void>;
@@ -11,4 +11,7 @@ export const deleteResource = {
11
11
  await kubectl.delete(toKubectlType(resource), resource.name);
12
12
  return undefined;
13
13
  },
14
+ describe: (resource) => {
15
+ return `Delete \`${resource.kind}\` "${resource.name}"`;
16
+ },
14
17
  } satisfies OneWayMutateDef<K8sResourceReference, void>;
@@ -17,5 +17,8 @@ export const exec = {
17
17
  revert: revert ? () => revert(context) : noopRevert,
18
18
  };
19
19
  },
20
+ describe: () => {
21
+ return "Execute arbitrary processing";
22
+ },
20
23
  // biome-ignore lint/suspicious/noExplicitAny: 本当はunknownにしたいが、createMutateFnとの噛み合せが難しいためanyにしている
21
24
  } satisfies MutateDef<ExecInput<any>, unknown>;
package/ts/actions/get.ts CHANGED
@@ -15,6 +15,9 @@ export const get = {
15
15
  ...finding,
16
16
  test: (fetched) => assertSameGVK<T>(finding, fetched),
17
17
  }),
18
+ describe: (finding) => {
19
+ return `Get \`${finding.kind}\` "${finding.name}"`;
20
+ },
18
21
  } satisfies QueryDef<K8sResourceReference, K8sResource>;
19
22
 
20
23
  function isSameGVK<T extends K8sResource>(
@@ -17,4 +17,7 @@ export const label = {
17
17
  });
18
18
  return undefined;
19
19
  },
20
+ describe: (input) => {
21
+ return `Label \`${input.kind}\` "${input.name}"`;
22
+ },
20
23
  } satisfies OneWayMutateDef<LabelInput, void>;
@@ -6,6 +6,7 @@ export interface MutateDef<Input, Output> {
6
6
  readonly type: "mutate";
7
7
  readonly name: string;
8
8
  readonly mutate: Mutate<Input, Output>;
9
+ readonly describe: (input: Input) => string;
9
10
  }
10
11
 
11
12
  export type Mutate<Input, Output> = (
@@ -27,6 +28,7 @@ export interface OneWayMutateDef<Input, Output> {
27
28
  readonly type: "oneWayMutate";
28
29
  readonly name: string;
29
30
  readonly mutate: OneWayMutate<Input, Output>;
31
+ readonly describe: (input: Input) => string;
30
32
  }
31
33
 
32
34
  export type OneWayMutate<Input, Output> = (
@@ -37,6 +39,7 @@ export interface QueryDef<Input, Output> {
37
39
  readonly type: "query";
38
40
  readonly name: string;
39
41
  readonly query: Query<Input, Output>;
42
+ readonly describe: (input: Input) => string;
40
43
  }
41
44
 
42
45
  export type Query<Input, Output> = (
package/ts/apis/index.ts CHANGED
@@ -54,6 +54,39 @@ export interface Scenario {
54
54
  options?: undefined | ActionOptions
55
55
  ): Promise<void>;
56
56
 
57
+ /**
58
+ * Creates a Kubernetes resource with `kubectl create`.
59
+ *
60
+ * The manifest is validated and then created. When the action succeeds, Kest
61
+ * registers a cleanup handler that deletes the resource using
62
+ * `kubectl delete <kind>/<metadata.name>` during scenario cleanup.
63
+ *
64
+ * Unlike {@link Scenario.apply}, this action uses `kubectl create` which
65
+ * fails if the resource already exists. Use this when you need to ensure the
66
+ * resource is freshly created (e.g. for resources that use `generateName` or
67
+ * when you want to guarantee no prior state).
68
+ *
69
+ * This action is retried when it throws.
70
+ *
71
+ * @template T - The expected Kubernetes resource shape.
72
+ * @param manifest - YAML string, resource object, or imported YAML module.
73
+ * @param options - Retry options such as timeout and polling interval.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * await s.create({
78
+ * apiVersion: "v1",
79
+ * kind: "ConfigMap",
80
+ * metadata: { name: "my-config" },
81
+ * data: { mode: "demo" },
82
+ * });
83
+ * ```
84
+ */
85
+ create<T extends K8sResource>(
86
+ manifest: ApplyingManifest<T>,
87
+ options?: undefined | ActionOptions
88
+ ): Promise<void>;
89
+
57
90
  /**
58
91
  * Applies the `status` subresource using server-side apply.
59
92
  *
@@ -442,6 +475,30 @@ export interface Cluster {
442
475
  options?: undefined | ActionOptions
443
476
  ): Promise<void>;
444
477
 
478
+ /**
479
+ * Creates a Kubernetes resource with `kubectl create` and registers cleanup.
480
+ *
481
+ * Unlike {@link Cluster.apply}, this uses `kubectl create` which fails if the
482
+ * resource already exists.
483
+ *
484
+ * @template T - The expected Kubernetes resource shape.
485
+ * @param manifest - YAML string, resource object, or imported YAML module.
486
+ * @param options - Retry options such as timeout and polling interval.
487
+ *
488
+ * @example
489
+ * ```ts
490
+ * await cluster.create({
491
+ * apiVersion: "v1",
492
+ * kind: "Namespace",
493
+ * metadata: { name: "my-team" },
494
+ * });
495
+ * ```
496
+ */
497
+ create<T extends K8sResource>(
498
+ manifest: ApplyingManifest<T>,
499
+ options?: undefined | ActionOptions
500
+ ): Promise<void>;
501
+
445
502
  /**
446
503
  * Applies the `status` subresource using server-side apply.
447
504
  *
@@ -683,6 +740,37 @@ export interface Namespace {
683
740
  options?: undefined | ActionOptions
684
741
  ): Promise<void>;
685
742
 
743
+ /**
744
+ * Creates a Kubernetes resource in this namespace with `kubectl create` and
745
+ * registers cleanup.
746
+ *
747
+ * The target namespace is controlled by this {@link Namespace} instance.
748
+ * Prefer omitting `manifest.metadata.namespace`; if it is set, it must match
749
+ * this namespace (otherwise `kubectl` fails).
750
+ *
751
+ * Unlike {@link Namespace.apply}, this uses `kubectl create` which fails if
752
+ * the resource already exists.
753
+ *
754
+ * @template T - The expected Kubernetes resource shape.
755
+ * @param manifest - YAML string, resource object, or imported YAML module.
756
+ * @param options - Retry options such as timeout and polling interval.
757
+ *
758
+ * @example
759
+ * ```ts
760
+ * const ns = await s.newNamespace("my-ns");
761
+ * await ns.create({
762
+ * apiVersion: "v1",
763
+ * kind: "ConfigMap",
764
+ * metadata: { name: "my-config" },
765
+ * data: { mode: "demo" },
766
+ * });
767
+ * ```
768
+ */
769
+ create<T extends K8sResource>(
770
+ manifest: ApplyingManifest<T>,
771
+ options?: undefined | ActionOptions
772
+ ): Promise<void>;
773
+
686
774
  /**
687
775
  * Applies the `status` subresource in this namespace using server-side apply.
688
776
  *
@@ -118,3 +118,25 @@ export interface ESM {
118
118
  function isESM(value: unknown): value is ESM {
119
119
  return typeof value === "object" && value !== null && "default" in value;
120
120
  }
121
+
122
+ export function getResourceMeta(
123
+ value: unknown
124
+ ): undefined | { kind: string; name: string } {
125
+ if (typeof value === "string") {
126
+ const result = parseK8sResourceYaml(value);
127
+ if (result.ok) {
128
+ return { kind: result.value.kind, name: result.value.metadata.name };
129
+ }
130
+ }
131
+ if (isESM(value)) {
132
+ const result = parseK8sResourceFromESM(value);
133
+ if (result.ok) {
134
+ return { kind: result.value.kind, name: result.value.metadata.name };
135
+ }
136
+ }
137
+ const result = parseK8sResource(value);
138
+ if (result.ok) {
139
+ return { kind: result.value.kind, name: result.value.metadata.name };
140
+ }
141
+ return undefined;
142
+ }
@@ -1,126 +1,92 @@
1
- interface BaseEvent<
2
- Kind extends string,
3
- Data extends Record<string, unknown> = Record<string, never>,
4
- > {
5
- readonly kind: Kind;
6
- readonly data: Data;
7
- }
8
-
9
1
  export type Event =
10
- | ScenarioStartedEvent
11
- | CommandRunEvent
12
- | CommandResultEvent
13
- | RetryEvent
2
+ | ScenarioEvent
14
3
  | ActionEvent
4
+ | CommandEvent
5
+ | RetryEvent
15
6
  | RevertingsEvent
16
7
  | BDDEvent;
17
8
 
18
- export type ScenarioStartedEvent = BaseEvent<
19
- "ScenarioStarted",
20
- {
21
- readonly name: string;
22
- }
23
- >;
24
-
25
- export type RetryEvent =
26
- | RetryStartEvent
27
- | RetryAttemptEvent
28
- | RetryFailureEvent
29
- | RetryEndEvent;
9
+ type ScenarioEvent =
10
+ | BaseEvent<
11
+ "ScenarioStart",
12
+ {
13
+ readonly name: string;
14
+ }
15
+ >
16
+ | BaseEvent<"ScenarioEnd", Record<string, never>>;
17
+
18
+ type ActionEvent =
19
+ | BaseEvent<
20
+ "ActionStart",
21
+ {
22
+ readonly description: string;
23
+ }
24
+ >
25
+ | BaseEvent<
26
+ "ActionEnd",
27
+ {
28
+ readonly ok: boolean;
29
+ readonly error?: undefined | Error;
30
+ }
31
+ >;
32
+
33
+ type CommandEvent =
34
+ | BaseEvent<
35
+ "CommandRun",
36
+ {
37
+ readonly cmd: string;
38
+ readonly args: ReadonlyArray<string>;
39
+ readonly stdin?: undefined | string;
40
+ readonly stdinLanguage?: undefined | string;
41
+ }
42
+ >
43
+ | BaseEvent<
44
+ "CommandResult",
45
+ {
46
+ readonly exitCode: number;
47
+ readonly stdout: string;
48
+ readonly stderr: string;
49
+ readonly stdoutLanguage?: undefined | string;
50
+ readonly stderrLanguage?: undefined | string;
51
+ }
52
+ >;
53
+
54
+ type RetryEvent =
55
+ | BaseEvent<"RetryStart", Record<string, never>>
56
+ | BaseEvent<
57
+ "RetryEnd",
58
+ | {
59
+ readonly attempts: number;
60
+ readonly success: true;
61
+ readonly reason: "success";
62
+ }
63
+ | {
64
+ readonly attempts: number;
65
+ readonly success: false;
66
+ readonly reason: "timeout";
67
+ readonly error: Error;
68
+ }
69
+ >;
70
+
71
+ type RevertingsEvent =
72
+ | BaseEvent<"RevertingsStart">
73
+ | BaseEvent<"RevertingsEnd">;
74
+
75
+ type BDDEvent =
76
+ | BaseEvent<"BDDGiven", { readonly description: string }>
77
+ | BaseEvent<"BDDWhen", { readonly description: string }>
78
+ | BaseEvent<"BDDThen", { readonly description: string }>
79
+ | BaseEvent<"BDDAnd", { readonly description: string }>
80
+ | BaseEvent<"BDBut", { readonly description: string }>;
30
81
 
31
- export interface ErrorSummary {
32
- readonly name?: undefined | string;
33
- readonly message: string;
82
+ interface BaseEvent<
83
+ Kind extends string,
84
+ Data extends Record<string, unknown> = Record<string, never>,
85
+ > {
86
+ readonly kind: Kind;
87
+ readonly data: Data;
34
88
  }
35
89
 
36
- export type RetryStartEvent = BaseEvent<"RetryStart", Record<string, never>>;
37
-
38
- export type RetryAttemptEvent = BaseEvent<
39
- "RetryAttempt",
40
- {
41
- readonly attempt: number;
42
- }
43
- >;
44
-
45
- export type RetryFailureEvent = BaseEvent<
46
- "RetryFailure",
47
- {
48
- readonly attempt: number;
49
- readonly error: ErrorSummary;
50
- }
51
- >;
52
-
53
- export type RetryEndEvent = BaseEvent<
54
- "RetryEnd",
55
- | {
56
- readonly attempts: number;
57
- readonly success: true;
58
- readonly reason: "success";
59
- }
60
- | {
61
- readonly attempts: number;
62
- readonly success: false;
63
- readonly reason: "timeout";
64
- readonly error: ErrorSummary;
65
- }
66
- >;
67
-
68
- export type CommandRunEvent = BaseEvent<
69
- "CommandRun",
70
- {
71
- readonly cmd: string;
72
- readonly args: ReadonlyArray<string>;
73
- readonly stdin?: undefined | string;
74
- readonly stdinLanguage?: undefined | string;
75
- }
76
- >;
77
-
78
- export type CommandResultEvent = BaseEvent<
79
- "CommandResult",
80
- {
81
- readonly exitCode: number;
82
- readonly stdout: string;
83
- readonly stderr: string;
84
- readonly stdoutLanguage?: undefined | string;
85
- readonly stderrLanguage?: undefined | string;
86
- }
87
- >;
88
-
89
- export type ActionEvent = ActionStartEvent | ActionEndEvent;
90
-
91
- export type ActionPhase = "mutate" | "revert" | "query";
92
-
93
- export type ActionStartEvent = BaseEvent<
94
- "ActionStart",
95
- {
96
- readonly action: string; // e.g. "CreateNamespaceAction"
97
- readonly phase: ActionPhase;
98
- readonly input?: undefined | Readonly<Record<string, unknown>>;
99
- }
100
- >;
101
-
102
- export type ActionEndEvent = BaseEvent<
103
- "ActionEnd",
104
- {
105
- readonly action: string; // e.g. "CreateNamespaceAction"
106
- readonly phase: ActionPhase;
107
- readonly ok: boolean;
108
- readonly error?: undefined | ErrorSummary;
109
- readonly output?: undefined | Readonly<Record<string, unknown>>;
110
- }
111
- >;
112
-
113
- export type RevertingsEvent = RevertingsStartEvent | RevertingsEndEvent;
114
- export type RevertingsStartEvent = BaseEvent<"RevertingsStart">;
115
- export type RevertingsEndEvent = BaseEvent<"RevertingsEnd">;
116
-
117
- export type BDDEvent = BaseEvent<
118
- "BDDGiven" | "BDDWhen" | "BDDThen" | "BDDAnd" | "BDBut",
119
- {
120
- readonly description: string;
121
- }
122
- >;
123
-
124
90
  export class Recorder {
125
91
  private readonly events: Array<Event> = [];
126
92
 
@@ -0,0 +1,23 @@
1
+ import type { Event } from "../../recording";
2
+ import type { Reporter } from "../interface";
3
+ import { parseEvents } from "./parser";
4
+ import { renderReport } from "./renderer";
5
+
6
+ export interface MarkdownReporterOptions {
7
+ /**
8
+ * If true, keep ANSI escape codes (colors) in error messages.
9
+ * If false (default), remove ANSI escape codes.
10
+ */
11
+ enableANSI?: undefined | boolean;
12
+ }
13
+
14
+ export function newMarkdownReporter(
15
+ options: MarkdownReporterOptions = {}
16
+ ): Reporter {
17
+ return {
18
+ report(events: ReadonlyArray<Event>): Promise<string> {
19
+ const report = parseEvents(events);
20
+ return renderReport(report, options);
21
+ },
22
+ };
23
+ }
@@ -0,0 +1,63 @@
1
+ export interface Report {
2
+ scenarios: Array<Scenario>;
3
+ }
4
+
5
+ export interface Scenario {
6
+ name: string;
7
+ overview: Array<OverviewItem>;
8
+ details: Array<Tagged<"BDDSection", BDDSection> | Tagged<"Action", Action>>;
9
+ cleanup: Array<CleanupItem>;
10
+ }
11
+
12
+ type Tagged<Tag extends string, Target extends object> = Target & {
13
+ readonly type: Tag;
14
+ };
15
+
16
+ export interface OverviewItem {
17
+ name: string;
18
+ status: "pending" | "success" | "failure";
19
+ }
20
+
21
+ export interface BDDSection {
22
+ keyword: "given" | "when" | "then" | "and" | "but";
23
+ description: string;
24
+ actions: Array<Action>;
25
+ }
26
+
27
+ export interface Action {
28
+ name: string;
29
+ attempts?: undefined | number;
30
+ command?: undefined | Command;
31
+ error?: undefined | Error;
32
+ }
33
+
34
+ export interface Command {
35
+ cmd: string;
36
+ args: Array<string>;
37
+ stdin?: Text;
38
+ stdout?: Text;
39
+ stderr?: Text;
40
+ }
41
+
42
+ export interface Text {
43
+ text: string;
44
+ language?: undefined | string;
45
+ }
46
+
47
+ export interface Error {
48
+ message: Text;
49
+ stack?: undefined | string;
50
+ }
51
+
52
+ export interface CleanupItem {
53
+ action: string;
54
+ resource?: undefined | string;
55
+ status: "success" | "failure";
56
+ command: CleanupCommand;
57
+ }
58
+
59
+ export interface CleanupCommand {
60
+ cmd: string;
61
+ args: Array<string>;
62
+ output: string;
63
+ }