@griffin-app/griffin-ts 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.
Files changed (89) hide show
  1. package/ASSERTIONS_QUICK_REF.md +161 -0
  2. package/README.md +297 -0
  3. package/dist/assertions.d.ts +136 -0
  4. package/dist/assertions.d.ts.map +1 -0
  5. package/dist/assertions.js +230 -0
  6. package/dist/assertions.js.map +1 -0
  7. package/dist/builder.d.ts +168 -0
  8. package/dist/builder.d.ts.map +1 -0
  9. package/dist/builder.js +165 -0
  10. package/dist/builder.js.map +1 -0
  11. package/dist/constants.d.ts +5 -0
  12. package/dist/constants.d.ts.map +1 -0
  13. package/dist/constants.js +6 -0
  14. package/dist/constants.js.map +1 -0
  15. package/dist/example-sequential.d.ts +11 -0
  16. package/dist/example-sequential.d.ts.map +1 -0
  17. package/dist/example-sequential.js +160 -0
  18. package/dist/example-sequential.js.map +1 -0
  19. package/dist/example.d.ts +9 -0
  20. package/dist/example.d.ts.map +1 -0
  21. package/dist/example.js +36 -0
  22. package/dist/example.js.map +1 -0
  23. package/dist/frequency.d.ts +15 -0
  24. package/dist/frequency.d.ts.map +1 -0
  25. package/dist/frequency.js +34 -0
  26. package/dist/frequency.js.map +1 -0
  27. package/dist/http-methods.d.ts +6 -0
  28. package/dist/http-methods.d.ts.map +1 -0
  29. package/dist/http-methods.js +9 -0
  30. package/dist/http-methods.js.map +1 -0
  31. package/dist/index.d.ts +24 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +62 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/response-formats.d.ts +4 -0
  36. package/dist/response-formats.d.ts.map +1 -0
  37. package/dist/response-formats.js +7 -0
  38. package/dist/response-formats.js.map +1 -0
  39. package/dist/schema-exports.d.ts +6 -0
  40. package/dist/schema-exports.d.ts.map +1 -0
  41. package/dist/schema-exports.js +43 -0
  42. package/dist/schema-exports.js.map +1 -0
  43. package/dist/schema.d.ts +311 -0
  44. package/dist/schema.d.ts.map +1 -0
  45. package/dist/schema.js +214 -0
  46. package/dist/schema.js.map +1 -0
  47. package/dist/secrets.d.ts +56 -0
  48. package/dist/secrets.d.ts.map +1 -0
  49. package/dist/secrets.js +74 -0
  50. package/dist/secrets.js.map +1 -0
  51. package/dist/sequential-builder.d.ts +127 -0
  52. package/dist/sequential-builder.d.ts.map +1 -0
  53. package/dist/sequential-builder.js +128 -0
  54. package/dist/sequential-builder.js.map +1 -0
  55. package/dist/shared.d.ts +8 -0
  56. package/dist/shared.d.ts.map +1 -0
  57. package/dist/shared.js +36 -0
  58. package/dist/shared.js.map +1 -0
  59. package/dist/target.d.ts +33 -0
  60. package/dist/target.d.ts.map +1 -0
  61. package/dist/target.js +53 -0
  62. package/dist/target.js.map +1 -0
  63. package/dist/type-exports.d.ts +6 -0
  64. package/dist/type-exports.d.ts.map +1 -0
  65. package/dist/type-exports.js +7 -0
  66. package/dist/type-exports.js.map +1 -0
  67. package/dist/wait.d.ts +9 -0
  68. package/dist/wait.d.ts.map +1 -0
  69. package/dist/wait.js +8 -0
  70. package/dist/wait.js.map +1 -0
  71. package/package.json +43 -0
  72. package/src/assertions.ts +327 -0
  73. package/src/builder.ts +336 -0
  74. package/src/constants.ts +5 -0
  75. package/src/example-sequential.ts +191 -0
  76. package/src/example.ts +55 -0
  77. package/src/frequency.ts +38 -0
  78. package/src/http-methods.ts +5 -0
  79. package/src/index.ts +70 -0
  80. package/src/response-formats.ts +3 -0
  81. package/src/schema-exports.ts +43 -0
  82. package/src/schema.ts +289 -0
  83. package/src/secrets.ts +112 -0
  84. package/src/sequential-builder.ts +254 -0
  85. package/src/shared.ts +46 -0
  86. package/src/target.ts +55 -0
  87. package/src/type-exports.ts +20 -0
  88. package/src/wait.ts +4 -0
  89. package/tsconfig.json +20 -0
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Rich assertion DSL for constructing type-safe test assertions with JSONPath support.
3
+ *
4
+ * This module provides:
5
+ * - StateProxy: Tracks node access patterns and converts them to JSONPath
6
+ * - Assert: Builder for creating assertions with fluent API
7
+ * - Support for unary and binary predicates with negation
8
+ */
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ /**
15
+ * Identifies which node and which part of its result we're asserting on
16
+ */
17
+ export interface PathDescriptor {
18
+ nodeId: string;
19
+ accessor: "body" | "headers" | "status";
20
+ path: string[]; // JSONPath segments (e.g., ["data", "id"])
21
+ }
22
+
23
+ /**
24
+ * Unary predicates that check a property without comparing to a value
25
+ */
26
+ export enum UnaryPredicate {
27
+ IS_NULL = "IS_NULL",
28
+ IS_NOT_NULL = "IS_NOT_NULL",
29
+ IS_TRUE = "IS_TRUE",
30
+ IS_FALSE = "IS_FALSE",
31
+ IS_EMPTY = "IS_EMPTY",
32
+ IS_NOT_EMPTY = "IS_NOT_EMPTY",
33
+ }
34
+
35
+ /**
36
+ * Binary predicate operators that compare against an expected value
37
+ */
38
+ export enum BinaryPredicateOperator {
39
+ EQUAL = "EQUAL",
40
+ NOT_EQUAL = "NOT_EQUAL",
41
+ GREATER_THAN = "GREATER_THAN",
42
+ LESS_THAN = "LESS_THAN",
43
+ GREATER_THAN_OR_EQUAL = "GREATER_THAN_OR_EQUAL",
44
+ LESS_THAN_OR_EQUAL = "LESS_THAN_OR_EQUAL",
45
+ CONTAINS = "CONTAINS",
46
+ NOT_CONTAINS = "NOT_CONTAINS",
47
+ STARTS_WITH = "STARTS_WITH",
48
+ NOT_STARTS_WITH = "NOT_STARTS_WITH",
49
+ ENDS_WITH = "ENDS_WITH",
50
+ NOT_ENDS_WITH = "NOT_ENDS_WITH",
51
+ }
52
+
53
+ /**
54
+ * Binary predicate with operator and expected value
55
+ */
56
+ export interface BinaryPredicate {
57
+ operator: BinaryPredicateOperator;
58
+ expected: unknown;
59
+ }
60
+
61
+ /**
62
+ * Serialized assertion ready for execution
63
+ */
64
+ export interface SerializedAssertion {
65
+ nodeId: string;
66
+ accessor: "body" | "headers" | "status";
67
+ path: string[];
68
+ predicate: UnaryPredicate | BinaryPredicate;
69
+ }
70
+
71
+ // ============================================================================
72
+ // State Proxy
73
+ // ============================================================================
74
+
75
+ /**
76
+ * Symbol used to store path metadata in proxy objects
77
+ */
78
+ const PATH_SYMBOL = Symbol("__path__");
79
+
80
+ /**
81
+ * Internal interface for proxy objects that carry path information
82
+ */
83
+ interface ProxyWithPath {
84
+ [PATH_SYMBOL]: PathDescriptor;
85
+ }
86
+
87
+ /**
88
+ * Proxy for accessing nested properties within a node result accessor (body, headers, status)
89
+ * Note: The 'at' method is available at runtime but typed as NestedProxy for simplicity
90
+ */
91
+ export type NestedProxy = ProxyWithPath & {
92
+ [key: string]: NestedProxy;
93
+ };
94
+
95
+ /**
96
+ * Proxy for a node result with body, headers, and status accessors
97
+ */
98
+ export type NodeResultProxy = {
99
+ body: NestedProxy;
100
+ headers: NestedProxy;
101
+ status: NestedProxy;
102
+ };
103
+
104
+ /**
105
+ * State proxy that maps node names to their result proxies
106
+ */
107
+ export type StateProxy<NodeNames extends string = string> = {
108
+ [K in NodeNames]: NodeResultProxy;
109
+ };
110
+
111
+ /**
112
+ * Creates a nested proxy that accumulates path segments
113
+ */
114
+ function createNestedProxy(descriptor: PathDescriptor): NestedProxy {
115
+ return new Proxy({} as NestedProxy, {
116
+ get(_, prop: string | symbol) {
117
+ if (prop === PATH_SYMBOL) {
118
+ return descriptor;
119
+ }
120
+ if (prop === "at") {
121
+ return (index: number) => {
122
+ return createNestedProxy({
123
+ ...descriptor,
124
+ path: [...descriptor.path, String(index)],
125
+ });
126
+ };
127
+ }
128
+ // Accumulate string property access
129
+ return createNestedProxy({
130
+ ...descriptor,
131
+ path: [...descriptor.path, String(prop)],
132
+ });
133
+ },
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Creates a proxy for a node result with body, headers, and status accessors
139
+ */
140
+ function createNodeResultProxy(nodeId: string): NodeResultProxy {
141
+ return {
142
+ body: createNestedProxy({ nodeId, accessor: "body", path: [] }),
143
+ headers: createNestedProxy({ nodeId, accessor: "headers", path: [] }),
144
+ status: createNestedProxy({ nodeId, accessor: "status", path: [] }),
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Creates a state proxy for the given node names
150
+ */
151
+ export function createStateProxy<NodeNames extends string>(
152
+ nodeNames: NodeNames[],
153
+ ): StateProxy<NodeNames> {
154
+ const proxy = new Proxy(
155
+ {},
156
+ {
157
+ get(_, nodeName: string | symbol) {
158
+ if (typeof nodeName === "symbol") {
159
+ return undefined;
160
+ }
161
+ return createNodeResultProxy(nodeName);
162
+ },
163
+ },
164
+ );
165
+ return proxy as StateProxy<NodeNames>;
166
+ }
167
+
168
+ // ============================================================================
169
+ // Assert Builder
170
+ // ============================================================================
171
+
172
+ export class AssertBuilder {
173
+ private negated = false;
174
+
175
+ constructor(private descriptor: PathDescriptor) {}
176
+
177
+ /**
178
+ * Negation modifier - flips the meaning of the subsequent predicate
179
+ *
180
+ * @example
181
+ * Assert(state["node"].body["id"]).not.isNull() // IS_NOT_NULL
182
+ * Assert(state["node"].body["name"]).not.equals("") // NOT_EQUAL
183
+ */
184
+ get not(): this {
185
+ const negatedBuilder = new AssertBuilder(this.descriptor);
186
+ negatedBuilder.negated = !this.negated;
187
+ return negatedBuilder as this;
188
+ }
189
+
190
+ // Unary predicates
191
+
192
+ isNull(): SerializedAssertion {
193
+ return this.createAssertion(
194
+ this.negated ? UnaryPredicate.IS_NOT_NULL : UnaryPredicate.IS_NULL,
195
+ );
196
+ }
197
+
198
+ isDefined(): SerializedAssertion {
199
+ return this.createAssertion(
200
+ this.negated ? UnaryPredicate.IS_NULL : UnaryPredicate.IS_NOT_NULL,
201
+ );
202
+ }
203
+
204
+ isTrue(): SerializedAssertion {
205
+ return this.createAssertion(
206
+ this.negated ? UnaryPredicate.IS_FALSE : UnaryPredicate.IS_TRUE,
207
+ );
208
+ }
209
+
210
+ isFalse(): SerializedAssertion {
211
+ return this.createAssertion(
212
+ this.negated ? UnaryPredicate.IS_TRUE : UnaryPredicate.IS_FALSE,
213
+ );
214
+ }
215
+
216
+ isEmpty(): SerializedAssertion {
217
+ return this.createAssertion(
218
+ this.negated ? UnaryPredicate.IS_NOT_EMPTY : UnaryPredicate.IS_EMPTY,
219
+ );
220
+ }
221
+
222
+ // Binary predicates
223
+
224
+ equals(expected: unknown): SerializedAssertion {
225
+ return this.createAssertion({
226
+ operator: this.negated
227
+ ? BinaryPredicateOperator.NOT_EQUAL
228
+ : BinaryPredicateOperator.EQUAL,
229
+ expected,
230
+ });
231
+ }
232
+
233
+ greaterThan(expected: number): SerializedAssertion {
234
+ return this.createAssertion({
235
+ operator: this.negated
236
+ ? BinaryPredicateOperator.LESS_THAN_OR_EQUAL
237
+ : BinaryPredicateOperator.GREATER_THAN,
238
+ expected,
239
+ });
240
+ }
241
+
242
+ lessThan(expected: number): SerializedAssertion {
243
+ return this.createAssertion({
244
+ operator: this.negated
245
+ ? BinaryPredicateOperator.GREATER_THAN_OR_EQUAL
246
+ : BinaryPredicateOperator.LESS_THAN,
247
+ expected,
248
+ });
249
+ }
250
+
251
+ greaterThanOrEqual(expected: number): SerializedAssertion {
252
+ return this.createAssertion({
253
+ operator: this.negated
254
+ ? BinaryPredicateOperator.LESS_THAN
255
+ : BinaryPredicateOperator.GREATER_THAN_OR_EQUAL,
256
+ expected,
257
+ });
258
+ }
259
+
260
+ lessThanOrEqual(expected: number): SerializedAssertion {
261
+ return this.createAssertion({
262
+ operator: this.negated
263
+ ? BinaryPredicateOperator.GREATER_THAN
264
+ : BinaryPredicateOperator.LESS_THAN_OR_EQUAL,
265
+ expected,
266
+ });
267
+ }
268
+
269
+ contains(expected: string): SerializedAssertion {
270
+ return this.createAssertion({
271
+ operator: this.negated
272
+ ? BinaryPredicateOperator.NOT_CONTAINS
273
+ : BinaryPredicateOperator.CONTAINS,
274
+ expected,
275
+ });
276
+ }
277
+
278
+ startsWith(expected: string): SerializedAssertion {
279
+ return this.createAssertion({
280
+ operator: this.negated
281
+ ? BinaryPredicateOperator.NOT_STARTS_WITH
282
+ : BinaryPredicateOperator.STARTS_WITH,
283
+ expected,
284
+ });
285
+ }
286
+
287
+ endsWith(expected: string): SerializedAssertion {
288
+ return this.createAssertion({
289
+ operator: this.negated
290
+ ? BinaryPredicateOperator.NOT_ENDS_WITH
291
+ : BinaryPredicateOperator.ENDS_WITH,
292
+ expected,
293
+ });
294
+ }
295
+
296
+ private createAssertion(
297
+ predicate: UnaryPredicate | BinaryPredicate,
298
+ ): SerializedAssertion {
299
+ return {
300
+ nodeId: this.descriptor.nodeId,
301
+ accessor: this.descriptor.accessor,
302
+ path: this.descriptor.path,
303
+ predicate,
304
+ };
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Creates an assertion builder from a state proxy reference
310
+ *
311
+ * @param proxyRef - A reference obtained from the state proxy (e.g., state["node"].body["id"])
312
+ * @returns An AssertBuilder for constructing the assertion
313
+ *
314
+ * @example
315
+ * Assert(state["create_user"].body["data"]["id"]).not.isNull()
316
+ * Assert(state["create_user"].status).equals(201)
317
+ * Assert(state["create_user"].headers["content-type"]).contains("application/json")
318
+ */
319
+ export function Assert(proxyRef: NestedProxy): AssertBuilder {
320
+ const descriptor = (proxyRef as ProxyWithPath)[PATH_SYMBOL];
321
+ if (!descriptor) {
322
+ throw new Error(
323
+ "Assert() must be called with a reference from the state proxy",
324
+ );
325
+ }
326
+ return new AssertBuilder(descriptor);
327
+ }
package/src/builder.ts ADDED
@@ -0,0 +1,336 @@
1
+ import {
2
+ TestPlanV1,
3
+ Node,
4
+ Edge,
5
+ Frequency,
6
+ HttpMethod,
7
+ ResponseFormat,
8
+ Endpoint,
9
+ NodeType,
10
+ Wait,
11
+ Assertions,
12
+ TEST_PLAN_VERSION,
13
+ JSONAssertion,
14
+ } from "./schema.js";
15
+ import { type START as StartType, type END as EndType } from "./constants";
16
+
17
+ type RawPlan = Omit<TestPlanV1, "id" | "environment">;
18
+
19
+ /**
20
+ * A node definition without the id field.
21
+ * The id is provided separately to addNode for cleaner separation of concerns.
22
+ */
23
+ export type NodeWithoutId = Omit<Node, "id">;
24
+
25
+ /**
26
+ * TestBuilder provides a type-safe DSL for constructing test plans with compile-time graph validation.
27
+ *
28
+ * Type parameters track the state of the graph during construction:
29
+ * @template NodeName - Union of all registered node names
30
+ * @template HasOutput - Union of nodes that have outgoing edges
31
+ * @template HasInput - Union of nodes that have incoming edges
32
+ */
33
+ export interface TestBuilder<
34
+ NodeName extends string = never,
35
+ HasOutput extends string = never,
36
+ HasInput extends string = never,
37
+ > {
38
+ /**
39
+ * Adds a node to the test graph.
40
+ *
41
+ * @param name - Unique identifier for this node in the graph
42
+ * @param node - Node definition (Endpoint, WaitNode, Assertion)
43
+ * @returns Updated builder with the node registered in NodeName
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * builder.addNode("health", Endpoint({ method: GET, path: "/health", response_format: JSON }))
48
+ * ```
49
+ */
50
+ addNode<Name extends string>(
51
+ name: Name,
52
+ node: NodeWithoutId,
53
+ ): TestBuilder<NodeName | Name, HasOutput, HasInput>;
54
+
55
+ /**
56
+ * Adds a directed edge between two nodes in the graph.
57
+ *
58
+ * Compile-time constraints:
59
+ * - `from` must be START or a node that doesn't already have an outgoing edge
60
+ * - `to` must be END or a node that isn't the same as `from`
61
+ * - Both nodes must exist (be registered via addNode)
62
+ *
63
+ * @param from - Source node name or START constant
64
+ * @param to - Target node name or END constant
65
+ * @returns Updated builder with edge tracking updated
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * builder
70
+ * .addEdge(START, "health")
71
+ * .addEdge("health", "wait")
72
+ * .addEdge("wait", END)
73
+ * ```
74
+ */
75
+ addEdge<
76
+ From extends StartType | Exclude<NodeName, HasOutput>,
77
+ To extends EndType | Exclude<NodeName, From>,
78
+ >(
79
+ from: From,
80
+ to: To,
81
+ ): TestBuilder<
82
+ NodeName,
83
+ From extends StartType ? HasOutput : HasOutput | From,
84
+ To extends EndType ? HasInput : HasInput | To
85
+ >;
86
+
87
+ /**
88
+ * Builds the final test plan.
89
+ *
90
+ * This method is only callable when all graph constraints are satisfied:
91
+ * - All nodes must have at least one outgoing edge
92
+ * - All nodes must have at least one incoming edge
93
+ *
94
+ * If constraints aren't met, the return type becomes `never`, causing a compile error.
95
+ */
96
+ build: [Exclude<NodeName, HasOutput>] extends [never]
97
+ ? [Exclude<NodeName, HasInput>] extends [never]
98
+ ? () => TestPlanV1
99
+ : {
100
+ error: "Some nodes have no incoming edges";
101
+ unconnected: Exclude<NodeName, HasInput>;
102
+ }
103
+ : {
104
+ error: "Some nodes have no outgoing edges";
105
+ unconnected: Exclude<NodeName, HasOutput>;
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Internal implementation class for TestBuilder.
111
+ * Uses explicit type assertions at method boundaries to maintain phantom type tracking.
112
+ */
113
+ class TestBuilderImpl<
114
+ NodeName extends string = never,
115
+ HasOutput extends string = never,
116
+ HasInput extends string = never,
117
+ > {
118
+ private nodes: Node[] = [];
119
+ private edges: Edge[] = [];
120
+
121
+ constructor(
122
+ private config: {
123
+ name: string;
124
+ frequency?: Frequency;
125
+ locations?: string[];
126
+ },
127
+ ) {}
128
+
129
+ addNode<Name extends string>(
130
+ name: Name,
131
+ node: NodeWithoutId,
132
+ ): TestBuilder<NodeName | Name, HasOutput, HasInput> {
133
+ // Merge the name into the node to create a complete Node
134
+ // Type assertion required: spreading Omit<Node, 'id'> + id produces a valid Node at runtime
135
+ this.nodes.push({ ...node, id: name } as unknown as Node);
136
+ // Type assertion: we've added Name to NodeName union
137
+ return this as unknown as TestBuilder<NodeName | Name, HasOutput, HasInput>;
138
+ }
139
+
140
+ addEdge<
141
+ From extends StartType | Exclude<NodeName, HasOutput>,
142
+ To extends EndType | Exclude<NodeName, From>,
143
+ >(
144
+ from: From,
145
+ to: To,
146
+ ): TestBuilder<
147
+ NodeName,
148
+ From extends StartType ? HasOutput : HasOutput | From,
149
+ To extends EndType ? HasInput : HasInput | To
150
+ > {
151
+ this.edges.push({ from, to });
152
+ // Type assertion: we've updated HasOutput and HasInput based on edge direction
153
+ return this as unknown as TestBuilder<
154
+ NodeName,
155
+ From extends StartType ? HasOutput : HasOutput | From,
156
+ To extends EndType ? HasInput : HasInput | To
157
+ >;
158
+ }
159
+
160
+ get build(): TestBuilder<NodeName, HasOutput, HasInput>["build"] {
161
+ const { name, frequency, locations } = this.config;
162
+ const nodes = this.nodes;
163
+ const edges = this.edges;
164
+
165
+ const buildFn = (): RawPlan => {
166
+ return {
167
+ name,
168
+ version: TEST_PLAN_VERSION,
169
+ frequency,
170
+ locations,
171
+ nodes,
172
+ edges,
173
+ };
174
+ };
175
+
176
+ // Type assertion: build is only callable when graph constraints are satisfied
177
+ // The conditional type in TestBuilder['build'] enforces this at the call site
178
+ return buildFn as TestBuilder<NodeName, HasOutput, HasInput>["build"];
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Creates a new test builder for constructing a test plan.
184
+ *
185
+ * @param config - Configuration for the test plan
186
+ * @param config.name - Name of the test
187
+ * @param config.frequency - Optional frequency for scheduled execution
188
+ * @param config.locations - Optional array of location identifiers where this test should run
189
+ * @returns A new TestBuilder instance
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * const plan = createGraphBuilder({
194
+ * name: "health-check",
195
+ * frequency: Frequency.every(5).minute(),
196
+ * locations: ["us-east-1", "eu-west-1"]
197
+ * })
198
+ * .addNode("health", Endpoint({ method: GET, path: "/health", response_format: JSON }))
199
+ * .addEdge(START, "health")
200
+ * .addEdge("health", END)
201
+ * .build();
202
+ * ```
203
+ */
204
+ export function createGraphBuilder(config: {
205
+ name: string;
206
+ frequency?: Frequency;
207
+ locations?: string[];
208
+ }): TestBuilder {
209
+ return new TestBuilderImpl(config);
210
+ }
211
+
212
+ // ============================================================================
213
+ // Node Factory Functions
214
+ // ============================================================================
215
+
216
+ /**
217
+ * Configuration for an Endpoint node
218
+ * Accepts DSL-friendly literal types which are converted to schema enums internally
219
+ */
220
+ export interface EndpointConfig {
221
+ method:
222
+ | "GET"
223
+ | "POST"
224
+ | "PUT"
225
+ | "DELETE"
226
+ | "PATCH"
227
+ | "HEAD"
228
+ | "OPTIONS"
229
+ | "CONNECT"
230
+ | "TRACE";
231
+ path: string;
232
+ base: Endpoint["base"];
233
+ response_format: "JSON" | "XML" | "TEXT";
234
+ headers?: Record<string, any>;
235
+ body?: any;
236
+ }
237
+
238
+ /**
239
+ * Creates an Endpoint node for making HTTP requests.
240
+ *
241
+ * @param config - Endpoint configuration (method, path, base, headers, etc.)
242
+ * @returns An Endpoint node (without id) ready to be added to a TestBuilder
243
+ *
244
+ * @example
245
+ * ```typescript
246
+ * import { target } from './target';
247
+ *
248
+ * builder.addNode("health", Endpoint({
249
+ * method: GET,
250
+ * path: "/health",
251
+ * base: target("api-gateway"),
252
+ * response_format: JSON
253
+ * }));
254
+ * ```
255
+ */
256
+ export function Endpoint(config: EndpointConfig): Omit<Endpoint, "id"> {
257
+ return {
258
+ type: NodeType.ENDPOINT,
259
+ method: config.method as HttpMethod,
260
+ path: config.path,
261
+ base: config.base,
262
+ response_format: config.response_format as ResponseFormat,
263
+ headers: config.headers,
264
+ body: config.body,
265
+ };
266
+ }
267
+
268
+ /**
269
+ * Duration specification for Wait nodes.
270
+ * Accepts either milliseconds directly or an object with seconds/minutes.
271
+ */
272
+ export type WaitDuration = number | { seconds: number } | { minutes: number };
273
+
274
+ /**
275
+ * Converts a WaitDuration to milliseconds.
276
+ */
277
+ function toMilliseconds(duration: WaitDuration): number {
278
+ if (typeof duration === "number") {
279
+ return duration;
280
+ }
281
+ if ("seconds" in duration) {
282
+ return duration.seconds * 1000;
283
+ }
284
+ if ("minutes" in duration) {
285
+ return duration.minutes * 60 * 1000;
286
+ }
287
+ throw new Error("Invalid duration format");
288
+ }
289
+
290
+ /**
291
+ * Creates a Wait node that pauses execution for a specified duration.
292
+ *
293
+ * @param duration - Duration to wait (milliseconds, or {seconds: n}, or {minutes: n})
294
+ * @returns A Wait node (without id) ready to be added to a TestBuilder
295
+ *
296
+ * @example
297
+ * ```typescript
298
+ * import { Wait, WaitDuration } from './index';
299
+ *
300
+ * builder.addNode("pause", Wait(WaitDuration.seconds(2)));
301
+ * // or with raw milliseconds:
302
+ * builder.addNode("pause", Wait(2000));
303
+ * ```
304
+ */
305
+ export function Wait(duration: WaitDuration): Omit<Wait, "id"> {
306
+ return {
307
+ type: NodeType.WAIT,
308
+ duration_ms: toMilliseconds(duration),
309
+ };
310
+ }
311
+
312
+ /**
313
+ * Creates an Assertion node that validates test conditions.
314
+ *
315
+ * @param assertions - Array of SerializedAssertions to evaluate
316
+ * @returns An Assertion node (without id) ready to be added to a TestBuilder
317
+ *
318
+ * @example
319
+ * ```typescript
320
+ * import { Assert } from './assertions';
321
+ *
322
+ * builder.addNode("checks", Assertion([
323
+ * Assert(state["node"].status).equals(200),
324
+ * Assert(state["node"].body["status"]).equals("ok")
325
+ * ]));
326
+ * ```
327
+ */
328
+ export function Assertion(assertions: JSONAssertion[]): Omit<Assertions, "id"> {
329
+ return {
330
+ type: NodeType.ASSERTION,
331
+ assertions: assertions.map((assertion) => ({
332
+ assertionType: ResponseFormat.JSON,
333
+ ...assertion,
334
+ })),
335
+ };
336
+ }
@@ -0,0 +1,5 @@
1
+ export const START = "__START__" as const;
2
+ export const END = "__END__" as const;
3
+
4
+ export type START = typeof START;
5
+ export type END = typeof END;