@xplane/devtools 0.0.0 → 0.9.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/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026 Service Victoria
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,175 @@
1
+ import { Composition, KubernetesResource } from '@xplane/core';
2
+ export { KubernetesResource } from '@xplane/core';
3
+
4
+ /** Symbol used to identify Matcher instances. */
5
+ declare const MATCHER: unique symbol;
6
+ /** Result of a match operation. */
7
+ interface MatchResult {
8
+ /** Whether the match succeeded. */
9
+ pass: boolean;
10
+ /** Human-readable failure messages (empty if pass is true). */
11
+ failures: string[];
12
+ }
13
+ /** Interface for custom matchers. */
14
+ interface Matcher {
15
+ readonly [MATCHER]: true;
16
+ test(actual: unknown): MatchResult;
17
+ }
18
+ /**
19
+ * Factory for composable matchers used in assertions.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * template.hasResourceSpec('v1', 'ConfigMap', {
24
+ * data: Match.objectLike({ key: 'value' }),
25
+ * });
26
+ * ```
27
+ */
28
+ declare class Match {
29
+ private constructor();
30
+ /** Deep-partial object match — actual may be a superset of pattern. */
31
+ static objectLike(pattern: object): Matcher;
32
+ /** Exact object match — actual must equal pattern exactly (same keys, same values). */
33
+ static objectEquals(pattern: object): Matcher;
34
+ /** Array subset match — items must appear in actual in order. */
35
+ static arrayWith(items: unknown[]): Matcher;
36
+ /** Exact array match — actual must equal items exactly. */
37
+ static arrayEquals(items: unknown[]): Matcher;
38
+ /** String regex match. */
39
+ static stringLikeRegexp(pattern: string | RegExp): Matcher;
40
+ /** Asserts the value is absent (undefined). */
41
+ static absent(): Matcher;
42
+ /** Asserts any non-null/non-undefined value is present. */
43
+ static anyValue(): Matcher;
44
+ /** Inverts a match — asserts the value does NOT match the given pattern. */
45
+ static not(pattern: unknown): Matcher;
46
+ }
47
+
48
+ /** Options for `Template.synthesize()`. */
49
+ interface SynthesizeOptions {
50
+ /** XR (composite resource) data to inject before instantiation. */
51
+ xr?: Record<string, unknown>;
52
+ /** Environment data to inject before instantiation. */
53
+ environment?: Record<string, unknown>;
54
+ }
55
+ /**
56
+ * A snapshot of rendered resources from a Composition, providing
57
+ * assertion methods for unit testing.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const template = Template.synthesize(MyComposition, {
62
+ * xr: { spec: { region: 'us-east-1' } },
63
+ * });
64
+ * template.hasResourceSpec('ec2.aws.crossplane.io/v1beta1', 'VPC', {
65
+ * forProvider: { region: 'us-east-1' },
66
+ * });
67
+ * ```
68
+ */
69
+ declare class Template {
70
+ private readonly _resources;
71
+ private constructor();
72
+ /**
73
+ * Ergonomic factory: injects XR/environment data and instantiates
74
+ * the Composition class, then builds a Template from the rendered resources.
75
+ *
76
+ * Users never need to touch `Composition._pendingXR` directly.
77
+ */
78
+ static synthesize(Ctor: new () => Composition, options?: SynthesizeOptions): Template;
79
+ /**
80
+ * Build a Template from an already-instantiated Composition.
81
+ */
82
+ static fromComposition(composition: Composition): Template;
83
+ /**
84
+ * Build a Template from a pre-built array of KubernetesResource objects.
85
+ * Used internally by Simulator.
86
+ */
87
+ static fromResources(resources: KubernetesResource[]): Template;
88
+ /** Get all resources matching apiVersion + kind. */
89
+ private _filterByGVK;
90
+ /**
91
+ * Assert the number of resources with the given apiVersion and kind.
92
+ */
93
+ resourceCountIs(apiVersion: string, kind: string, count: number): void;
94
+ /**
95
+ * Assert that at least one resource of the given type matches the expected properties.
96
+ * Uses deep-partial matching by default (actual can be a superset of expected).
97
+ */
98
+ hasResource(apiVersion: string, kind: string, props?: object): void;
99
+ /**
100
+ * Assert that at least one resource of the given type has a spec matching the expected properties.
101
+ * Shorthand for matching against the `spec` field only.
102
+ */
103
+ hasResourceSpec(apiVersion: string, kind: string, specProps: object): void;
104
+ /**
105
+ * Assert that at least one resource of the given type has metadata matching the expected properties.
106
+ */
107
+ hasResourceMetadata(apiVersion: string, kind: string, metaProps: object): void;
108
+ /**
109
+ * Assert that ALL resources of the given type match the expected properties.
110
+ */
111
+ allResources(apiVersion: string, kind: string, props: object): void;
112
+ /**
113
+ * Find all resources of the given type that match the expected properties.
114
+ * Returns matches — never throws.
115
+ */
116
+ findResources(apiVersion: string, kind: string, props?: object): KubernetesResource[];
117
+ /**
118
+ * Serialize all resources to a JSON-compatible array for snapshot testing.
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * expect(template.toJSON()).toMatchSnapshot();
123
+ * ```
124
+ */
125
+ toJSON(): KubernetesResource[];
126
+ }
127
+
128
+ /** Result of a simulation run. */
129
+ interface SimulationResult {
130
+ /** Resources that are ready to emit (all dependencies satisfied). */
131
+ emitted: Template;
132
+ /** Resources that are blocked on unresolved dependencies. */
133
+ blocked: Template;
134
+ }
135
+ /**
136
+ * Simulates the full rendering pipeline including observed state injection,
137
+ * edge resolution, and sequencing — mimicking what `@xplane/function` does at runtime.
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * const result = Simulator.synthesize(MyComposition, { xr: { ... } })
142
+ * .withObserved([{ apiVersion: '...', kind: 'VPC', metadata: { name: 'vpc-abc' }, status: { atProvider: { vpcId: 'vpc-123' } } }])
143
+ * .run();
144
+ *
145
+ * result.emitted.hasResourceSpec('ec2.aws.crossplane.io/v1beta1', 'Subnet', {
146
+ * forProvider: { vpcId: 'vpc-123' },
147
+ * });
148
+ * ```
149
+ */
150
+ declare class Simulator {
151
+ private readonly _composition;
152
+ private _observed;
153
+ private constructor();
154
+ /**
155
+ * Ergonomic factory: injects XR/environment data, instantiates the
156
+ * Composition class, and returns a Simulator ready for `.withObserved().run()`.
157
+ */
158
+ static synthesize(Ctor: new () => Composition, options?: SynthesizeOptions): Simulator;
159
+ /**
160
+ * Build a Simulator from an already-instantiated Composition.
161
+ */
162
+ static fromComposition(composition: Composition): Simulator;
163
+ /**
164
+ * Provide observed (cluster) state for resources.
165
+ * Each resource is matched to a declared resource by its construct path
166
+ * (i.e., `metadata.name` in observed state maps to `resource.path` in the composition).
167
+ */
168
+ withObserved(resources: KubernetesResource[]): this;
169
+ /**
170
+ * Run the simulation: inject observed state, resolve edges, determine sequencing.
171
+ */
172
+ run(): SimulationResult;
173
+ }
174
+
175
+ export { Match, type MatchResult, type Matcher, type SimulationResult, Simulator, type SynthesizeOptions, Template };
@@ -0,0 +1,562 @@
1
+ // src/assertions/match.ts
2
+ var MATCHER = /* @__PURE__ */ Symbol.for("xplane.devtools.matcher");
3
+ function isMatcher(value) {
4
+ return typeof value === "object" && value !== null && MATCHER in value && value[MATCHER] === true;
5
+ }
6
+ function pass() {
7
+ return { pass: true, failures: [] };
8
+ }
9
+ function fail(message) {
10
+ return { pass: false, failures: [message] };
11
+ }
12
+ function merge(results) {
13
+ const failures = results.flatMap((r) => r.failures);
14
+ return { pass: failures.length === 0, failures };
15
+ }
16
+ function deepMatch(actual, expected, path = "") {
17
+ if (isMatcher(expected)) {
18
+ const result = expected.test(actual);
19
+ if (!result.pass) {
20
+ return { pass: false, failures: result.failures.map((f) => path ? `${path}: ${f}` : f) };
21
+ }
22
+ return pass();
23
+ }
24
+ if (expected === null || expected === void 0 || typeof expected !== "object") {
25
+ if (actual === expected) return pass();
26
+ return fail(
27
+ path ? `${path}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}` : `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`
28
+ );
29
+ }
30
+ if (Array.isArray(expected)) {
31
+ if (!Array.isArray(actual)) {
32
+ return fail(`${path || "value"}: expected an array, got ${typeof actual}`);
33
+ }
34
+ if (actual.length !== expected.length) {
35
+ return fail(
36
+ `${path || "value"}: expected array of length ${expected.length}, got length ${actual.length}`
37
+ );
38
+ }
39
+ const results2 = [];
40
+ for (let i = 0; i < expected.length; i++) {
41
+ results2.push(deepMatch(actual[i], expected[i], `${path}[${i}]`));
42
+ }
43
+ return merge(results2);
44
+ }
45
+ if (typeof actual !== "object" || actual === null || Array.isArray(actual)) {
46
+ return fail(`${path || "value"}: expected an object, got ${JSON.stringify(actual)}`);
47
+ }
48
+ const expectedObj = expected;
49
+ const actualObj = actual;
50
+ const results = [];
51
+ for (const key of Object.keys(expectedObj)) {
52
+ results.push(deepMatch(actualObj[key], expectedObj[key], path ? `${path}.${key}` : key));
53
+ }
54
+ for (const key of Object.keys(actualObj)) {
55
+ if (!(key in expectedObj)) {
56
+ results.push(fail(`${path ? `${path}.${key}` : key}: unexpected key`));
57
+ }
58
+ }
59
+ return merge(results);
60
+ }
61
+ function deepPartialMatch(actual, expected, path = "") {
62
+ if (isMatcher(expected)) {
63
+ const result = expected.test(actual);
64
+ if (!result.pass) {
65
+ return { pass: false, failures: result.failures.map((f) => path ? `${path}: ${f}` : f) };
66
+ }
67
+ return pass();
68
+ }
69
+ if (expected === null || expected === void 0 || typeof expected !== "object") {
70
+ if (actual === expected) return pass();
71
+ return fail(
72
+ path ? `${path}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}` : `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`
73
+ );
74
+ }
75
+ if (Array.isArray(expected)) {
76
+ if (!Array.isArray(actual)) {
77
+ return fail(`${path || "value"}: expected an array, got ${typeof actual}`);
78
+ }
79
+ if (actual.length !== expected.length) {
80
+ return fail(
81
+ `${path || "value"}: expected array of length ${expected.length}, got length ${actual.length}`
82
+ );
83
+ }
84
+ const results2 = [];
85
+ for (let i = 0; i < expected.length; i++) {
86
+ results2.push(deepPartialMatch(actual[i], expected[i], `${path}[${i}]`));
87
+ }
88
+ return merge(results2);
89
+ }
90
+ if (typeof actual !== "object" || actual === null || Array.isArray(actual)) {
91
+ return fail(`${path || "value"}: expected an object, got ${JSON.stringify(actual)}`);
92
+ }
93
+ const expectedObj = expected;
94
+ const actualObj = actual;
95
+ const results = [];
96
+ for (const key of Object.keys(expectedObj)) {
97
+ if (!(key in actualObj)) {
98
+ results.push(fail(`${path ? `${path}.${key}` : key}: key not found in actual`));
99
+ } else {
100
+ results.push(
101
+ deepPartialMatch(actualObj[key], expectedObj[key], path ? `${path}.${key}` : key)
102
+ );
103
+ }
104
+ }
105
+ return merge(results);
106
+ }
107
+ var ObjectLikeMatcher = class {
108
+ constructor(pattern) {
109
+ this.pattern = pattern;
110
+ }
111
+ pattern;
112
+ [MATCHER] = true;
113
+ test(actual) {
114
+ return deepPartialMatch(actual, this.pattern);
115
+ }
116
+ };
117
+ var ObjectEqualsMatcher = class {
118
+ constructor(pattern) {
119
+ this.pattern = pattern;
120
+ }
121
+ pattern;
122
+ [MATCHER] = true;
123
+ test(actual) {
124
+ return deepMatch(actual, this.pattern);
125
+ }
126
+ };
127
+ var ArrayWithMatcher = class {
128
+ constructor(items) {
129
+ this.items = items;
130
+ }
131
+ items;
132
+ [MATCHER] = true;
133
+ test(actual) {
134
+ if (!Array.isArray(actual)) {
135
+ return fail(`expected an array, got ${typeof actual}`);
136
+ }
137
+ let searchStart = 0;
138
+ for (let i = 0; i < this.items.length; i++) {
139
+ let found = false;
140
+ for (let j = searchStart; j < actual.length; j++) {
141
+ const r = deepPartialMatch(actual[j], this.items[i]);
142
+ if (r.pass) {
143
+ searchStart = j + 1;
144
+ found = true;
145
+ break;
146
+ }
147
+ }
148
+ if (!found) {
149
+ return fail(
150
+ `arrayWith: could not find match for item at index ${i}: ${JSON.stringify(this.items[i])}`
151
+ );
152
+ }
153
+ }
154
+ return pass();
155
+ }
156
+ };
157
+ var ArrayEqualsMatcher = class {
158
+ constructor(items) {
159
+ this.items = items;
160
+ }
161
+ items;
162
+ [MATCHER] = true;
163
+ test(actual) {
164
+ return deepMatch(actual, this.items);
165
+ }
166
+ };
167
+ var StringLikeRegexpMatcher = class {
168
+ [MATCHER] = true;
169
+ regex;
170
+ constructor(pattern) {
171
+ this.regex = typeof pattern === "string" ? new RegExp(pattern) : pattern;
172
+ }
173
+ test(actual) {
174
+ if (typeof actual !== "string") {
175
+ return fail(`expected a string, got ${typeof actual}`);
176
+ }
177
+ if (!this.regex.test(actual)) {
178
+ return fail(`expected string matching ${this.regex}, got "${actual}"`);
179
+ }
180
+ return pass();
181
+ }
182
+ };
183
+ var AbsentMatcher = class {
184
+ [MATCHER] = true;
185
+ test(actual) {
186
+ if (actual !== void 0) {
187
+ return fail(`expected absent (undefined), got ${JSON.stringify(actual)}`);
188
+ }
189
+ return pass();
190
+ }
191
+ };
192
+ var AnyValueMatcher = class {
193
+ [MATCHER] = true;
194
+ test(actual) {
195
+ if (actual === null || actual === void 0) {
196
+ return fail(`expected any value, got ${actual}`);
197
+ }
198
+ return pass();
199
+ }
200
+ };
201
+ var NotMatcher = class {
202
+ constructor(pattern) {
203
+ this.pattern = pattern;
204
+ }
205
+ pattern;
206
+ [MATCHER] = true;
207
+ test(actual) {
208
+ const inner = isMatcher(this.pattern) ? this.pattern.test(actual) : deepPartialMatch(actual, this.pattern);
209
+ if (inner.pass) {
210
+ return fail(`expected NOT to match, but matched: ${JSON.stringify(actual)}`);
211
+ }
212
+ return pass();
213
+ }
214
+ };
215
+ var Match = class {
216
+ constructor() {
217
+ }
218
+ /** Deep-partial object match — actual may be a superset of pattern. */
219
+ static objectLike(pattern) {
220
+ return new ObjectLikeMatcher(pattern);
221
+ }
222
+ /** Exact object match — actual must equal pattern exactly (same keys, same values). */
223
+ static objectEquals(pattern) {
224
+ return new ObjectEqualsMatcher(pattern);
225
+ }
226
+ /** Array subset match — items must appear in actual in order. */
227
+ static arrayWith(items) {
228
+ return new ArrayWithMatcher(items);
229
+ }
230
+ /** Exact array match — actual must equal items exactly. */
231
+ static arrayEquals(items) {
232
+ return new ArrayEqualsMatcher(items);
233
+ }
234
+ /** String regex match. */
235
+ static stringLikeRegexp(pattern) {
236
+ return new StringLikeRegexpMatcher(pattern);
237
+ }
238
+ /** Asserts the value is absent (undefined). */
239
+ static absent() {
240
+ return new AbsentMatcher();
241
+ }
242
+ /** Asserts any non-null/non-undefined value is present. */
243
+ static anyValue() {
244
+ return new AnyValueMatcher();
245
+ }
246
+ /** Inverts a match — asserts the value does NOT match the given pattern. */
247
+ static not(pattern) {
248
+ return new NotMatcher(pattern);
249
+ }
250
+ };
251
+
252
+ // src/assertions/simulator.ts
253
+ import {
254
+ resolveSequencing
255
+ } from "@xplane/core";
256
+
257
+ // src/assertions/template.ts
258
+ var Template = class _Template {
259
+ _resources;
260
+ constructor(resources) {
261
+ this._resources = resources;
262
+ }
263
+ /**
264
+ * Ergonomic factory: injects XR/environment data and instantiates
265
+ * the Composition class, then builds a Template from the rendered resources.
266
+ *
267
+ * Users never need to touch `Composition._pendingXR` directly.
268
+ */
269
+ static synthesize(Ctor, options = {}) {
270
+ let base = Ctor;
271
+ while (base && !Object.hasOwn(base, "_pendingXR")) {
272
+ base = Object.getPrototypeOf(base);
273
+ }
274
+ if (!base) {
275
+ throw new Error("Could not find Composition base class with _pendingXR");
276
+ }
277
+ const BaseComposition = base;
278
+ BaseComposition._pendingXR = options.xr;
279
+ BaseComposition._pendingEnvironment = options.environment;
280
+ try {
281
+ const instance = new Ctor();
282
+ return _Template.fromComposition(instance);
283
+ } finally {
284
+ BaseComposition._pendingXR = void 0;
285
+ BaseComposition._pendingEnvironment = void 0;
286
+ }
287
+ }
288
+ /**
289
+ * Build a Template from an already-instantiated Composition.
290
+ */
291
+ static fromComposition(composition) {
292
+ const resources = [
293
+ ...composition.resources.values()
294
+ ].map((r) => r.toDesired());
295
+ return new _Template(resources);
296
+ }
297
+ /**
298
+ * Build a Template from a pre-built array of KubernetesResource objects.
299
+ * Used internally by Simulator.
300
+ */
301
+ static fromResources(resources) {
302
+ return new _Template(resources);
303
+ }
304
+ /** Get all resources matching apiVersion + kind. */
305
+ _filterByGVK(apiVersion, kind) {
306
+ return this._resources.filter((r) => r.apiVersion === apiVersion && r.kind === kind);
307
+ }
308
+ /**
309
+ * Assert the number of resources with the given apiVersion and kind.
310
+ */
311
+ resourceCountIs(apiVersion, kind, count) {
312
+ const matched = this._filterByGVK(apiVersion, kind);
313
+ if (matched.length !== count) {
314
+ throw new Error(
315
+ `Expected ${count} resource(s) of type ${apiVersion}/${kind}, found ${matched.length}`
316
+ );
317
+ }
318
+ }
319
+ /**
320
+ * Assert that at least one resource of the given type matches the expected properties.
321
+ * Uses deep-partial matching by default (actual can be a superset of expected).
322
+ */
323
+ hasResource(apiVersion, kind, props) {
324
+ const matched = this._filterByGVK(apiVersion, kind);
325
+ if (matched.length === 0) {
326
+ throw new Error(`No resources found with type ${apiVersion}/${kind}`);
327
+ }
328
+ if (!props) return;
329
+ const allFailures = [];
330
+ for (const resource of matched) {
331
+ const result = deepPartialMatch(resource, props);
332
+ if (result.pass) return;
333
+ allFailures.push(
334
+ ` Resource: ${JSON.stringify(resource.metadata?.name ?? "(unnamed)")}
335
+ ${result.failures.join("\n ")}`
336
+ );
337
+ }
338
+ throw new Error(
339
+ `No resource of type ${apiVersion}/${kind} matches the expected properties:
340
+ ${allFailures.join("\n")}`
341
+ );
342
+ }
343
+ /**
344
+ * Assert that at least one resource of the given type has a spec matching the expected properties.
345
+ * Shorthand for matching against the `spec` field only.
346
+ */
347
+ hasResourceSpec(apiVersion, kind, specProps) {
348
+ const matched = this._filterByGVK(apiVersion, kind);
349
+ if (matched.length === 0) {
350
+ throw new Error(`No resources found with type ${apiVersion}/${kind}`);
351
+ }
352
+ const allFailures = [];
353
+ for (const resource of matched) {
354
+ const result = deepPartialMatch(resource.spec ?? {}, specProps);
355
+ if (result.pass) return;
356
+ allFailures.push(
357
+ ` Resource: ${JSON.stringify(resource.metadata?.name ?? "(unnamed)")}
358
+ ${result.failures.join("\n ")}`
359
+ );
360
+ }
361
+ throw new Error(
362
+ `No resource of type ${apiVersion}/${kind} has spec matching the expected properties:
363
+ ${allFailures.join("\n")}`
364
+ );
365
+ }
366
+ /**
367
+ * Assert that at least one resource of the given type has metadata matching the expected properties.
368
+ */
369
+ hasResourceMetadata(apiVersion, kind, metaProps) {
370
+ const matched = this._filterByGVK(apiVersion, kind);
371
+ if (matched.length === 0) {
372
+ throw new Error(`No resources found with type ${apiVersion}/${kind}`);
373
+ }
374
+ const allFailures = [];
375
+ for (const resource of matched) {
376
+ const result = deepPartialMatch(resource.metadata ?? {}, metaProps);
377
+ if (result.pass) return;
378
+ allFailures.push(
379
+ ` Resource: ${JSON.stringify(resource.metadata?.name ?? "(unnamed)")}
380
+ ${result.failures.join("\n ")}`
381
+ );
382
+ }
383
+ throw new Error(
384
+ `No resource of type ${apiVersion}/${kind} has metadata matching the expected properties:
385
+ ${allFailures.join("\n")}`
386
+ );
387
+ }
388
+ /**
389
+ * Assert that ALL resources of the given type match the expected properties.
390
+ */
391
+ allResources(apiVersion, kind, props) {
392
+ const matched = this._filterByGVK(apiVersion, kind);
393
+ if (matched.length === 0) {
394
+ throw new Error(`No resources found with type ${apiVersion}/${kind}`);
395
+ }
396
+ const failures = [];
397
+ for (const resource of matched) {
398
+ const result = deepPartialMatch(resource, props);
399
+ if (!result.pass) {
400
+ failures.push(
401
+ ` Resource: ${JSON.stringify(resource.metadata?.name ?? "(unnamed)")}
402
+ ${result.failures.join("\n ")}`
403
+ );
404
+ }
405
+ }
406
+ if (failures.length > 0) {
407
+ throw new Error(
408
+ `Not all resources of type ${apiVersion}/${kind} match:
409
+ ${failures.join("\n")}`
410
+ );
411
+ }
412
+ }
413
+ /**
414
+ * Find all resources of the given type that match the expected properties.
415
+ * Returns matches — never throws.
416
+ */
417
+ findResources(apiVersion, kind, props) {
418
+ const matched = this._filterByGVK(apiVersion, kind);
419
+ if (!props) return matched;
420
+ return matched.filter((resource) => {
421
+ const result = deepPartialMatch(resource, props);
422
+ return result.pass;
423
+ });
424
+ }
425
+ /**
426
+ * Serialize all resources to a JSON-compatible array for snapshot testing.
427
+ *
428
+ * @example
429
+ * ```ts
430
+ * expect(template.toJSON()).toMatchSnapshot();
431
+ * ```
432
+ */
433
+ toJSON() {
434
+ return structuredClone(this._resources);
435
+ }
436
+ };
437
+
438
+ // src/assertions/simulator.ts
439
+ function getNestedValue(obj, path) {
440
+ let current = obj;
441
+ for (const segment of path.split(".")) {
442
+ if (current === null || current === void 0 || typeof current !== "object") {
443
+ return void 0;
444
+ }
445
+ current = current[segment];
446
+ }
447
+ return current;
448
+ }
449
+ function setNestedValue(obj, path, value) {
450
+ const segments = path.split(".");
451
+ let current = obj;
452
+ for (let i = 0; i < segments.length - 1; i++) {
453
+ const seg = segments[i];
454
+ if (!(seg in current) || typeof current[seg] !== "object" || current[seg] === null) {
455
+ current[seg] = {};
456
+ }
457
+ current = current[seg];
458
+ }
459
+ const lastSeg = segments[segments.length - 1];
460
+ if (lastSeg !== void 0) {
461
+ current[lastSeg] = value;
462
+ }
463
+ }
464
+ var Simulator = class _Simulator {
465
+ _composition;
466
+ _observed = [];
467
+ constructor(composition) {
468
+ this._composition = composition;
469
+ }
470
+ /**
471
+ * Ergonomic factory: injects XR/environment data, instantiates the
472
+ * Composition class, and returns a Simulator ready for `.withObserved().run()`.
473
+ */
474
+ static synthesize(Ctor, options = {}) {
475
+ let base = Ctor;
476
+ while (base && !Object.hasOwn(base, "_pendingXR")) {
477
+ base = Object.getPrototypeOf(base);
478
+ }
479
+ if (!base) {
480
+ throw new Error("Could not find Composition base class with _pendingXR");
481
+ }
482
+ const BaseComposition = base;
483
+ BaseComposition._pendingXR = options.xr;
484
+ BaseComposition._pendingEnvironment = options.environment;
485
+ try {
486
+ const instance = new Ctor();
487
+ return new _Simulator(instance);
488
+ } finally {
489
+ BaseComposition._pendingXR = void 0;
490
+ BaseComposition._pendingEnvironment = void 0;
491
+ }
492
+ }
493
+ /**
494
+ * Build a Simulator from an already-instantiated Composition.
495
+ */
496
+ static fromComposition(composition) {
497
+ return new _Simulator(composition);
498
+ }
499
+ /**
500
+ * Provide observed (cluster) state for resources.
501
+ * Each resource is matched to a declared resource by its construct path
502
+ * (i.e., `metadata.name` in observed state maps to `resource.path` in the composition).
503
+ */
504
+ withObserved(resources) {
505
+ this._observed = resources;
506
+ return this;
507
+ }
508
+ /**
509
+ * Run the simulation: inject observed state, resolve edges, determine sequencing.
510
+ */
511
+ run() {
512
+ const composition = this._composition;
513
+ const resources = composition.resources;
514
+ const collector = composition.collector;
515
+ const graph = composition.graph;
516
+ const observedMap = /* @__PURE__ */ new Map();
517
+ for (const obs of this._observed) {
518
+ const name = obs.metadata?.name;
519
+ if (name) {
520
+ observedMap.set(name, obs);
521
+ }
522
+ }
523
+ graph.addEdges(collector.edges);
524
+ for (const [path, resource] of resources) {
525
+ const observed = observedMap.get(path);
526
+ if (observed) {
527
+ resource.setObserved(observed);
528
+ }
529
+ }
530
+ for (const edge of collector.edges) {
531
+ const observed = observedMap.get(edge.from.id);
532
+ if (!observed) continue;
533
+ const value = getNestedValue(observed, edge.fromPath);
534
+ if (value === void 0 || value === null) continue;
535
+ const targetResource = resources.get(edge.to.id);
536
+ if (!targetResource) continue;
537
+ const toPath = edge.toPath;
538
+ if (toPath.startsWith("spec.")) {
539
+ setNestedValue(
540
+ targetResource.spec,
541
+ toPath.slice("spec.".length),
542
+ value
543
+ );
544
+ }
545
+ }
546
+ const sequencing = resolveSequencing(
547
+ resources,
548
+ graph,
549
+ observedMap
550
+ );
551
+ return {
552
+ emitted: Template.fromResources(sequencing.emit.map((r) => r.toDesired())),
553
+ blocked: Template.fromResources(sequencing.blocked.map((r) => r.toDesired()))
554
+ };
555
+ }
556
+ };
557
+ export {
558
+ Match,
559
+ Simulator,
560
+ Template
561
+ };
562
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/assertions/match.ts","../../src/assertions/simulator.ts","../../src/assertions/template.ts"],"sourcesContent":["/** Symbol used to identify Matcher instances. */\nexport const MATCHER = Symbol.for(\"xplane.devtools.matcher\");\n\n/** Result of a match operation. */\nexport interface MatchResult {\n /** Whether the match succeeded. */\n pass: boolean;\n /** Human-readable failure messages (empty if pass is true). */\n failures: string[];\n}\n\n/** Interface for custom matchers. */\nexport interface Matcher {\n readonly [MATCHER]: true;\n test(actual: unknown): MatchResult;\n}\n\n/** Check whether a value is a Matcher instance. */\nexport function isMatcher(value: unknown): value is Matcher {\n return (\n typeof value === \"object\" &&\n value !== null &&\n MATCHER in value &&\n (value as Matcher)[MATCHER] === true\n );\n}\n\nfunction pass(): MatchResult {\n return { pass: true, failures: [] };\n}\n\nfunction fail(message: string): MatchResult {\n return { pass: false, failures: [message] };\n}\n\nfunction merge(results: MatchResult[]): MatchResult {\n const failures = results.flatMap((r) => r.failures);\n return { pass: failures.length === 0, failures };\n}\n\n/**\n * Deep-match `actual` against `expected`.\n * - If `expected` is a Matcher, delegates to its `.test()`.\n * - Literal objects are matched recursively (deep-partial by default via objectLike semantics at the top level is handled by the caller).\n * - This function performs EXACT matching — partial matching is handled by ObjectLikeMatcher.\n */\nexport function deepMatch(actual: unknown, expected: unknown, path = \"\"): MatchResult {\n if (isMatcher(expected)) {\n const result = expected.test(actual);\n if (!result.pass) {\n return { pass: false, failures: result.failures.map((f) => (path ? `${path}: ${f}` : f)) };\n }\n return pass();\n }\n\n // Null / undefined / primitives\n if (expected === null || expected === undefined || typeof expected !== \"object\") {\n if (actual === expected) return pass();\n return fail(\n path\n ? `${path}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`\n : `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,\n );\n }\n\n // Arrays\n if (Array.isArray(expected)) {\n if (!Array.isArray(actual)) {\n return fail(`${path || \"value\"}: expected an array, got ${typeof actual}`);\n }\n if (actual.length !== expected.length) {\n return fail(\n `${path || \"value\"}: expected array of length ${expected.length}, got length ${actual.length}`,\n );\n }\n const results: MatchResult[] = [];\n for (let i = 0; i < expected.length; i++) {\n results.push(deepMatch(actual[i], expected[i], `${path}[${i}]`));\n }\n return merge(results);\n }\n\n // Objects (exact match — all keys in expected must match, no extra keys allowed)\n if (typeof actual !== \"object\" || actual === null || Array.isArray(actual)) {\n return fail(`${path || \"value\"}: expected an object, got ${JSON.stringify(actual)}`);\n }\n const expectedObj = expected as Record<string, unknown>;\n const actualObj = actual as Record<string, unknown>;\n const results: MatchResult[] = [];\n\n // Check expected keys exist and match\n for (const key of Object.keys(expectedObj)) {\n results.push(deepMatch(actualObj[key], expectedObj[key], path ? `${path}.${key}` : key));\n }\n // Check no extra keys in actual\n for (const key of Object.keys(actualObj)) {\n if (!(key in expectedObj)) {\n results.push(fail(`${path ? `${path}.${key}` : key}: unexpected key`));\n }\n }\n return merge(results);\n}\n\n/**\n * Deep-partial match: all keys in `expected` must match in `actual`, but\n * `actual` may have additional keys at any level.\n */\nexport function deepPartialMatch(actual: unknown, expected: unknown, path = \"\"): MatchResult {\n if (isMatcher(expected)) {\n const result = expected.test(actual);\n if (!result.pass) {\n return { pass: false, failures: result.failures.map((f) => (path ? `${path}: ${f}` : f)) };\n }\n return pass();\n }\n\n if (expected === null || expected === undefined || typeof expected !== \"object\") {\n if (actual === expected) return pass();\n return fail(\n path\n ? `${path}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`\n : `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,\n );\n }\n\n if (Array.isArray(expected)) {\n if (!Array.isArray(actual)) {\n return fail(`${path || \"value\"}: expected an array, got ${typeof actual}`);\n }\n if (actual.length !== expected.length) {\n return fail(\n `${path || \"value\"}: expected array of length ${expected.length}, got length ${actual.length}`,\n );\n }\n const results: MatchResult[] = [];\n for (let i = 0; i < expected.length; i++) {\n results.push(deepPartialMatch(actual[i], expected[i], `${path}[${i}]`));\n }\n return merge(results);\n }\n\n if (typeof actual !== \"object\" || actual === null || Array.isArray(actual)) {\n return fail(`${path || \"value\"}: expected an object, got ${JSON.stringify(actual)}`);\n }\n\n const expectedObj = expected as Record<string, unknown>;\n const actualObj = actual as Record<string, unknown>;\n const results: MatchResult[] = [];\n\n // Only check keys present in expected — actual may have extras\n for (const key of Object.keys(expectedObj)) {\n if (!(key in actualObj)) {\n results.push(fail(`${path ? `${path}.${key}` : key}: key not found in actual`));\n } else {\n results.push(\n deepPartialMatch(actualObj[key], expectedObj[key], path ? `${path}.${key}` : key),\n );\n }\n }\n return merge(results);\n}\n\n// ─── Matcher Implementations ────────────────────────────────────────────\n\nclass ObjectLikeMatcher implements Matcher {\n readonly [MATCHER] = true as const;\n constructor(private readonly pattern: object) {}\n test(actual: unknown): MatchResult {\n return deepPartialMatch(actual, this.pattern);\n }\n}\n\nclass ObjectEqualsMatcher implements Matcher {\n readonly [MATCHER] = true as const;\n constructor(private readonly pattern: object) {}\n test(actual: unknown): MatchResult {\n return deepMatch(actual, this.pattern);\n }\n}\n\nclass ArrayWithMatcher implements Matcher {\n readonly [MATCHER] = true as const;\n constructor(private readonly items: unknown[]) {}\n test(actual: unknown): MatchResult {\n if (!Array.isArray(actual)) {\n return fail(`expected an array, got ${typeof actual}`);\n }\n // Each item in `this.items` must appear in `actual` in order (not necessarily contiguous)\n let searchStart = 0;\n for (let i = 0; i < this.items.length; i++) {\n let found = false;\n for (let j = searchStart; j < actual.length; j++) {\n const r = deepPartialMatch(actual[j], this.items[i]);\n if (r.pass) {\n searchStart = j + 1;\n found = true;\n break;\n }\n }\n if (!found) {\n return fail(\n `arrayWith: could not find match for item at index ${i}: ${JSON.stringify(this.items[i])}`,\n );\n }\n }\n return pass();\n }\n}\n\nclass ArrayEqualsMatcher implements Matcher {\n readonly [MATCHER] = true as const;\n constructor(private readonly items: unknown[]) {}\n test(actual: unknown): MatchResult {\n return deepMatch(actual, this.items);\n }\n}\n\nclass StringLikeRegexpMatcher implements Matcher {\n readonly [MATCHER] = true as const;\n private readonly regex: RegExp;\n constructor(pattern: string | RegExp) {\n this.regex = typeof pattern === \"string\" ? new RegExp(pattern) : pattern;\n }\n test(actual: unknown): MatchResult {\n if (typeof actual !== \"string\") {\n return fail(`expected a string, got ${typeof actual}`);\n }\n if (!this.regex.test(actual)) {\n return fail(`expected string matching ${this.regex}, got \"${actual}\"`);\n }\n return pass();\n }\n}\n\nclass AbsentMatcher implements Matcher {\n readonly [MATCHER] = true as const;\n test(actual: unknown): MatchResult {\n if (actual !== undefined) {\n return fail(`expected absent (undefined), got ${JSON.stringify(actual)}`);\n }\n return pass();\n }\n}\n\nclass AnyValueMatcher implements Matcher {\n readonly [MATCHER] = true as const;\n test(actual: unknown): MatchResult {\n if (actual === null || actual === undefined) {\n return fail(`expected any value, got ${actual}`);\n }\n return pass();\n }\n}\n\nclass NotMatcher implements Matcher {\n readonly [MATCHER] = true as const;\n constructor(private readonly pattern: unknown) {}\n test(actual: unknown): MatchResult {\n const inner = isMatcher(this.pattern)\n ? this.pattern.test(actual)\n : deepPartialMatch(actual, this.pattern);\n if (inner.pass) {\n return fail(`expected NOT to match, but matched: ${JSON.stringify(actual)}`);\n }\n return pass();\n }\n}\n\n/**\n * Factory for composable matchers used in assertions.\n *\n * @example\n * ```ts\n * template.hasResourceSpec('v1', 'ConfigMap', {\n * data: Match.objectLike({ key: 'value' }),\n * });\n * ```\n */\nexport class Match {\n private constructor() {}\n\n /** Deep-partial object match — actual may be a superset of pattern. */\n static objectLike(pattern: object): Matcher {\n return new ObjectLikeMatcher(pattern);\n }\n\n /** Exact object match — actual must equal pattern exactly (same keys, same values). */\n static objectEquals(pattern: object): Matcher {\n return new ObjectEqualsMatcher(pattern);\n }\n\n /** Array subset match — items must appear in actual in order. */\n static arrayWith(items: unknown[]): Matcher {\n return new ArrayWithMatcher(items);\n }\n\n /** Exact array match — actual must equal items exactly. */\n static arrayEquals(items: unknown[]): Matcher {\n return new ArrayEqualsMatcher(items);\n }\n\n /** String regex match. */\n static stringLikeRegexp(pattern: string | RegExp): Matcher {\n return new StringLikeRegexpMatcher(pattern);\n }\n\n /** Asserts the value is absent (undefined). */\n static absent(): Matcher {\n return new AbsentMatcher();\n }\n\n /** Asserts any non-null/non-undefined value is present. */\n static anyValue(): Matcher {\n return new AnyValueMatcher();\n }\n\n /** Inverts a match — asserts the value does NOT match the given pattern. */\n static not(pattern: unknown): Matcher {\n return new NotMatcher(pattern);\n }\n}\n","import {\n type Composition,\n type DependencyGraph,\n type KubernetesResource,\n type Resource,\n resolveSequencing,\n} from \"@xplane/core\";\nimport { type SynthesizeOptions, Template } from \"./template.js\";\n\n/** Result of a simulation run. */\nexport interface SimulationResult {\n /** Resources that are ready to emit (all dependencies satisfied). */\n emitted: Template;\n /** Resources that are blocked on unresolved dependencies. */\n blocked: Template;\n}\n\n// ─── Path Utilities (same logic as @xplane/function handler) ─────────\n\nfunction getNestedValue(obj: unknown, path: string): unknown {\n let current: unknown = obj;\n for (const segment of path.split(\".\")) {\n if (current === null || current === undefined || typeof current !== \"object\") {\n return undefined;\n }\n current = (current as Record<string, unknown>)[segment];\n }\n return current;\n}\n\nfunction setNestedValue(obj: Record<string, unknown>, path: string, value: unknown): void {\n const segments = path.split(\".\");\n let current: Record<string, unknown> = obj;\n for (let i = 0; i < segments.length - 1; i++) {\n const seg = segments[i]!;\n if (!(seg in current) || typeof current[seg] !== \"object\" || current[seg] === null) {\n current[seg] = {};\n }\n current = current[seg] as Record<string, unknown>;\n }\n const lastSeg = segments[segments.length - 1];\n if (lastSeg !== undefined) {\n current[lastSeg] = value;\n }\n}\n\n/**\n * Simulates the full rendering pipeline including observed state injection,\n * edge resolution, and sequencing — mimicking what `@xplane/function` does at runtime.\n *\n * @example\n * ```ts\n * const result = Simulator.synthesize(MyComposition, { xr: { ... } })\n * .withObserved([{ apiVersion: '...', kind: 'VPC', metadata: { name: 'vpc-abc' }, status: { atProvider: { vpcId: 'vpc-123' } } }])\n * .run();\n *\n * result.emitted.hasResourceSpec('ec2.aws.crossplane.io/v1beta1', 'Subnet', {\n * forProvider: { vpcId: 'vpc-123' },\n * });\n * ```\n */\nexport class Simulator {\n private readonly _composition: Composition;\n private _observed: KubernetesResource[] = [];\n\n private constructor(composition: Composition) {\n this._composition = composition;\n }\n\n /**\n * Ergonomic factory: injects XR/environment data, instantiates the\n * Composition class, and returns a Simulator ready for `.withObserved().run()`.\n */\n static synthesize(Ctor: new () => Composition, options: SynthesizeOptions = {}): Simulator {\n // Find base Composition class with _pendingXR\n let base = Ctor as unknown as Record<string, unknown>;\n while (base && !Object.hasOwn(base, \"_pendingXR\")) {\n base = Object.getPrototypeOf(base) as Record<string, unknown>;\n }\n if (!base) {\n throw new Error(\"Could not find Composition base class with _pendingXR\");\n }\n\n const BaseComposition = base as unknown as {\n _pendingXR: Record<string, unknown> | undefined;\n _pendingEnvironment: Record<string, unknown> | undefined;\n };\n\n BaseComposition._pendingXR = options.xr;\n BaseComposition._pendingEnvironment = options.environment;\n try {\n const instance = new Ctor();\n return new Simulator(instance);\n } finally {\n BaseComposition._pendingXR = undefined;\n BaseComposition._pendingEnvironment = undefined;\n }\n }\n\n /**\n * Build a Simulator from an already-instantiated Composition.\n */\n static fromComposition(composition: Composition): Simulator {\n return new Simulator(composition);\n }\n\n /**\n * Provide observed (cluster) state for resources.\n * Each resource is matched to a declared resource by its construct path\n * (i.e., `metadata.name` in observed state maps to `resource.path` in the composition).\n */\n withObserved(resources: KubernetesResource[]): this {\n this._observed = resources;\n return this;\n }\n\n /**\n * Run the simulation: inject observed state, resolve edges, determine sequencing.\n */\n run(): SimulationResult {\n const composition = this._composition;\n const resources = (composition as unknown as { resources: ReadonlyMap<string, ResourceLike> })\n .resources;\n const collector = (\n composition as unknown as { collector: { edges: ReadonlyArray<DependencyEdgeLike> } }\n ).collector;\n const graph = (composition as unknown as { graph: DependencyGraph }).graph;\n\n // Build observed map keyed by resource path (same as handler)\n const observedMap = new Map<string, KubernetesResource>();\n for (const obs of this._observed) {\n const name = obs.metadata?.name;\n if (name) {\n observedMap.set(name, obs);\n }\n }\n\n // Add edges to graph\n graph.addEdges(collector.edges);\n\n // Feed observed state into resources\n for (const [path, resource] of resources) {\n const observed = observedMap.get(path);\n if (observed) {\n resource.setObserved(observed);\n }\n }\n\n // Resolve cross-resource edge values from observed state\n for (const edge of collector.edges) {\n const observed = observedMap.get(edge.from.id);\n if (!observed) continue;\n\n const value = getNestedValue(observed, edge.fromPath);\n if (value === undefined || value === null) continue;\n\n const targetResource = resources.get(edge.to.id);\n if (!targetResource) continue;\n\n const toPath = edge.toPath;\n if (toPath.startsWith(\"spec.\")) {\n setNestedValue(\n targetResource.spec as Record<string, unknown>,\n toPath.slice(\"spec.\".length),\n value,\n );\n }\n }\n\n // Resolve sequencing\n const sequencing = resolveSequencing(\n resources as unknown as ReadonlyMap<string, Resource>,\n graph as DependencyGraph,\n observedMap,\n );\n\n return {\n emitted: Template.fromResources(sequencing.emit.map((r) => r.toDesired())),\n blocked: Template.fromResources(sequencing.blocked.map((r) => r.toDesired())),\n };\n }\n}\n\n// ─── Internal type helpers (avoid importing private types) ───────────\n\ninterface ResourceLike {\n path: string;\n spec: Record<string, unknown>;\n setObserved(observed: KubernetesResource): void;\n toDesired(): KubernetesResource;\n}\n\ninterface DependencyEdgeLike {\n from: { id: string };\n fromPath: string;\n to: { id: string };\n toPath: string;\n}\n","import type { Composition, KubernetesResource } from \"@xplane/core\";\nimport { deepPartialMatch } from \"./match.js\";\n\n/** Options for `Template.synthesize()`. */\nexport interface SynthesizeOptions {\n /** XR (composite resource) data to inject before instantiation. */\n xr?: Record<string, unknown>;\n /** Environment data to inject before instantiation. */\n environment?: Record<string, unknown>;\n}\n\n/**\n * A snapshot of rendered resources from a Composition, providing\n * assertion methods for unit testing.\n *\n * @example\n * ```ts\n * const template = Template.synthesize(MyComposition, {\n * xr: { spec: { region: 'us-east-1' } },\n * });\n * template.hasResourceSpec('ec2.aws.crossplane.io/v1beta1', 'VPC', {\n * forProvider: { region: 'us-east-1' },\n * });\n * ```\n */\nexport class Template {\n private readonly _resources: KubernetesResource[];\n\n private constructor(resources: KubernetesResource[]) {\n this._resources = resources;\n }\n\n /**\n * Ergonomic factory: injects XR/environment data and instantiates\n * the Composition class, then builds a Template from the rendered resources.\n *\n * Users never need to touch `Composition._pendingXR` directly.\n */\n static synthesize(Ctor: new () => Composition, options: SynthesizeOptions = {}): Template {\n // Walk up prototype chain to find the base Composition class with _pendingXR\n let base = Ctor as unknown as Record<string, unknown>;\n while (base && !Object.hasOwn(base, \"_pendingXR\")) {\n base = Object.getPrototypeOf(base) as Record<string, unknown>;\n }\n if (!base) {\n throw new Error(\"Could not find Composition base class with _pendingXR\");\n }\n\n const BaseComposition = base as unknown as {\n _pendingXR: Record<string, unknown> | undefined;\n _pendingEnvironment: Record<string, unknown> | undefined;\n };\n\n BaseComposition._pendingXR = options.xr;\n BaseComposition._pendingEnvironment = options.environment;\n try {\n const instance = new Ctor();\n return Template.fromComposition(instance);\n } finally {\n BaseComposition._pendingXR = undefined;\n BaseComposition._pendingEnvironment = undefined;\n }\n }\n\n /**\n * Build a Template from an already-instantiated Composition.\n */\n static fromComposition(composition: Composition): Template {\n const resources = [\n ...(\n composition as unknown as {\n resources: ReadonlyMap<string, { toDesired(): KubernetesResource }>;\n }\n ).resources.values(),\n ].map((r) => r.toDesired());\n return new Template(resources);\n }\n\n /**\n * Build a Template from a pre-built array of KubernetesResource objects.\n * Used internally by Simulator.\n */\n static fromResources(resources: KubernetesResource[]): Template {\n return new Template(resources);\n }\n\n /** Get all resources matching apiVersion + kind. */\n private _filterByGVK(apiVersion: string, kind: string): KubernetesResource[] {\n return this._resources.filter((r) => r.apiVersion === apiVersion && r.kind === kind);\n }\n\n /**\n * Assert the number of resources with the given apiVersion and kind.\n */\n resourceCountIs(apiVersion: string, kind: string, count: number): void {\n const matched = this._filterByGVK(apiVersion, kind);\n if (matched.length !== count) {\n throw new Error(\n `Expected ${count} resource(s) of type ${apiVersion}/${kind}, found ${matched.length}`,\n );\n }\n }\n\n /**\n * Assert that at least one resource of the given type matches the expected properties.\n * Uses deep-partial matching by default (actual can be a superset of expected).\n */\n hasResource(apiVersion: string, kind: string, props?: object): void {\n const matched = this._filterByGVK(apiVersion, kind);\n if (matched.length === 0) {\n throw new Error(`No resources found with type ${apiVersion}/${kind}`);\n }\n\n if (!props) return; // Just checking existence\n\n const allFailures: string[] = [];\n for (const resource of matched) {\n const result = deepPartialMatch(resource, props);\n if (result.pass) return; // At least one matches\n allFailures.push(\n ` Resource: ${JSON.stringify(resource.metadata?.name ?? \"(unnamed)\")}\\n ${result.failures.join(\"\\n \")}`,\n );\n }\n throw new Error(\n `No resource of type ${apiVersion}/${kind} matches the expected properties:\\n${allFailures.join(\"\\n\")}`,\n );\n }\n\n /**\n * Assert that at least one resource of the given type has a spec matching the expected properties.\n * Shorthand for matching against the `spec` field only.\n */\n hasResourceSpec(apiVersion: string, kind: string, specProps: object): void {\n const matched = this._filterByGVK(apiVersion, kind);\n if (matched.length === 0) {\n throw new Error(`No resources found with type ${apiVersion}/${kind}`);\n }\n\n const allFailures: string[] = [];\n for (const resource of matched) {\n const result = deepPartialMatch(resource.spec ?? {}, specProps);\n if (result.pass) return;\n allFailures.push(\n ` Resource: ${JSON.stringify(resource.metadata?.name ?? \"(unnamed)\")}\\n ${result.failures.join(\"\\n \")}`,\n );\n }\n throw new Error(\n `No resource of type ${apiVersion}/${kind} has spec matching the expected properties:\\n${allFailures.join(\"\\n\")}`,\n );\n }\n\n /**\n * Assert that at least one resource of the given type has metadata matching the expected properties.\n */\n hasResourceMetadata(apiVersion: string, kind: string, metaProps: object): void {\n const matched = this._filterByGVK(apiVersion, kind);\n if (matched.length === 0) {\n throw new Error(`No resources found with type ${apiVersion}/${kind}`);\n }\n\n const allFailures: string[] = [];\n for (const resource of matched) {\n const result = deepPartialMatch(resource.metadata ?? {}, metaProps);\n if (result.pass) return;\n allFailures.push(\n ` Resource: ${JSON.stringify(resource.metadata?.name ?? \"(unnamed)\")}\\n ${result.failures.join(\"\\n \")}`,\n );\n }\n throw new Error(\n `No resource of type ${apiVersion}/${kind} has metadata matching the expected properties:\\n${allFailures.join(\"\\n\")}`,\n );\n }\n\n /**\n * Assert that ALL resources of the given type match the expected properties.\n */\n allResources(apiVersion: string, kind: string, props: object): void {\n const matched = this._filterByGVK(apiVersion, kind);\n if (matched.length === 0) {\n throw new Error(`No resources found with type ${apiVersion}/${kind}`);\n }\n\n const failures: string[] = [];\n for (const resource of matched) {\n const result = deepPartialMatch(resource, props);\n if (!result.pass) {\n failures.push(\n ` Resource: ${JSON.stringify(resource.metadata?.name ?? \"(unnamed)\")}\\n ${result.failures.join(\"\\n \")}`,\n );\n }\n }\n if (failures.length > 0) {\n throw new Error(\n `Not all resources of type ${apiVersion}/${kind} match:\\n${failures.join(\"\\n\")}`,\n );\n }\n }\n\n /**\n * Find all resources of the given type that match the expected properties.\n * Returns matches — never throws.\n */\n findResources(apiVersion: string, kind: string, props?: object): KubernetesResource[] {\n const matched = this._filterByGVK(apiVersion, kind);\n if (!props) return matched;\n\n return matched.filter((resource) => {\n const result = deepPartialMatch(resource, props);\n return result.pass;\n });\n }\n\n /**\n * Serialize all resources to a JSON-compatible array for snapshot testing.\n *\n * @example\n * ```ts\n * expect(template.toJSON()).toMatchSnapshot();\n * ```\n */\n toJSON(): KubernetesResource[] {\n return structuredClone(this._resources);\n }\n}\n"],"mappings":";AACO,IAAM,UAAU,uBAAO,IAAI,yBAAyB;AAiBpD,SAAS,UAAU,OAAkC;AAC1D,SACE,OAAO,UAAU,YACjB,UAAU,QACV,WAAW,SACV,MAAkB,OAAO,MAAM;AAEpC;AAEA,SAAS,OAAoB;AAC3B,SAAO,EAAE,MAAM,MAAM,UAAU,CAAC,EAAE;AACpC;AAEA,SAAS,KAAK,SAA8B;AAC1C,SAAO,EAAE,MAAM,OAAO,UAAU,CAAC,OAAO,EAAE;AAC5C;AAEA,SAAS,MAAM,SAAqC;AAClD,QAAM,WAAW,QAAQ,QAAQ,CAAC,MAAM,EAAE,QAAQ;AAClD,SAAO,EAAE,MAAM,SAAS,WAAW,GAAG,SAAS;AACjD;AAQO,SAAS,UAAU,QAAiB,UAAmB,OAAO,IAAiB;AACpF,MAAI,UAAU,QAAQ,GAAG;AACvB,UAAM,SAAS,SAAS,KAAK,MAAM;AACnC,QAAI,CAAC,OAAO,MAAM;AAChB,aAAO,EAAE,MAAM,OAAO,UAAU,OAAO,SAAS,IAAI,CAAC,MAAO,OAAO,GAAG,IAAI,KAAK,CAAC,KAAK,CAAE,EAAE;AAAA,IAC3F;AACA,WAAO,KAAK;AAAA,EACd;AAGA,MAAI,aAAa,QAAQ,aAAa,UAAa,OAAO,aAAa,UAAU;AAC/E,QAAI,WAAW,SAAU,QAAO,KAAK;AACrC,WAAO;AAAA,MACL,OACI,GAAG,IAAI,cAAc,KAAK,UAAU,QAAQ,CAAC,SAAS,KAAK,UAAU,MAAM,CAAC,KAC5E,YAAY,KAAK,UAAU,QAAQ,CAAC,SAAS,KAAK,UAAU,MAAM,CAAC;AAAA,IACzE;AAAA,EACF;AAGA,MAAI,MAAM,QAAQ,QAAQ,GAAG;AAC3B,QAAI,CAAC,MAAM,QAAQ,MAAM,GAAG;AAC1B,aAAO,KAAK,GAAG,QAAQ,OAAO,4BAA4B,OAAO,MAAM,EAAE;AAAA,IAC3E;AACA,QAAI,OAAO,WAAW,SAAS,QAAQ;AACrC,aAAO;AAAA,QACL,GAAG,QAAQ,OAAO,8BAA8B,SAAS,MAAM,gBAAgB,OAAO,MAAM;AAAA,MAC9F;AAAA,IACF;AACA,UAAMA,WAAyB,CAAC;AAChC,aAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AACxC,MAAAA,SAAQ,KAAK,UAAU,OAAO,CAAC,GAAG,SAAS,CAAC,GAAG,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;AAAA,IACjE;AACA,WAAO,MAAMA,QAAO;AAAA,EACtB;AAGA,MAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,MAAM,GAAG;AAC1E,WAAO,KAAK,GAAG,QAAQ,OAAO,6BAA6B,KAAK,UAAU,MAAM,CAAC,EAAE;AAAA,EACrF;AACA,QAAM,cAAc;AACpB,QAAM,YAAY;AAClB,QAAM,UAAyB,CAAC;AAGhC,aAAW,OAAO,OAAO,KAAK,WAAW,GAAG;AAC1C,YAAQ,KAAK,UAAU,UAAU,GAAG,GAAG,YAAY,GAAG,GAAG,OAAO,GAAG,IAAI,IAAI,GAAG,KAAK,GAAG,CAAC;AAAA,EACzF;AAEA,aAAW,OAAO,OAAO,KAAK,SAAS,GAAG;AACxC,QAAI,EAAE,OAAO,cAAc;AACzB,cAAQ,KAAK,KAAK,GAAG,OAAO,GAAG,IAAI,IAAI,GAAG,KAAK,GAAG,kBAAkB,CAAC;AAAA,IACvE;AAAA,EACF;AACA,SAAO,MAAM,OAAO;AACtB;AAMO,SAAS,iBAAiB,QAAiB,UAAmB,OAAO,IAAiB;AAC3F,MAAI,UAAU,QAAQ,GAAG;AACvB,UAAM,SAAS,SAAS,KAAK,MAAM;AACnC,QAAI,CAAC,OAAO,MAAM;AAChB,aAAO,EAAE,MAAM,OAAO,UAAU,OAAO,SAAS,IAAI,CAAC,MAAO,OAAO,GAAG,IAAI,KAAK,CAAC,KAAK,CAAE,EAAE;AAAA,IAC3F;AACA,WAAO,KAAK;AAAA,EACd;AAEA,MAAI,aAAa,QAAQ,aAAa,UAAa,OAAO,aAAa,UAAU;AAC/E,QAAI,WAAW,SAAU,QAAO,KAAK;AACrC,WAAO;AAAA,MACL,OACI,GAAG,IAAI,cAAc,KAAK,UAAU,QAAQ,CAAC,SAAS,KAAK,UAAU,MAAM,CAAC,KAC5E,YAAY,KAAK,UAAU,QAAQ,CAAC,SAAS,KAAK,UAAU,MAAM,CAAC;AAAA,IACzE;AAAA,EACF;AAEA,MAAI,MAAM,QAAQ,QAAQ,GAAG;AAC3B,QAAI,CAAC,MAAM,QAAQ,MAAM,GAAG;AAC1B,aAAO,KAAK,GAAG,QAAQ,OAAO,4BAA4B,OAAO,MAAM,EAAE;AAAA,IAC3E;AACA,QAAI,OAAO,WAAW,SAAS,QAAQ;AACrC,aAAO;AAAA,QACL,GAAG,QAAQ,OAAO,8BAA8B,SAAS,MAAM,gBAAgB,OAAO,MAAM;AAAA,MAC9F;AAAA,IACF;AACA,UAAMA,WAAyB,CAAC;AAChC,aAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AACxC,MAAAA,SAAQ,KAAK,iBAAiB,OAAO,CAAC,GAAG,SAAS,CAAC,GAAG,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;AAAA,IACxE;AACA,WAAO,MAAMA,QAAO;AAAA,EACtB;AAEA,MAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,MAAM,GAAG;AAC1E,WAAO,KAAK,GAAG,QAAQ,OAAO,6BAA6B,KAAK,UAAU,MAAM,CAAC,EAAE;AAAA,EACrF;AAEA,QAAM,cAAc;AACpB,QAAM,YAAY;AAClB,QAAM,UAAyB,CAAC;AAGhC,aAAW,OAAO,OAAO,KAAK,WAAW,GAAG;AAC1C,QAAI,EAAE,OAAO,YAAY;AACvB,cAAQ,KAAK,KAAK,GAAG,OAAO,GAAG,IAAI,IAAI,GAAG,KAAK,GAAG,2BAA2B,CAAC;AAAA,IAChF,OAAO;AACL,cAAQ;AAAA,QACN,iBAAiB,UAAU,GAAG,GAAG,YAAY,GAAG,GAAG,OAAO,GAAG,IAAI,IAAI,GAAG,KAAK,GAAG;AAAA,MAClF;AAAA,IACF;AAAA,EACF;AACA,SAAO,MAAM,OAAO;AACtB;AAIA,IAAM,oBAAN,MAA2C;AAAA,EAEzC,YAA6B,SAAiB;AAAjB;AAAA,EAAkB;AAAA,EAAlB;AAAA,EAD7B,CAAU,OAAO,IAAI;AAAA,EAErB,KAAK,QAA8B;AACjC,WAAO,iBAAiB,QAAQ,KAAK,OAAO;AAAA,EAC9C;AACF;AAEA,IAAM,sBAAN,MAA6C;AAAA,EAE3C,YAA6B,SAAiB;AAAjB;AAAA,EAAkB;AAAA,EAAlB;AAAA,EAD7B,CAAU,OAAO,IAAI;AAAA,EAErB,KAAK,QAA8B;AACjC,WAAO,UAAU,QAAQ,KAAK,OAAO;AAAA,EACvC;AACF;AAEA,IAAM,mBAAN,MAA0C;AAAA,EAExC,YAA6B,OAAkB;AAAlB;AAAA,EAAmB;AAAA,EAAnB;AAAA,EAD7B,CAAU,OAAO,IAAI;AAAA,EAErB,KAAK,QAA8B;AACjC,QAAI,CAAC,MAAM,QAAQ,MAAM,GAAG;AAC1B,aAAO,KAAK,0BAA0B,OAAO,MAAM,EAAE;AAAA,IACvD;AAEA,QAAI,cAAc;AAClB,aAAS,IAAI,GAAG,IAAI,KAAK,MAAM,QAAQ,KAAK;AAC1C,UAAI,QAAQ;AACZ,eAAS,IAAI,aAAa,IAAI,OAAO,QAAQ,KAAK;AAChD,cAAM,IAAI,iBAAiB,OAAO,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC;AACnD,YAAI,EAAE,MAAM;AACV,wBAAc,IAAI;AAClB,kBAAQ;AACR;AAAA,QACF;AAAA,MACF;AACA,UAAI,CAAC,OAAO;AACV,eAAO;AAAA,UACL,qDAAqD,CAAC,KAAK,KAAK,UAAU,KAAK,MAAM,CAAC,CAAC,CAAC;AAAA,QAC1F;AAAA,MACF;AAAA,IACF;AACA,WAAO,KAAK;AAAA,EACd;AACF;AAEA,IAAM,qBAAN,MAA4C;AAAA,EAE1C,YAA6B,OAAkB;AAAlB;AAAA,EAAmB;AAAA,EAAnB;AAAA,EAD7B,CAAU,OAAO,IAAI;AAAA,EAErB,KAAK,QAA8B;AACjC,WAAO,UAAU,QAAQ,KAAK,KAAK;AAAA,EACrC;AACF;AAEA,IAAM,0BAAN,MAAiD;AAAA,EAC/C,CAAU,OAAO,IAAI;AAAA,EACJ;AAAA,EACjB,YAAY,SAA0B;AACpC,SAAK,QAAQ,OAAO,YAAY,WAAW,IAAI,OAAO,OAAO,IAAI;AAAA,EACnE;AAAA,EACA,KAAK,QAA8B;AACjC,QAAI,OAAO,WAAW,UAAU;AAC9B,aAAO,KAAK,0BAA0B,OAAO,MAAM,EAAE;AAAA,IACvD;AACA,QAAI,CAAC,KAAK,MAAM,KAAK,MAAM,GAAG;AAC5B,aAAO,KAAK,4BAA4B,KAAK,KAAK,UAAU,MAAM,GAAG;AAAA,IACvE;AACA,WAAO,KAAK;AAAA,EACd;AACF;AAEA,IAAM,gBAAN,MAAuC;AAAA,EACrC,CAAU,OAAO,IAAI;AAAA,EACrB,KAAK,QAA8B;AACjC,QAAI,WAAW,QAAW;AACxB,aAAO,KAAK,oCAAoC,KAAK,UAAU,MAAM,CAAC,EAAE;AAAA,IAC1E;AACA,WAAO,KAAK;AAAA,EACd;AACF;AAEA,IAAM,kBAAN,MAAyC;AAAA,EACvC,CAAU,OAAO,IAAI;AAAA,EACrB,KAAK,QAA8B;AACjC,QAAI,WAAW,QAAQ,WAAW,QAAW;AAC3C,aAAO,KAAK,2BAA2B,MAAM,EAAE;AAAA,IACjD;AACA,WAAO,KAAK;AAAA,EACd;AACF;AAEA,IAAM,aAAN,MAAoC;AAAA,EAElC,YAA6B,SAAkB;AAAlB;AAAA,EAAmB;AAAA,EAAnB;AAAA,EAD7B,CAAU,OAAO,IAAI;AAAA,EAErB,KAAK,QAA8B;AACjC,UAAM,QAAQ,UAAU,KAAK,OAAO,IAChC,KAAK,QAAQ,KAAK,MAAM,IACxB,iBAAiB,QAAQ,KAAK,OAAO;AACzC,QAAI,MAAM,MAAM;AACd,aAAO,KAAK,uCAAuC,KAAK,UAAU,MAAM,CAAC,EAAE;AAAA,IAC7E;AACA,WAAO,KAAK;AAAA,EACd;AACF;AAYO,IAAM,QAAN,MAAY;AAAA,EACT,cAAc;AAAA,EAAC;AAAA;AAAA,EAGvB,OAAO,WAAW,SAA0B;AAC1C,WAAO,IAAI,kBAAkB,OAAO;AAAA,EACtC;AAAA;AAAA,EAGA,OAAO,aAAa,SAA0B;AAC5C,WAAO,IAAI,oBAAoB,OAAO;AAAA,EACxC;AAAA;AAAA,EAGA,OAAO,UAAU,OAA2B;AAC1C,WAAO,IAAI,iBAAiB,KAAK;AAAA,EACnC;AAAA;AAAA,EAGA,OAAO,YAAY,OAA2B;AAC5C,WAAO,IAAI,mBAAmB,KAAK;AAAA,EACrC;AAAA;AAAA,EAGA,OAAO,iBAAiB,SAAmC;AACzD,WAAO,IAAI,wBAAwB,OAAO;AAAA,EAC5C;AAAA;AAAA,EAGA,OAAO,SAAkB;AACvB,WAAO,IAAI,cAAc;AAAA,EAC3B;AAAA;AAAA,EAGA,OAAO,WAAoB;AACzB,WAAO,IAAI,gBAAgB;AAAA,EAC7B;AAAA;AAAA,EAGA,OAAO,IAAI,SAA2B;AACpC,WAAO,IAAI,WAAW,OAAO;AAAA,EAC/B;AACF;;;AChUA;AAAA,EAKE;AAAA,OACK;;;ACmBA,IAAM,WAAN,MAAM,UAAS;AAAA,EACH;AAAA,EAET,YAAY,WAAiC;AACnD,SAAK,aAAa;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,OAAO,WAAW,MAA6B,UAA6B,CAAC,GAAa;AAExF,QAAI,OAAO;AACX,WAAO,QAAQ,CAAC,OAAO,OAAO,MAAM,YAAY,GAAG;AACjD,aAAO,OAAO,eAAe,IAAI;AAAA,IACnC;AACA,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,MAAM,uDAAuD;AAAA,IACzE;AAEA,UAAM,kBAAkB;AAKxB,oBAAgB,aAAa,QAAQ;AACrC,oBAAgB,sBAAsB,QAAQ;AAC9C,QAAI;AACF,YAAM,WAAW,IAAI,KAAK;AAC1B,aAAO,UAAS,gBAAgB,QAAQ;AAAA,IAC1C,UAAE;AACA,sBAAgB,aAAa;AAC7B,sBAAgB,sBAAsB;AAAA,IACxC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,gBAAgB,aAAoC;AACzD,UAAM,YAAY;AAAA,MAChB,GACE,YAGA,UAAU,OAAO;AAAA,IACrB,EAAE,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC;AAC1B,WAAO,IAAI,UAAS,SAAS;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,cAAc,WAA2C;AAC9D,WAAO,IAAI,UAAS,SAAS;AAAA,EAC/B;AAAA;AAAA,EAGQ,aAAa,YAAoB,MAAoC;AAC3E,WAAO,KAAK,WAAW,OAAO,CAAC,MAAM,EAAE,eAAe,cAAc,EAAE,SAAS,IAAI;AAAA,EACrF;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,YAAoB,MAAc,OAAqB;AACrE,UAAM,UAAU,KAAK,aAAa,YAAY,IAAI;AAClD,QAAI,QAAQ,WAAW,OAAO;AAC5B,YAAM,IAAI;AAAA,QACR,YAAY,KAAK,wBAAwB,UAAU,IAAI,IAAI,WAAW,QAAQ,MAAM;AAAA,MACtF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,YAAY,YAAoB,MAAc,OAAsB;AAClE,UAAM,UAAU,KAAK,aAAa,YAAY,IAAI;AAClD,QAAI,QAAQ,WAAW,GAAG;AACxB,YAAM,IAAI,MAAM,gCAAgC,UAAU,IAAI,IAAI,EAAE;AAAA,IACtE;AAEA,QAAI,CAAC,MAAO;AAEZ,UAAM,cAAwB,CAAC;AAC/B,eAAW,YAAY,SAAS;AAC9B,YAAM,SAAS,iBAAiB,UAAU,KAAK;AAC/C,UAAI,OAAO,KAAM;AACjB,kBAAY;AAAA,QACV,eAAe,KAAK,UAAU,SAAS,UAAU,QAAQ,WAAW,CAAC;AAAA,MAAS,OAAO,SAAS,KAAK,QAAQ,CAAC;AAAA,MAC9G;AAAA,IACF;AACA,UAAM,IAAI;AAAA,MACR,uBAAuB,UAAU,IAAI,IAAI;AAAA,EAAsC,YAAY,KAAK,IAAI,CAAC;AAAA,IACvG;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,gBAAgB,YAAoB,MAAc,WAAyB;AACzE,UAAM,UAAU,KAAK,aAAa,YAAY,IAAI;AAClD,QAAI,QAAQ,WAAW,GAAG;AACxB,YAAM,IAAI,MAAM,gCAAgC,UAAU,IAAI,IAAI,EAAE;AAAA,IACtE;AAEA,UAAM,cAAwB,CAAC;AAC/B,eAAW,YAAY,SAAS;AAC9B,YAAM,SAAS,iBAAiB,SAAS,QAAQ,CAAC,GAAG,SAAS;AAC9D,UAAI,OAAO,KAAM;AACjB,kBAAY;AAAA,QACV,eAAe,KAAK,UAAU,SAAS,UAAU,QAAQ,WAAW,CAAC;AAAA,MAAS,OAAO,SAAS,KAAK,QAAQ,CAAC;AAAA,MAC9G;AAAA,IACF;AACA,UAAM,IAAI;AAAA,MACR,uBAAuB,UAAU,IAAI,IAAI;AAAA,EAAgD,YAAY,KAAK,IAAI,CAAC;AAAA,IACjH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,oBAAoB,YAAoB,MAAc,WAAyB;AAC7E,UAAM,UAAU,KAAK,aAAa,YAAY,IAAI;AAClD,QAAI,QAAQ,WAAW,GAAG;AACxB,YAAM,IAAI,MAAM,gCAAgC,UAAU,IAAI,IAAI,EAAE;AAAA,IACtE;AAEA,UAAM,cAAwB,CAAC;AAC/B,eAAW,YAAY,SAAS;AAC9B,YAAM,SAAS,iBAAiB,SAAS,YAAY,CAAC,GAAG,SAAS;AAClE,UAAI,OAAO,KAAM;AACjB,kBAAY;AAAA,QACV,eAAe,KAAK,UAAU,SAAS,UAAU,QAAQ,WAAW,CAAC;AAAA,MAAS,OAAO,SAAS,KAAK,QAAQ,CAAC;AAAA,MAC9G;AAAA,IACF;AACA,UAAM,IAAI;AAAA,MACR,uBAAuB,UAAU,IAAI,IAAI;AAAA,EAAoD,YAAY,KAAK,IAAI,CAAC;AAAA,IACrH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa,YAAoB,MAAc,OAAqB;AAClE,UAAM,UAAU,KAAK,aAAa,YAAY,IAAI;AAClD,QAAI,QAAQ,WAAW,GAAG;AACxB,YAAM,IAAI,MAAM,gCAAgC,UAAU,IAAI,IAAI,EAAE;AAAA,IACtE;AAEA,UAAM,WAAqB,CAAC;AAC5B,eAAW,YAAY,SAAS;AAC9B,YAAM,SAAS,iBAAiB,UAAU,KAAK;AAC/C,UAAI,CAAC,OAAO,MAAM;AAChB,iBAAS;AAAA,UACP,eAAe,KAAK,UAAU,SAAS,UAAU,QAAQ,WAAW,CAAC;AAAA,MAAS,OAAO,SAAS,KAAK,QAAQ,CAAC;AAAA,QAC9G;AAAA,MACF;AAAA,IACF;AACA,QAAI,SAAS,SAAS,GAAG;AACvB,YAAM,IAAI;AAAA,QACR,6BAA6B,UAAU,IAAI,IAAI;AAAA,EAAY,SAAS,KAAK,IAAI,CAAC;AAAA,MAChF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,cAAc,YAAoB,MAAc,OAAsC;AACpF,UAAM,UAAU,KAAK,aAAa,YAAY,IAAI;AAClD,QAAI,CAAC,MAAO,QAAO;AAEnB,WAAO,QAAQ,OAAO,CAAC,aAAa;AAClC,YAAM,SAAS,iBAAiB,UAAU,KAAK;AAC/C,aAAO,OAAO;AAAA,IAChB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,SAA+B;AAC7B,WAAO,gBAAgB,KAAK,UAAU;AAAA,EACxC;AACF;;;AD5MA,SAAS,eAAe,KAAc,MAAuB;AAC3D,MAAI,UAAmB;AACvB,aAAW,WAAW,KAAK,MAAM,GAAG,GAAG;AACrC,QAAI,YAAY,QAAQ,YAAY,UAAa,OAAO,YAAY,UAAU;AAC5E,aAAO;AAAA,IACT;AACA,cAAW,QAAoC,OAAO;AAAA,EACxD;AACA,SAAO;AACT;AAEA,SAAS,eAAe,KAA8B,MAAc,OAAsB;AACxF,QAAM,WAAW,KAAK,MAAM,GAAG;AAC/B,MAAI,UAAmC;AACvC,WAAS,IAAI,GAAG,IAAI,SAAS,SAAS,GAAG,KAAK;AAC5C,UAAM,MAAM,SAAS,CAAC;AACtB,QAAI,EAAE,OAAO,YAAY,OAAO,QAAQ,GAAG,MAAM,YAAY,QAAQ,GAAG,MAAM,MAAM;AAClF,cAAQ,GAAG,IAAI,CAAC;AAAA,IAClB;AACA,cAAU,QAAQ,GAAG;AAAA,EACvB;AACA,QAAM,UAAU,SAAS,SAAS,SAAS,CAAC;AAC5C,MAAI,YAAY,QAAW;AACzB,YAAQ,OAAO,IAAI;AAAA,EACrB;AACF;AAiBO,IAAM,YAAN,MAAM,WAAU;AAAA,EACJ;AAAA,EACT,YAAkC,CAAC;AAAA,EAEnC,YAAY,aAA0B;AAC5C,SAAK,eAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,WAAW,MAA6B,UAA6B,CAAC,GAAc;AAEzF,QAAI,OAAO;AACX,WAAO,QAAQ,CAAC,OAAO,OAAO,MAAM,YAAY,GAAG;AACjD,aAAO,OAAO,eAAe,IAAI;AAAA,IACnC;AACA,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,MAAM,uDAAuD;AAAA,IACzE;AAEA,UAAM,kBAAkB;AAKxB,oBAAgB,aAAa,QAAQ;AACrC,oBAAgB,sBAAsB,QAAQ;AAC9C,QAAI;AACF,YAAM,WAAW,IAAI,KAAK;AAC1B,aAAO,IAAI,WAAU,QAAQ;AAAA,IAC/B,UAAE;AACA,sBAAgB,aAAa;AAC7B,sBAAgB,sBAAsB;AAAA,IACxC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,gBAAgB,aAAqC;AAC1D,WAAO,IAAI,WAAU,WAAW;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,aAAa,WAAuC;AAClD,SAAK,YAAY;AACjB,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAwB;AACtB,UAAM,cAAc,KAAK;AACzB,UAAM,YAAa,YAChB;AACH,UAAM,YACJ,YACA;AACF,UAAM,QAAS,YAAsD;AAGrE,UAAM,cAAc,oBAAI,IAAgC;AACxD,eAAW,OAAO,KAAK,WAAW;AAChC,YAAM,OAAO,IAAI,UAAU;AAC3B,UAAI,MAAM;AACR,oBAAY,IAAI,MAAM,GAAG;AAAA,MAC3B;AAAA,IACF;AAGA,UAAM,SAAS,UAAU,KAAK;AAG9B,eAAW,CAAC,MAAM,QAAQ,KAAK,WAAW;AACxC,YAAM,WAAW,YAAY,IAAI,IAAI;AACrC,UAAI,UAAU;AACZ,iBAAS,YAAY,QAAQ;AAAA,MAC/B;AAAA,IACF;AAGA,eAAW,QAAQ,UAAU,OAAO;AAClC,YAAM,WAAW,YAAY,IAAI,KAAK,KAAK,EAAE;AAC7C,UAAI,CAAC,SAAU;AAEf,YAAM,QAAQ,eAAe,UAAU,KAAK,QAAQ;AACpD,UAAI,UAAU,UAAa,UAAU,KAAM;AAE3C,YAAM,iBAAiB,UAAU,IAAI,KAAK,GAAG,EAAE;AAC/C,UAAI,CAAC,eAAgB;AAErB,YAAM,SAAS,KAAK;AACpB,UAAI,OAAO,WAAW,OAAO,GAAG;AAC9B;AAAA,UACE,eAAe;AAAA,UACf,OAAO,MAAM,QAAQ,MAAM;AAAA,UAC3B;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,UAAM,aAAa;AAAA,MACjB;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,WAAO;AAAA,MACL,SAAS,SAAS,cAAc,WAAW,KAAK,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AAAA,MACzE,SAAS,SAAS,cAAc,WAAW,QAAQ,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AAAA,IAC9E;AAAA,EACF;AACF;","names":["results"]}
package/package.json CHANGED
@@ -1,12 +1,42 @@
1
1
  {
2
2
  "name": "@xplane/devtools",
3
- "version": "0.0.0",
4
- "description": "",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
3
+ "version": "0.9.1",
4
+ "type": "module",
5
+ "description": "Developer tools and testing utilities for @xplane/core compositions",
6
+ "license": "Apache-2.0",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/sv-oss/xplane",
10
+ "directory": "packages/devtools"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "exports": {
16
+ "./assertions": {
17
+ "import": "./dist/assertions/index.js",
18
+ "types": "./dist/assertions/index.d.ts"
19
+ }
8
20
  },
9
- "keywords": [],
10
- "author": "",
11
- "license": "ISC"
12
- }
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "devDependencies": {
25
+ "constructs": "^10.6.0",
26
+ "tsup": "8.5.1",
27
+ "typescript": "6.0.3",
28
+ "vitest": "4.1.6",
29
+ "@xplane/core": "0.9.1"
30
+ },
31
+ "peerDependencies": {
32
+ "constructs": "^10.0.0",
33
+ "@xplane/core": "0.9.1"
34
+ },
35
+ "scripts": {
36
+ "build": "tsup",
37
+ "test": "vitest run",
38
+ "test:watch": "vitest",
39
+ "typecheck": "tsc --noEmit",
40
+ "clean": "rm -rf dist .turbo"
41
+ }
42
+ }